Package Gnumed :: Package wxpython :: Module gmAuthWidgets |
[frames] | no frames]

# Source Code for Module Gnumed.wxpython.gmAuthWidgets

      1  # -*- coding: utf-8 -*-
      2  """GNUmed authentication widgets.
      3
      4  This module contains widgets and GUI
      5  functions for authenticating users.
      6  """
      7  #================================================================
      8  __author__ = "karsten.hilbert@gmx.net, H.Herb, H.Berger, R.Terry"
      9  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
     10
     11
     12  # stdlib
     13  import sys
     14  import os.path
     15  import logging
     16  import re as regex
     17
     18
     19  # 3rd party
     20  import wx
     21
     22
     23  # GNUmed
     24  if __name__ == '__main__':
     25          sys.path.insert(0, '../../')
     26  from Gnumed.pycommon import gmLoginInfo
     27  from Gnumed.pycommon import gmPG2
     28  from Gnumed.pycommon import gmBackendListener
     29  from Gnumed.pycommon import gmTools
     30  from Gnumed.pycommon import gmCfg2
     31  from Gnumed.pycommon import gmI18N
     32  from Gnumed.pycommon import gmLog2
     33
     34  from Gnumed.business import gmPraxis
     35
     36  from Gnumed.wxpython import gmGuiHelpers
     37  from Gnumed.wxpython import gmExceptionHandlingWidgets
     38
     39
     40  _log = logging.getLogger('gm.ui')
     41  _cfg = gmCfg2.gmCfgData()
     42
     43
     44  msg_generic = _("""
     45  GNUmed database version mismatch.
     46
     47  This database version cannot be used with this client:
     48
     49   client version: %s
     50   database version detected: %s
     51   database version needed: %s
     52
     53  Currently connected to database:
     54
     55   host: %s
     56   database: %s
     57   user: %s
     58  """)
     59
     60  msg_time_skew_fail = _("""\
     61  The server and client clocks are off
     62  by more than %s minutes !
     63
     64  You must fix the time settings before
     65  you can use this database with this
     66  client.
     67
     68  You may have to contact your
     69  administrator for help.""")
     70
     71  msg_time_skew_warn = _("""\
     72  The server and client clocks are off
     73  by more than %s minutes !
     74
     75  You should fix the time settings.
     76  Otherwise clinical data may appear to
     77  have been entered at the wrong time.
     78
     79  You may have to contact your
     80  administrator for help.""")
     81
     82  msg_insanity = _("""
     83  There is a serious problem with the database settings:
     84
     85  %s
     86
     87  You may have to contact your administrator for help.""")
     88
     89  msg_fail = _("""
     90  You must connect to a different database in order
     91  to use the GNUmed client. You may have to contact
     92  your administrator for help.""")
     93
     94  msg_override = _("""
     95  The client will, however, continue to start up because
     96  you are running a development/test version of GNUmed.
     97
     98  There may be schema related errors. Please report and/or
     99  fix them. Do not rely on this database to work properly
    100  in all cases !""")
    101
    102  #================================================================
    103  # convenience functions
    104  #----------------------------------------------------------------
    105 -def connect_to_database(max_attempts=3, expected_version=None, require_version=True):
    106          """Display the login dialog and try to log into the backend.
    107
    108          - up to max_attempts times
    109          - returns True/False
    110          """
    111          # force programmer to set a valid expected_version
    112          expected_hash = gmPG2.known_schema_hashes[expected_version]
    113          client_version = _cfg.get(option = 'client_version')
    114          global current_db_name
    115          current_db_name = 'gnumed_v%s' % expected_version
    116
    117          attempt = 0
    118
    119          dlg = cLoginDialog(None, -1, client_version = client_version)
    120          dlg.Centre(wx.BOTH)
    121
    122          while attempt < max_attempts:
    123
    124                  _log.debug('login attempt %s of %s', (attempt+1), max_attempts)
    125
    126                  connected = False
    127
    128                  dlg.ShowModal()
    129                  login = dlg.panel.GetLoginInfo()
    130                  if login is None:
    131                          _log.info("user cancelled login dialog")
    132                          break
    133
    134                  gmLog2.add_word2hide(login.password)
    135
    136                  # try getting a connection to verify the DSN works
    137                  dsn = gmPG2.make_psycopg2_dsn (
    138                          database = login.database,
    139                          host = login.host,
    140                          port = login.port,
    141                          user = login.user,
    142                          password = login.password
    143                  )
    144                  try:
    145                          conn = gmPG2.get_raw_connection(dsn = dsn, verbose = True, readonly = True)
    146                          connected = True
    147
    148                  except gmPG2.cAuthenticationError as e:
    149                          attempt += 1
    150                          _log.error("login attempt failed: %s", e)
    151                          if attempt < max_attempts:
    152                                  if ('host=127.0.0.1' in ('%s' % e)) or ('host=' not in ('%s' % e)):
    153                                          msg = _(
    154                                                  'Unable to connect to database:\n\n'
    155                                                  '%s\n\n'
    156                                                  "Are you sure you have got a local database installed ?\n"
    157                                                  '\n'
    158                                                  "Please retry with proper credentials or cancel.\n"
    159                                                  '\n'
    160                                                  ' (for the public and any new GNUmed data-\n'
    161                                                  '  bases the default user name and password\n'
    162                                                  '  are {any-doc, any-doc})\n'
    163                                                  '\n'
    164                                                  'You may also need to check the PostgreSQL client\n'
    165                                                  'authentication configuration in pg_hba.conf. For\n'
    166                                                  'details see:\n'
    167                                                  '\n'
    168                                                  'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL'
    169                                          )
    170                                  else:
    171                                          msg = _(
    172                                                  "Unable to connect to database:\n\n"
    173                                                  "%s\n\n"
    174                                                  "Please retry with proper credentials or cancel.\n"
    175                                                  "\n"
    176                                                  "For the public and any new GNUmed databases the\n"
    177                                                  "default user name and password are {any-doc, any-doc}.\n"
    178                                                  "\n"
    179                                                  'You may also need to check the PostgreSQL client\n'
    180                                                  'authentication configuration in pg_hba.conf. For\n'
    181                                                  'details see:\n'
    182                                                  '\n'
    183                                                  'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL'
    184                                          )
    185                                  msg = msg % e
    186                                  msg = regex.sub(r'password=[^\s]+', 'password=%s' % gmTools.u_replacement_character, msg)
    187                                  gmGuiHelpers.gm_show_error (
    188                                          msg,
    189                                          _('Connecting to backend')
    190                                  )
    191                          del e
    192                          continue
    193
    194                  except gmPG2.dbapi.OperationalError as exc:
    195                          _log.exception('login attempt failed')
    196                          gmPG2.log_pg_exception_details(exc)
    197                          msg = _(
    198                                  "Unable to connect to database:\n\n"
    199                                  "%s\n\n"
    200                                  "Please retry another backend / user / password combination !\n"
    201                                  "\n"
    202                                  " (for the public and any new GNUmed databases\n"
    203                                  "  the default user name and password are\n"
    204                                  "  {any-doc, any-doc})\n"
    205                                  "\n"
    206                          ) % exc
    207                          msg = regex.sub(r'password=[^\s]+', 'password=%s' % gmTools.u_replacement_character, msg)
    208                          gmGuiHelpers.gm_show_error(msg, _('Connecting to backend'))
    209                          del exc
    210                          continue
    211
    212                  conn.close()
    213
    214                  # connect was successful
    215                  gmPG2.set_default_login(login = login)
    216                  gmPG2.set_default_client_encoding(encoding = dlg.panel.backend_profile.encoding)
    217
    218                  seems_bootstrapped = gmPG2.schema_exists(schema = 'gm')
    219                  if not seems_bootstrapped:
    220                          _log.error('schema [gm] does not exist - database not bootstrapped ?')
    221                          msg = _(
    222                                  'The database you connected to does not seem\n'
    223                                  'to have been boostrapped properly.\n'
    224                                  '\n'
    225                                  'Make sure you have run the GNUmed database\n'
    226                                  'bootstrapper tool to create a new database.\n'
    227                                  '\n'
    228                                  'Further help can be found on the website at\n'
    229                                  '\n'
    230                                  '  http://wiki.gnumed.de\n'
    231                                  '\n'
    232                                  'or on the GNUmed mailing list.'
    233                          )
    234                          gmGuiHelpers.gm_show_error(msg, _('Verifying database'))
    235                          connected = False
    236                          break
    237
    238                  compatible = gmPG2.database_schema_compatible(version = expected_version)
    239                  if compatible or not require_version:
    240                          dlg.panel.save_state()
    241
    242                  if not compatible:
    243                          connected_db_version = gmPG2.get_schema_version()
    244                          msg = msg_generic % (
    245                                  client_version,
    246                                  connected_db_version,
    247                                  expected_version,
    248                                  gmTools.coalesce(login.host, '<localhost>'),
    249                                  login.database,
    250                                  login.user
    251                          )
    252                          if require_version:
    253                                  gmGuiHelpers.gm_show_error(msg + msg_fail, _('Verifying database version'))
    254                                  connected = False
    255                                  continue
    256                          gmGuiHelpers.gm_show_info(msg + msg_override, _('Verifying database version'))
    257
    258                  # FIXME: make configurable
    259                  max_skew = 1            # minutes
    260                  if _cfg.get(option = 'debug'):
    261                          max_skew = 10
    262                  if not gmPG2.sanity_check_time_skew(tolerance = (max_skew * 60)):
    263                          if _cfg.get(option = 'debug'):
    264                                  gmGuiHelpers.gm_show_warning(msg_time_skew_warn % max_skew, _('Verifying database settings'))
    265                          else:
    266                                  gmGuiHelpers.gm_show_error(msg_time_skew_fail % max_skew, _('Verifying database settings'))
    267                                  connected = False
    268                                  continue
    269
    270                  sanity_level, message = gmPG2.sanity_check_database_settings()
    271                  if sanity_level != 0:
    272                          gmGuiHelpers.gm_show_error((msg_insanity % message), _('Verifying database settings'))
    273                          if sanity_level == 2:
    274                                  connected = False
    275                                  continue
    276
    277                  gmExceptionHandlingWidgets.set_is_public_database(login.public_db)
    278                  gmExceptionHandlingWidgets.set_helpdesk(login.helpdesk)
    279
    280                  conn = gmPG2.get_connection(verbose = True, connection_name = 'GNUmed-[DbListenerThread]', pooled = False)
    281                  listener = gmBackendListener.gmBackendListener(conn = conn)
    282                  break
    283
    284          dlg.DestroyLater()
    285
    286          return connected
    287
    288  #================================================================
    289 -def get_dbowner_connection(procedure=None, dbo_password=None, dbo_account='gm-dbo'):
    290          if procedure is None:
    291                  procedure = _('<restricted procedure>')
    292
    293          # 1) get password for gm-dbo
    294          if dbo_password is None:
    295                  dbo_password = wx.GetPasswordFromUser (
    296                          message = _("""
    297   [%s]
    298
    299  This is a restricted procedure. We need the
    300  current password for the GNUmed database owner.
    301
    302  Please enter the current password for <%s>:""") % (
    303                                  procedure,
    304                                  dbo_account
    305                          ),
    306                          caption = procedure
    307                  )
    308                  if dbo_password == '':
    309                          return None
    310
    311          gmLog2.add_word2hide(dbo_password)
    312
    313          # 2) connect as gm-dbo
    314          login = gmPG2.get_default_login()
    315          dsn = gmPG2.make_psycopg2_dsn (
    316                  database = login.database,
    317                  host = login.host,
    318                  port = login.port,
    319                  user = dbo_account,
    320                  password = dbo_password
    321          )
    322          try:
    323                  conn = gmPG2.get_connection (
    324                          dsn = dsn,
    325                          readonly = False,
    326                          verbose = True,
    327                          pooled = False
    328                  )
    329          except:
    330                  _log.exception('cannot connect')
    331                  gmGuiHelpers.gm_show_error (
    332                          aMessage = _('Cannot connect as the GNUmed database owner <%s>.') % dbo_account,
    333                          aTitle = procedure
    334                  )
    335                  gmPG2.log_database_access(action = 'failed to connect as database owner for [%s]' % procedure)
    336                  return None
    337
    338          return conn
    339
    340  #================================================================
    341 -def change_gmdbowner_password():
    342
    343          title = _('Changing GNUmed database owner password')
    344
    345          dbo_account = wx.GetTextFromUser (
    346                  message = _("Enter the account name of the GNUmed database owner:"),
    347                  caption = title,
    348                  default_value = ''
    349          )
    350
    351          if dbo_account.strip() == '':
    352                  return False
    353
    354          dbo_conn = get_dbowner_connection (
    355                  procedure = title,
    356                  dbo_account = dbo_account
    357          )
    358          if dbo_conn is None:
    359                  return False
    360
    361          dbo_pwd_new_1 = wx.GetPasswordFromUser (
    362                  message = _("Enter the NEW password for the GNUmed database owner:"),
    363                  caption = title
    364          )
    365          if dbo_pwd_new_1.strip() == '':
    366                  return False
    367
    368          gmLog2.add_word2hide(dbo_pwd_new_1)
    369
    370          dbo_pwd_new_2 = wx.GetPasswordFromUser (
    371                  message = _("""Enter the NEW password for the GNUmed database owner, again.
    372
    373  (This will protect you from typos.)
    374                  """),
    375                  caption = title
    376          )
    377          if dbo_pwd_new_2.strip() == '':
    378                  return False
    379
    380          if dbo_pwd_new_1 != dbo_pwd_new_2:
    381                  return False
    382
    383          # pwd2 == pwd1 at this point so no need to hide (again)
    384
    385          """     On Mon, Mar 13, 2017 at 12:19:22PM -0400, Tom Lane wrote:
    386                  > Date: Mon, 13 Mar 2017 12:19:22 -0400
    387                  > From: Tom Lane <tgl@sss.pgh.pa.us>
    388                  > To: Adrian Klaver <adrian.klaver@aklaver.com>
    389                  > cc: Schmid Andreas <Andreas.Schmid@bd.so.ch>,
    390                  >  "'pgsql-general@postgresql.org'" <pgsql-general@postgresql.org>
    391                  > Subject: Re: [GENERAL] createuser: How to specify a database to connect to
    392                  >
    393                  > Adrian Klaver <adrian.klaver@aklaver.com> writes:
    394                  > > On 03/13/2017 08:52 AM, Tom Lane wrote:
    395                  > >> If by "history" you're worried about the server-side statement log, this
    396                  > >> is merest fantasy: the createuser program is not magic, it just constructs
    397                  > >> and sends a CREATE USER command for you.  You'd actually be more secure
    398                  > >> using psql, where (if you're superuser) you could shut off log_statement
    399                  > >> for your session first.
    400                  >
    401                  > > There is a difference though:
    402                  >
    403                  > > psql> CREATE USER:
    404                  >
    405                  > > postgres-2017-03-13 09:03:27.147 PDT-0LOG:  statement: create user
    406                  > > dummy_user with login password '1234';
    407                  >
    408                  > Well, what you're supposed to do is
    409                  >
    410                  > postgres=# create user dummy_user;
    411                  > postgres=# \password dummy_user
    412                  > Enter new password:
    413                  > Enter it again:
    414                  > postgres=#
    415                  >
    416                  > which will result in sending something like
    417                  >
    418                  > ALTER USER dummy_user PASSWORD 'md5c5e9567bc40082671d02c654260e0e09'
    419                  >
    420                  > You can additionally protect that by wrapping it into one transaction
    421                  > (if you have a setup where the momentary existence of the role without a
    422                  > password would be problematic) and/or shutting off logging beforehand.
    423          """
    424
    425          # this REALLY should be prefixed with md5 and the md5sum sent rather than the pwd
    426          cmd = """ALTER ROLE "%s" ENCRYPTED PASSWORD '%s';""" % (
    427                  dbo_account,
    428                  dbo_pwd_new_2
    429          )
    430          gmPG2.run_rw_queries(link_obj = dbo_conn, queries = [{'cmd': cmd}], end_tx = True)
    431
    432          return True
    433
    434  #================================================================
    435 -class cBackendProfile:
    436          pass
    437
    438  #================================================================
    439 -class cLoginDialog(wx.Dialog):
    440          """cLoginDialog - window holding cLoginPanel"""
    441
    442 -        def __init__(self, parent, id, title = _("Welcome to"), client_version = '*** unknown ***'):
    443                  wx.Dialog.__init__(self, parent, id, title)
    444                  self.panel = cLoginPanel(self, -1, isDialog=1, client_version = client_version)
    445                  self.Fit() # needed for Windoze.
    446                  self.Centre()
    447
    448                  self.SetIcon(gmTools.get_icon(wx = wx))
    449
    450  #================================================================
    451 -class cLoginPanel(wx.Panel):
    452          """GUI panel class that interactively gets Postgres login parameters.
    453
    454                  It features combo boxes which "remember" any number of
    455                  previously entered settings.
    456          """
    457 -        def __init__(self, parent, id,
    458                                          pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.TAB_TRAVERSAL,
    459                                          isDialog = 0, client_version = '*** unknown ***'):
    460                  """Create login panel.
    461
    462                  isDialog:       if this panel is the main panel of a dialog, the panel will
    463                                          resize the dialog automatically to display everything neatly
    464                                          if isDialog is set to True
    465                  """
    466                  wx.Panel.__init__(self, parent, id, pos, size, style)
    467                  self.parent = parent
    468
    469                  #True if dialog was cancelled by user
    470                  #if the dialog is closed manually, login should be cancelled
    471                  self.cancelled = True
    472
    473                  # True if this panel is displayed within a dialog (will resize the dialog automatically then)
    474                  self.isDialog = isDialog
    475
    476                  self.topsizer = wx.BoxSizer(wx.VERTICAL)
    477
    478                  # find bitmap
    479                  paths = gmTools.gmPaths(app_name = 'gnumed', wx = wx)
    480                  bitmap = os.path.join(paths.system_app_data_dir, 'bitmaps', 'gnumedlogo.png')
    481                  try:
    482                          png = wx.Image(bitmap, wx.BITMAP_TYPE_PNG).ConvertToBitmap()
    483                          bmp = wx.StaticBitmap(self, -1, png, wx.Point(10, 10), wx.Size(png.GetWidth(), png.GetHeight()))
    484                          self.topsizer.Add (
    485                                  bmp,
    486                                  proportion = 0,
    487                                  flag = wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER_HORIZONTAL|wx.ALL,
    488                                  border = 10
    489                          )
    490                  except:
    491                          self.topsizer.Add (
    492                                  wx.StaticText (
    493                                          self,
    494                                          -1,
    495                                          label = _("Cannot find image") + bitmap,
    496                                          style = wx.ALIGN_CENTRE
    497                                  ),
    498                                  proportion = 0,
    499                                  flag = wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
    500                                  border = 10
    501                          )
    502
    503                  paramsbox_caption = _('Workplace "%s" (version %s)') % (gmPraxis.gmCurrentPraxisBranch().active_workplace, client_version)
    504
    505                  # FIXME: why doesn't this align in the centre ?
    506                  self.paramsbox = wx.StaticBox( self, -1, paramsbox_caption, style = wx.ALIGN_CENTRE_HORIZONTAL)
    507                  self.paramsboxsizer = wx.StaticBoxSizer( self.paramsbox, wx.VERTICAL )
    508                  self.paramsbox.SetForegroundColour(wx.Colour(35, 35, 142))
    509                  self.paramsbox.SetFont(wx.Font(
    510                          pointSize = 12,
    511                          family = wx.SWISS,
    512                          style = wx.NORMAL,
    513                          weight = wx.BOLD,
    514                          underline = False
    515                  ))
    516                  self.pboxgrid = wx.FlexGridSizer(5, 2, 5, 5)
    517                  self.pboxgrid.AddGrowableCol(1)
    518
    519                  # PROFILE COMBO
    520                  label = wx.StaticText( self, -1, _('Log into'), wx.DefaultPosition, wx.DefaultSize, 0)
    521                  label.SetForegroundColour(wx.Colour(35, 35, 142))
    522                  self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
    523                  self.__backend_profiles = self.__get_backend_profiles()
    524                  self._CBOX_profile = wx.ComboBox (
    525                          self,
    526                          -1,
    527                          list(self.__backend_profiles.keys())[0],
    528                          wx.DefaultPosition,
    529                          size = wx.Size(550,-1),
    530                          choices = list(self.__backend_profiles.keys()),
    531                          style = wx.CB_READONLY
    532                  )
    533                  self.pboxgrid.Add (self._CBOX_profile, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
    534
    535                  # USER NAME COMBO
    536                  label = wx.StaticText( self, -1, _("Username"), wx.DefaultPosition, wx.DefaultSize, 0 )
    537                  label.SetForegroundColour(wx.Colour(35, 35, 142))
    538                  self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
    539                  self.__previously_used_accounts = self.__get_previously_used_accounts()
    540                  self._CBOX_user = wx.ComboBox (
    541                          self,
    542                          -1,
    543                          self.__previously_used_accounts[0],
    544                          wx.DefaultPosition,
    545                          wx.Size(150,-1),
    546                          self.__previously_used_accounts,
    547                          wx.CB_DROPDOWN
    548                  )
    549                  self.pboxgrid.Add( self._CBOX_user, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
    550
    551                  #PASSWORD TEXT ENTRY
    552                  label = wx.StaticText( self, -1, _("Password"), wx.DefaultPosition, wx.DefaultSize, 0 )
    553                  label.SetForegroundColour(wx.Colour(35, 35, 142))
    554                  self.pboxgrid.Add( label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
    555                  self.pwdentry = wx.TextCtrl( self, 1, '', wx.DefaultPosition, wx.Size(80,-1), wx.TE_PASSWORD )
    556                  # set focus on password entry
    557                  self.pwdentry.SetFocus()
    558                  self.pboxgrid.Add( self.pwdentry, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
    559
    560                  # --debug checkbox
    561                  label = wx.StaticText(self, -1, _('Options'), wx.DefaultPosition, wx.DefaultSize, 0)
    562                  label.SetForegroundColour(wx.Colour(35, 35, 142))
    563                  self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
    564                  self._CHBOX_debug = wx.CheckBox(self, -1, _('&Debug mode'))
    565                  self._CHBOX_debug.SetToolTip(_('Check this to run GNUmed client in debugging mode.'))
    566                  self.pboxgrid.Add(self._CHBOX_debug, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
    567
    568                  # --slave checkbox
    569                  label = wx.StaticText(self, -1, '', wx.DefaultPosition, wx.DefaultSize, 0)
    570                  label.SetForegroundColour(wx.Colour(35, 35, 142))
    571                  self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
    572                  self._CHBOX_slave = wx.CheckBox(self, -1, _('Enable &remote control'))
    573                  self._CHBOX_slave.SetToolTip(_('Check this to run GNUmed client in slave mode for remote control.'))
    574                  self.pboxgrid.Add(self._CHBOX_slave, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
    575
    576                  #----------------------------------------------------------------------
    577                  #new button code inserted rterry 06Sept02
    578                  #button order re-arraged to make it consistant with usual dialog format
    579                  #in most operating systems ie  btns ok and cancel are standard and
    580                  #in that order
    581                  #ie Order is now help, ok and cancel
    582                  #The order of creation is the tab order
    583                  #login-ok button automatically is the default when tabbing (or <enter>)
    584                  #from password
    585                  #this eliminates the heavy border when you use the default
    586                  #?is the default word needed for any other reason?
    587                  #----------------------------------------------------------------------
    588                  self.button_gridsizer = wx.GridSizer(1,3,0,0)
    589                  #---------------------
    590                  #1:create login ok button
    591                  #---------------------
    592                  ID_BUTTON_LOGIN = wx.NewId()
    593                  button_login_ok = wx.Button(self, ID_BUTTON_LOGIN, _("&Ok"), wx.DefaultPosition, wx.DefaultSize, 0 )
    594                  button_login_ok.SetToolTip(wx.ToolTip(_("Proceed with login.")) )
    595                  button_login_ok.SetDefault()
    596
    597                  #---------------------
    598                  #2:create cancel button
    599                  #---------------------
    600                  ID_BUTTON_CANCEL = wx.NewId()
    601                  button_cancel = wx.Button(self, ID_BUTTON_CANCEL, _("&Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 )
    602                  button_cancel.SetToolTip(wx.ToolTip(_("Cancel Login.")) )
    603                  #---------------------
    604                  #3:create Help button
    605                  #---------------------
    606                  ID_BUTTON_HELP = wx.NewId()
    607                  button_help = wx.Button(self, ID_BUTTON_HELP, _("&Help"), wx.DefaultPosition, wx.DefaultSize, 0 )
    608                  button_help.SetToolTip(wx.ToolTip(_("Help for login screen")))
    609                  #----------------------------
    610                  #Add buttons to the gridsizer
    611                  #----------------------------
    612                  self.button_gridsizer.Add (button_help,0,wx.EXPAND|wx.ALL,5)
    613                  self.button_gridsizer.Add (button_login_ok,0,wx.EXPAND|wx.ALL,5)
    614                  self.button_gridsizer.Add (button_cancel,0,wx.EXPAND|wx.ALL,5)
    615
    616                  self.paramsboxsizer.Add(self.pboxgrid, 1, wx.GROW|wx.ALL, 10)
    617                  self.topsizer.Add(self.paramsboxsizer, 1, wx.GROW|wx.ALL, 10)
    618                  self.topsizer.Add( self.button_gridsizer, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
    619
    620                  self.__load_state()
    621
    622                  self.SetAutoLayout(True)
    623                  self.SetSizer( self.topsizer)
    624                  self.topsizer.Fit( self )
    625                  if self.isDialog:
    626                          self.topsizer.SetSizeHints(parent)
    627
    628                  button_help.Bind(wx.EVT_BUTTON, self.OnHelp)
    629                  button_login_ok.Bind(wx.EVT_BUTTON, self.__on_login_button_pressed)
    630                  button_cancel.Bind(wx.EVT_BUTTON, self.OnCancel)
    631                  #wx.EVT_BUTTON(self, ID_BUTTON_HELP, self.OnHelp)
    632                  #wx.EVT_BUTTON(self, ID_BUTTON_LOGIN, self.__on_login_button_pressed)
    633                  #wx.EVT_BUTTON(self, ID_BUTTON_CANCEL, self.OnCancel)
    634
    635          #----------------------------------------------------------
    636          # internal helper methods
    637          #----------------------------------------------------------
    638 -        def __get_previously_used_accounts(self):
    639
    640                  accounts = gmTools.coalesce (
    641                          _cfg.get (
    642                                  group = 'backend',
    643                                  option = 'logins',
    644                                  source_order = [
    645                                          ('explicit', 'extend'),
    646                                          ('user', 'extend'),
    647                                          ('workbase', 'extend')
    648                                  ]
    649                          ),
    650                          ['any-doc']
    651                  )
    652                  # FIXME: make unique
    653
    654                  return accounts
    655          #----------------------------------------------------
    656 -        def __get_backend_profiles(self):
    657                  """Get server profiles from the configuration files.
    658
    659                  1) from system-wide file
    660                  2) from user file
    661
    662                  Profiles in the user file which have the same name
    663                  as a profile in the system file will override the
    664                  system file.
    665                  """
    666                  # find active profiles
    667                  src_order = [
    668                          ('explicit', 'extend'),
    669                          ('system', 'extend'),
    670                          ('user', 'extend'),
    671                          ('workbase', 'extend')
    672                  ]
    673
    674                  profile_names = gmTools.coalesce (
    675                          _cfg.get(group = 'backend', option = 'profiles', source_order = src_order),
    676                          []
    677                  )
    678
    679                  # find data for active profiles
    680                  src_order = [
    681                          ('explicit', 'return'),
    682                          ('workbase', 'return'),
    683                          ('user', 'return'),
    684                          ('system', 'return')
    685                  ]
    686
    687                  profiles = {}
    688
    689                  for profile_name in profile_names:
    690                          # FIXME: once the profile has been found always use the corresponding source !
    691                          # FIXME: maybe not or else we cannot override parts of the profile
    692                          profile = cBackendProfile()
    693                          profile_section = 'profile %s' % profile_name
    694
    695                          profile.name = profile_name
    696                          profile.host = gmTools.coalesce(_cfg.get(profile_section, 'host', src_order), '').strip()
    697                          port = gmTools.coalesce(_cfg.get(profile_section, 'port', src_order), 5432)
    698                          try:
    699                                  profile.port = int(port)
    700                                  if profile.port < 1024:
    701                                          raise ValueError('refusing to use priviledged port (< 1024)')
    702                          except ValueError:
    703                                  _log.warning('invalid port definition: [%s], skipping profile [%s]', port, profile_name)
    704                                  continue
    705                          profile.database = gmTools.coalesce(_cfg.get(profile_section, 'database', src_order), '').strip()
    706                          if profile.database == '':
    707                                  _log.warning('database name not specified, skipping profile [%s]', profile_name)
    708                                  continue
    709                          profile.encoding = gmTools.coalesce(_cfg.get(profile_section, 'encoding', src_order), 'UTF8')
    710                          profile.public_db = bool(_cfg.get(profile_section, 'public/open access', src_order))
    711                          profile.helpdesk = _cfg.get(profile_section, 'help desk', src_order)
    712
    713                          label = '%s (%s@%s)' % (profile_name, profile.database, profile.host)
    714                          profiles[label] = profile
    715
    716                  # sort out profiles with incompatible database versions if not --debug
    717                  # NOTE: this essentially hardcodes the database name in production ...
    718                  if not (_cfg.get(option = 'debug') or current_db_name.endswith('_devel')):
    719                          profiles2remove = []
    720                          for label in profiles:
    721                                  if profiles[label].database != current_db_name:
    722                                          profiles2remove.append(label)
    723                          for label in profiles2remove:
    724                                  del profiles[label]
    725
    726                  if len(profiles) == 0:
    727                          host = 'publicdb.gnumed.de'
    728                          label = 'public GNUmed database (%s@%s)' % (current_db_name, host)
    729                          profiles[label] = cBackendProfile()
    730                          profiles[label].name = label
    731                          profiles[label].host = host
    732                          profiles[label].port = 5432
    733                          profiles[label].database = current_db_name
    734                          profiles[label].encoding = 'UTF8'
    735                          profiles[label].public_db = True
    736                          profiles[label].helpdesk = 'http://wiki.gnumed.de'
    737
    738                  return profiles
    739          #----------------------------------------------------------
    740 -        def __load_state(self):
    741
    742                  src_order = [
    743                          ('explicit', 'return'),
    744                          ('user', 'return'),
    745                  ]
    746
    747                  self._CBOX_user.SetValue (
    748                          gmTools.coalesce (
    749                                  _cfg.get('preferences', 'login', src_order),
    750                                  self.__previously_used_accounts[0]
    751                          )
    752                  )
    753
    754                  last_used_profile_label = _cfg.get('preferences', 'profile', src_order)
    755                  if last_used_profile_label in self.__backend_profiles.keys():
    756                          self._CBOX_profile.SetValue(last_used_profile_label)
    757                  else:
    758                          self._CBOX_profile.SetValue(list(self.__backend_profiles.keys())[0])
    759
    760                  self._CHBOX_debug.SetValue(_cfg.get(option = 'debug'))
    761                  self._CHBOX_slave.SetValue(_cfg.get(option = 'slave'))
    762          #----------------------------------------------------
    763 -        def save_state(self):
    764                  """Save parameter settings to standard configuration file"""
    765                  prefs_name = _cfg.get(option = 'user_preferences_file')
    766                  _log.debug('saving login preferences in [%s]', prefs_name)
    767
    768                  gmCfg2.set_option_in_INI_file (
    769                          filename = prefs_name,
    770                          group = 'preferences',
    771                          option = 'login',
    772                          value = self._CBOX_user.GetValue()
    773                  )
    774
    775                  gmCfg2.set_option_in_INI_file (
    776                          filename = prefs_name,
    777                          group = 'preferences',
    778                          option = 'profile',
    779                          value = self._CBOX_profile.GetValue()
    780                  )
    781          #############################################################################
    782          # Retrieve current settings from user interface widgets
    783          #############################################################################
    784 -        def GetLoginInfo(self):
    785                  """convenience function for compatibility with gmLoginInfo.LoginInfo"""
    786                  if self.cancelled:
    787                          return None
    788
    789                  # FIXME: do not assume conf file is latin1 !
    790                  profile = self.__backend_profiles[self._CBOX_profile.GetValue().strip()]
    791                  _log.info('backend profile "%s" selected', profile.name)
    792                  _log.info(' details: <%s> on %s@%s:%s (%s, %s)',
    793                          self._CBOX_user.GetValue(),
    794                          profile.database,
    795                          profile.host,
    796                          profile.port,
    797                          profile.encoding,
    798                          gmTools.bool2subst(profile.public_db, 'public', 'private')
    799                  )
    800                  _log.info(' helpdesk: "%s"', profile.helpdesk)
    801                  login = gmLoginInfo.LoginInfo (
    802                          user = self._CBOX_user.GetValue(),
    803                          password = self.pwdentry.GetValue(),
    804                          host = profile.host,
    805                          database = profile.database,
    806                          port = profile.port
    807                  )
    808                  login.public_db = profile.public_db
    809                  login.helpdesk = profile.helpdesk
    810                  login.backend_profile = profile.name
    811                  return login
    812          #----------------------------
    813          # event handlers
    814          #----------------------------
    815 -        def OnHelp(self, event):
    816                  praxis = gmPraxis.gmCurrentPraxisBranch()
    817                  wx.MessageBox(_(
    818  """Unable to connect to the database ?
    819
    820   "PostgreSQL: FATAL:  password authentication failed ..."
    821
    822  The default user name and password are {any-doc, any-doc}
    823  for the public and any new GNUmed databases.
    824
    825   "... could not connect to server ..."
    826
    827  Mostly this is a case of new users who did not yet install
    828  or configure a PostgreSQL server and/or a GNUmed database
    829  of their own, which you must do before you can connect to
    830  anything other than the public demonstration database, see
    831
    832   http://wiki.gnumed.de/bin/view/Gnumed/GmManualServerInstall
    833
    834  For assistance on using GNUmed please consult the wiki:
    835
    836   http://wiki.gnumed.de/bin/view/Gnumed/GnumedManual
    837
    838  For more help than the above, please contact:
    839
    840   GNUmed Development List <gnumed-bugs@gnu.org>
    841
    842  For local assistance please contact:
    843
    844   %s""") % praxis.helpdesk,
    845                  caption = _('HELP for GNUmed main login screen'))
    846
    847          #----------------------------
    848 -        def __on_login_button_pressed(self, event):
    849
    850                  root_logger = logging.getLogger()
    851                  if self._CHBOX_debug.GetValue():
    852                          _log.info('debug mode enabled')
    853                          _cfg.set_option(option = 'debug', value = True)
    854                          root_logger.setLevel(logging.DEBUG)
    855                  else:
    856                          _log.info('debug mode disabled')
    857                          _cfg.set_option(option = 'debug', value = False)
    858                          if _cfg.get(option = '--quiet', source_order = [('cli', 'return')]):
    859                                  root_logger.setLevel(logging.ERROR)
    860                          else:
    861                                  root_logger.setLevel(logging.WARNING)
    862
    863                  if self._CHBOX_slave.GetValue():
    864                          _log.info('slave mode enabled')
    865                          _cfg.set_option(option = 'slave', value = True)
    866                  else:
    867                          _log.info('slave mode disabled')
    868                          _cfg.set_option(option = 'slave', value = False)
    869
    870                  self.backend_profile = self.__backend_profiles[self._CBOX_profile.GetValue().strip()]
    871  #               self.user = self._CBOX_user.GetValue().strip()
    872  #               self.password = self.GetPassword()
    873                  self.cancelled = False
    874                  self.parent.Close()
    875          #----------------------------
    876 -        def OnCancel(self, event):
    877                  self.cancelled = True
    878                  self.parent.Close()
    879
    880  #================================================================
    881  # main
    882  #----------------------------------------------------------------
    883  if __name__ == "__main__":
    884
    885          if len(sys.argv) < 2:
    886                  sys.exit()
    887
    888          if sys.argv[1] != 'test':
    889                  sys.exit()
    890
    891          # we don't have tests yet
    892          sys.exit()
    893
    894          from Gnumed.pycommon import gmI18N
    895
    896          logging.basicConfig(level = logging.DEBUG)
    897
    898          gmI18N.activate_locale()
    899          gmI18N.install_domain(domain='gnumed')
    900          #-----------------------------------------------
    901          #-----------------------------------------------
    902 -        def test():
    903                  app = wx.PyWidgetTester(size = (300,400))
    904                  #show the login panel in a main window
    905  #               app.SetWidget(cLoginPanel, -1)
    906                  #and pop the login dialog up modally
    907                  dlg = cLoginDialog(None, -1) #, png_bitmap = 'bitmaps/gnumedlogo.png')
    908                  dlg.ShowModal()
    909                  #demonstration how to access the login dialog values
    910                  lp = dlg.panel.GetLoginInfo()
    911                  if lp is None:
    912                          wx.MessageBox(_("Dialog was cancelled by user"))
    913                  else:
    914                          wx.MessageBox(_("You tried to log in as [%s] with password [%s].\nHost:%s, DB: %s, Port: %s") % (lp.GetUser(),lp.GetPassword(),lp.GetHost(),lp.GetDatabase(),lp.GetPort()))
    915                  dlg.DestroyLater()
    916  #               app.MainLoop()
    917
    918  #================================================================
    919