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

Source Code for Module Gnumed.business.gmPersonSearch

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