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 return queries 376 377 # no we don't 378 _log.debug('[%s]: not a search term with a "suggestive" structure' % search_term) 379 380 search_term = search_term.strip().strip(u',').strip(u';').strip() 381 normalized = self._normalize_soundalikes(search_term) 382 383 queries = [] 384 385 # "<CHARS>" - single name part 386 # yes, I know, this is culture specific (did you read the docs ?) 387 if regex.match(u"^(\s|\t)*[a-zäöüßéáúóçøA-ZÄÖÜÇØ]+(\s|\t)*$", search_term, flags = regex.LOCALE | regex.UNICODE): 388 # there's no intermediate whitespace due to the regex 389 cmd = u""" 390 SELECT DISTINCT ON (pk_identity) * FROM ( 391 SELECT * FROM (( 392 SELECT 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.lastnames) ~* lower(%s) 393 ) union all ( 394 -- first name 395 SELECT 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) 396 ) union all ( 397 -- anywhere in name 398 SELECT 399 vbp.*, 400 %s::text AS match_type 401 FROM 402 dem.v_basic_person vbp, 403 dem.names n 404 WHERE 405 vbp.pk_identity = n.id_identity 406 AND 407 lower(n.firstnames || ' ' || n.lastnames || ' ' || coalesce(n.preferred, '')) ~* lower(%s) 408 )) AS super_list ORDER BY lastnames, firstnames, dob 409 ) AS sorted_list""" 410 tmp = normalized.strip() 411 args = [] 412 args.append(_('last name')) 413 args.append('^' + tmp) 414 args.append(_('first name')) 415 args.append('^' + tmp) 416 args.append(_('any name part')) 417 args.append(tmp) 418 419 queries.append ({ 420 'cmd': cmd, 421 'args': args 422 }) 423 return queries 424 425 # try to split on (major) part separators 426 parts_list = regex.split(u",|;", normalized) 427 428 # ignore empty parts 429 parts_list = [ p.strip() for p in parts_list if p.strip() != u'' ] 430 431 # only one "major" part ? (i.e. no ",;" ?) 432 if len(parts_list) == 1: 433 # re-split on whitespace 434 sub_parts_list = regex.split(u"\s*|\t*", normalized) 435 # ignore empty parts 436 sub_parts_list = [ p.strip() for p in sub_parts_list if p.strip() != u'' ] 437 438 # parse into name/date parts 439 date_count = 0 440 name_parts = [] 441 for part in sub_parts_list: 442 # skip empty parts 443 if part.strip() == u'': 444 continue 445 # any digit signifies a date 446 # FIXME: what about "<40" ? 447 if regex.search(u"\d", part, flags = regex.LOCALE | regex.UNICODE): 448 date_count = date_count + 1 449 date_part = part 450 else: 451 name_parts.append(part) 452 453 # exactly 2 words ? 454 if len(sub_parts_list) == 2: 455 # no date = "first last" or "last first" 456 if date_count == 0: 457 # assumption: first last 458 queries.append ({ 459 '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", 460 'args': [_('name: first-last'), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES)] 461 }) 462 queries.append ({ 463 '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)", 464 'args': [_('name: first-last'), '^' + name_parts[0], '^' + name_parts[1]] 465 }) 466 # assumption: last first 467 queries.append ({ 468 '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", 469 'args': [_('name: last-first'), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES)] 470 }) 471 queries.append ({ 472 '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)", 473 'args': [_('name: last-first'), '^' + name_parts[1], '^' + name_parts[0]] 474 }) 475 # name parts anywhere in name - third order query ... 476 queries.append ({ 477 'cmd': u"""SELECT DISTINCT ON (id_identity) 478 vbp.*, 479 %s::text AS match_type 480 FROM 481 dem.v_basic_person vbp, 482 dem.names n 483 WHERE 484 vbp.pk_identity = n.id_identity 485 AND 486 -- name_parts[0] 487 lower(n.firstnames || ' ' || n.lastnames) ~* lower(%s) 488 AND 489 -- name_parts[1] 490 lower(n.firstnames || ' ' || n.lastnames) ~* lower(%s)""", 491 'args': [_('name'), name_parts[0], name_parts[1]] 492 }) 493 return queries 494 # FIXME: either "name date" or "date date" 495 _log.error("don't know how to generate queries for [%s]" % search_term) 496 return queries 497 498 # exactly 3 words ? 499 if len(sub_parts_list) == 3: 500 # special case: 3 words, exactly 1 of them a date, no ",;" 501 if date_count == 1: 502 # assumption: first, last, dob - first order 503 queries.append ({ 504 '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)", 505 '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'.')] 506 }) 507 queries.append ({ 508 '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)", 509 'args': [_('names: first-last, date of birth'), '^' + name_parts[0], '^' + name_parts[1], date_part.replace(u',', u'.')] 510 }) 511 # assumption: last, first, dob - second order query 512 queries.append ({ 513 '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)", 514 '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'.')] 515 }) 516 queries.append ({ 517 '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)", 518 'args': [_('names: last-first, dob'), '^' + name_parts[1], '^' + name_parts[0], date_part.replace(u',', u'.')] 519 }) 520 # name parts anywhere in name - third order query ... 521 queries.append ({ 522 'cmd': u"""SELECT DISTINCT ON (id_identity) 523 vbp.*, 524 %s::text AS match_type 525 FROM 526 dem.v_basic_person vbp, 527 dem.names n 528 WHERE 529 vbp.pk_identity = n.id_identity 530 AND 531 lower(n.firstnames || ' ' || n.lastnames) ~* lower(%s) 532 AND 533 lower(n.firstnames || ' ' || n.lastnames) ~* lower(%s) 534 AND 535 dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone) 536 """, 537 'args': [_('name, date of birth'), name_parts[0], name_parts[1], date_part.replace(u',', u'.')] 538 }) 539 return queries 540 # FIXME: "name name name" or "name date date" 541 queries.append(self._generate_dumb_brute_query(search_term)) 542 return queries 543 544 # FIXME: no ',;' but neither "name name" nor "name name date" 545 queries.append(self._generate_dumb_brute_query(search_term)) 546 return queries 547 548 # more than one major part (separated by ';,') 549 else: 550 # parse into name and date parts 551 date_parts = [] 552 name_parts = [] 553 name_count = 0 554 for part in parts_list: 555 if part.strip() == u'': 556 continue 557 # any digits ? 558 if regex.search(u"\d+", part, flags = regex.LOCALE | regex.UNICODE): 559 # FIXME: parse out whitespace *not* adjacent to a *word* 560 date_parts.append(part) 561 else: 562 tmp = part.strip() 563 tmp = regex.split(u"\s*|\t*", tmp) 564 name_count = name_count + len(tmp) 565 name_parts.append(tmp) 566 567 where_parts = [] 568 # first, handle name parts 569 # special case: "<date(s)>, <name> <name>, <date(s)>" 570 if (len(name_parts) == 1) and (name_count == 2): 571 # usually "first last" 572 where_parts.append ({ 573 'conditions': u"firstnames ~ %s and lastnames ~ %s", 574 'args': [_('names: first last'), '^' + gmTools.capitalize(name_parts[0][0], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0][1], mode=gmTools.CAPS_NAMES)] 575 }) 576 where_parts.append ({ 577 'conditions': u"lower(firstnames) ~* lower(%s) and lower(lastnames) ~* lower(%s)", 578 'args': [_('names: first last'), '^' + name_parts[0][0], '^' + name_parts[0][1]] 579 }) 580 # but sometimes "last first"" 581 where_parts.append ({ 582 'conditions': u"firstnames ~ %s and lastnames ~ %s", 583 'args': [_('names: last, first'), '^' + gmTools.capitalize(name_parts[0][1], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0][0], mode=gmTools.CAPS_NAMES)] 584 }) 585 where_parts.append ({ 586 'conditions': u"lower(firstnames) ~* lower(%s) and lower(lastnames) ~* lower(%s)", 587 'args': [_('names: last, first'), '^' + name_parts[0][1], '^' + name_parts[0][0]] 588 }) 589 # or even substrings anywhere in name 590 where_parts.append ({ 591 'conditions': u"lower(firstnames || ' ' || lastnames) ~* lower(%s) OR lower(firstnames || ' ' || lastnames) ~* lower(%s)", 592 'args': [_('name'), name_parts[0][0], name_parts[0][1]] 593 }) 594 595 # special case: "<date(s)>, <name(s)>, <name(s)>, <date(s)>" 596 elif len(name_parts) == 2: 597 # usually "last, first" 598 where_parts.append ({ 599 'conditions': u"firstnames ~ %s AND lastnames ~ %s", 600 'args': [_('name: last, first'), '^' + ' '.join(map(gmTools.capitalize, name_parts[1])), '^' + ' '.join(map(gmTools.capitalize, name_parts[0]))] 601 }) 602 where_parts.append ({ 603 'conditions': u"lower(firstnames) ~* lower(%s) AND lower(lastnames) ~* lower(%s)", 604 'args': [_('name: last, first'), '^' + ' '.join(name_parts[1]), '^' + ' '.join(name_parts[0])] 605 }) 606 # but sometimes "first, last" 607 where_parts.append ({ 608 'conditions': u"firstnames ~ %s AND lastnames ~ %s", 609 'args': [_('name: last, first'), '^' + ' '.join(map(gmTools.capitalize, name_parts[0])), '^' + ' '.join(map(gmTools.capitalize, name_parts[1]))] 610 }) 611 where_parts.append ({ 612 'conditions': u"lower(firstnames) ~* lower(%s) AND lower(lastnames) ~* lower(%s)", 613 'args': [_('name: last, first'), '^' + ' '.join(name_parts[0]), '^' + ' '.join(name_parts[1])] 614 }) 615 # or even substrings anywhere in name 616 where_parts.append ({ 617 'conditions': u"lower(firstnames || ' ' || lastnames) ~* lower(%s) AND lower(firstnames || ' ' || lastnames) ~* lower(%s)", 618 'args': [_('name'), ' '.join(name_parts[0]), ' '.join(name_parts[1])] 619 }) 620 621 # big trouble - arbitrary number of names 622 else: 623 # FIXME: deep magic, not sure of rationale ... 624 if len(name_parts) == 1: 625 for part in name_parts[0]: 626 where_parts.append ({ 627 'conditions': u"lower(firstnames || ' ' || lastnames) ~* lower(%s)", 628 'args': [_('name'), part] 629 }) 630 else: 631 tmp = [] 632 for part in name_parts: 633 tmp.append(' '.join(part)) 634 for part in tmp: 635 where_parts.append ({ 636 'conditions': u"lower(firstnames || ' ' || lastnames) ~* lower(%s)", 637 'args': [_('name'), part] 638 }) 639 640 # secondly handle date parts 641 # FIXME: this needs a considerable smart-up ! 642 if len(date_parts) == 1: 643 if len(where_parts) == 0: 644 where_parts.append ({ 645 'conditions': u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 646 'args': [_('date of birth'), date_parts[0].replace(u',', u'.')] 647 }) 648 if len(where_parts) > 0: 649 where_parts[0]['conditions'] += u" AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)" 650 where_parts[0]['args'].append(date_parts[0].replace(u',', u'.')) 651 where_parts[0]['args'][0] += u', ' + _('date of birth') 652 if len(where_parts) > 1: 653 where_parts[1]['conditions'] += u" AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)" 654 where_parts[1]['args'].append(date_parts[0].replace(u',', u'.')) 655 where_parts[1]['args'][0] += u', ' + _('date of birth') 656 elif len(date_parts) > 1: 657 if len(where_parts) == 0: 658 where_parts.append ({ 659 'conditions': u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp witih time zone) AND dem.date_trunc_utc('day'::text, dem.identity.deceased) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 660 'args': [_('date of birth/death'), date_parts[0].replace(u',', u'.'), date_parts[1].replace(u',', u'.')] 661 }) 662 if len(where_parts) > 0: 663 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)", 664 where_parts[0]['args'].append(date_parts[0].replace(u',', u'.'), date_parts[1].replace(u',', u'.')) 665 where_parts[0]['args'][0] += u', ' + _('date of birth/death') 666 if len(where_parts) > 1: 667 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)", 668 where_parts[1]['args'].append(date_parts[0].replace(u',', u'.'), date_parts[1].replace(u',', u'.')) 669 where_parts[1]['args'][0] += u', ' + _('date of birth/death') 670 671 # and finally generate the queries ... 672 for where_part in where_parts: 673 queries.append ({ 674 'cmd': u"SELECT *, %%s::text AS match_type FROM dem.v_basic_person WHERE %s" % where_part['conditions'], 675 'args': where_part['args'] 676 }) 677 return queries 678 679 return []
680 #--------------------------------------------------------
681 - def _generate_dumb_brute_query(self, search_term=''):
682 683 _log.debug('_generate_dumb_brute_query("%s")' % search_term) 684 685 where_clause = '' 686 args = [] 687 # FIXME: split on more than just ' ' 688 for arg in search_term.strip().split(): 689 where_clause += u" AND lower(coalesce(vbp.title, '') || ' ' || vbp.firstnames || ' ' || vbp.lastnames) ~* lower(%s)" 690 args.append(arg) 691 692 query = u""" 693 SELECT DISTINCT ON (pk_identity) * FROM ( 694 SELECT 695 vbp.*, 696 '%s'::text AS match_type 697 FROM 698 dem.v_basic_person vbp, 699 dem.names n 700 WHERE 701 vbp.pk_identity = n.id_identity 702 %s 703 ORDER BY 704 lastnames, 705 firstnames, 706 dob 707 ) AS ordered_list""" % (_('full name'), where_clause) 708 709 return ({'cmd': query, 'args': args})
710 #============================================================
711 -def ask_for_patient():
712 """Text mode UI function to ask for patient.""" 713 714 person_searcher = cPatientSearcher_SQL() 715 716 while True: 717 search_fragment = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 718 719 if search_fragment in ['exit', 'quit', 'bye', None]: 720 print "user cancelled patient search" 721 return None 722 723 pats = person_searcher.get_patients(search_term = search_fragment) 724 725 if (pats is None) or (len(pats) == 0): 726 print "No patient matches the query term." 727 print "" 728 continue 729 730 if len(pats) > 1: 731 print "Several patients match the query term:" 732 print "" 733 for pat in pats: 734 print pat 735 print "" 736 continue 737 738 return pats[0] 739 740 return None
741 #============================================================ 742 # main/testing 743 #============================================================ 744 if __name__ == '__main__': 745 746 if len(sys.argv) == 1: 747 sys.exit() 748 749 if sys.argv[1] != 'test': 750 sys.exit() 751 752 import datetime 753 754 gmI18N.activate_locale() 755 gmI18N.install_domain() 756 gmDateTime.init() 757 758 #--------------------------------------------------------
759 - def test_search_by_dto():
760 dto = gmPerson.cDTO_person() 761 dto.firstnames = 'Sigrid' 762 dto.lastnames = 'Kiesewetter' 763 dto.gender = 'female' 764 # dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone) 765 dto.dob = datetime.datetime(1939,6,24,23,0,0,0,gmDateTime.gmCurrentLocalTimezone) 766 print dto 767 768 searcher = cPatientSearcher_SQL() 769 pats = searcher.get_patients(dto = dto) 770 print pats
771 #--------------------------------------------------------
772 - def test_patient_search_queries():
773 searcher = cPatientSearcher_SQL() 774 775 print "testing _normalize_soundalikes()" 776 print "--------------------------------" 777 # FIXME: support Ähler -> Äler and Dähler -> Däler 778 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'] 779 for name in data: 780 print '%s: %s' % (name, searcher._normalize_soundalikes(name)) 781 782 raw_input('press [ENTER] to continue') 783 print "============" 784 785 print "testing _generate_simple_query()" 786 print "----------------------------" 787 data = ['51234', '1 134 153', '#13 41 34', '#3-AFY322.4', '22-04-1906', '1235/32/3525', ' , johnny'] 788 for fragment in data: 789 print "fragment:", fragment 790 qs = searcher._generate_simple_query(fragment) 791 for q in qs: 792 print " match on:", q['args'][0] 793 print " query :", q['cmd'] 794 raw_input('press [ENTER] to continue') 795 print "============" 796 797 print "testing _generate_queries_from_dto()" 798 print "------------------------------------" 799 dto = cDTO_person() 800 dto.gender = 'm' 801 dto.lastnames = 'Kirk' 802 dto.firstnames = 'James' 803 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone) 804 q = searcher._generate_queries_from_dto(dto)[0] 805 print "dto:", dto 806 print " match on:", q['args'][0] 807 print " query:", q['cmd'] 808 809 raw_input('press [ENTER] to continue') 810 print "============" 811 812 print "testing _generate_queries_de()" 813 print "------------------------------" 814 qs = searcher._generate_queries_de('Kirk, James') 815 for q in qs: 816 print " match on:", q['args'][0] 817 print " query :", q['cmd'] 818 print " args :", q['args'] 819 raw_input('press [ENTER] to continue') 820 print "============" 821 822 qs = searcher._generate_queries_de(u'müller') 823 for q in qs: 824 print " match on:", q['args'][0] 825 print " query :", q['cmd'] 826 print " args :", q['args'] 827 raw_input('press [ENTER] to continue') 828 print "============" 829 830 qs = searcher._generate_queries_de(u'özdemir') 831 for q in qs: 832 print " match on:", q['args'][0] 833 print " query :", q['cmd'] 834 print " args :", q['args'] 835 raw_input('press [ENTER] to continue') 836 print "============" 837 838 qs = searcher._generate_queries_de(u'Özdemir') 839 for q in qs: 840 print " match on:", q['args'][0] 841 print " query :", q['cmd'] 842 print " args :", q['args'] 843 raw_input('press [ENTER] to continue') 844 print "============" 845 846 print "testing _generate_dumb_brute_query()" 847 print "------------------------------------" 848 q = searcher._generate_dumb_brute_query('Kirk, James Tiberius') 849 print " match on:", q['args'][0] 850 print " query:", q['cmd'] 851 print " args:", q['args'] 852 853 raw_input('press [ENTER] to continue')
854 #--------------------------------------------------------
855 - def test_ask_for_patient():
856 while 1: 857 myPatient = ask_for_patient() 858 if myPatient is None: 859 break 860 print "ID ", myPatient.ID 861 print "names ", myPatient.get_names() 862 print "addresses:", myPatient.get_addresses(address_type='home') 863 print "recent birthday:", myPatient.dob_in_range() 864 myPatient.export_as_gdt(filename='apw.gdt', encoding = 'cp850')
865 # docs = myPatient.get_document_folder() 866 # print "docs ", docs 867 # emr = myPatient.get_emr() 868 # print "EMR ", emr 869 #-------------------------------------------------------- 870 #test_patient_search_queries() 871 #test_ask_for_patient() 872 test_search_by_dto() 873 874 #============================================================ 875