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