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

Source Code for Module Gnumed.business.gmPerson

   1  # -*- coding: utf-8 -*- 
   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 io 
  18  import threading 
  19  import logging 
  20  import io 
  21  import inspect 
  22  from xml.etree import ElementTree as etree 
  23   
  24   
  25  # GNUmed 
  26  if __name__ == '__main__': 
  27          logging.basicConfig(level = logging.DEBUG) 
  28          sys.path.insert(0, '../../') 
  29  from Gnumed.pycommon import gmExceptions 
  30  from Gnumed.pycommon import gmDispatcher 
  31  from Gnumed.pycommon import gmBorg 
  32  from Gnumed.pycommon import gmI18N 
  33  if __name__ == '__main__': 
  34          gmI18N.activate_locale() 
  35          gmI18N.install_domain() 
  36  from Gnumed.pycommon import gmNull 
  37  from Gnumed.pycommon import gmBusinessDBObject 
  38  from Gnumed.pycommon import gmTools 
  39  from Gnumed.pycommon import gmPG2 
  40  from Gnumed.pycommon import gmDateTime 
  41  from Gnumed.pycommon import gmMatchProvider 
  42  from Gnumed.pycommon import gmLog2 
  43  from Gnumed.pycommon import gmHooks 
  44   
  45  from Gnumed.business import gmDemographicRecord 
  46  from Gnumed.business import gmClinicalRecord 
  47  from Gnumed.business import gmXdtMappings 
  48  from Gnumed.business import gmProviderInbox 
  49  from Gnumed.business import gmExportArea 
  50  from Gnumed.business import gmBilling 
  51  from Gnumed.business import gmAutoHints 
  52  from Gnumed.business.gmDocuments import cDocumentFolder 
  53   
  54   
  55  _log = logging.getLogger('gm.person') 
  56   
  57  __gender_list = None 
  58  __gender_idx = None 
  59   
  60  __gender2salutation_map = None 
  61  __gender2string_map = None 
  62   
  63  #============================================================ 
  64  _MERGE_SCRIPT_HEADER = """-- GNUmed patient merge script 
  65  -- created: %(date)s 
  66  -- patient to keep : #%(pat2keep)s 
  67  -- patient to merge: #%(pat2del)s 
  68  -- 
  69  -- You can EASILY cause mangled data by uncritically applying this script, so ... 
  70  -- ... BE POSITIVELY SURE YOU UNDERSTAND THE FULL EXTENT OF WHAT IT DOES ! 
  71   
  72   
  73  --set default_transaction_read_only to off; 
  74   
  75  BEGIN; 
  76  """ 
  77   
  78  #============================================================ 
