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

Source Code for Module Gnumed.business.gmPersonSearch

  1  # -*- coding: utf-8 -*- 
  2  """GNUmed person searching code.""" 
  3  #============================================================ 
  4  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>" 
  5  __license__ = "GPL" 
  6   
  7  # std lib 
  8  import sys, logging, re as regex 
  9   
 10   
 11  # GNUmed 
 12  if __name__ == '__main__': 
 13          sys.path.insert(0, '../../') 
 14  from Gnumed.pycommon import gmPG2, gmI18N, gmTools, gmDateTime 
 15  from Gnumed.business import gmPerson 
 16   
 17   
 18  _log = logging.getLogger('gm.person') 
 19   
 20  #============================================================ 
21 -class cPatientSearcher_SQL:
22 """UI independant i18n aware patient searcher."""
23 - def __init__(self):
24 self._generate_queries = self._generate_queries_de 25 # make a cursor 26 self.conn = gmPG2.get_connection() 27 self.curs = self.conn.cursor()
28 #--------------------------------------------------------
29 - def __del__(self):
30 try: 31 self.curs.close() 32 except: pass 33 try: 34 self.conn.close() 35 except: pass
36 #-------------------------------------------------------- 37 # public API 38 #--------------------------------------------------------
39 - def get_patients(self, search_term = None, a_locale = None, dto = None):
40 identities = self.get_identities(search_term, a_locale, dto) 41 if identities is None: 42 return None 43 return [ gmPerson.cPatient(aPK_obj=ident['pk_identity']) for ident in identities ]
44 #--------------------------------------------------------
45 - def get_identities(self, search_term = None, a_locale = None, dto = None):
46 """Get patient identity objects for given parameters. 47 48 - either search term or search dict 49 - dto contains structured data that doesn't need to be parsed (cDTO_person) 50 - dto takes precedence over search_term 51 """ 52 parse_search_term = (dto is None) 53 54 if not parse_search_term: 55 queries = self._generate_queries_from_dto(dto) 56 if queries is None: 57 parse_search_term = True 58 if len(queries) == 0: 59 parse_search_term = True 60 61 if parse_search_term: 62 # temporary change of locale for selecting query generator 63 if a_locale is not None: 64 print("temporary change of locale on patient search not implemented") 65 _log.warning("temporary change of locale on patient search not implemented") 66 # generate queries 67 if search_term is None: 68 raise ValueError('need search term (dto AND search_term are None)') 69 70 queries = self._generate_queries(search_term) 71 72 # anything to do ? 73 if len(queries) == 0: 74 _log.error('query tree empty') 75 _log.error('[%s] [%s] [%s]' % (search_term, a_locale, str(dto))) 76 return None 77 78 # collect IDs here 79 identities = [] 80 # cycle through query list 81 for query in queries: 82 _log.debug("running %s" % query) 83 try: 84 rows, idx = gmPG2.run_ro_queries(queries = [query], get_col_idx=True) 85 except: 86 _log.exception('error running query') 87 continue 88 if len(rows) == 0: 89 continue 90 identities.extend ( 91 [ gmPerson.cPerson(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ] 92 ) 93 94 pks = [] 95 unique_identities = [] 96 for identity in identities: 97 if identity['pk_identity'] in pks: 98 continue 99 pks.append(identity['pk_identity']) 100 unique_identities.append(identity) 101 102 return unique_identities
103 #-------------------------------------------------------- 104 # internal helpers 105 #--------------------------------------------------------
106 - def _normalize_soundalikes(self, aString = None, aggressive = False):
107 """Transform some characters into a regex.""" 108 if aString.strip() == '': 109 return aString 110 111 # umlauts 112 normalized = aString.replace('Ä', '(Ä|AE|Ae|A|E)') 113 normalized = normalized.replace('Ö', '(Ö|OE|Oe|O)') 114 normalized = normalized.replace('Ü', '(Ü|UE|Ue|U)') 115 normalized = normalized.replace('ä', '(ä|ae|e|a)') 116 normalized = normalized.replace('ö', '(ö|oe|o)') 117 normalized = normalized.replace('ü', '(ü|ue|u|y)') 118 normalized = normalized.replace('ß', '(ß|sz|ss|s)') 119 120 # common soundalikes 121 # - René, Desiré, Inés ... 122 normalized = normalized.replace('é', '***DUMMY***') 123 normalized = normalized.replace('è', '***DUMMY***') 124 normalized = normalized.replace('***DUMMY***', '(é|e|è|ä|ae)') 125 126 # FIXME: missing i/a/o - but uncommon in German 127 normalized = normalized.replace('v', '***DUMMY***') 128 normalized = normalized.replace('f', '***DUMMY***') 129 normalized = normalized.replace('ph', '***DUMMY***') # now, this is *really* specific for German 130 normalized = normalized.replace('***DUMMY***', '(v|f|ph)') 131 132 # silent characters (Thomas vs Tomas) 133 normalized = normalized.replace('Th','***DUMMY***') 134 normalized = normalized.replace('T', '***DUMMY***') 135 normalized = normalized.replace('***DUMMY***', '(Th|T)') 136 normalized = normalized.replace('th', '***DUMMY***') 137 normalized = normalized.replace('t', '***DUMMY***') 138 normalized = normalized.replace('***DUMMY***', '(th|t)') 139 140 # apostrophes, hyphens et al 141 normalized = normalized.replace('"', '***DUMMY***') 142 normalized = normalized.replace("'", '***DUMMY***') 143 normalized = normalized.replace('`', '***DUMMY***') 144 normalized = normalized.replace('***DUMMY***', """("|'|`|***DUMMY***|\s)*""") 145 normalized = normalized.replace('-', """(-|\s)*""") 146 normalized = normalized.replace('|***DUMMY***|', '|-|') 147 148 if aggressive: 149 pass 150 # some more here 151 152 _log.debug('[%s] -> [%s]' % (aString, normalized)) 153 154 return normalized
155 #-------------------------------------------------------- 156 # write your own query generator and add it here: 157 # use compile() for speedup 158 # must escape strings before use !! 159 # ORDER BY ! 160 # FIXME: what about "< 40" ? 161 #--------------------------------------------------------
162 - def _generate_simple_query(self, raw):
163 """Compose queries if search term seems unambigous.""" 164 queries = [] 165 166 raw = raw.strip().rstrip(',').rstrip(';').strip() 167 168 # "<digits>" - GNUmed patient PK or DOB 169 if regex.match("^(\s|\t)*\d+(\s|\t)*$", raw, flags = regex.UNICODE): 170 _log.debug("[%s]: a PK or DOB" % raw) 171 tmp = raw.strip() 172 queries.append ({ 173 'cmd': "SELECT *, %s::text AS match_type FROM dem.v_active_persons WHERE pk_identity = %s ORDER BY lastnames, firstnames, dob", 174 'args': [_('internal patient ID'), tmp] 175 }) 176 if len(tmp) > 7: # DOB needs at least 8 digits 177 queries.append ({ 178 'cmd': "SELECT *, %s::text AS match_type FROM dem.v_active_persons WHERE dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) ORDER BY lastnames, firstnames, dob", 179 'args': [_('date of birth'), tmp.replace(',', '.')] 180 }) 181 queries.append ({ 182 'cmd': """ 183 SELECT vba.*, %s::text AS match_type 184 FROM 185 dem.lnk_identity2ext_id li2ext_id, 186 dem.v_active_persons vba 187 WHERE 188 vba.pk_identity = li2ext_id.id_identity and lower(li2ext_id.external_id) ~* lower(%s) 189 ORDER BY 190 lastnames, firstnames, dob 191 """, 192 'args': [_('external patient ID'), tmp] 193 }) 194 return queries 195 196 # "<d igi ts>" - DOB or patient PK 197 if regex.match("^(\d|\s|\t)+$", raw, flags = regex.UNICODE): 198 _log.debug("[%s]: a DOB or PK" % raw) 199 queries.append ({ 200 'cmd': "SELECT *, %s::text AS match_type FROM dem.v_active_persons WHERE dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) ORDER BY lastnames, firstnames, dob", 201 'args': [_('date of birth'), raw.replace(',', '.')] 202 }) 203 tmp = raw.replace(' ', '') 204 tmp = tmp.replace('\t', '') 205 queries.append ({ 206 'cmd': "SELECT *, %s::text AS match_type FROM dem.v_active_persons WHERE pk_identity LIKE %s%%", 207 'args': [_('internal patient ID'), tmp] 208 }) 209 return queries 210 211 # "#<di git s>" - GNUmed patient PK 212 if regex.match("^(\s|\t)*#(\d|\s|\t)+$", raw, flags = regex.UNICODE): 213 _log.debug("[%s]: a PK or external ID" % raw) 214 tmp = raw.replace('#', '') 215 tmp = tmp.strip() 216 tmp = tmp.replace(' ', '') 217 tmp = tmp.replace('\t', '') 218 # this seemingly stupid query ensures the PK actually exists 219 queries.append ({ 220 'cmd': "SELECT *, %s::text AS match_type FROM dem.v_active_persons WHERE pk_identity = %s ORDER BY lastnames, firstnames, dob", 221 'args': [_('internal patient ID'), tmp] 222 }) 223 # but might also be an external ID 224 tmp = raw.replace('#', '') 225 tmp = tmp.strip() 226 tmp = tmp.replace(' ', '***DUMMY***') 227 tmp = tmp.replace('\t', '***DUMMY***') 228 tmp = tmp.replace('***DUMMY***', '(\s|\t|-|/)*') 229 queries.append ({ 230 'cmd': """ 231 SELECT vba.*, %s::text AS match_type FROM dem.lnk_identity2ext_id li2ext_id, dem.v_active_persons vba 232 WHERE vba.pk_identity = li2ext_id.id_identity and lower(li2ext_id.external_id) ~* lower(%s) 233 ORDER BY lastnames, firstnames, dob""", 234 'args': [_('external patient ID'), tmp] 235 }) 236 return queries 237 238 # "#<di/git s or c-hars>" - external ID 239 if regex.match("^(\s|\t)*#.+$", raw, flags = regex.UNICODE): 240 _log.debug("[%s]: an external ID" % raw) 241 tmp = raw.replace('#', '') 242 tmp = tmp.strip() 243 tmp = tmp.replace(' ', '***DUMMY***') 244 tmp = tmp.replace('\t', '***DUMMY***') 245 tmp = tmp.replace('-', '***DUMMY***') 246 tmp = tmp.replace('/', '***DUMMY***') 247 tmp = tmp.replace('***DUMMY***', '(\s|\t|-|/)*') 248 queries.append ({ 249 'cmd': """ 250 SELECT 251 vba.*, 252 %s::text AS match_type 253 FROM 254 dem.lnk_identity2ext_id li2ext_id, 255 dem.v_active_persons vba 256 WHERE 257 vba.pk_identity = li2ext_id.id_identity 258 AND 259 lower(li2ext_id.external_id) ~* lower(%s) 260 ORDER BY 261 lastnames, firstnames, dob""", 262 'args': [_('external patient ID'), tmp] 263 }) 264 return queries 265 266 # digits interspersed with "./-" or blank space - DOB 267 if regex.match("^(\s|\t)*\d+(\s|\t|\.|\-|/)*\d+(\s|\t|\.|\-|/)*\d+(\s|\t|\.)*$", raw, flags = regex.UNICODE): 268 _log.debug("[%s]: a DOB" % raw) 269 tmp = raw.strip() 270 while '\t\t' in tmp: tmp = tmp.replace('\t\t', ' ') 271 while ' ' in tmp: tmp = tmp.replace(' ', ' ') 272 # apparently not needed due to PostgreSQL smarts... 273 #tmp = tmp.replace('-', '.') 274 #tmp = tmp.replace('/', '.') 275 queries.append ({ 276 'cmd': "SELECT *, %s AS match_type FROM dem.v_active_persons WHERE dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) ORDER BY lastnames, firstnames, dob", 277 'args': [_('date of birth'), tmp.replace(',', '.')] 278 }) 279 return queries 280 281 # " , <alpha>" - first name 282 if regex.match("^(\s|\t)*,(\s|\t)*([^0-9])+(\s|\t)*$", raw, flags = regex.UNICODE): 283 _log.debug("[%s]: a firstname" % raw) 284 tmp = self._normalize_soundalikes(raw[1:].strip()) 285 cmd = """ 286 SELECT DISTINCT ON (pk_identity) * FROM ( 287 SELECT *, %s AS match_type FROM (( 288 SELECT d_vap.* 289 FROM dem.names, dem.v_active_persons d_vap 290 WHERE dem.names.firstnames ~ %s and d_vap.pk_identity = dem.names.id_identity 291 ) union all ( 292 SELECT d_vap.* 293 FROM dem.names, dem.v_active_persons d_vap 294 WHERE dem.names.firstnames ~ %s and d_vap.pk_identity = dem.names.id_identity 295 )) AS super_list ORDER BY lastnames, firstnames, dob 296 ) AS sorted_list""" 297 queries.append ({ 298 'cmd': cmd, 299 'args': [_('first name'), '^' + gmTools.capitalize(tmp, mode=gmTools.CAPS_NAMES), '^' + tmp] 300 }) 301 return queries 302 303 # "*|$<...>" - DOB 304 if regex.match("^(\s|\t)*(\*|\$).+$", raw, flags = regex.UNICODE): 305 _log.debug("[%s]: a DOB" % raw) 306 tmp = raw.replace('*', '') 307 tmp = tmp.replace('$', '') 308 queries.append ({ 309 'cmd': "SELECT *, %s AS match_type FROM dem.v_active_persons WHERE dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) ORDER BY lastnames, firstnames, dob", 310 'args': [_('date of birth'), tmp.replace(',', '.')] 311 }) 312 return queries 313 314 return queries # = []
315 #-------------------------------------------------------- 316 # generic, locale independant queries 317 #--------------------------------------------------------
318 - def _generate_queries_from_dto(self, dto = None):
319 """Generate generic queries. 320 321 - not locale dependant 322 - data -> firstnames, lastnames, dob, gender 323 """ 324 _log.debug('_generate_queries_from_dto("%s")' % dto) 325 326 if not isinstance(dto, gmPerson.cDTO_person): 327 return None 328 329 vals = [_('name, gender, date of birth')] 330 where_snippets = [] 331 332 vals.append(dto.firstnames) 333 where_snippets.append('firstnames=%s') 334 vals.append(dto.lastnames) 335 where_snippets.append('lastnames=%s') 336 337 if dto.dob is not None: 338 vals.append(dto.dob) 339 #where_snippets.append(u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)") 340 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s)") 341 342 if dto.gender is not None: 343 vals.append(dto.gender) 344 where_snippets.append('gender=%s') 345 346 # sufficient data ? 347 if len(where_snippets) == 0: 348 _log.error('invalid search dict structure') 349 _log.debug(data) 350 return None 351 352 cmd = """ 353 SELECT *, %%s AS match_type FROM dem.v_active_persons 354 WHERE pk_identity in ( 355 SELECT id_identity FROM dem.names WHERE %s 356 ) ORDER BY lastnames, firstnames, dob""" % ' and '.join(where_snippets) 357 358 queries = [ 359 {'cmd': cmd, 'args': vals} 360 ] 361 362 # shall we mogrify name parts ? probably not 363 364 return queries
365 #-------------------------------------------------------- 366 # queries for DE 367 #--------------------------------------------------------
368 - def _generate_queries_de(self, search_term=None):
369 370 if search_term is None: 371 return [] 372 373 # check to see if we get away with a simple query ... 374 queries = self._generate_simple_query(search_term) 375 if len(queries) > 0: 376 _log.debug('[%s]: search term with a simple, unambigous structure' % search_term) 377 return queries 378 379 # no we don't 380 _log.debug('[%s]: not a search term with a simple, unambigous structure' % search_term) 381 382 search_term = search_term.strip().strip(',').strip(';').strip() 383 normalized = self._normalize_soundalikes(search_term) 384 385 queries = [] 386 387 # "<CHARS>" - single name part 388 # yes, I know, this is culture specific (did you read the docs ?) 389 if regex.match("^(\s|\t)*[a-zäöüßéáúóçøA-ZÄÖÜÇØ]+(\s|\t)*$", search_term, flags = regex.UNICODE): 390 _log.debug("[%s]: a single name part", search_term) 391 # there's no intermediate whitespace due to the regex 392 cmd = """ 393 SELECT DISTINCT ON (pk_identity) * FROM ( 394 SELECT * FROM (( 395 -- lastname 396 SELECT d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n 397 WHERE d_vap.pk_identity = n.id_identity and lower(n.lastnames) ~* lower(%s) 398 ) union all ( 399 -- firstname 400 SELECT d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n 401 WHERE d_vap.pk_identity = n.id_identity and lower(n.firstnames) ~* lower(%s) 402 ) union all ( 403 -- nickname 404 SELECT d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n 405 WHERE d_vap.pk_identity = n.id_identity and lower(n.preferred) ~* lower(%s) 406 ) union all ( 407 -- anywhere in name 408 SELECT 409 d_vap.*, 410 %s::text AS match_type 411 FROM 412 dem.v_active_persons d_vap, 413 dem.names n 414 WHERE 415 d_vap.pk_identity = n.id_identity 416 AND 417 lower(n.firstnames || ' ' || n.lastnames || ' ' || coalesce(n.preferred, '')) ~* lower(%s) 418 )) AS super_list ORDER BY lastnames, firstnames, dob 419 ) AS sorted_list 420 """ 421 tmp = normalized.strip() 422 args = [] 423 args.append(_('lastname')) 424 args.append('^' + tmp) 425 args.append(_('firstname')) 426 args.append('^' + tmp) 427 args.append(_('nickname')) 428 args.append('^' + tmp) 429 args.append(_('any name part')) 430 args.append(tmp) 431 432 queries.append ({ 433 'cmd': cmd, 434 'args': args 435 }) 436 return queries 437 438 # try to split on (major) part separators 439 parts_list = regex.split(",|;", normalized) 440 441 # ignore empty parts 442 parts_list = [ p.strip() for p in parts_list if p.strip() != '' ] 443 444 # only one "major" part ? (i.e. no ",;" ?) 445 if len(parts_list) == 1: 446 # re-split on whitespace 447 sub_parts_list = regex.split("\s*|\t*", normalized) 448 # ignore empty parts 449 sub_parts_list = [ p.strip() for p in sub_parts_list if p.strip() != '' ] 450 451 # parse into name/date parts 452 date_count = 0 453 name_parts = [] 454 for part in sub_parts_list: 455 # skip empty parts 456 if part.strip() == '': 457 continue 458 # any digit signifies a date 459 # FIXME: what about "<40" ? 460 if regex.search("\d", part, flags = regex.UNICODE): 461 date_count = date_count + 1 462 date_part = part 463 else: 464 name_parts.append(part) 465 466 # exactly 2 words ? 467 if len(sub_parts_list) == 2: 468 # no date = "first last" or "last first" 469 if date_count == 0: 470 # assumption: first last 471 queries.append ({ 472 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and n.firstnames ~ %s AND n.lastnames ~ %s", 473 'args': [_('name: first-last'), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES)] 474 }) 475 queries.append ({ 476 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and lower(n.firstnames) ~* lower(%s) AND lower(n.lastnames) ~* lower(%s)", 477 'args': [_('name: first-last'), '^' + name_parts[0], '^' + name_parts[1]] 478 }) 479 # assumption: last first 480 queries.append ({ 481 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and n.firstnames ~ %s AND n.lastnames ~ %s", 482 'args': [_('name: last-first'), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES)] 483 }) 484 queries.append ({ 485 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and lower(n.firstnames) ~* lower(%s) AND lower(n.lastnames) ~* lower(%s)", 486 'args': [_('name: last-first'), '^' + name_parts[1], '^' + name_parts[0]] 487 }) 488 # assumption: last nick 489 queries.append ({ 490 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and n.preferred ~ %s AND n.lastnames ~ %s", 491 'args': [_('name: last-nick'), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES)] 492 }) 493 queries.append ({ 494 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and lower(n.preferred) ~* lower(%s) AND lower(n.lastnames) ~* lower(%s)", 495 'args': [_('name: last-nick'), '^' + name_parts[1], '^' + name_parts[0]] 496 }) 497 # name parts anywhere inside name - third order query ... 498 queries.append ({ 499 'cmd': """SELECT DISTINCT ON (id_identity) 500 d_vap.*, 501 %s::text AS match_type 502 FROM 503 dem.v_active_persons d_vap, 504 dem.names n 505 WHERE 506 d_vap.pk_identity = n.id_identity 507 AND 508 -- name_parts[0] 509 lower(n.firstnames || ' ' || n.lastnames) ~* lower(%s) 510 AND 511 -- name_parts[1] 512 lower(n.firstnames || ' ' || n.lastnames) ~* lower(%s)""", 513 'args': [_('name'), name_parts[0], name_parts[1]] 514 }) 515 return queries 516 # FIXME: either "name date" or "date date" 517 _log.error("don't know how to generate queries for [%s]" % search_term) 518 return queries 519 520 # exactly 3 words ? 521 if len(sub_parts_list) == 3: 522 # special case: 3 words, exactly 1 of them a date, no ",;" 523 if date_count == 1: 524 # assumption: first, last, dob - first order 525 queries.append ({ 526 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and n.firstnames ~ %s AND n.lastnames ~ %s AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 527 'args': [_('names: first-last, date of birth'), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES), date_part.replace(',', '.')] 528 }) 529 queries.append ({ 530 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and lower(n.firstnames) ~* lower(%s) AND lower(n.lastnames) ~* lower(%s) AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 531 'args': [_('names: first-last, date of birth'), '^' + name_parts[0], '^' + name_parts[1], date_part.replace(',', '.')] 532 }) 533 # assumption: last, first, dob - second order query 534 queries.append ({ 535 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and n.firstnames ~ %s AND n.lastnames ~ %s AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 536 'args': [_('names: last-first, date of birth'), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES), date_part.replace(',', '.')] 537 }) 538 queries.append ({ 539 'cmd': "SELECT DISTINCT ON (id_identity) d_vap.*, %s::text AS match_type FROM dem.v_active_persons d_vap, dem.names n WHERE d_vap.pk_identity = n.id_identity and lower(n.firstnames) ~* lower(%s) AND lower(n.lastnames) ~* lower(%s) AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 540 'args': [_('names: last-first, dob'), '^' + name_parts[1], '^' + name_parts[0], date_part.replace(',', '.')] 541 }) 542 # name parts anywhere in name - third order query ... 543 queries.append ({ 544 'cmd': """SELECT DISTINCT ON (id_identity) 545 d_vap.*, 546 %s::text AS match_type 547 FROM 548 dem.v_active_persons d_vap, 549 dem.names n 550 WHERE 551 d_vap.pk_identity = n.id_identity 552 AND 553 lower(n.firstnames || ' ' || n.lastnames) ~* lower(%s) 554 AND 555 lower(n.firstnames || ' ' || n.lastnames) ~* lower(%s) 556 AND 557 dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) 558 """, 559 'args': [_('name, date of birth'), name_parts[0], name_parts[1], date_part.replace(',', '.')] 560 }) 561 return queries 562 # FIXME: "name name name" or "name date date" 563 queries.append(self._generate_dumb_brute_query(search_term)) 564 return queries 565 566 # FIXME: no ',;' but neither "name name" nor "name name date" 567 queries.append(self._generate_dumb_brute_query(search_term)) 568 return queries 569 570 # more than one major part (separated by ';,') 571 else: 572 # parse into name and date parts 573 date_parts = [] 574 name_parts = [] 575 name_count = 0 576 for part in parts_list: 577 if part.strip() == '': 578 continue 579 # any digits ? 580 if regex.search("\d+", part, flags = regex.UNICODE): 581 # FIXME: parse out whitespace *not* adjacent to a *word* 582 date_parts.append(part) 583 else: 584 tmp = part.strip() 585 tmp = regex.split("\s*|\t*", tmp) 586 name_count = name_count + len(tmp) 587 name_parts.append(tmp) 588 589 where_parts = [] 590 # first, handle name parts 591 # special case: "<date(s)>, <name> <name>, <date(s)>" 592 if (len(name_parts) == 1) and (name_count == 2): 593 # usually "first last" 594 where_parts.append ({ 595 'conditions': "firstnames ~ %s and lastnames ~ %s", 596 'args': [_('names: first last'), '^' + gmTools.capitalize(name_parts[0][0], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0][1], mode=gmTools.CAPS_NAMES)] 597 }) 598 where_parts.append ({ 599 'conditions': "lower(firstnames) ~* lower(%s) and lower(lastnames) ~* lower(%s)", 600 'args': [_('names: first last'), '^' + name_parts[0][0], '^' + name_parts[0][1]] 601 }) 602 # but sometimes "last first"" 603 where_parts.append ({ 604 'conditions': "firstnames ~ %s and lastnames ~ %s", 605 'args': [_('names: last, first'), '^' + gmTools.capitalize(name_parts[0][1], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0][0], mode=gmTools.CAPS_NAMES)] 606 }) 607 where_parts.append ({ 608 'conditions': "lower(firstnames) ~* lower(%s) and lower(lastnames) ~* lower(%s)", 609 'args': [_('names: last, first'), '^' + name_parts[0][1], '^' + name_parts[0][0]] 610 }) 611 # or even substrings anywhere in name 612 where_parts.append ({ 613 'conditions': "lower(firstnames || ' ' || lastnames) ~* lower(%s) OR lower(firstnames || ' ' || lastnames) ~* lower(%s)", 614 'args': [_('name'), name_parts[0][0], name_parts[0][1]] 615 }) 616 617 # special case: "<date(s)>, <name(s)>, <name(s)>, <date(s)>" 618 elif len(name_parts) == 2: 619 # usually "last, first" 620 where_parts.append ({ 621 'conditions': "firstnames ~ %s AND lastnames ~ %s", 622 'args': [_('name: last, first'), '^' + ' '.join(map(gmTools.capitalize, name_parts[1])), '^' + ' '.join(map(gmTools.capitalize, name_parts[0]))] 623 }) 624 where_parts.append ({ 625 'conditions': "lower(firstnames) ~* lower(%s) AND lower(lastnames) ~* lower(%s)", 626 'args': [_('name: last, first'), '^' + ' '.join(name_parts[1]), '^' + ' '.join(name_parts[0])] 627 }) 628 # but sometimes "first, last" 629 where_parts.append ({ 630 'conditions': "firstnames ~ %s AND lastnames ~ %s", 631 'args': [_('name: last, first'), '^' + ' '.join(map(gmTools.capitalize, name_parts[0])), '^' + ' '.join(map(gmTools.capitalize, name_parts[1]))] 632 }) 633 where_parts.append ({ 634 'conditions': "lower(firstnames) ~* lower(%s) AND lower(lastnames) ~* lower(%s)", 635 'args': [_('name: last, first'), '^' + ' '.join(name_parts[0]), '^' + ' '.join(name_parts[1])] 636 }) 637 # and sometimes "last, nick" 638 where_parts.append ({ 639 'conditions': "preferred ~ %s AND lastnames ~ %s", 640 'args': [_('name: last, first'), '^' + ' '.join(map(gmTools.capitalize, name_parts[1])), '^' + ' '.join(map(gmTools.capitalize, name_parts[0]))] 641 }) 642 where_parts.append ({ 643 'conditions': "lower(preferred) ~* lower(%s) AND lower(lastnames) ~* lower(%s)", 644 'args': [_('name: last, first'), '^' + ' '.join(name_parts[1]), '^' + ' '.join(name_parts[0])] 645 }) 646 647 # or even substrings anywhere in name 648 where_parts.append ({ 649 'conditions': "lower(firstnames || ' ' || lastnames) ~* lower(%s) AND lower(firstnames || ' ' || lastnames) ~* lower(%s)", 650 'args': [_('name'), ' '.join(name_parts[0]), ' '.join(name_parts[1])] 651 }) 652 653 # big trouble - arbitrary number of names 654 else: 655 # FIXME: deep magic, not sure of rationale ... 656 if len(name_parts) == 1: 657 for part in name_parts[0]: 658 where_parts.append ({ 659 'conditions': "lower(firstnames || ' ' || lastnames) ~* lower(%s)", 660 'args': [_('name'), part] 661 }) 662 else: 663 tmp = [] 664 for part in name_parts: 665 tmp.append(' '.join(part)) 666 for part in tmp: 667 where_parts.append ({ 668 'conditions': "lower(firstnames || ' ' || lastnames) ~* lower(%s)", 669 'args': [_('name'), part] 670 }) 671 672 # secondly handle date parts 673 # FIXME: this needs a considerable smart-up ! 674 if len(date_parts) == 1: 675 if len(where_parts) == 0: 676 where_parts.append ({ 677 'conditions': "dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 678 'args': [_('date of birth'), date_parts[0].replace(',', '.')] 679 }) 680 if len(where_parts) > 0: 681 where_parts[0]['conditions'] += " AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)" 682 where_parts[0]['args'].append(date_parts[0].replace(',', '.')) 683 where_parts[0]['args'][0] += ', ' + _('date of birth') 684 if len(where_parts) > 1: 685 where_parts[1]['conditions'] += " AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)" 686 where_parts[1]['args'].append(date_parts[0].replace(',', '.')) 687 where_parts[1]['args'][0] += ', ' + _('date of birth') 688 elif len(date_parts) > 1: 689 if len(where_parts) == 0: 690 where_parts.append ({ 691 'conditions': "dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) AND dem.date_trunc_utc('day'::text, dem.identity.deceased) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 692 'args': [_('date of birth/death'), date_parts[0].replace(',', '.'), date_parts[1].replace(',', '.')] 693 }) 694 if len(where_parts) > 0: 695 where_parts[0]['conditions'] += " AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) AND dem.date_trunc_utc('day'::text, dem.identity.deceased) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 696 where_parts[0]['args'].append(date_parts[0].replace(',', '.'), date_parts[1].replace(',', '.')) 697 where_parts[0]['args'][0] += ', ' + _('date of birth/death') 698 if len(where_parts) > 1: 699 where_parts[1]['conditions'] += " AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) AND dem.date_trunc_utc('day'::text, dem.identity.deceased) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 700 where_parts[1]['args'].append(date_parts[0].replace(',', '.'), date_parts[1].replace(',', '.')) 701 where_parts[1]['args'][0] += ', ' + _('date of birth/death') 702 703 # and finally generate the queries ... 704 for where_part in where_parts: 705 queries.append ({ 706 'cmd': "SELECT *, %%s::text AS match_type FROM dem.v_active_persons WHERE %s" % where_part['conditions'], 707 'args': where_part['args'] 708 }) 709 return queries 710 711 return []
712 #--------------------------------------------------------
713 - def _generate_dumb_brute_query(self, search_term=''):
714 715 _log.debug('_generate_dumb_brute_query("%s")' % search_term) 716 717 where_clause = '' 718 args = [] 719 # FIXME: split on more than just ' ' 720 for arg in search_term.strip().split(): 721 where_clause += " AND lower(coalesce(d_vap.title, '') || ' ' || d_vap.firstnames || ' ' || d_vap.lastnames) ~* lower(%s)" 722 args.append(arg) 723 724 query = """ 725 SELECT DISTINCT ON (pk_identity) * FROM ( 726 SELECT 727 d_vap.*, 728 '%s'::text AS match_type 729 FROM 730 dem.v_active_persons d_vap, 731 dem.names n 732 WHERE 733 d_vap.pk_identity = n.id_identity 734 %s 735 ORDER BY 736 lastnames, 737 firstnames, 738 dob 739 ) AS ordered_list""" % (_('full name'), where_clause) 740 741 return ({'cmd': query, 'args': args})
742 743 #============================================================
744 -def ask_for_patient():
745 """Text mode UI function to ask for patient.""" 746 747 person_searcher = cPatientSearcher_SQL() 748 749 while True: 750 search_fragment = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 751 752 if search_fragment in ['exit', 'quit', 'bye', None]: 753 print("user cancelled patient search") 754 return None 755 756 pats = person_searcher.get_patients(search_term = search_fragment) 757 758 if (pats is None) or (len(pats) == 0): 759 print("No patient matches the search term.") 760 print("") 761 continue 762 763 if len(pats) > 1: 764 print("Several patients match the search term:") 765 print("") 766 for pat in pats: 767 print(pat) 768 print("") 769 print("Please refine the search term so it matches one patient only.") 770 continue 771 772 return pats[0] 773 774 return None
775 776 #============================================================ 777 # main/testing 778 #============================================================ 779 if __name__ == '__main__': 780 781 if len(sys.argv) == 1: 782 sys.exit() 783 784 if sys.argv[1] != 'test': 785 sys.exit() 786 787 import datetime 788 789 gmI18N.activate_locale() 790 gmI18N.install_domain() 791 gmDateTime.init() 792 793 #--------------------------------------------------------
794 - def test_search_by_dto():
795 dto = gmPerson.cDTO_person() 796 dto.firstnames = 'Sigrid' 797 dto.lastnames = 'Kiesewetter' 798 dto.gender = 'female' 799 # dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone) 800 dto.dob = datetime.datetime(1939,6,24,23,0,0,0,gmDateTime.gmCurrentLocalTimezone) 801 print(dto) 802 803 searcher = cPatientSearcher_SQL() 804 pats = searcher.get_patients(dto = dto) 805 print(pats)
806 #--------------------------------------------------------
807 - def test_patient_search_queries():
808 searcher = cPatientSearcher_SQL() 809 810 print("testing _normalize_soundalikes()") 811 print("--------------------------------") 812 # FIXME: support Ähler -> Äler and Dähler -> Däler 813 data = ['Krüger', 'Krueger', 'Kruger', 'Überle', 'Böger', 'Boger', 'Öder', 'Ähler', 'Däler', 'Großer', 'müller', 'Özdemir', 'özdemir'] 814 for name in data: 815 print('%s: %s' % (name, searcher._normalize_soundalikes(name))) 816 817 input('press [ENTER] to continue') 818 print("============") 819 820 print("testing _generate_simple_query()") 821 print("----------------------------") 822 data = ['51234', '1 134 153', '#13 41 34', '#3-AFY322.4', '22-04-1906', '1235/32/3525', ' , johnny'] 823 for fragment in data: 824 print("fragment:", fragment) 825 qs = searcher._generate_simple_query(fragment) 826 for q in qs: 827 print(" match on:", q['args'][0]) 828 print(" query :", q['cmd']) 829 input('press [ENTER] to continue') 830 print("============") 831 832 print("testing _generate_queries_from_dto()") 833 print("------------------------------------") 834 dto = cDTO_person() 835 dto.gender = 'm' 836 dto.lastnames = 'Kirk' 837 dto.firstnames = 'James' 838 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone) 839 q = searcher._generate_queries_from_dto(dto)[0] 840 print("dto:", dto) 841 print(" match on:", q['args'][0]) 842 print(" query:", q['cmd']) 843 844 input('press [ENTER] to continue') 845 print("============") 846 847 print("testing _generate_queries_de()") 848 print("------------------------------") 849 qs = searcher._generate_queries_de('Kirk, James') 850 for q in qs: 851 print(" match on:", q['args'][0]) 852 print(" query :", q['cmd']) 853 print(" args :", q['args']) 854 input('press [ENTER] to continue') 855 print("============") 856 857 qs = searcher._generate_queries_de('müller') 858 for q in qs: 859 print(" match on:", q['args'][0]) 860 print(" query :", q['cmd']) 861 print(" args :", q['args']) 862 input('press [ENTER] to continue') 863 print("============") 864 865 qs = searcher._generate_queries_de('özdemir') 866 for q in qs: 867 print(" match on:", q['args'][0]) 868 print(" query :", q['cmd']) 869 print(" args :", q['args']) 870 input('press [ENTER] to continue') 871 print("============") 872 873 qs = searcher._generate_queries_de('Özdemir') 874 for q in qs: 875 print(" match on:", q['args'][0]) 876 print(" query :", q['cmd']) 877 print(" args :", q['args']) 878 input('press [ENTER] to continue') 879 print("============") 880 881 print("testing _generate_dumb_brute_query()") 882 print("------------------------------------") 883 q = searcher._generate_dumb_brute_query('Kirk, James Tiberius') 884 print(" match on:", q['args'][0]) 885 print(" query:", q['cmd']) 886 print(" args:", q['args']) 887 888 input('press [ENTER] to continue')
889 #--------------------------------------------------------
890 - def test_ask_for_patient():
891 while 1: 892 myPatient = ask_for_patient() 893 if myPatient is None: 894 break 895 print("ID ", myPatient.ID) 896 print("names ", myPatient.get_names()) 897 print("addresses:", myPatient.get_addresses(address_type='home')) 898 print("recent birthday:", myPatient.dob_in_range()) 899 myPatient.export_as_gdt(filename='apw.gdt', encoding = 'cp850')
900 # docs = myPatient.get_document_folder() 901 # print "docs ", docs 902 # emr = myPatient.emr 903 # print "EMR ", emr 904 #-------------------------------------------------------- 905 #test_patient_search_queries() 906 #test_ask_for_patient() 907 test_search_by_dto() 908 909 #============================================================ 910