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 queries.append ({ 176 '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", 177 'args': [_('date of birth'), tmp.replace(',', '.')] 178 }) 179 queries.append ({ 180 'cmd': u""" 181 SELECT vba.*, %s::text AS match_type FROM dem.lnk_identity2ext_id li2ext_id, dem.v_basic_person vba 182 WHERE vba.pk_identity = li2ext_id.id_identity and lower(li2ext_id.external_id) ~* lower(%s) 183 ORDER BY lastnames, firstnames, dob""", 184 'args': [_('external patient ID'), tmp] 185 }) 186 return queries 187 188 # "<d igi ts>" - DOB or patient PK 189 if regex.match(u"^(\d|\s|\t)+$", raw, flags = regex.LOCALE | regex.UNICODE): 190 _log.debug("[%s]: a DOB or PK" % raw) 191 queries.append ({ 192 '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", 193 'args': [_('date of birth'), raw.replace(',', '.')] 194 }) 195 tmp = raw.replace(u' ', u'') 196 tmp = tmp.replace(u'\t', u'') 197 queries.append ({ 198 'cmd': u"SELECT *, %s::text AS match_type FROM dem.v_basic_person WHERE pk_identity LIKE %s%%", 199 'args': [_('internal patient ID'), tmp] 200 }) 201 return queries 202 203 # "#<di git s>" - GNUmed patient PK 204 if regex.match(u"^(\s|\t)*#(\d|\s|\t)+$", raw, flags = regex.LOCALE | regex.UNICODE): 205 _log.debug("[%s]: a PK or external ID" % raw) 206 tmp = raw.replace(u'#', u'') 207 tmp = tmp.strip() 208 tmp = tmp.replace(u' ', u'') 209 tmp = tmp.replace(u'\t', u'') 210 # this seemingly stupid query ensures the PK actually exists 211 queries.append ({ 212 'cmd': u"SELECT *, %s::text AS match_type FROM dem.v_basic_person WHERE pk_identity = %s ORDER BY lastnames, firstnames, dob", 213 'args': [_('internal patient ID'), tmp] 214 }) 215 # but might also be an external ID 216 tmp = raw.replace(u'#', u'') 217 tmp = tmp.strip() 218 tmp = tmp.replace(u' ', u'***DUMMY***') 219 tmp = tmp.replace(u'\t', u'***DUMMY***') 220 tmp = tmp.replace(u'***DUMMY***', u'(\s|\t|-|/)*') 221 queries.append ({ 222 'cmd': u""" 223 SELECT vba.*, %s::text AS match_type FROM dem.lnk_identity2ext_id li2ext_id, dem.v_basic_person vba 224 WHERE vba.pk_identity = li2ext_id.id_identity and lower(li2ext_id.external_id) ~* lower(%s) 225 ORDER BY lastnames, firstnames, dob""", 226 'args': [_('external patient ID'), tmp] 227 }) 228 return queries 229 230 # "#<di/git s or c-hars>" - external ID (or PUPIC) 231 if regex.match(u"^(\s|\t)*#.+$", raw, flags = regex.LOCALE | regex.UNICODE): 232 _log.debug("[%s]: an external ID" % raw) 233 tmp = raw.replace(u'#', u'') 234 tmp = tmp.strip() 235 tmp = tmp.replace(u' ', u'***DUMMY***') 236 tmp = tmp.replace(u'\t', u'***DUMMY***') 237 tmp = tmp.replace(u'-', u'***DUMMY***') 238 tmp = tmp.replace(u'/', u'***DUMMY***') 239 tmp = tmp.replace(u'***DUMMY***', u'(\s|\t|-|/)*') 240 queries.append ({ 241 'cmd': u""" 242 SELECT 243 vba.*, 244 %s::text AS match_type 245 FROM 246 dem.lnk_identity2ext_id li2ext_id, 247 dem.v_basic_person vba 248 WHERE 249 vba.pk_identity = li2ext_id.id_identity 250 AND 251 lower(li2ext_id.external_id) ~* lower(%s) 252 ORDER BY 253 lastnames, firstnames, dob""", 254 'args': [_('external patient ID'), tmp] 255 }) 256 return queries 257 258 # digits interspersed with "./-" or blank space - DOB 259 if regex.match(u"^(\s|\t)*\d+(\s|\t|\.|\-|/)*\d+(\s|\t|\.|\-|/)*\d+(\s|\t|\.)*$", raw, flags = regex.LOCALE | regex.UNICODE): 260 _log.debug("[%s]: a DOB" % raw) 261 tmp = raw.strip() 262 while u'\t\t' in tmp: tmp = tmp.replace(u'\t\t', u' ') 263 while u' ' in tmp: tmp = tmp.replace(u' ', u' ') 264 # apparently not needed due to PostgreSQL smarts... 265 #tmp = tmp.replace('-', '.') 266 #tmp = tmp.replace('/', '.') 267 queries.append ({ 268 '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", 269 'args': [_('date of birth'), tmp.replace(',', '.')] 270 }) 271 return queries 272 273 # " , <alpha>" - first name 274 if regex.match(u"^(\s|\t)*,(\s|\t)*([^0-9])+(\s|\t)*$", raw, flags = regex.LOCALE | regex.UNICODE): 275 _log.debug("[%s]: a firstname" % raw) 276 tmp = self._normalize_soundalikes(raw[1:].strip()) 277 cmd = u""" 278 SELECT DISTINCT ON (pk_identity) * FROM ( 279 SELECT *, %s AS match_type FROM (( 280 SELECT vbp.* 281 FROM dem.names, dem.v_basic_person vbp 282 WHERE dem.names.firstnames ~ %s and vbp.pk_identity = dem.names.id_identity 283 ) union all ( 284 SELECT vbp.* 285 FROM dem.names, dem.v_basic_person vbp 286 WHERE dem.names.firstnames ~ %s and vbp.pk_identity = dem.names.id_identity 287 )) AS super_list ORDER BY lastnames, firstnames, dob 288 ) AS sorted_list""" 289 queries.append ({ 290 'cmd': cmd, 291 'args': [_('first name'), '^' + gmTools.capitalize(tmp, mode=gmTools.CAPS_NAMES), '^' + tmp] 292 }) 293 return queries 294 295 # "*|$<...>" - DOB 296 if regex.match(u"^(\s|\t)*(\*|\$).+$", raw, flags = regex.LOCALE | regex.UNICODE): 297 _log.debug("[%s]: a DOB" % raw) 298 tmp = raw.replace(u'*', u'') 299 tmp = tmp.replace(u'$', u'') 300 queries.append ({ 301 '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", 302 'args': [_('date of birth'), tmp.replace(u',', u'.')] 303 }) 304 return queries 305 306 return queries # = []
307 #-------------------------------------------------------- 308 # generic, locale independant queries 309 #--------------------------------------------------------
310 - def _generate_queries_from_dto(self, dto = None):
311 """Generate generic queries. 312 313 - not locale dependant 314 - data -> firstnames, lastnames, dob, gender 315 """ 316 _log.debug(u'_generate_queries_from_dto("%s")' % dto) 317 318 if not isinstance(dto, gmPerson.cDTO_person): 319 return None 320 321 vals = [_('name, gender, date of birth')] 322 where_snippets = [] 323 324 vals.append(dto.firstnames) 325 where_snippets.append(u'firstnames=%s') 326 vals.append(dto.lastnames) 327 where_snippets.append(u'lastnames=%s') 328 329 if dto.dob is not None: 330 vals.append(dto.dob) 331 #where_snippets.append(u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)") 332 where_snippets.append(u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s)") 333 334 if dto.gender is not None: 335 vals.append(dto.gender) 336 where_snippets.append('gender=%s') 337 338 # sufficient data ? 339 if len(where_snippets) == 0: 340 _log.error('invalid search dict structure') 341 _log.debug(data) 342 return None 343 344 cmd = u""" 345 SELECT *, %%s AS match_type FROM dem.v_basic_person 346 WHERE pk_identity in ( 347 SELECT id_identity FROM dem.names WHERE %s 348 ) ORDER BY lastnames, firstnames, dob""" % ' and '.join(where_snippets) 349 350 queries = [ 351 {'cmd': cmd, 'args': vals} 352 ] 353 354 # shall we mogrify name parts ? probably not 355 356 return queries
357 #-------------------------------------------------------- 358 # queries for DE 359 #--------------------------------------------------------
360 - def _generate_queries_de(self, search_term = None):
361 362 if search_term is None: 363 return [] 364 365 # check to see if we get away with a simple query ... 366 queries = self._generate_simple_query(search_term) 367 if len(queries) > 0: 368 return queries 369 370 # no we don't 371 _log.debug('[%s]: not a search term with a "suggestive" structure' % search_term) 372 373 search_term = search_term.strip(u',').strip(u';') 374 normalized = self._normalize_soundalikes(search_term) 375 376 queries = [] 377 378 # "<CHARS>" - single name part 379 # yes, I know, this is culture specific (did you read the docs ?) 380 if regex.match(u"^(\s|\t)*[a-zäöüßéáúóçøA-ZÄÖÜÇØ]+(\s|\t)*$", search_term, flags = regex.LOCALE | regex.UNICODE): 381 # there's no intermediate whitespace due to the regex 382 cmd = u""" 383 SELECT DISTINCT ON (pk_identity) * FROM ( 384 SELECT * FROM (( 385 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) 386 ) union all ( 387 -- first name 388 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) 389 ) union all ( 390 -- anywhere in name 391 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) 392 )) AS super_list ORDER BY lastnames, firstnames, dob 393 ) AS sorted_list""" 394 tmp = normalized.strip() 395 args = [] 396 args.append(_('last name')) 397 args.append('^' + tmp) 398 args.append(_('first name')) 399 args.append('^' + tmp) 400 args.append(_('any name part')) 401 args.append(tmp) 402 403 queries.append ({ 404 'cmd': cmd, 405 'args': args 406 }) 407 return queries 408 409 # try to split on (major) part separators 410 parts_list = regex.split(u",|;", normalized) 411 412 # ignore empty parts 413 parts_list = [ p.strip() for p in parts_list if p.strip() != u'' ] 414 415 # only one "major" part ? (i.e. no ",;" ?) 416 if len(parts_list) == 1: 417 # re-split on whitespace 418 sub_parts_list = regex.split(u"\s*|\t*", normalized) 419 420 # parse into name/date parts 421 date_count = 0 422 name_parts = [] 423 for part in sub_parts_list: 424 # any digit signifies a date 425 # FIXME: what about "<40" ? 426 if regex.search(u"\d", part, flags = regex.LOCALE | regex.UNICODE): 427 date_count = date_count + 1 428 date_part = part 429 else: 430 name_parts.append(part) 431 432 # exactly 2 words ? 433 if len(sub_parts_list) == 2: 434 # no date = "first last" or "last first" 435 if date_count == 0: 436 # assumption: first last 437 queries.append ({ 438 '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", 439 'args': [_('name: first-last'), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES)] 440 }) 441 queries.append ({ 442 '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)", 443 'args': [_('name: first-last'), '^' + name_parts[0], '^' + name_parts[1]] 444 }) 445 # assumption: last first 446 queries.append ({ 447 '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", 448 'args': [_('name: last-first'), '^' + gmTools.capitalize(name_parts[1], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0], mode=gmTools.CAPS_NAMES)] 449 }) 450 queries.append ({ 451 '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)", 452 'args': [_('name: last-first'), '^' + name_parts[1], '^' + name_parts[0]] 453 }) 454 # name parts anywhere in name - third order query ... 455 queries.append ({ 456 '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)", 457 'args': [_('name'), name_parts[0], name_parts[1]] 458 }) 459 return queries 460 # FIXME: either "name date" or "date date" 461 _log.error("don't know how to generate queries for [%s]" % search_term) 462 return queries 463 464 # exactly 3 words ? 465 if len(sub_parts_list) == 3: 466 # special case: 3 words, exactly 1 of them a date, no ",;" 467 if date_count == 1: 468 # assumption: first, last, dob - first order 469 queries.append ({ 470 '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)", 471 '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'.')] 472 }) 473 queries.append ({ 474 '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)", 475 'args': [_('names: first-last, date of birth'), '^' + name_parts[0], '^' + name_parts[1], date_part.replace(u',', u'.')] 476 }) 477 # assumption: last, first, dob - second order query 478 queries.append ({ 479 '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)", 480 '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'.')] 481 }) 482 queries.append ({ 483 '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)", 484 'args': [_('names: last-first, dob'), '^' + name_parts[1], '^' + name_parts[0], date_part.replace(u',', u'.')] 485 }) 486 # name parts anywhere in name - third order query ... 487 queries.append ({ 488 '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)", 489 'args': [_('name, date of birth'), name_parts[0], name_parts[1], date_part.replace(u',', u'.')] 490 }) 491 return queries 492 # FIXME: "name name name" or "name date date" 493 queries.append(self._generate_dumb_brute_query(search_term)) 494 return queries 495 496 # FIXME: no ',;' but neither "name name" nor "name name date" 497 queries.append(self._generate_dumb_brute_query(search_term)) 498 return queries 499 500 # more than one major part (separated by ';,') 501 else: 502 # parse into name and date parts 503 date_parts = [] 504 name_parts = [] 505 name_count = 0 506 for part in parts_list: 507 # any digits ? 508 if regex.search(u"\d+", part, flags = regex.LOCALE | regex.UNICODE): 509 # FIXME: parse out whitespace *not* adjacent to a *word* 510 date_parts.append(part) 511 else: 512 tmp = part.strip() 513 tmp = regex.split(u"\s*|\t*", tmp) 514 name_count = name_count + len(tmp) 515 name_parts.append(tmp) 516 517 where_parts = [] 518 # first, handle name parts 519 # special case: "<date(s)>, <name> <name>, <date(s)>" 520 if (len(name_parts) == 1) and (name_count == 2): 521 # usually "first last" 522 where_parts.append ({ 523 'conditions': u"firstnames ~ %s and lastnames ~ %s", 524 'args': [_('names: first last'), '^' + gmTools.capitalize(name_parts[0][0], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0][1], mode=gmTools.CAPS_NAMES)] 525 }) 526 where_parts.append ({ 527 'conditions': u"lower(firstnames) ~* lower(%s) and lower(lastnames) ~* lower(%s)", 528 'args': [_('names: first last'), '^' + name_parts[0][0], '^' + name_parts[0][1]] 529 }) 530 # but sometimes "last first"" 531 where_parts.append ({ 532 'conditions': u"firstnames ~ %s and lastnames ~ %s", 533 'args': [_('names: last, first'), '^' + gmTools.capitalize(name_parts[0][1], mode=gmTools.CAPS_NAMES), '^' + gmTools.capitalize(name_parts[0][0], mode=gmTools.CAPS_NAMES)] 534 }) 535 where_parts.append ({ 536 'conditions': u"lower(firstnames) ~* lower(%s) and lower(lastnames) ~* lower(%s)", 537 'args': [_('names: last, first'), '^' + name_parts[0][1], '^' + name_parts[0][0]] 538 }) 539 # or even substrings anywhere in name 540 where_parts.append ({ 541 'conditions': u"lower(firstnames || lastnames) ~* lower(%s) OR lower(firstnames || lastnames) ~* lower(%s)", 542 'args': [_('name'), name_parts[0][0], name_parts[0][1]] 543 }) 544 545 # special case: "<date(s)>, <name(s)>, <name(s)>, <date(s)>" 546 elif len(name_parts) == 2: 547 # usually "last, first" 548 where_parts.append ({ 549 'conditions': u"firstnames ~ %s AND lastnames ~ %s", 550 'args': [_('name: last, first'), '^' + ' '.join(map(gmTools.capitalize, name_parts[1])), '^' + ' '.join(map(gmTools.capitalize, name_parts[0]))] 551 }) 552 where_parts.append ({ 553 'conditions': u"lower(firstnames) ~* lower(%s) AND lower(lastnames) ~* lower(%s)", 554 'args': [_('name: last, first'), '^' + ' '.join(name_parts[1]), '^' + ' '.join(name_parts[0])] 555 }) 556 # but sometimes "first, last" 557 where_parts.append ({ 558 'conditions': u"firstnames ~ %s AND lastnames ~ %s", 559 'args': [_('name: last, first'), '^' + ' '.join(map(gmTools.capitalize, name_parts[0])), '^' + ' '.join(map(gmTools.capitalize, name_parts[1]))] 560 }) 561 where_parts.append ({ 562 'conditions': u"lower(firstnames) ~* lower(%s) AND lower(lastnames) ~* lower(%s)", 563 'args': [_('name: last, first'), '^' + ' '.join(name_parts[0]), '^' + ' '.join(name_parts[1])] 564 }) 565 # or even substrings anywhere in name 566 where_parts.append ({ 567 'conditions': u"lower(firstnames || lastnames) ~* lower(%s) AND lower(firstnames || lastnames) ~* lower(%s)", 568 'args': [_('name'), ' '.join(name_parts[0]), ' '.join(name_parts[1])] 569 }) 570 571 # big trouble - arbitrary number of names 572 else: 573 # FIXME: deep magic, not sure of rationale ... 574 if len(name_parts) == 1: 575 for part in name_parts[0]: 576 where_parts.append ({ 577 'conditions': u"lower(firstnames || lastnames) ~* lower(%s)", 578 'args': [_('name'), part] 579 }) 580 else: 581 tmp = [] 582 for part in name_parts: 583 tmp.append(' '.join(part)) 584 for part in tmp: 585 where_parts.append ({ 586 'conditions': u"lower(firstnames || lastnames) ~* lower(%s)", 587 'args': [_('name'), part] 588 }) 589 590 # secondly handle date parts 591 # FIXME: this needs a considerable smart-up ! 592 if len(date_parts) == 1: 593 if len(where_parts) == 0: 594 where_parts.append ({ 595 'conditions': u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)", 596 'args': [_('date of birth'), date_parts[0].replace(u',', u'.')] 597 }) 598 if len(where_parts) > 0: 599 where_parts[0]['conditions'] += u" AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)" 600 where_parts[0]['args'].append(date_parts[0].replace(u',', u'.')) 601 where_parts[0]['args'][0] += u', ' + _('date of birth') 602 if len(where_parts) > 1: 603 where_parts[1]['conditions'] += u" AND dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %s::timestamp with time zone)" 604 where_parts[1]['args'].append(date_parts[0].replace(u',', u'.')) 605 where_parts[1]['args'][0] += u', ' + _('date of birth') 606 elif len(date_parts) > 1: 607 if len(where_parts) == 0: 608 where_parts.append ({ 609 '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)", 610 'args': [_('date of birth/death'), date_parts[0].replace(u',', u'.'), date_parts[1].replace(u',', u'.')] 611 }) 612 if len(where_parts) > 0: 613 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)", 614 where_parts[0]['args'].append(date_parts[0].replace(u',', u'.'), date_parts[1].replace(u',', u'.')) 615 where_parts[0]['args'][0] += u', ' + _('date of birth/death') 616 if len(where_parts) > 1: 617 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)", 618 where_parts[1]['args'].append(date_parts[0].replace(u',', u'.'), date_parts[1].replace(u',', u'.')) 619 where_parts[1]['args'][0] += u', ' + _('date of birth/death') 620 621 # and finally generate the queries ... 622 for where_part in where_parts: 623 queries.append ({ 624 'cmd': u"SELECT *, %%s::text AS match_type FROM dem.v_basic_person WHERE %s" % where_part['conditions'], 625 'args': where_part['args'] 626 }) 627 return queries 628 629 return []
630 #--------------------------------------------------------
631 - def _generate_dumb_brute_query(self, search_term=''):
632 633 _log.debug('_generate_dumb_brute_query("%s")' % search_term) 634 635 where_clause = '' 636 args = [] 637 # FIXME: split on more than just ' ' 638 for arg in search_term.strip().split(): 639 where_clause += u" AND lower(coalesce(vbp.title, '') || vbp.firstnames || vbp.lastnames) ~* lower(%s)" 640 args.append(arg) 641 642 query = u""" 643 SELECT DISTINCT ON (pk_identity) * FROM ( 644 SELECT 645 vbp.*, 646 '%s'::text AS match_type 647 FROM 648 dem.v_basic_person vbp, 649 dem.names n 650 WHERE 651 vbp.pk_identity = n.id_identity 652 %s 653 ORDER BY 654 lastnames, 655 firstnames, 656 dob 657 ) AS ordered_list""" % (_('full name'), where_clause) 658 659 return ({'cmd': query, 'args': args})
660 #============================================================
661 -def ask_for_patient():
662 """Text mode UI function to ask for patient.""" 663 664 person_searcher = cPatientSearcher_SQL() 665 666 while True: 667 search_fragment = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 668 669 if search_fragment in ['exit', 'quit', 'bye', None]: 670 print "user cancelled patient search" 671 return None 672 673 pats = person_searcher.get_patients(search_term = search_fragment) 674 675 if (pats is None) or (len(pats) == 0): 676 print "No patient matches the query term." 677 print "" 678 continue 679 680 if len(pats) > 1: 681 print "Several patients match the query term:" 682 print "" 683 for pat in pats: 684 print pat 685 print "" 686 continue 687 688 return pats[0] 689 690 return None
691 #============================================================ 692 # main/testing 693 #============================================================ 694 if __name__ == '__main__': 695 696 if len(sys.argv) == 1: 697 sys.exit() 698 699 if sys.argv[1] != 'test': 700 sys.exit() 701 702 import datetime 703 704 gmI18N.activate_locale() 705 gmI18N.install_domain() 706 gmDateTime.init() 707 708 #--------------------------------------------------------
709 - def test_search_by_dto():
710 dto = gmPerson.cDTO_person() 711 dto.firstnames = 'Sigrid' 712 dto.lastnames = 'Kiesewetter' 713 dto.gender = 'female' 714 # dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone) 715 dto.dob = datetime.datetime(1939,6,24,23,0,0,0,gmDateTime.gmCurrentLocalTimezone) 716 print dto 717 718 searcher = cPatientSearcher_SQL() 719 pats = searcher.get_patients(dto = dto) 720 print pats
721 #--------------------------------------------------------
722 - def test_patient_search_queries():
723 searcher = cPatientSearcher_SQL() 724 725 print "testing _normalize_soundalikes()" 726 print "--------------------------------" 727 # FIXME: support Ähler -> Äler and Dähler -> Däler 728 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'] 729 for name in data: 730 print '%s: %s' % (name, searcher._normalize_soundalikes(name)) 731 732 raw_input('press [ENTER] to continue') 733 print "============" 734 735 print "testing _generate_simple_query()" 736 print "----------------------------" 737 data = ['51234', '1 134 153', '#13 41 34', '#3-AFY322.4', '22-04-1906', '1235/32/3525', ' , johnny'] 738 for fragment in data: 739 print "fragment:", fragment 740 qs = searcher._generate_simple_query(fragment) 741 for q in qs: 742 print " match on:", q['args'][0] 743 print " query :", q['cmd'] 744 raw_input('press [ENTER] to continue') 745 print "============" 746 747 print "testing _generate_queries_from_dto()" 748 print "------------------------------------" 749 dto = cDTO_person() 750 dto.gender = 'm' 751 dto.lastnames = 'Kirk' 752 dto.firstnames = 'James' 753 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone) 754 q = searcher._generate_queries_from_dto(dto)[0] 755 print "dto:", dto 756 print " match on:", q['args'][0] 757 print " query:", q['cmd'] 758 759 raw_input('press [ENTER] to continue') 760 print "============" 761 762 print "testing _generate_queries_de()" 763 print "------------------------------" 764 qs = searcher._generate_queries_de('Kirk, James') 765 for q in qs: 766 print " match on:", q['args'][0] 767 print " query :", q['cmd'] 768 print " args :", q['args'] 769 raw_input('press [ENTER] to continue') 770 print "============" 771 772 qs = searcher._generate_queries_de(u'müller') 773 for q in qs: 774 print " match on:", q['args'][0] 775 print " query :", q['cmd'] 776 print " args :", q['args'] 777 raw_input('press [ENTER] to continue') 778 print "============" 779 780 qs = searcher._generate_queries_de(u'özdemir') 781 for q in qs: 782 print " match on:", q['args'][0] 783 print " query :", q['cmd'] 784 print " args :", q['args'] 785 raw_input('press [ENTER] to continue') 786 print "============" 787 788 qs = searcher._generate_queries_de(u'Özdemir') 789 for q in qs: 790 print " match on:", q['args'][0] 791 print " query :", q['cmd'] 792 print " args :", q['args'] 793 raw_input('press [ENTER] to continue') 794 print "============" 795 796 print "testing _generate_dumb_brute_query()" 797 print "------------------------------------" 798 q = searcher._generate_dumb_brute_query('Kirk, James Tiberius') 799 print " match on:", q['args'][0] 800 print " query:", q['cmd'] 801 print " args:", q['args'] 802 803 raw_input('press [ENTER] to continue')
804 #--------------------------------------------------------
805 - def test_ask_for_patient():
806 while 1: 807 myPatient = ask_for_patient() 808 if myPatient is None: 809 break 810 print "ID ", myPatient.ID 811 print "names ", myPatient.get_names() 812 print "addresses:", myPatient.get_addresses(address_type='home') 813 print "recent birthday:", myPatient.dob_in_range() 814 myPatient.export_as_gdt(filename='apw.gdt', encoding = 'cp850')
815 # docs = myPatient.get_document_folder() 816 # print "docs ", docs 817 # emr = myPatient.get_emr() 818 # print "EMR ", emr 819 #-------------------------------------------------------- 820 #test_patient_search_queries() 821 #test_ask_for_patient() 822 test_search_by_dto() 823 824 #============================================================ 825