79 -def external_id_exists(pk_issuer, value):
80 cmd = 'SELECT COUNT(1) FROM dem.lnk_identity2ext_id WHERE fk_origin = %(issuer)s AND external_id = %(val)s' 81 args = {'issuer': pk_issuer, 'val': value} 82 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 83 return rows[0][0]
84 85 #============================================================
86 -def person_exists(lastnames, dob, firstnames=None, active_only=True):
87 args = { 88 'last': lastnames, 89 'dob': dob 90 } 91 where_parts = [ 92 "lastnames = %(last)s", 93 "dem.date_trunc_utc('day', dob) = dem.date_trunc_utc('day', %(dob)s)" 94 ] 95 if firstnames is not None: 96 if firstnames.strip() != '': 97 #where_parts.append(u"position(%(first)s in firstnames) = 1") 98 where_parts.append("firstnames ~* %(first)s") 99 args['first'] = '\\m' + firstnames 100 if active_only: 101 cmd = """SELECT COUNT(1) FROM dem.v_active_persons WHERE %s""" % ' AND '.join(where_parts) 102 else: 103 cmd = """SELECT COUNT(1) FROM dem.v_all_persons WHERE %s""" % ' AND '.join(where_parts) 104 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 105 return rows[0][0]
106 107 #============================================================ 108 # FIXME: make this work as a mapping type, too
109 -class cDTO_person(object):
110
111 - def __init__(self):
112 self.identity = None 113 self.external_ids = [] 114 self.comm_channels = [] 115 self.addresses = [] 116 117 self.firstnames = None 118 self.lastnames = None 119 self.title = None 120 self.gender = None 121 self.dob = None 122 self.dob_is_estimated = False 123 self.source = self.__class__.__name__
124 #-------------------------------------------------------- 125 # external API 126 #--------------------------------------------------------
127 - def keys(self):
128 return 'firstnames lastnames dob gender title'.split()
129 #--------------------------------------------------------
130 - def delete_from_source(self):
131 pass
132 #--------------------------------------------------------
133 - def is_unique(self):
134 where_snippets = [ 135 'firstnames = %(first)s', 136 'lastnames = %(last)s' 137 ] 138 args = { 139 'first': self.firstnames, 140 'last': self.lastnames 141 } 142 if self.dob is not None: 143 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)") 144 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59) 145 if self.gender is not None: 146 where_snippets.append('gender = %(sex)s') 147 args['sex'] = self.gender 148 cmd = 'SELECT count(1) FROM dem.v_person_names WHERE %s' % ' AND '.join(where_snippets) 149 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 150 151 return rows[0][0] == 1
152 153 is_unique = property(is_unique, lambda x:x) 154 #--------------------------------------------------------
155 - def exists(self):
156 where_snippets = [ 157 'firstnames = %(first)s', 158 'lastnames = %(last)s' 159 ] 160 args = { 161 'first': self.firstnames, 162 'last': self.lastnames 163 } 164 if self.dob is not None: 165 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)") 166 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59) 167 if self.gender is not None: 168 where_snippets.append('gender = %(sex)s') 169 args['sex'] = self.gender 170 cmd = 'SELECT count(1) FROM dem.v_person_names WHERE %s' % ' AND '.join(where_snippets) 171 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 172 173 return rows[0][0] > 0
174 175 exists = property(exists, lambda x:x) 176 #--------------------------------------------------------
177 - def get_candidate_identities(self, can_create=False):
178 """Generate generic queries. 179 180 - not locale dependant 181 - data -> firstnames, lastnames, dob, gender 182 183 shall we mogrify name parts ? probably not as external 184 sources should know what they do 185 186 finds by inactive name, too, but then shows 187 the corresponding active name ;-) 188 189 Returns list of matching identities (may be empty) 190 or None if it was told to create an identity but couldn't. 191 """ 192 where_snippets = [] 193 args = {} 194 195 where_snippets.append('lower(firstnames) = lower(%(first)s)') 196 args['first'] = self.firstnames 197 198 where_snippets.append('lower(lastnames) = lower(%(last)s)') 199 args['last'] = self.lastnames 200 201 if self.dob is not None: 202 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)") 203 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59) 204 205 if self.gender is not None: 206 where_snippets.append('lower(gender) = lower(%(sex)s)') 207 args['sex'] = self.gender 208 209 # FIXME: allow disabled persons ? 210 cmd = """ 211 SELECT *, '%s' AS match_type 212 FROM dem.v_active_persons 213 WHERE 214 pk_identity IN ( 215 SELECT pk_identity FROM dem.v_person_names WHERE %s 216 ) 217 ORDER BY lastnames, firstnames, dob""" % ( 218 _('external patient source (name, gender, date of birth)'), 219 ' AND '.join(where_snippets) 220 ) 221 222 try: 223 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True) 224 except: 225 _log.error('cannot get candidate identities for dto "%s"' % self) 226 _log.exception('query %s' % cmd) 227 rows = [] 228 229 if len(rows) == 0: 230 _log.debug('no candidate identity matches found') 231 if not can_create: 232 return [] 233 ident = self.import_into_database() 234 if ident is None: 235 return None 236 identities = [ident] 237 else: 238 identities = [ cPerson(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ] 239 240 return identities
241 #--------------------------------------------------------
242 - def import_into_database(self):
243 """Imports self into the database.""" 244 245 self.identity = create_identity ( 246 firstnames = self.firstnames, 247 lastnames = self.lastnames, 248 gender = self.gender, 249 dob = self.dob 250 ) 251 252 if self.identity is None: 253 return None 254 255 if self.dob_is_estimated: 256 self.identity['dob_is_estimated'] = True 257 if self.title is not None: 258 self.identity['title'] = self.title 259 self.identity.save() 260 261 for ext_id in self.external_ids: 262 try: 263 self.identity.add_external_id ( 264 type_name = ext_id['name'], 265 value = ext_id['value'], 266 issuer = ext_id['issuer'], 267 comment = ext_id['comment'] 268 ) 269 except Exception: 270 _log.exception('cannot import <external ID> from external data source') 271 gmLog2.log_stack_trace() 272 273 for comm in self.comm_channels: 274 try: 275 self.identity.link_comm_channel ( 276 comm_medium = comm['channel'], 277 url = comm['url'] 278 ) 279 except Exception: 280 _log.exception('cannot import <comm channel> from external data source') 281 gmLog2.log_stack_trace() 282 283 for adr in self.addresses: 284 try: 285 self.identity.link_address ( 286 adr_type = adr['type'], 287 number = adr['number'], 288 subunit = adr['subunit'], 289 street = adr['street'], 290 postcode = adr['zip'], 291 urb = adr['urb'], 292 region_code = adr['region_code'], 293 country_code = adr['country_code'] 294 ) 295 except Exception: 296 _log.exception('cannot import <address> from external data source') 297 gmLog2.log_stack_trace() 298 299 return self.identity
300 #--------------------------------------------------------
301 - def import_extra_data(self, *args, **kwargs):
302 pass
303 #--------------------------------------------------------
304 - def remember_external_id(self, name=None, value=None, issuer=None, comment=None):
305 value = value.strip() 306 if value == '': 307 return 308 name = name.strip() 309 if name == '': 310 raise ValueError(_('<name> cannot be empty')) 311 issuer = issuer.strip() 312 if issuer == '': 313 raise ValueError(_('<issuer> cannot be empty')) 314 self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
315 #--------------------------------------------------------
316 - def remember_comm_channel(self, channel=None, url=None):
317 url = url.strip() 318 if url == '': 319 return 320 channel = channel.strip() 321 if channel == '': 322 raise ValueError(_('<channel> cannot be empty')) 323 self.comm_channels.append({'channel': channel, 'url': url})
324 #--------------------------------------------------------
325 - def remember_address(self, number=None, street=None, urb=None, region_code=None, zip=None, country_code=None, adr_type=None, subunit=None):
326 number = number.strip() 327 if number == '': 328 raise ValueError(_('<number> cannot be empty')) 329 street = street.strip() 330 if street == '': 331 raise ValueError(_('<street> cannot be empty')) 332 urb = urb.strip() 333 if urb == '': 334 raise ValueError(_('<urb> cannot be empty')) 335 zip = zip.strip() 336 if zip == '': 337 raise ValueError(_('<zip> cannot be empty')) 338 country_code = country_code.strip() 339 if country_code == '': 340 raise ValueError(_('<country_code> cannot be empty')) 341 if region_code is not None: 342 region_code = region_code.strip() 343 if region_code in [None, '']: 344 region_code = '??' 345 self.addresses.append ({ 346 'type': adr_type, 347 'number': number, 348 'subunit': subunit, 349 'street': street, 350 'zip': zip, 351 'urb': urb, 352 'region_code': region_code, 353 'country_code': country_code 354 })
355 #-------------------------------------------------------- 356 # customizing behaviour 357 #--------------------------------------------------------
358 - def __str__(self):
359 return '<%s (%s) @ %s: %s %s (%s) %s>' % ( 360 self.__class__.__name__, 361 self.source, 362 id(self), 363 self.firstnames, 364 self.lastnames, 365 self.gender, 366 self.dob 367 )
368 #--------------------------------------------------------
369 - def __setattr__(self, attr, val):
370 """Do some sanity checks on self.* access.""" 371 372 if attr == 'gender': 373 if val is None: 374 object.__setattr__(self, attr, val) 375 return 376 glist, idx = get_gender_list() 377 for gender in glist: 378 if str(val) in [gender[0], gender[1], gender[2], gender[3]]: 379 val = gender[idx['tag']] 380 object.__setattr__(self, attr, val) 381 return 382 raise ValueError('invalid gender: [%s]' % val) 383 384 if attr == 'dob': 385 if val is not None: 386 if not isinstance(val, pyDT.datetime): 387 raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val)) 388 if val.tzinfo is None: 389 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat()) 390 391 object.__setattr__(self, attr, val) 392 return
393 #--------------------------------------------------------
394 - def __getitem__(self, attr):
395 return getattr(self, attr)
396 397 #============================================================
398 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
399 _cmd_fetch_payload = "SELECT * FROM dem.v_person_names WHERE pk_name = %s" 400 _cmds_store_payload = [ 401 """UPDATE dem.names SET 402 active = FALSE 403 WHERE 404 %(active_name)s IS TRUE -- act only when needed and only 405 AND 406 id_identity = %(pk_identity)s -- on names of this identity 407 AND 408 active IS TRUE -- which are active 409 AND 410 id != %(pk_name)s -- but NOT *this* name 411 """, 412 """update dem.names set 413 active = %(active_name)s, 414 preferred = %(preferred)s, 415 comment = %(comment)s 416 where 417 id = %(pk_name)s and 418 id_identity = %(pk_identity)s and -- belt and suspenders 419 xmin = %(xmin_name)s""", 420 """select xmin as xmin_name from dem.names where id = %(pk_name)s""" 421 ] 422 _updatable_fields = ['active_name', 'preferred', 'comment'] 423 #--------------------------------------------------------
424 - def __setitem__(self, attribute, value):
425 if attribute == 'active_name': 426 # cannot *directly* deactivate a name, only indirectly 427 # by activating another one 428 # FIXME: should be done at DB level 429 if self._payload[self._idx['active_name']] is True: 430 return 431 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
432 #--------------------------------------------------------
433 - def _get_description(self):
434 return '%(last)s, %(title)s %(first)s%(nick)s' % { 435 'last': self._payload[self._idx['lastnames']], 436 'title': gmTools.coalesce ( 437 self._payload[self._idx['title']], 438 map_gender2salutation(self._payload[self._idx['gender']]) 439 ), 440 'first': self._payload[self._idx['firstnames']], 441 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'", '%s') 442 }
443 444 description = property(_get_description, lambda x:x)
445 446 #============================================================ 447 _SQL_get_active_person = "SELECT * FROM dem.v_active_persons WHERE pk_identity = %s" 448 _SQL_get_any_person = "SELECT * FROM dem.v_all_persons WHERE pk_identity = %s" 449
450 -class cPerson(gmBusinessDBObject.cBusinessDBObject):
451 _cmd_fetch_payload = _SQL_get_any_person 452 _cmds_store_payload = [ 453 """UPDATE dem.identity SET 454 gender = %(gender)s, 455 dob = %(dob)s, 456 dob_is_estimated = %(dob_is_estimated)s, 457 tob = %(tob)s, 458 title = gm.nullify_empty_string(%(title)s), 459 fk_marital_status = %(pk_marital_status)s, 460 deceased = %(deceased)s, 461 emergency_contact = gm.nullify_empty_string(%(emergency_contact)s), 462 fk_emergency_contact = %(pk_emergency_contact)s, 463 fk_primary_provider = %(pk_primary_provider)s, 464 comment = gm.nullify_empty_string(%(comment)s) 465 WHERE 466 pk = %(pk_identity)s and 467 xmin = %(xmin_identity)s 468 RETURNING 469 xmin AS xmin_identity""" 470 ] 471 _updatable_fields = [ 472 "title", 473 "dob", 474 "tob", 475 "gender", 476 "pk_marital_status", 477 'deceased', 478 'emergency_contact', 479 'pk_emergency_contact', 480 'pk_primary_provider', 481 'comment', 482 'dob_is_estimated' 483 ] 484 #--------------------------------------------------------
485 - def _get_ID(self):
486 return self._payload[self._idx['pk_identity']]
487 - def _set_ID(self, value):
488 raise AttributeError('setting ID of identity is not allowed')
489 490 ID = property(_get_ID, _set_ID) 491 492 #--------------------------------------------------------
493 - def __setitem__(self, attribute, value):
494 495 if attribute == 'dob': 496 if value is not None: 497 498 if isinstance(value, pyDT.datetime): 499 if value.tzinfo is None: 500 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat()) 501 else: 502 raise TypeError('[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value)) 503 504 # compare DOB at seconds level 505 if self._payload[self._idx['dob']] is not None: 506 old_dob = gmDateTime.pydt_strftime ( 507 self._payload[self._idx['dob']], 508 format = '%Y %m %d %H %M %S', 509 accuracy = gmDateTime.acc_seconds 510 ) 511 new_dob = gmDateTime.pydt_strftime ( 512 value, 513 format = '%Y %m %d %H %M %S', 514 accuracy = gmDateTime.acc_seconds 515 ) 516 if new_dob == old_dob: 517 return 518 519 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
520 521 #--------------------------------------------------------
522 - def cleanup(self):
523 pass
524 525 #--------------------------------------------------------
526 - def _get_is_patient(self):
527 return identity_is_patient(self._payload[self._idx['pk_identity']])
528
529 - def _set_is_patient(self, turn_into_patient):
530 if turn_into_patient: 531 return turn_identity_into_patient(self._payload[self._idx['pk_identity']]) 532 return False
533 534 is_patient = property(_get_is_patient, _set_is_patient) 535 536 #--------------------------------------------------------
537 - def _get_as_patient(self):
538 return cPatient(self._payload[self._idx['pk_identity']])
539 540 as_patient = property(_get_as_patient, lambda x:x) 541 542 #--------------------------------------------------------
543 - def _get_staff_id(self):
544 cmd = "SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s" 545 args = {'pk': self._payload[self._idx['pk_identity']]} 546 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 547 if len(rows) == 0: 548 return None 549 return rows[0][0]
550 551 staff_id = property(_get_staff_id, lambda x:x) 552 553 #-------------------------------------------------------- 554 # identity API 555 #--------------------------------------------------------
556 - def _get_gender_symbol(self):
557 return map_gender2symbol[self._payload[self._idx['gender']]]
558 559 gender_symbol = property(_get_gender_symbol, lambda x:x) 560 #--------------------------------------------------------
561 - def _get_gender_string(self):
562 return map_gender2string(gender = self._payload[self._idx['gender']])
563 564 gender_string = property(_get_gender_string, lambda x:x) 565 #--------------------------------------------------------
566 - def _get_gender_list(self):
567 gender_list, tmp = get_gender_list() 568 return gender_list
569 570 gender_list = property(_get_gender_list, lambda x:x) 571 #--------------------------------------------------------
572 - def get_active_name(self):
573 names = self.get_names(active_only = True) 574 if len(names) == 0: 575 _log.error('cannot retrieve active name for patient [%s]', self._payload[self._idx['pk_identity']]) 576 return None 577 return names[0]
578 579 active_name = property(get_active_name, lambda x:x) 580 #--------------------------------------------------------
581 - def get_names(self, active_only=False, exclude_active=False):
582 583 args = {'pk_pat': self._payload[self._idx['pk_identity']]} 584 where_parts = ['pk_identity = %(pk_pat)s'] 585 if active_only: 586 where_parts.append('active_name is True') 587 if exclude_active: 588 where_parts.append('active_name is False') 589 cmd = """ 590 SELECT * 591 FROM dem.v_person_names 592 WHERE %s 593 ORDER BY active_name DESC, lastnames, firstnames 594 """ % ' AND '.join(where_parts) 595 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 596 597 if len(rows) == 0: 598 # no names registered for patient 599 return [] 600 601 names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ] 602 return names
603 #--------------------------------------------------------
604 - def get_description_gender(self, with_nickname=True):
605 if with_nickname: 606 template = _('%(last)s,%(title)s %(first)s%(nick)s (%(sex)s)') 607 else: 608 template = _('%(last)s,%(title)s %(first)s (%(sex)s)') 609 return template % { 610 'last': self._payload[self._idx['lastnames']], 611 'title': gmTools.coalesce(self._payload[self._idx['title']], '', ' %s'), 612 'first': self._payload[self._idx['firstnames']], 613 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'"), 614 'sex': self.gender_symbol 615 }
616 617 #--------------------------------------------------------
618 - def get_description(self, with_nickname=True):
619 if with_nickname: 620 template = _('%(last)s,%(title)s %(first)s%(nick)s') 621 else: 622 template = _('%(last)s,%(title)s %(first)s') 623 return template % { 624 'last': self._payload[self._idx['lastnames']], 625 'title': gmTools.coalesce(self._payload[self._idx['title']], '', ' %s'), 626 'first': self._payload[self._idx['firstnames']], 627 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'") 628 }
629 630 #--------------------------------------------------------
631 - def add_name(self, firstnames, lastnames, active=True):
632 """Add a name. 633 634 @param firstnames The first names. 635 @param lastnames The last names. 636 @param active When True, the new name will become the active one (hence setting other names to inactive) 637 @type active A bool instance 638 """ 639 name = create_name(self.ID, firstnames, lastnames, active) 640 if active: 641 self.refetch_payload() 642 return name
643 644 #--------------------------------------------------------
645 - def delete_name(self, name=None):
646 cmd = "delete from dem.names where id = %(name)s and id_identity = %(pat)s" 647 args = {'name': name['pk_name'], 'pat': self.ID} 648 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
649 # can't have been the active name as that would raise an 650 # exception (since no active name would be left) so no 651 # data refetch needed 652 653 #--------------------------------------------------------
654 - def set_nickname(self, nickname=None):
655 """ 656 Set the nickname. Setting the nickname only makes sense for the currently 657 active name. 658 @param nickname The preferred/nick/warrior name to set. 659 """ 660 if self._payload[self._idx['preferred']] == nickname: 661 return True 662 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': "SELECT dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}]) 663 # setting nickname doesn't change dem.identity, so other fields 664 # of dem.v_active_persons do not get changed as a consequence of 665 # setting the nickname, hence locally setting nickname matches 666 # in-database reality 667 self._payload[self._idx['preferred']] = nickname 668 #self.refetch_payload() 669 return True
670 671 #--------------------------------------------------------
672 - def get_tags(self, order_by=None):
673 if order_by is None: 674 order_by = '' 675 else: 676 order_by = 'ORDER BY %s' % order_by 677 678 cmd = gmDemographicRecord._SQL_get_person_tags % ('pk_identity = %%(pat)s %s' % order_by) 679 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': self.ID}}], get_col_idx = True) 680 681 return [ gmDemographicRecord.cPersonTag(row = {'data': r, 'idx': idx, 'pk_field': 'pk_identity_tag'}) for r in rows ]
682 683 tags = property(get_tags, lambda x:x) 684 685 #--------------------------------------------------------
686 - def add_tag(self, tag):
687 args = { 688 'tag': tag, 689 'identity': self.ID 690 } 691 692 # already exists ? 693 cmd = "SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s" 694 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 695 if len(rows) > 0: 696 return gmDemographicRecord.cPersonTag(aPK_obj = rows[0]['pk']) 697 698 # no, add 699 cmd = """ 700 INSERT INTO dem.identity_tag ( 701 fk_tag, 702 fk_identity 703 ) VALUES ( 704 %(tag)s, 705 %(identity)s 706 ) 707 RETURNING pk 708 """ 709 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False) 710 return gmDemographicRecord.cPersonTag(aPK_obj = rows[0]['pk'])
711 712 #--------------------------------------------------------
713 - def remove_tag(self, tag):
714 cmd = "DELETE FROM dem.identity_tag WHERE pk = %(pk)s" 715 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
716 717 #-------------------------------------------------------- 718 # external ID API 719 # 720 # since external IDs are not treated as first class 721 # citizens (classes in their own right, that is), we 722 # handle them *entirely* within cPerson, also they 723 # only make sense with one single person (like names) 724 # and are not reused (like addresses), so they are 725 # truly added/deleted, not just linked/unlinked 726 #--------------------------------------------------------
727 - def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
728 """Adds an external ID to the patient. 729 730 creates ID type if necessary 731 """ 732 # check for existing ID 733 if pk_type is not None: 734 cmd = """ 735 select * from dem.v_external_ids4identity where 736 pk_identity = %(pat)s and 737 pk_type = %(pk_type)s and 738 value = %(val)s""" 739 else: 740 # by type/value/issuer 741 if issuer is None: 742 cmd = """ 743 select * from dem.v_external_ids4identity where 744 pk_identity = %(pat)s and 745 name = %(name)s and 746 value = %(val)s""" 747 else: 748 cmd = """ 749 select * from dem.v_external_ids4identity where 750 pk_identity = %(pat)s and 751 name = %(name)s and 752 value = %(val)s and 753 issuer = %(issuer)s""" 754 args = { 755 'pat': self.ID, 756 'name': type_name, 757 'val': value, 758 'issuer': issuer, 759 'pk_type': pk_type 760 } 761 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 762 763 # create new ID if not found 764 if len(rows) == 0: 765 766 args = { 767 'pat': self.ID, 768 'val': value, 769 'type_name': type_name, 770 'pk_type': pk_type, 771 'issuer': issuer, 772 'comment': comment 773 } 774 775 if pk_type is None: 776 cmd = """insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values ( 777 %(val)s, 778 (select dem.add_external_id_type(%(type_name)s, %(issuer)s)), 779 %(comment)s, 780 %(pat)s 781 )""" 782 else: 783 cmd = """insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values ( 784 %(val)s, 785 %(pk_type)s, 786 %(comment)s, 787 %(pat)s 788 )""" 789 790 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}]) 791 792 # or update comment of existing ID 793 else: 794 row = rows[0] 795 if comment is not None: 796 # comment not already there ? 797 if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1: 798 comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip) 799 cmd = "update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s" 800 args = {'comment': comment, 'pk': row['pk_id']} 801 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
802 803 #--------------------------------------------------------
804 - def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
805 """Edits an existing external ID. 806 807 Creates ID type if necessary. 808 """ 809 cmd = """ 810 UPDATE dem.lnk_identity2ext_id SET 811 fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)), 812 external_id = %(value)s, 813 comment = gm.nullify_empty_string(%(comment)s) 814 WHERE 815 id = %(pk)s 816 """ 817 args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment} 818 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
819 820 #--------------------------------------------------------
821 - def get_external_ids(self, id_type=None, issuer=None):
822 where_parts = ['pk_identity = %(pat)s'] 823 args = {'pat': self.ID} 824 825 if id_type is not None: 826 where_parts.append('name = %(name)s') 827 args['name'] = id_type.strip() 828 829 if issuer is not None: 830 where_parts.append('issuer = %(issuer)s') 831 args['issuer'] = issuer.strip() 832 833 cmd = "SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts) 834 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 835 836 return rows
837 838 external_ids = property(get_external_ids, lambda x:x) 839 840 #--------------------------------------------------------
841 - def delete_external_id(self, pk_ext_id=None):
842 cmd = """ 843 DELETE FROM dem.lnk_identity2ext_id 844 WHERE id_identity = %(pat)s AND id = %(pk)s""" 845 args = {'pat': self.ID, 'pk': pk_ext_id} 846 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
847 848 #--------------------------------------------------------
849 - def suggest_external_id(self, target=None, encoding=None):
850 name = self.active_name 851 last = ' '.join(p for p in name['lastnames'].split("-")) 852 last = ' '.join(p for p in last.split(".")) 853 last = ' '.join(p for p in last.split("'")) 854 last = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in last.split(' ')) 855 first = ' '.join(p for p in name['firstnames'].split("-")) 856 first = ' '.join(p for p in first.split(".")) 857 first = ' '.join(p for p in first.split("'")) 858 first = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in first.split(' ')) 859 suggestion = 'GMd-%s%s%s%s%s' % ( 860 gmTools.coalesce(target, '', '%s-'), 861 last, 862 first, 863 self.get_formatted_dob(format = '-%Y%m%d', none_string = ''), 864 gmTools.coalesce(self['gender'], '', '-%s') 865 ) 866 try: 867 import unidecode 868 return unidecode.unidecode(suggestion) 869 except ImportError: 870 _log.debug('cannot transliterate external ID suggestion, <unidecode> module not installed') 871 if encoding is None: 872 return suggestion 873 return suggestion.encode(encoding)
874 875 external_id_suggestion = property(suggest_external_id, lambda x:x) 876 877 #--------------------------------------------------------
878 - def suggest_external_ids(self, target=None, encoding=None):
879 names2use = [self.active_name] 880 names2use.extend(self.get_names(active_only = False, exclude_active = True)) 881 target = gmTools.coalesce(target, '', '%s-') 882 dob = self.get_formatted_dob(format = '-%Y%m%d', none_string = '') 883 gender = gmTools.coalesce(self['gender'], '', '-%s') 884 suggestions = [] 885 for name in names2use: 886 last = ' '.join(p for p in name['lastnames'].split("-")) 887 last = ' '.join(p for p in last.split(".")) 888 last = ' '.join(p for p in last.split("'")) 889 last = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in last.split(' ')) 890 first = ' '.join(p for p in name['firstnames'].split("-")) 891 first = ' '.join(p for p in first.split(".")) 892 first = ' '.join(p for p in first.split("'")) 893 first = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in first.split(' ')) 894 suggestion = 'GMd-%s%s%s%s%s' % (target, last, first, dob, gender) 895 try: 896 import unidecode 897 suggestions.append(unidecode.unidecode(suggestion)) 898 continue 899 except ImportError: 900 _log.debug('cannot transliterate external ID suggestion, <unidecode> module not installed') 901 if encoding is None: 902 suggestions.append(suggestion) 903 else: 904 suggestions.append(suggestion.encode(encoding)) 905 return suggestions
906 907 #-------------------------------------------------------- 908 #--------------------------------------------------------
909 - def assimilate_identity(self, other_identity=None, link_obj=None):
910 """Merge another identity into this one. 911 912 Keep this one. Delete other one.""" 913 914 if other_identity.ID == self.ID: 915 return True, None 916 917 curr_pat = gmCurrentPatient() 918 if curr_pat.connected: 919 if other_identity.ID == curr_pat.ID: 920 return False, _('Cannot merge active patient into another patient.') 921 922 now_here = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here()) 923 distinguisher = _('merge of #%s into #%s @ %s') % (other_identity.ID, self.ID, now_here) 924 925 queries = [] 926 args = {'pat2del': other_identity.ID, 'pat2keep': self.ID} 927 928 # merge allergy state 929 queries.append ({ 930 'cmd': """ 931 UPDATE clin.allergy_state SET 932 has_allergy = greatest ( 933 (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s), 934 (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s) 935 ), 936 -- perhaps use least() to play it safe and make it appear longer ago than it might have been, actually ? 937 last_confirmed = greatest ( 938 (SELECT last_confirmed FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s), 939 (SELECT last_confirmed FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s) 940 ) 941 WHERE 942 pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s) 943 """, 944 'args': args 945 }) 946 # delete old allergy state 947 queries.append ({ 948 'cmd': 'DELETE FROM clin.allergy_state WHERE pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s)', 949 'args': args 950 }) 951 952 # merge patient proxy 953 queries.append ({ 954 'cmd': """ 955 UPDATE clin.patient SET 956 edc = coalesce ( 957 edc, 958 (SELECT edc FROM clin.patient WHERE fk_identity = %(pat2del)s) 959 ) 960 WHERE 961 fk_identity = %(pat2keep)s 962 """, 963 'args': args 964 }) 965 966 # transfer names 967 # 1) disambiguate names in old patient 968 queries.append ({ 969 'cmd': """ 970 UPDATE dem.names d_n1 SET 971 lastnames = lastnames || coalesce ( 972 ' (from "' || (SELECT comment FROM dem.identity WHERE pk = %%(pat2del)s) || '")', 973 ' (%s)' 974 ) 975 WHERE 976 d_n1.id_identity = %%(pat2del)s 977 AND 978 EXISTS ( 979 SELECT 1 FROM dem.names d_n2 980 WHERE 981 d_n2.id_identity = %%(pat2keep)s 982 AND 983 d_n2.lastnames = d_n1.lastnames 984 AND 985 d_n2.firstnames = d_n1.firstnames 986 )""" % distinguisher, 987 'args': args 988 }) 989 # 2) move inactive ones (dupes are expected to have been eliminated in step 1 above) 990 queries.append ({ 991 'cmd': """ 992 UPDATE dem.names SET 993 id_identity = %%(pat2keep)s, 994 comment = coalesce(comment || ' (%s)', '%s') 995 WHERE 996 id_identity = %%(pat2del)s 997 AND 998 active IS false 999 """ % (distinguisher, distinguisher), 1000 'args': args 1001 }) 1002 # 3) copy active name (because each identity MUST have at most one 1003 # *active* name so we can't just UPDATE over to pat2keep), 1004 # # also, needs de-duplication or else it would conflict with *itself* 1005 # # on pat2keep, said de-duplication happened in step 0 above 1006 # # whereby pat2del is made unique by means of adding a pseudo-random 1007 # # dem.identity.comment 1008 queries.append ({ 1009 'cmd': """ 1010 INSERT INTO dem.names ( 1011 id_identity, active, lastnames, firstnames, preferred, comment 1012 ) 1013 SELECT 1014 %%(pat2keep)s, false, lastnames, firstnames, preferred, coalesce(comment || ' (%s)', '%s') 1015 FROM dem.names d_n 1016 WHERE 1017 d_n.id_identity = %%(pat2del)s 1018 AND 1019 d_n.active IS true 1020 """ % (distinguisher, distinguisher), 1021 'args': args 1022 }) 1023 1024 # disambiguate potential dupes 1025 # - same-url comm channels 1026 queries.append ({ 1027 'cmd': """ 1028 UPDATE dem.lnk_identity2comm 1029 SET url = url || ' (%s)' 1030 WHERE 1031 fk_identity = %%(pat2del)s 1032 AND 1033 EXISTS ( 1034 SELECT 1 FROM dem.lnk_identity2comm d_li2c 1035 WHERE d_li2c.fk_identity = %%(pat2keep)s AND d_li2c.url = url 1036 ) 1037 """ % distinguisher, 1038 'args': args 1039 }) 1040 # - same-value external IDs 1041 queries.append ({ 1042 'cmd': """ 1043 UPDATE dem.lnk_identity2ext_id 1044 SET external_id = external_id || ' (%s)' 1045 WHERE 1046 id_identity = %%(pat2del)s 1047 AND 1048 EXISTS ( 1049 SELECT 1 FROM dem.lnk_identity2ext_id d_li2e 1050 WHERE 1051 d_li2e.id_identity = %%(pat2keep)s 1052 AND 1053 d_li2e.external_id = external_id 1054 AND 1055 d_li2e.fk_origin = fk_origin 1056 ) 1057 """ % distinguisher, 1058 'args': args 1059 }) 1060 # - same addresses 1061 queries.append ({ 1062 'cmd': """ 1063 DELETE FROM dem.lnk_person_org_address 1064 WHERE 1065 id_identity = %(pat2del)s 1066 AND 1067 id_address IN ( 1068 SELECT id_address FROM dem.lnk_person_org_address d_lpoa 1069 WHERE d_lpoa.id_identity = %(pat2keep)s 1070 ) 1071 """, 1072 'args': args 1073 }) 1074 1075 # find FKs pointing to dem.identity.pk 1076 FKs = gmPG2.get_foreign_keys2column ( 1077 schema = 'dem', 1078 table = 'identity', 1079 column = 'pk' 1080 ) 1081 # find FKs pointing to clin.patient.fk_identity 1082 FKs.extend (gmPG2.get_foreign_keys2column ( 1083 schema = 'clin', 1084 table = 'patient', 1085 column = 'fk_identity' 1086 )) 1087 1088 # generate UPDATEs 1089 cmd_template = 'UPDATE %s SET %s = %%(pat2keep)s WHERE %s = %%(pat2del)s' 1090 for FK in FKs: 1091 if FK['referencing_table'] in ['dem.names', 'clin.patient']: 1092 continue 1093 queries.append ({ 1094 'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']), 1095 'args': args 1096 }) 1097 1098 # delete old patient proxy 1099 queries.append ({ 1100 'cmd': 'DELETE FROM clin.patient WHERE fk_identity = %(pat2del)s', 1101 'args': args 1102 }) 1103 1104 # remove old identity entry 1105 queries.append ({ 1106 'cmd': 'delete from dem.identity where pk = %(pat2del)s', 1107 'args': args 1108 }) 1109 1110 script_name = gmTools.get_unique_filename(prefix = 'gm-assimilate-%(pat2del)s-into-%(pat2keep)s-' % args, suffix = '.sql') 1111 _log.warning('identity [%s] is about to assimilate identity [%s], SQL script [%s]', self.ID, other_identity.ID, script_name) 1112 1113 script = io.open(script_name, 'wt') 1114 args['date'] = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%Y %B %d %H:%M') 1115 script.write(_MERGE_SCRIPT_HEADER % args) 1116 for query in queries: 1117 script.write(query['cmd'] % args) 1118 script.write(';\n') 1119 script.write('\nROLLBACK;\n') 1120 script.write('--COMMIT;\n') 1121 script.close() 1122 1123 gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True) 1124 1125 self.add_external_id ( 1126 type_name = 'merged GNUmed identity primary key', 1127 value = 'GNUmed::pk::%s' % other_identity.ID, 1128 issuer = 'GNUmed' 1129 ) 1130 1131 return True, None
1132 1133 #-------------------------------------------------------- 1134 #--------------------------------------------------------
1135 - def put_on_waiting_list(self, urgency=0, comment=None, zone=None):
1136 cmd = """ 1137 insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position) 1138 values ( 1139 %(pat)s, 1140 %(urg)s, 1141 %(cmt)s, 1142 %(area)s, 1143 (select coalesce((max(list_position) + 1), 1) from clin.waiting_list) 1144 )""" 1145 args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone} 1146 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose = True)
1147 #--------------------------------------------------------
1148 - def get_waiting_list_entry(self):
1149 cmd = """SELECT * FROM clin.v_waiting_list WHERE pk_identity = %(pat)s""" 1150 args = {'pat': self.ID} 1151 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 1152 return rows
1153 1154 waiting_list_entries = property(get_waiting_list_entry, lambda x:x) 1155 #--------------------------------------------------------
1156 - def _get_export_area(self):
1157 return gmExportArea.cExportArea(self.ID)
1158 1159 export_area = property(_get_export_area, lambda x:x) 1160 #--------------------------------------------------------
1161 - def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
1162 1163 template = '%s%s%s\r\n' 1164 1165 if filename is None: 1166 filename = gmTools.get_unique_filename ( 1167 prefix = 'gm-patient-', 1168 suffix = '.gdt' 1169 ) 1170 1171 gdt_file = io.open(filename, mode = 'wt', encoding = encoding, errors = 'strict') 1172 1173 gdt_file.write(template % ('013', '8000', '6301')) 1174 gdt_file.write(template % ('013', '9218', '2.10')) 1175 if external_id_type is None: 1176 gdt_file.write(template % ('%03d' % (9 + len(str(self.ID))), '3000', self.ID)) 1177 else: 1178 ext_ids = self.get_external_ids(id_type = external_id_type) 1179 if len(ext_ids) > 0: 1180 gdt_file.write(template % ('%03d' % (9 + len(ext_ids[0]['value'])), '3000', ext_ids[0]['value'])) 1181 gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['lastnames']])), '3101', self._payload[self._idx['lastnames']])) 1182 gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['firstnames']])), '3102', self._payload[self._idx['firstnames']])) 1183 gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['dob']].strftime('%d%m%Y'))), '3103', self._payload[self._idx['dob']].strftime('%d%m%Y'))) 1184 gdt_file.write(template % ('010', '3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]])) 1185 gdt_file.write(template % ('025', '6330', 'GNUmed::9206::encoding')) 1186 gdt_file.write(template % ('%03d' % (9 + len(encoding)), '6331', encoding)) 1187 if external_id_type is None: 1188 gdt_file.write(template % ('029', '6332', 'GNUmed::3000::source')) 1189 gdt_file.write(template % ('017', '6333', 'internal')) 1190 else: 1191 if len(ext_ids) > 0: 1192 gdt_file.write(template % ('029', '6332', 'GNUmed::3000::source')) 1193 gdt_file.write(template % ('%03d' % (9 + len(external_id_type)), '6333', external_id_type)) 1194 1195 gdt_file.close() 1196 1197 return filename
1198 #--------------------------------------------------------
1199 - def export_as_xml_linuxmednews(self, filename=None):
1200 1201 if filename is None: 1202 filename = gmTools.get_unique_filename ( 1203 prefix = 'gm-LinuxMedNews_demographics-', 1204 suffix = '.xml' 1205 ) 1206 1207 dob_format = '%Y-%m-%d' 1208 pat = etree.Element('patient') 1209 1210 first = etree.SubElement(pat, 'firstname') 1211 first.text = gmTools.coalesce(self._payload[self._idx['firstnames']], '') 1212 1213 last = etree.SubElement(pat, 'lastname') 1214 last.text = gmTools.coalesce(self._payload[self._idx['lastnames']], '') 1215 1216 # privacy 1217 #middle = etree.SubElement(pat, u'middlename') 1218 #middle.set(u'comment', _('preferred name/call name/...')) 1219 #middle.text = gmTools.coalesce(self._payload[self._idx['preferred']], u'') 1220 1221 pref = etree.SubElement(pat, 'name_prefix') 1222 pref.text = gmTools.coalesce(self._payload[self._idx['title']], '') 1223 1224 suff = etree.SubElement(pat, 'name_suffix') 1225 suff.text = '' 1226 1227 dob = etree.SubElement(pat, 'DOB') 1228 dob.set('format', dob_format) 1229 dob.text = gmDateTime.pydt_strftime(self._payload[self._idx['dob']], dob_format, accuracy = gmDateTime.acc_days, none_str = '') 1230 1231 gender = etree.SubElement(pat, 'gender') 1232 gender.set('comment', self.gender_string) 1233 if self._payload[self._idx['gender']] is None: 1234 gender.text = '' 1235 else: 1236 gender.text = map_gender2mf[self._payload[self._idx['gender']]] 1237 1238 home = etree.SubElement(pat, 'home_address') 1239 adrs = self.get_addresses(address_type = 'home') 1240 if len(adrs) > 0: 1241 adr = adrs[0] 1242 city = etree.SubElement(home, 'city') 1243 city.set('comment', gmTools.coalesce(adr['suburb'], '')) 1244 city.text = gmTools.coalesce(adr['urb'], '') 1245 1246 region = etree.SubElement(home, 'region') 1247 region.set('comment', gmTools.coalesce(adr['l10n_region'], '')) 1248 region.text = gmTools.coalesce(adr['code_region'], '') 1249 1250 zipcode = etree.SubElement(home, 'postal_code') 1251 zipcode.text = gmTools.coalesce(adr['postcode'], '') 1252 1253 street = etree.SubElement(home, 'street') 1254 street.set('comment', gmTools.coalesce(adr['notes_street'], '')) 1255 street.text = gmTools.coalesce(adr['street'], '') 1256 1257 no = etree.SubElement(home, 'number') 1258 no.set('subunit', gmTools.coalesce(adr['subunit'], '')) 1259 no.set('comment', gmTools.coalesce(adr['notes_subunit'], '')) 1260 no.text = gmTools.coalesce(adr['number'], '') 1261 1262 country = etree.SubElement(home, 'country') 1263 country.set('comment', adr['l10n_country']) 1264 country.text = gmTools.coalesce(adr['code_country'], '') 1265 1266 phone = etree.SubElement(pat, 'home_phone') 1267 rec = self.get_comm_channels(comm_medium = 'homephone') 1268 if len(rec) > 0: 1269 if not rec[0]['is_confidential']: 1270 phone.set('comment', gmTools.coalesce(rec[0]['comment'], '')) 1271 phone.text = rec[0]['url'] 1272 1273 phone = etree.SubElement(pat, 'work_phone') 1274 rec = self.get_comm_channels(comm_medium = 'workphone') 1275 if len(rec) > 0: 1276 if not rec[0]['is_confidential']: 1277 phone.set('comment', gmTools.coalesce(rec[0]['comment'], '')) 1278 phone.text = rec[0]['url'] 1279 1280 phone = etree.SubElement(pat, 'cell_phone') 1281 rec = self.get_comm_channels(comm_medium = 'mobile') 1282 if len(rec) > 0: 1283 if not rec[0]['is_confidential']: 1284 phone.set('comment', gmTools.coalesce(rec[0]['comment'], '')) 1285 phone.text = rec[0]['url'] 1286 1287 tree = etree.ElementTree(pat) 1288 tree.write(filename, encoding = 'UTF-8') 1289 1290 return filename
1291 1292 #--------------------------------------------------------
1293 - def export_as_vcard(self, filename=None):
1294 # http://vobject.skyhouseconsulting.com/usage.html 1295 # http://en.wikipedia.org/wiki/VCard 1296 # http://svn.osafoundation.org/vobject/trunk/vobject/vcard.py 1297 # http://www.ietf.org/rfc/rfc2426.txt 1298 1299 dob_format = '%Y%m%d' 1300 1301 import vobject 1302 1303 vc = vobject.vCard() 1304 vc.add('kind') 1305 vc.kind.value = 'individual' 1306 1307 vc.add('fn') 1308 vc.fn.value = self.get_description() 1309 vc.add('n') 1310 vc.n.value = vobject.vcard.Name(family = self._payload[self._idx['lastnames']], given = self._payload[self._idx['firstnames']]) 1311 # privacy 1312 #vc.add(u'nickname') 1313 #vc.nickname.value = gmTools.coalesce(self._payload[self._idx['preferred']], u'') 1314 vc.add('title') 1315 vc.title.value = gmTools.coalesce(self._payload[self._idx['title']], '') 1316 vc.add('gender') 1317 # FIXME: dont know how to add gender_string after ';' 1318 vc.gender.value = map_gender2vcard[self._payload[self._idx['gender']]]#, self.gender_string 1319 vc.add('bday') 1320 vc.bday.value = gmDateTime.pydt_strftime(self._payload[self._idx['dob']], dob_format, accuracy = gmDateTime.acc_days, none_str = '') 1321 1322 channels = self.get_comm_channels(comm_medium = 'homephone') 1323 if len(channels) > 0: 1324 if not channels[0]['is_confidential']: 1325 vc.add('tel') 1326 vc.tel.value = channels[0]['url'] 1327 vc.tel.type_param = 'HOME' 1328 channels = self.get_comm_channels(comm_medium = 'workphone') 1329 if len(channels) > 0: 1330 if not channels[0]['is_confidential']: 1331 vc.add('tel') 1332 vc.tel.value = channels[0]['url'] 1333 vc.tel.type_param = 'WORK' 1334 channels = self.get_comm_channels(comm_medium = 'mobile') 1335 if len(channels) > 0: 1336 if not channels[0]['is_confidential']: 1337 vc.add('tel') 1338 vc.tel.value = channels[0]['url'] 1339 vc.tel.type_param = 'CELL' 1340 channels = self.get_comm_channels(comm_medium = 'fax') 1341 if len(channels) > 0: 1342 if not channels[0]['is_confidential']: 1343 vc.add('tel') 1344 vc.tel.value = channels[0]['url'] 1345 vc.tel.type_param = 'FAX' 1346 channels = self.get_comm_channels(comm_medium = 'email') 1347 if len(channels) > 0: 1348 if not channels[0]['is_confidential']: 1349 vc.add('email') 1350 vc.tel.value = channels[0]['url'] 1351 vc.tel.type_param = 'INTERNET' 1352 channels = self.get_comm_channels(comm_medium = 'web') 1353 if len(channels) > 0: 1354 if not channels[0]['is_confidential']: 1355 vc.add('url') 1356 vc.tel.value = channels[0]['url'] 1357 vc.tel.type_param = 'INTERNET' 1358 1359 adrs = self.get_addresses(address_type = 'home') 1360 if len(adrs) > 0: 1361 home_adr = adrs[0] 1362 vc.add('adr') 1363 vc.adr.type_param = 'HOME' 1364 vc.adr.value = vobject.vcard.Address() 1365 vc_adr = vc.adr.value 1366 vc_adr.extended = gmTools.coalesce(home_adr['subunit'], '') 1367 vc_adr.street = gmTools.coalesce(home_adr['street'], '', '%s ') + gmTools.coalesce(home_adr['number'], '') 1368 vc_adr.region = gmTools.coalesce(home_adr['l10n_region'], '') 1369 vc_adr.code = gmTools.coalesce(home_adr['postcode'], '') 1370 vc_adr.city = gmTools.coalesce(home_adr['urb'], '') 1371 vc_adr.country = gmTools.coalesce(home_adr['l10n_country'], '') 1372 1373 #photo (base64) 1374 1375 if filename is None: 1376 filename = gmTools.get_unique_filename ( 1377 prefix = 'gm-patient-', 1378 suffix = '.vcf' 1379 ) 1380 vcf = io.open(filename, mode = 'wt', encoding = 'utf8') 1381 try: 1382 vcf.write(vc.serialize().decode('utf-8')) 1383 except UnicodeDecodeError: 1384 _log.exception('failed to serialize VCF data') 1385 vcf.close() 1386 return 'cannot-serialize.vcf' 1387 vcf.close() 1388 1389 return filename
1390 #-------------------------------------------------------- 1391 # occupations API 1392 #--------------------------------------------------------
1393 - def get_occupations(self):
1394 return gmDemographicRecord.get_occupations(pk_identity = self.pk_obj)
1395 1396 #-------------------------------------------------------- 1433 #-------------------------------------------------------- 1441 #-------------------------------------------------------- 1442 # comms API 1443 #--------------------------------------------------------
1444 - def get_comm_channels(self, comm_medium=None):
1445 cmd = "select * from dem.v_person_comms where pk_identity = %s" 1446 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True) 1447 1448 filtered = rows 1449 1450 if comm_medium is not None: 1451 filtered = [] 1452 for row in rows: 1453 if row['comm_type'] == comm_medium: 1454 filtered.append(row) 1455 1456 return [ gmDemographicRecord.cCommChannel(row = { 1457 'pk_field': 'pk_lnk_identity2comm', 1458 'data': r, 1459 'idx': idx 1460 }) for r in filtered 1461 ]
1462 1463 comm_channels = property(get_comm_channels, lambda x:x) 1464 #-------------------------------------------------------- 1482 #-------------------------------------------------------- 1488 #-------------------------------------------------------- 1489 # contacts API 1490 #--------------------------------------------------------
1491 - def get_addresses(self, address_type=None):
1492 1493 cmd = "SELECT * FROM dem.v_pat_addresses WHERE pk_identity = %(pat)s" 1494 args = {'pat': self.pk_obj} 1495 if address_type is not None: 1496 cmd = cmd + " AND address_type = %(typ)s" 1497 args['typ'] = address_type 1498 1499 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1500 1501 return [ 1502 gmDemographicRecord.cPatientAddress(row = {'idx': idx, 'data': r, 'pk_field': 'pk_address'}) 1503 for r in rows 1504 ]
1505 #-------------------------------------------------------- 1551 #---------------------------------------------------------------------- 1572 #---------------------------------------------------------------------- 1573 # bills API 1574 #----------------------------------------------------------------------
1575 - def get_bills(self, order_by=None, pk_patient=None):
1576 return gmBilling.get_bills ( 1577 order_by = order_by, 1578 pk_patient = self.pk_obj 1579 )
1580 1581 bills = property(get_bills, lambda x:x) 1582 #---------------------------------------------------------------------- 1583 # relatives API 1584 #----------------------------------------------------------------------
1585 - def get_relatives(self):
1586 cmd = """ 1587 SELECT 1588 d_rt.description, 1589 d_vap.* 1590 FROM 1591 dem.v_all_persons d_vap, 1592 dem.relation_types d_rt, 1593 dem.lnk_person2relative d_lp2r 1594 WHERE 1595 ( d_lp2r.id_identity = %(pk)s 1596 AND 1597 d_vap.pk_identity = d_lp2r.id_relative 1598 AND 1599 d_rt.id = d_lp2r.id_relation_type 1600 ) or ( 1601 d_lp2r.id_relative = %(pk)s 1602 AND 1603 d_vap.pk_identity = d_lp2r.id_identity 1604 AND 1605 d_rt.inverse = d_lp2r.id_relation_type 1606 )""" 1607 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}]) 1608 if len(rows) == 0: 1609 return [] 1610 return [(row[0], cPerson(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk_identity'})) for row in rows]
1611 #-------------------------------------------------------- 1631 #----------------------------------------------------------------------
1632 - def delete_relative(self, relation):
1633 # unlink only, don't delete relative itself 1634 self.set_relative(None, relation)
1635 #--------------------------------------------------------
1637 if self._payload[self._idx['pk_emergency_contact']] is None: 1638 return None 1639 return cPerson(self._payload[self._idx['pk_emergency_contact']])
1640 1641 emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x) 1642 1643 #---------------------------------------------------------------------- 1644 # age/dob related 1645 #----------------------------------------------------------------------
1646 - def get_formatted_dob(self, format='%Y %b %d', none_string=None, honor_estimation=False):
1647 return gmDateTime.format_dob ( 1648 self._payload[self._idx['dob']], 1649 format = format, 1650 none_string = none_string, 1651 dob_is_estimated = self._payload[self._idx['dob_is_estimated']] and honor_estimation 1652 )
1653 1654 #----------------------------------------------------------------------
1655 - def get_medical_age(self):
1656 dob = self['dob'] 1657 1658 if dob is None: 1659 return '??' 1660 1661 if dob > gmDateTime.pydt_now_here(): 1662 return _('invalid age: DOB in the future') 1663 1664 death = self['deceased'] 1665 1666 if death is None: 1667 return '%s%s' % ( 1668 gmTools.bool2subst ( 1669 self._payload[self._idx['dob_is_estimated']], 1670 gmTools.u_almost_equal_to, 1671 '' 1672 ), 1673 gmDateTime.format_apparent_age_medically ( 1674 age = gmDateTime.calculate_apparent_age(start = dob) 1675 ) 1676 ) 1677 1678 if dob > death: 1679 return _('invalid age: DOB after death') 1680 1681 return '%s%s%s' % ( 1682 gmTools.u_latin_cross, 1683 gmTools.bool2subst ( 1684 self._payload[self._idx['dob_is_estimated']], 1685 gmTools.u_almost_equal_to, 1686 '' 1687 ), 1688 gmDateTime.format_apparent_age_medically ( 1689 age = gmDateTime.calculate_apparent_age ( 1690 start = dob, 1691 end = self['deceased'] 1692 ) 1693 ) 1694 )
1695 1696 #----------------------------------------------------------------------
1697 - def dob_in_range(self, min_distance='1 week', max_distance='1 week'):
1698 if self['dob'] is None: 1699 return False 1700 cmd = 'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)' 1701 rows, idx = gmPG2.run_ro_queries ( 1702 queries = [{ 1703 'cmd': cmd, 1704 'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance} 1705 }] 1706 ) 1707 return rows[0][0]
1708 1709 #----------------------------------------------------------------------
1711 if self['dob'] is None: 1712 return None 1713 now = gmDateTime.pydt_now_here() 1714 if now.month < self['dob'].month: 1715 return False 1716 if now.month > self['dob'].month: 1717 return True 1718 # -> DOB is this month 1719 if now.day < self['dob'].day: 1720 return False 1721 if now.day > self['dob'].day: 1722 return True 1723 # -> DOB is today 1724 return False
1725 1726 current_birthday_passed = property(_get_current_birthday_passed) 1727 1728 #----------------------------------------------------------------------
1729 - def _get_birthday_this_year(self):
1730 if self['dob'] is None: 1731 return None 1732 now = gmDateTime.pydt_now_here() 1733 return gmDateTime.pydt_replace ( 1734 dt = self['dob'], 1735 year = now.year, 1736 strict = False 1737 )
1738 1739 birthday_this_year = property(_get_birthday_this_year) 1740 1741 #----------------------------------------------------------------------
1742 - def _get_birthday_next_year(self):
1743 if self['dob'] is None: 1744 return None 1745 now = gmDateTime.pydt_now_here() 1746 return gmDateTime.pydt_replace ( 1747 dt = self['dob'], 1748 year = now.year + 1, 1749 strict = False 1750 )
1751 1752 birthday_next_year = property(_get_birthday_next_year) 1753 1754 #----------------------------------------------------------------------
1755 - def _get_birthday_last_year(self):
1756 if self['dob'] is None: 1757 return None 1758 now = gmDateTime.pydt_now_here() 1759 return gmDateTime.pydt_replace ( 1760 dt = self['dob'], 1761 year = now.year - 1, 1762 strict = False 1763 )
1764 1765 birthday_last_year = property(_get_birthday_last_year, lambda x:x) 1766 1767 #---------------------------------------------------------------------- 1768 # practice related 1769 #----------------------------------------------------------------------
1770 - def get_last_encounter(self):
1771 cmd = 'select * from clin.v_most_recent_encounters where pk_patient=%s' 1772 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}]) 1773 if len(rows) > 0: 1774 return rows[0] 1775 else: 1776 return None
1777 #--------------------------------------------------------
1778 - def get_messages(self, order_by=None):
1779 return gmProviderInbox.get_inbox_messages(pk_patient = self._payload[self._idx['pk_identity']], order_by = order_by)
1780 1781 messages = property(get_messages, lambda x:x) 1782 #--------------------------------------------------------
1783 - def _get_overdue_messages(self):
1784 return gmProviderInbox.get_overdue_messages(pk_patient = self._payload[self._idx['pk_identity']])
1785 1786 overdue_messages = property(_get_overdue_messages, lambda x:x) 1787 1788 #--------------------------------------------------------
1789 - def delete_message(self, pk=None):
1790 return gmProviderInbox.delete_inbox_message(inbox_message = pk)
1791 1792 #--------------------------------------------------------
1793 - def _get_dynamic_hints(self, pk_encounter=None):
1794 return gmAutoHints.get_hints_for_patient ( 1795 pk_identity = self._payload[self._idx['pk_identity']], 1796 pk_encounter = pk_encounter 1797 )
1798 1799 dynamic_hints = property(_get_dynamic_hints, lambda x:x) 1800 1801 #--------------------------------------------------------
1802 - def _get_suppressed_hints(self):
1803 return gmAutoHints.get_suppressed_hints(pk_identity = self._payload[self._idx['pk_identity']])
1804 1805 suppressed_hints = property(_get_suppressed_hints, lambda x:x) 1806 1807 #--------------------------------------------------------
1809 if self._payload[self._idx['pk_primary_provider']] is None: 1810 return None 1811 cmd = "SELECT * FROM dem.v_all_persons WHERE pk_identity = (SELECT pk_identity FROM dem.v_staff WHERE pk_staff = %(pk_staff)s)" 1812 args = {'pk_staff': self._payload[self._idx['pk_primary_provider']]} 1813 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1814 if len(rows) == 0: 1815 return None 1816 return cPerson(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_identity'})
1817 1818 primary_provider_identity = property(_get_primary_provider_identity, lambda x:x) 1819 1820 #--------------------------------------------------------
1821 - def _get_primary_provider(self):
1822 if self._payload[self._idx['pk_primary_provider']] is None: 1823 return None 1824 from Gnumed.business import gmStaff 1825 return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
1826 1827 primary_provider = property(_get_primary_provider, lambda x:x) 1828 1829 #---------------------------------------------------------------------- 1830 # convenience 1831 #----------------------------------------------------------------------
1832 - def get_subdir_name(self):
1833 """Format patient demographics into patient specific path name fragment.""" 1834 1835 return gmTools.fname_sanitize('%s-%s-%s' % ( 1836 self._payload[self._idx['lastnames']], 1837 self._payload[self._idx['firstnames']], 1838 self.get_formatted_dob(format = '%Y-%m-%d') 1839 ))
1840 # return (u'%s-%s-%s' % ( 1841 # self._payload[self._idx['lastnames']].replace(u' ', u'_'), 1842 # self._payload[self._idx['firstnames']].replace(u' ', u'_'), 1843 # self.get_formatted_dob(format = '%Y-%m-%d') 1844 # )).replace ( 1845 # u"'", u"" 1846 # ).replace ( 1847 # u'"', u'' 1848 # ).replace ( 1849 # u'/', u'_' 1850 # ).replace ( 1851 # u'\\', u'_' 1852 # ).replace ( 1853 # u'~', u'' 1854 # ).replace ( 1855 # u'|', u'_' 1856 # ).replace ( 1857 # u'*', u'' 1858 # ).replace ( 1859 # u'\u2248', u'' # "approximately", having been added by dob_is_estimated 1860 # ) 1861 1862 1863 subdir_name = property(get_subdir_name, lambda x:x)
1864 1865 #============================================================
1866 -def identity_is_patient(pk_identity):
1867 cmd = 'SELECT 1 FROM clin.patient WHERE fk_identity = %(pk_pat)s' 1868 args = {'pk_pat': pk_identity} 1869 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1870 if len(rows) == 0: 1871 return False 1872 return True
1873 1874 #------------------------------------------------------------
1875 -def turn_identity_into_patient(pk_identity):
1876 cmd = """ 1877 INSERT INTO clin.patient (fk_identity) 1878 SELECT %(pk_ident)s WHERE NOT EXISTS ( 1879 SELECT 1 FROM clin.patient c_p WHERE fk_identity = %(pk_ident)s 1880 )""" 1881 args = {'pk_ident': pk_identity} 1882 queries = [{'cmd': cmd, 'args': args}] 1883 gmPG2.run_rw_queries(queries = queries) 1884 return True
1885 1886 #============================================================ 1887 # helper functions 1888 #------------------------------------------------------------ 1889 _yield = lambda x:x 1890
1891 -def set_yielder(yielder):
1892 if not callable(yielder): 1893 raise TypeError('yielder <%s> is not callable' % yielder) 1894 global _yield 1895 _yield = yielder 1896 _log.debug('setting yielder to <%s>', yielder)
1897 1898 #============================================================
1899 -class cPatient(cPerson):
1900 """Represents a person which is a patient. 1901 1902 - a specializing subclass of cPerson turning it into a patient 1903 - its use is to cache subobjects like EMR and document folder 1904 """
1905 - def __init__(self, aPK_obj=None, row=None):
1906 cPerson.__init__(self, aPK_obj = aPK_obj, row = row) 1907 self.__emr_access_lock = threading.Lock() 1908 self.__emr = None 1909 self.__doc_folder = None
1910 1911 #--------------------------------------------------------
1912 - def cleanup(self):
1913 """Do cleanups before dying. 1914 1915 - note that this may be called in a thread 1916 """ 1917 if self.__emr is not None: 1918 self.__emr.cleanup() 1919 if self.__doc_folder is not None: 1920 self.__doc_folder.cleanup() 1921 cPerson.cleanup(self)
1922 1923 #----------------------------------------------------------------
1924 - def ensure_has_allergy_state(self, pk_encounter=None):
1925 from Gnumed.business.gmAllergy import ensure_has_allergy_state 1926 ensure_has_allergy_state(encounter = pk_encounter) 1927 return True
1928 1929 #----------------------------------------------------------
1930 - def get_emr(self):
1931 _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident()) 1932 1933 # fast path: already set, just return it 1934 if self.__emr is not None: 1935 return self.__emr 1936 1937 stack_logged = False 1938 got_lock = self.__emr_access_lock.acquire(False) 1939 if not got_lock: 1940 # do some logging as we failed to get the lock 1941 call_stack = inspect.stack() 1942 call_stack.reverse() 1943 for idx in range(1, len(call_stack)): 1944 caller = call_stack[idx] 1945 _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1]) 1946 del call_stack 1947 stack_logged = True 1948 # now loop a bit 1949 for idx in range(500): 1950 _yield() 1951 time.sleep(0.1) 1952 _yield() 1953 got_lock = self.__emr_access_lock.acquire(False) 1954 if got_lock: 1955 break 1956 if not got_lock: 1957 _log.error('still failed to acquire EMR access lock, aborting (thread [%s])', threading.get_ident()) 1958 self.__emr_access_lock.release() 1959 raise AttributeError('cannot lock access to EMR for identity [%s]' % self._payload[self._idx['pk_identity']]) 1960 1961 _log.debug('pulling chart for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident()) 1962 if not stack_logged: 1963 # do some logging as we are pulling the chart for the first time 1964 call_stack = inspect.stack() 1965 call_stack.reverse() 1966 for idx in range(1, len(call_stack)): 1967 caller = call_stack[idx] 1968 _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1]) 1969 del call_stack 1970 stack_logged = True 1971 1972 self.is_patient = True 1973 from Gnumed.business import gmClinicalRecord 1974 emr = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']]) 1975 1976 _log.debug('returning EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident()) 1977 self.__emr = emr 1978 self.__emr_access_lock.release() 1979 return self.__emr
1980 1981 emr = property(get_emr, lambda x:x) 1982 1983 #----------------------------------------------------------
1984 - def get_document_folder(self):
1985 if self.__doc_folder is None: 1986 self.__doc_folder = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']]) 1987 return self.__doc_folder
1988 1989 document_folder = property(get_document_folder, lambda x:x)
1990 1991 #============================================================
1992 -class gmCurrentPatient(gmBorg.cBorg):
1993 """Patient Borg to hold the currently active patient. 1994 1995 There may be many instances of this but they all share state. 1996 1997 The underlying dem.identity row must have .deleted set to FALSE. 1998 1999 The sequence of events when changing the active patient: 2000 2001 1) Registered callbacks are run. 2002 Those are run synchronously. If a callback 2003 returns False or throws an exception the 2004 patient switch is aborted. Callback code 2005 can rely on the patient still being active 2006 and to not go away until it returns. It 2007 is not passed any arguments and must return 2008 False or True. 2009 2010 2) Signal "pre_patient_unselection" is sent. 2011 This does not wait for nor check results. 2012 The keyword pk_identity contains the 2013 PK of the person being switched away 2014 from. 2015 2016 3) the current patient is unset (gmNull.cNull) 2017 2018 4) Signal "current_patient_unset" is sent 2019 At this point resetting GUI fields to 2020 empty should be done. The active patient 2021 is not there anymore. 2022 2023 This does not wait for nor check results. 2024 2025 5) The current patient is set to the new value. 2026 The new patient can also remain gmNull.cNull 2027 in case the calling code explicitely unset 2028 the current patient. 2029 2030 6) Signal "post_patient_selection" is sent. 2031 Code listening to this signal can 2032 assume that the new patient is 2033 already active. 2034 """
2035 - def __init__(self, patient=None, forced_reload=False):
2036 """Change or get currently active patient. 2037 2038 patient: 2039 * None: get currently active patient 2040 * -1: unset currently active patient 2041 * cPatient instance: set active patient if possible 2042 """ 2043 # make sure we do have a patient pointer 2044 try: 2045 self.patient 2046 except AttributeError: 2047 self.patient = gmNull.cNull() 2048 self.__register_interests() 2049 # set initial lock state, 2050 # this lock protects against activating another patient 2051 # when we are controlled from a remote application 2052 self.__lock_depth = 0 2053 # initialize callback state 2054 self.__callbacks_before_switching_away_from_patient = [] 2055 2056 # user wants copy of current patient 2057 if patient is None: 2058 return None 2059 2060 # do nothing if patient is locked 2061 if self.locked: 2062 _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient)) 2063 return None 2064 2065 # user wants to explicitly unset current patient 2066 if patient == -1: 2067 _log.debug('explicitly unsetting current patient') 2068 if not self.__run_callbacks_before_switching_away_from_patient(): 2069 _log.error('not unsetting current patient, at least one pre-change callback failed') 2070 return None 2071 self.__send_pre_unselection_notification() 2072 self.patient.cleanup() 2073 self.patient = gmNull.cNull() 2074 self.__send_unselection_notification() 2075 # give it some time 2076 time.sleep(0.5) 2077 self.__send_selection_notification() 2078 return None 2079 2080 # must be cPatient instance, then 2081 if not isinstance(patient, cPatient): 2082 _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient)) 2083 raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient)) 2084 2085 # same ID, no change needed 2086 if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload: 2087 return None 2088 2089 if patient['is_deleted']: 2090 _log.error('cannot set active patient to disabled dem.identity row: %s', patient) 2091 raise ValueError('gmPerson.gmCurrentPatient.__init__(): <patient> is disabled: %s' % patient) 2092 2093 # user wants different patient 2094 _log.info('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity']) 2095 2096 if not self.__run_callbacks_before_switching_away_from_patient(): 2097 _log.error('not changing current patient, at least one pre-change callback failed') 2098 return None 2099 2100 # everything seems swell 2101 self.__send_pre_unselection_notification() 2102 self.patient.cleanup() 2103 self.patient = gmNull.cNull() 2104 self.__send_unselection_notification() 2105 # give it some time 2106 time.sleep(0.5) 2107 self.patient = patient 2108 # for good measure ... 2109 # however, actually we want to get rid of that 2110 self.patient.emr 2111 self.__send_selection_notification() 2112 2113 return None
2114 2115 #--------------------------------------------------------
2116 - def __register_interests(self):
2117 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
2118 2119 #--------------------------------------------------------
2120 - def _on_database_signal(self, **kwds):
2121 # we don't have a patient: don't process signals 2122 if isinstance(self.patient, gmNull.cNull): 2123 return True 2124 2125 # we only care about identity and name changes 2126 if kwds['table'] not in ['dem.identity', 'dem.names']: 2127 return True 2128 2129 # signal is not about our patient: ignore signal 2130 if int(kwds['pk_identity']) != self.patient.ID: 2131 return True 2132 2133 if kwds['table'] == 'dem.identity': 2134 # we don't care about newly INSERTed or DELETEd patients 2135 if kwds['operation'] != 'UPDATE': 2136 return True 2137 2138 self.patient.refetch_payload() 2139 return True
2140 2141 #-------------------------------------------------------- 2142 # external API 2143 #--------------------------------------------------------
2144 - def register_before_switching_from_patient_callback(self, callback=None):
2145 # callbacks are run synchronously before 2146 # switching *away* from the current patient, 2147 # if a callback returns false the current 2148 # patient will not be switched away from, 2149 # callbacks will not be passed any arguments 2150 if not callable(callback): 2151 raise TypeError('callback [%s] not callable' % callback) 2152 2153 self.__callbacks_before_switching_away_from_patient.append(callback)
2154 2155 #--------------------------------------------------------
2156 - def _get_connected(self):
2157 return (not isinstance(self.patient, gmNull.cNull))
2158 2159 connected = property(_get_connected, lambda x:x) 2160 2161 #--------------------------------------------------------
2162 - def _get_locked(self):
2163 return (self.__lock_depth > 0)
2164
2165 - def _set_locked(self, locked):
2166 if locked: 2167 self.__lock_depth = self.__lock_depth + 1 2168 gmDispatcher.send(signal = 'patient_locked', sender = self.__class__.__name__) 2169 else: 2170 if self.__lock_depth == 0: 2171 _log.error('lock/unlock imbalance, tried to refcount lock depth below 0') 2172 return 2173 else: 2174 self.__lock_depth = self.__lock_depth - 1 2175 gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
2176 2177 locked = property(_get_locked, _set_locked) 2178 2179 #--------------------------------------------------------
2180 - def force_unlock(self):
2181 _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth) 2182 self.__lock_depth = 0 2183 gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
2184 2185 #-------------------------------------------------------- 2186 # patient change handling 2187 #--------------------------------------------------------
2189 if isinstance(self.patient, gmNull.cNull): 2190 return True 2191 2192 for call_back in self.__callbacks_before_switching_away_from_patient: 2193 try: 2194 successful = call_back() 2195 except: 2196 _log.exception('callback [%s] failed', call_back) 2197 print("*** pre-change callback failed ***") 2198 print(type(call_back)) 2199 print(call_back) 2200 return False 2201 2202 if not successful: 2203 _log.error('callback [%s] returned False', call_back) 2204 return False 2205 2206 return True
2207 2208 #--------------------------------------------------------
2210 """Sends signal when current patient is about to be unset. 2211 2212 This does NOT wait for signal handlers to complete. 2213 """ 2214 kwargs = { 2215 'signal': 'pre_patient_unselection', 2216 'sender': self.__class__.__name__, 2217 'pk_identity': self.patient['pk_identity'] 2218 } 2219 gmDispatcher.send(**kwargs)
2220 2221 #--------------------------------------------------------
2223 """Sends signal when the previously active patient has 2224 been unset during a change of active patient. 2225 2226 This is the time to initialize GUI fields to empty values. 2227 2228 This does NOT wait for signal handlers to complete. 2229 """ 2230 kwargs = { 2231 'signal': 'current_patient_unset', 2232 'sender': self.__class__.__name__ 2233 } 2234 gmDispatcher.send(**kwargs)
2235 2236 #--------------------------------------------------------
2238 """Sends signal when another patient has actually been made active.""" 2239 kwargs = { 2240 'signal': 'post_patient_selection', 2241 'sender': self.__class__.__name__, 2242 'pk_identity': self.patient['pk_identity'] 2243 } 2244 gmDispatcher.send(**kwargs)
2245 2246 #-------------------------------------------------------- 2247 # __getattr__ handling 2248 #--------------------------------------------------------
2249 - def __getattr__(self, attribute):
2250 # override __getattr__ here, not __getattribute__ because 2251 # the former is used _after_ ordinary attribute lookup 2252 # failed while the latter is applied _before_ ordinary 2253 # lookup (and is easy to drive into infinite recursion), 2254 # this is also why subsequent access to self.patient 2255 # simply returns the .patient member value :-) 2256 if attribute == 'patient': 2257 raise AttributeError 2258 if isinstance(self.patient, gmNull.cNull): 2259 _log.error("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient", self, self.patient, attribute) 2260 raise AttributeError("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient" % (self, self.patient, attribute)) 2261 return getattr(self.patient, attribute)
2262 2263 #-------------------------------------------------------- 2264 # __get/setitem__ handling 2265 #--------------------------------------------------------
2266 - def __getitem__(self, attribute = None):
2267 """Return any attribute if known how to retrieve it by proxy. 2268 """ 2269 return self.patient[attribute]
2270 2271 #--------------------------------------------------------
2272 - def __setitem__(self, attribute, value):
2273 self.patient[attribute] = value
2274 2275 #============================================================ 2276 # match providers 2277 #============================================================
2278 -class cMatchProvider_Provider(gmMatchProvider.cMatchProvider_SQL2):
2279 - def __init__(self):
2280 gmMatchProvider.cMatchProvider_SQL2.__init__( 2281 self, 2282 queries = [ 2283 """SELECT 2284 pk_staff AS data, 2285 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label, 2286 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label 2287 FROM dem.v_staff 2288 WHERE 2289 is_active AND ( 2290 short_alias %(fragment_condition)s OR 2291 firstnames %(fragment_condition)s OR 2292 lastnames %(fragment_condition)s OR 2293 db_user %(fragment_condition)s 2294 ) 2295 """ 2296 ] 2297 ) 2298 self.setThresholds(1, 2, 3)
2299 2300 #============================================================ 2301 # convenience functions 2302 #============================================================
2303 -def create_name(pk_person, firstnames, lastnames, active=False):
2304 queries = [{ 2305 'cmd': "select dem.add_name(%s, %s, %s, %s)", 2306 'args': [pk_person, firstnames, lastnames, active] 2307 }] 2308 rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True) 2309 name = cPersonName(aPK_obj = rows[0][0]) 2310 return name
2311 2312 #============================================================
2313 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None):
2314 2315 cmd1 = """INSERT INTO dem.identity (gender, dob) VALUES (%s, %s)""" 2316 cmd2 = """ 2317 INSERT INTO dem.names ( 2318 id_identity, lastnames, firstnames 2319 ) VALUES ( 2320 currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx') 2321 ) RETURNING id_identity""" 2322 # cmd2 = u"select dem.add_name(currval('dem.identity_pk_seq')::integer, coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx'), True)" 2323 rows, idx = gmPG2.run_rw_queries ( 2324 queries = [ 2325 {'cmd': cmd1, 'args': [gender, dob]}, 2326 {'cmd': cmd2, 'args': [lastnames, firstnames]} 2327 #{'cmd': cmd2, 'args': [firstnames, lastnames]} 2328 ], 2329 return_data = True 2330 ) 2331 ident = cPerson(aPK_obj = rows[0][0]) 2332 gmHooks.run_hook_script(hook = 'post_person_creation') 2333 return ident
2334 2335 #============================================================
2336 -def disable_identity(pk_identity):
2337 _log.info('disabling identity [%s]', pk_identity) 2338 cmd = "UPDATE dem.identity SET deleted = true WHERE pk = %(pk)s" 2339 args = {'pk': pk_identity} 2340 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}]) 2341 return True
2342 2343 #============================================================
2344 -def create_dummy_identity():
2345 cmd = "INSERT INTO dem.identity(gender) VALUES (NULL::text) RETURNING pk" 2346 rows, idx = gmPG2.run_rw_queries ( 2347 queries = [{'cmd': cmd}], 2348 return_data = True 2349 ) 2350 return gmDemographicRecord.cPerson(aPK_obj = rows[0][0])
2351 2352 #============================================================
2353 -def identity_exists(pk_identity):
2354 cmd = 'SELECT EXISTS(SELECT 1 FROM dem.identity where pk = %(pk)s)' 2355 args = {'pk': pk_identity} 2356 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 2357 return rows[0][0]
2358 2359 #============================================================
2360 -def set_active_patient(patient=None, forced_reload=False):
2361 """Set active patient. 2362 2363 If patient is -1 the active patient will be UNset. 2364 """ 2365 if isinstance(patient, gmCurrentPatient): 2366 return True 2367 2368 if isinstance(patient, cPatient): 2369 pat = patient 2370 elif isinstance(patient, cPerson): 2371 pat = pat.as_patient 2372 elif patient == -1: 2373 pat = patient 2374 else: 2375 # maybe integer ? 2376 success, pk = gmTools.input2int(initial = patient, minval = 1) 2377 if not success: 2378 raise ValueError('<patient> must be either -1, >0, or a cPatient, cPerson or gmCurrentPatient instance, is: %s' % patient) 2379 # but also valid patient ID ? 2380 try: 2381 pat = cPatient(aPK_obj = pk) 2382 except: 2383 _log.exception('identity [%s] not found' % patient) 2384 return False 2385 2386 # attempt to switch 2387 try: 2388 gmCurrentPatient(patient = pat, forced_reload = forced_reload) 2389 except: 2390 _log.exception('error changing active patient to [%s]' % patient) 2391 return False 2392 2393 return True
2394 2395 #============================================================ 2396 # gender related 2397 #------------------------------------------------------------
2398 -def get_gender_list():
2399 """Retrieves the list of known genders from the database.""" 2400 global __gender_idx 2401 global __gender_list 2402 2403 if __gender_list is None: 2404 cmd = "SELECT tag, l10n_tag, label, l10n_label, sort_weight FROM dem.v_gender_labels ORDER BY sort_weight DESC" 2405 __gender_list, __gender_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True) 2406 _log.debug('genders in database: %s' % __gender_list) 2407 2408 return (__gender_list, __gender_idx)
2409 2410 #------------------------------------------------------------ 2411 map_gender2mf = { 2412 'm': 'm', 2413 'f': 'f', 2414 'tf': 'f', 2415 'tm': 'm', 2416 'h': 'mf' 2417 } 2418 2419 # https://tools.ietf.org/html/rfc6350#section-6.2.7 2420 # M F O N U 2421 map_gender2vcard = { 2422 'm': 'M', 2423 'f': 'F', 2424 'tf': 'F', 2425 'tm': 'M', 2426 'h': 'O', 2427 None: 'U' 2428 } 2429 2430 #------------------------------------------------------------ 2431 # maps GNUmed related i18n-aware gender specifiers to a unicode symbol 2432 map_gender2symbol = { 2433 'm': '\u2642', 2434 'f': '\u2640', 2435 'tf': '\u26A5\u2640', 2436 # 'tf': u'\u2642\u2640-\u2640', 2437 'tm': '\u26A5\u2642', 2438 # 'tm': u'\u2642\u2640-\u2642', 2439 'h': '\u26A5', 2440 # 'h': u'\u2642\u2640', 2441 None: '?\u26A5?' 2442 } 2443 #------------------------------------------------------------
2444 -def map_gender2string(gender=None):
2445 """Maps GNUmed related i18n-aware gender specifiers to a human-readable string.""" 2446 2447 global __gender2string_map 2448 2449 if __gender2string_map is None: 2450 genders, idx = get_gender_list() 2451 __gender2string_map = { 2452 'm': _('male'), 2453 'f': _('female'), 2454 'tf': '', 2455 'tm': '', 2456 'h': '', 2457 None: _('unknown gender') 2458 } 2459 for g in genders: 2460 __gender2string_map[g[idx['l10n_tag']]] = g[idx['l10n_label']] 2461 __gender2string_map[g[idx['tag']]] = g[idx['l10n_label']] 2462 _log.debug('gender -> string mapping: %s' % __gender2string_map) 2463 2464 return __gender2string_map[gender]
2465 #------------------------------------------------------------
2466 -def map_gender2salutation(gender=None):
2467 """Maps GNUmed related i18n-aware gender specifiers to a human-readable salutation.""" 2468 2469 global __gender2salutation_map 2470 2471 if __gender2salutation_map is None: 2472 genders, idx = get_gender_list() 2473 __gender2salutation_map = { 2474 'm': _('Mr'), 2475 'f': _('Mrs'), 2476 'tf': '', 2477 'tm': '', 2478 'h': '', 2479 None: '' 2480 } 2481 for g in genders: 2482 __gender2salutation_map[g[idx['l10n_tag']]] = __gender2salutation_map[g[idx['tag']]] 2483 __gender2salutation_map[g[idx['label']]] = __gender2salutation_map[g[idx['tag']]] 2484 __gender2salutation_map[g[idx['l10n_label']]] = __gender2salutation_map[g[idx['tag']]] 2485 _log.debug('gender -> salutation mapping: %s' % __gender2salutation_map) 2486 2487 return __gender2salutation_map[gender]
2488 #------------------------------------------------------------
2489 -def map_firstnames2gender(firstnames=None):
2490 """Try getting the gender for the given first name.""" 2491 2492 if firstnames is None: 2493 return None 2494 2495 rows, idx = gmPG2.run_ro_queries(queries = [{ 2496 'cmd': "SELECT gender FROM dem.name_gender_map WHERE name ILIKE %(fn)s LIMIT 1", 2497 'args': {'fn': firstnames} 2498 }]) 2499 2500 if len(rows) == 0: 2501 return None 2502 2503 return rows[0][0]
2504 #============================================================
2505 -def get_person_IDs():
2506 cmd = 'SELECT pk FROM dem.identity' 2507 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False) 2508 return [ r[0] for r in rows ]
2509 2510 #============================================================
2511 -def get_persons_from_pks(pks=None):
2512 return [ cPerson(aPK_obj = pk) for pk in pks ]
2513 #============================================================
2514 -def get_person_from_xdt(filename=None, encoding=None, dob_format=None):
2515 from Gnumed.business import gmXdtObjects 2516 return gmXdtObjects.read_person_from_xdt(filename=filename, encoding=encoding, dob_format=dob_format)
2517 #============================================================
2518 -def get_persons_from_pracsoft_file(filename=None, encoding='ascii'):
2519 from Gnumed.business import gmPracSoftAU 2520 return gmPracSoftAU.read_persons_from_pracsoft_file(filename=filename, encoding=encoding)
2521 2522 #============================================================ 2523 # main/testing 2524 #============================================================ 2525 if __name__ == '__main__': 2526 2527 if len(sys.argv) == 1: 2528 sys.exit() 2529 2530 if sys.argv[1] != 'test': 2531 sys.exit() 2532 2533 import datetime 2534 2535 gmI18N.activate_locale() 2536 gmI18N.install_domain() 2537 gmDateTime.init() 2538 2539 #--------------------------------------------------------
2540 - def test_set_active_pat():
2541 2542 ident = cPerson(1) 2543 print("setting active patient with", ident) 2544 set_active_patient(patient=ident) 2545 2546 patient = cPatient(12) 2547 print("setting active patient with", patient) 2548 set_active_patient(patient=patient) 2549 2550 pat = gmCurrentPatient() 2551 print(pat['dob']) 2552 #pat['dob'] = 'test' 2553 2554 # staff = cStaff() 2555 # print "setting active patient with", staff 2556 # set_active_patient(patient=staff) 2557 2558 print("setting active patient with -1") 2559 set_active_patient(patient=-1)
2560 #--------------------------------------------------------
2561 - def test_dto_person():
2562 dto = cDTO_person() 2563 dto.firstnames = 'Sepp' 2564 dto.lastnames = 'Herberger' 2565 dto.gender = 'male' 2566 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone) 2567 print(dto) 2568 2569 print(dto['firstnames']) 2570 print(dto['lastnames']) 2571 print(dto['gender']) 2572 print(dto['dob']) 2573 2574 for key in dto.keys(): 2575 print(key)
2576 #--------------------------------------------------------
2577 - def test_identity():
2578 # create patient 2579 print('\n\nCreating identity...') 2580 new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames') 2581 print('Identity created: %s' % new_identity) 2582 2583 print('\nSetting title and gender...') 2584 new_identity['title'] = 'test title'; 2585 new_identity['gender'] = 'f'; 2586 new_identity.save_payload() 2587 print('Refetching identity from db: %s' % cPerson(aPK_obj=new_identity['pk_identity'])) 2588 2589 print('\nGetting all names...') 2590 for a_name in new_identity.get_names(): 2591 print(a_name) 2592 print('Active name: %s' % (new_identity.get_active_name())) 2593 print('Setting nickname...') 2594 new_identity.set_nickname(nickname='test nickname') 2595 print('Refetching all names...') 2596 for a_name in new_identity.get_names(): 2597 print(a_name) 2598 print('Active name: %s' % (new_identity.get_active_name())) 2599 2600 print('\nIdentity occupations: %s' % new_identity['occupations']) 2601 print('Creating identity occupation...') 2602 new_identity.link_occupation('test occupation') 2603 print('Identity occupations: %s' % new_identity['occupations']) 2604 2605 print('\nIdentity addresses: %s' % new_identity.get_addresses()) 2606 print('Creating identity address...') 2607 # make sure the state exists in the backend 2608 new_identity.link_address ( 2609 number = 'test 1234', 2610 street = 'test street', 2611 postcode = 'test postcode', 2612 urb = 'test urb', 2613 region_code = 'SN', 2614 country_code = 'DE' 2615 ) 2616 print('Identity addresses: %s' % new_identity.get_addresses()) 2617 2618 print('\nIdentity communications: %s' % new_identity.get_comm_channels()) 2619 print('Creating identity communication...') 2620 new_identity.link_comm_channel('homephone', '1234566') 2621 print('Identity communications: %s' % new_identity.get_comm_channels())
2622 #--------------------------------------------------------
2623 - def test_name():
2624 for pk in range(1,16): 2625 name = cPersonName(aPK_obj=pk) 2626 print(name.description) 2627 print(' ', name)
2628 #--------------------------------------------------------
2629 - def test_gender_list():
2630 genders, idx = get_gender_list() 2631 print("\n\nRetrieving gender enum (tag, label, weight):") 2632 for gender in genders: 2633 print("%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']]))
2634 #--------------------------------------------------------
2635 - def test_export_area():
2636 person = cPerson(aPK_obj = 12) 2637 print(person) 2638 print(person.export_area) 2639 print(person.export_area.items)
2640 #--------------------------------------------------------
2641 - def test_ext_id():
2642 person = cPerson(aPK_obj = 9) 2643 print(person.get_external_ids(id_type='Fachgebiet', issuer='Ärztekammer'))
2644 #print person.get_external_ids() 2645 #--------------------------------------------------------
2646 - def test_vcf():
2647 person = cPerson(aPK_obj = 12) 2648 print(person.export_as_vcard())
2649 2650 #--------------------------------------------------------
2651 - def test_current_patient():
2652 pat = gmCurrentPatient() 2653 print("pat.emr", pat.emr)
2654 2655 #--------------------------------------------------------
2656 - def test_ext_id():
2657 person = cPerson(aPK_obj = 12) 2658 print(person.suggest_external_id(target = 'Orthanc'))
2659 #-------------------------------------------------------- 2660 #test_dto_person() 2661 #test_identity() 2662 #test_set_active_pat() 2663 #test_search_by_dto() 2664 #test_name() 2665 #test_gender_list() 2666 2667 #map_gender2salutation('m') 2668 # module functions 2669 2670 #comms = get_comm_list() 2671 #print "\n\nRetrieving communication media enum (id, description): %s" % comms 2672 #test_export_area() 2673 #test_ext_id() 2674 #test_vcf() 2675 test_ext_id() 2676 test_current_patient() 2677 2678 #============================================================ 2679