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

# Source Code for Module Gnumed.business.gmPersonSearch

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