- bump version
[gnumed:gnumed-fedora.git] / gnumed / gnumed / client / wxpython / gmGuiMain.py
1 # -*- coding: utf8 -*-
2 """GNUmed GUI client.
3
4 This contains the GUI application framework and main window
5 of the all signing all dancing GNUmed Python Reference
6 client. It relies on the <gnumed.py> launcher having set up
7 the non-GUI-related runtime environment.
8
9 This source code is protected by the GPL licensing scheme.
10 Details regarding the GPL are available at http://www.gnu.org
11 You may use and share it as long as you don't deny this right
12 to anybody else.
13
14 copyright: authors
15 """
16 #==============================================================================
17 # $Source: /home/ncq/Projekte/cvs2git/vcs-mirror/gnumed/gnumed/client/wxpython/gmGuiMain.py,v $
18 # $Id: gmGuiMain.py,v 1.426.2.10 2008-11-23 17:23:51 ncq Exp $
19 __version__ = "$Revision: 1.426.2.10 $"
20 __author__  = "H. Herb <hherb@gnumed.net>,\
21                            K. Hilbert <Karsten.Hilbert@gmx.net>,\
22                            I. Haywood <i.haywood@ugrad.unimelb.edu.au>"
23 __license__ = 'GPL (details at http://www.gnu.org)'
24
25 # stdlib
26 import sys, time, os, cPickle, zlib, locale, os.path, datetime as pyDT, webbrowser, shutil, logging
27
28
29 # 3rd party libs
30 # wxpython version cannot be enforced inside py2exe and friends
31 if not hasattr(sys, 'frozen'):
32         import wxversion
33         wxversion.ensureMinimal('2.8-unicode', optionsRequired=True)
34
35 try:
36         import wx
37 except ImportError:
38         print "GNUmed startup: Cannot import wxPython library."
39         print "GNUmed startup: Make sure wxPython is installed."
40         print 'CRITICAL ERROR: Error importing wxPython. Halted.'
41         raise
42
43 # do this check just in case, so we can make sure
44 # py2exe and friends include the proper version, too
45 version = int(u'%s%s' % (wx.MAJOR_VERSION, wx.MINOR_VERSION))
46 if (version < 28) or ('unicode' not in wx.PlatformInfo):
47         print "GNUmed startup: Unsupported wxPython version (%s: %s)." % (wx.VERSION_STRING, wx.PlatformInfo)
48         print "GNUmed startup: wxPython 2.8+ with unicode support is required."
49         print 'CRITICAL ERROR: Proper wxPython version not found. Halted.'
50         raise ValueError('wxPython 2.8+ with unicode support not found')
51
52
53 # GNUmed libs
54 from Gnumed.pycommon import gmCfg, gmPG2, gmDispatcher, gmGuiBroker, gmI18N, gmExceptions, gmShellAPI, gmTools, gmDateTime, gmHooks, gmBackendListener, gmCfg2, gmLog2
55 from Gnumed.business import gmPerson, gmClinicalRecord, gmSurgery, gmEMRStructItems
56 from Gnumed.exporters import gmPatientExporter
57 from Gnumed.wxpython import gmGuiHelpers, gmHorstSpace, gmEMRBrowser, gmDemographicsWidgets, gmEMRStructWidgets
58 from Gnumed.wxpython import gmStaffWidgets, gmMedDocWidgets, gmPatSearchWidgets, gmAllergyWidgets, gmListWidgets
59 from Gnumed.wxpython import gmFormWidgets, gmSnellen, gmProviderInboxWidgets, gmCfgWidgets, gmExceptionHandlingWidgets
60 from Gnumed.wxpython import gmTimer, gmMeasurementWidgets, gmNarrativeWidgets
61
62 try:
63         _('do-not-translate-but-make-epydoc-happy')
64 except NameError:
65         _ = lambda x:x
66
67 _cfg = gmCfg2.gmCfgData()
68 _provider = None
69 _scripting_listener = None
70
71 expected_db_ver = u'v9'
72
73 current_client_ver = u'0.3.7'
74 current_client_branch = '0.3'
75
76 _log = logging.getLogger('gm.main')
77 _log.info(__version__)
78 _log.info('wxPython GUI framework: %s %s' % (wx.VERSION_STRING, wx.PlatformInfo))
79 _log.info('GNUmed client version [%s] on branch [%s]', current_client_ver, current_client_branch)
80 _log.info('expected database version [%s]', expected_db_ver)
81
82 #==============================================================================
83
84 icon_serpent = \
85 """x\xdae\x8f\xb1\x0e\x83 \x10\x86w\x9f\xe2\x92\x1blb\xf2\x07\x96\xeaH:0\xd6\
86 \xc1\x85\xd5\x98N5\xa5\xef?\xf5N\xd0\x8a\xdcA\xc2\xf7qw\x84\xdb\xfa\xb5\xcd\
87 \xd4\xda;\xc9\x1a\xc8\xb6\xcd<\xb5\xa0\x85\x1e\xeb\xbc\xbc7b!\xf6\xdeHl\x1c\
88 \x94\x073\xec<*\xf7\xbe\xf7\x99\x9d\xb21~\xe7.\xf5\x1f\x1c\xd3\xbdVlL\xc2\
89 \xcf\xf8ye\xd0\x00\x90\x0etH \x84\x80B\xaa\x8a\x88\x85\xc4(U\x9d$\xfeR;\xc5J\
90 \xa6\x01\xbbt9\xceR\xc8\x81e_$\x98\xb9\x9c\xa9\x8d,y\xa9t\xc8\xcf\x152\xe0x\
91 \xe9$\xf5\x07\x95\x0cD\x95t:\xb1\x92\xae\x9cI\xa8~\x84\x1f\xe0\xa3ec"""
92
93 #==============================================================================
94 # FIXME: this belongs elsewhere
95 def jump_to_ifap(import_drugs=False):
96
97         dbcfg = gmCfg.cCfgSQL()
98
99         ifap_cmd = dbcfg.get2 (
100                 option = 'external.ifap-win.shell_command',
101                 workplace = gmSurgery.gmCurrentPractice().active_workplace,
102                 bias = 'workplace',
103                 default = 'wine "C:\Ifapwin\WIAMDB.EXE"'
104         )
105         found, binary = gmShellAPI.detect_external_binary(ifap_cmd)
106         if not found:
107                 gmDispatcher.send('statustext', msg = _('Cannot call IFAP via [%s].') % ifap_cmd)
108                 return False
109         ifap_cmd = binary
110
111         if import_drugs:
112                 transfer_file = os.path.expanduser(dbcfg.get2 (
113                         option = 'external.ifap-win.transfer_file',
114                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
115                         bias = 'workplace',
116                         default = '~/.wine/drive_c/Ifapwin/ifap2gnumed.csv'
117                 ))
118                 # file must exist for Ifap to write into it
119                 try:
120                         f = open(transfer_file, 'w+b').close()
121                 except IOError:
122                         _log.exception('Cannot create IFAP <-> GNUmed transfer file [%s]', transfer_file)
123                         gmDispatcher.send('statustext', msg = _('Cannot create IFAP <-> GNUmed transfer file [%s].') % transfer_file)
124                         return False
125
126         wx.BeginBusyCursor()
127         gmShellAPI.run_command_in_shell(command = ifap_cmd, blocking = import_drugs)
128         wx.EndBusyCursor()
129
130         if import_drugs:
131                 # COMMENT: this file must exist PRIOR to invoking IFAP
132                 # COMMENT: or else IFAP will not write data into it ...
133                 try:
134                         csv_file = open(transfer_file, 'rb')                                            # FIXME: encoding
135                 except:
136                         _log.exception('cannot access [%s]', fname)
137                         csv_file = None
138
139                 if csv_file is not None:
140                         import csv
141                         csv_lines = csv.DictReader (
142                                 csv_file,
143                                 fieldnames = u'PZN Handelsname Form Abpackungsmenge Einheit Preis1 Hersteller Preis2 rezeptpflichtig Festbetrag Packungszahl Packungsgröße'.split(),
144                                 delimiter = ';'
145                         )
146                         pat = gmPerson.gmCurrentPatient()
147                         emr = pat.get_emr()
148                         # dummy episode for now
149                         epi = emr.add_episode(episode_name = _('Current medication'))
150                         for line in csv_lines:
151                                 narr = u'%sx %s %s %s (\u2258 %s %s) von %s (%s)' % (
152                                         line['Packungszahl'].strip(),
153                                         line['Handelsname'].strip(),
154                                         line['Form'].strip(),
155                                         line[u'Packungsgröße'].strip(),
156                                         line['Abpackungsmenge'].strip(),
157                                         line['Einheit'].strip(),
158                                         line['Hersteller'].strip(),
159                                         line['PZN'].strip()
160                                 )
161                                 emr.add_clin_narrative(note = narr, soap_cat = 's', episode = epi)
162                         csv_file.close()
163
164         return True
165 #==============================================================================
166 # FIXME: this belongs elsewhere
167 def check_for_updates():
168
169         dbcfg = gmCfg.cCfgSQL()
170
171         url = dbcfg.get2 (
172                 option = u'horstspace.update.url',
173                 workplace = gmSurgery.gmCurrentPractice().active_workplace,
174                 bias = 'workplace',
175                 default = u'http://www.gnumed.de/downloads/gnumed-versions.txt'
176         )
177
178         consider_latest_branch = bool(dbcfg.get2 (
179                 option = u'horstspace.update.consider_latest_branch',
180                 workplace = gmSurgery.gmCurrentPractice().active_workplace,
181                 bias = 'workplace',
182                 default = True
183         ))
184
185         found, msg = gmTools.check_for_update (
186                 url = url,
187                 current_branch = current_client_branch,
188                 current_version = current_client_ver,
189                 consider_latest_branch = consider_latest_branch
190         )
191
192         if found is False:
193                 gmDispatcher.send(signal = 'statustext', msg = _('Your client is up to date.'))
194                 return
195
196         gmGuiHelpers.gm_show_info (
197                 msg,
198                 _('Checking for client updates')
199         )
200
201 #==============================================================================
202 class gmTopLevelFrame(wx.Frame):
203         """GNUmed client's main windows frame.
204
205         This is where it all happens. Avoid popping up any other windows.
206         Most user interaction should happen to and from widgets within this frame
207         """
208         #----------------------------------------------
209         def __init__(self, parent, id, title, size=wx.DefaultSize):
210                 """You'll have to browse the source to understand what the constructor does
211                 """
212                 wx.Frame.__init__ (
213                         self,
214                         parent,
215                         id,
216                         title,
217                         size,
218                         style = wx.DEFAULT_FRAME_STYLE
219                 )
220
221                 #initialize the gui broker
222                 self.__gb = gmGuiBroker.GuiBroker()
223                 self.__gb['main.frame'] = self
224                 self.bar_width = -1
225                 self.__pre_exit_callbacks = []
226
227                 _log.info('workplace is >>>%s<<<', gmSurgery.gmCurrentPractice().active_workplace)
228
229                 self.__setup_main_menu()
230                 self.SetupStatusBar()
231                 self.SetStatusText(_('You are logged in as %s%s.%s (%s). DB account <%s>.') % (
232                         gmTools.coalesce(_provider['title'], ''),
233                         _provider['firstnames'][:1],
234                         _provider['lastnames'],
235                         _provider['short_alias'],
236                         _provider['db_user']
237                 ))
238
239                 # set window title via template
240                 if _cfg.get(option = 'slave'):
241                         self.__title_template = _('Enslaved GNUmed [%s%s.%s@%s] %s')
242                 else:
243                         self.__title_template = 'GNUmed [%s%s.%s@%s] %s'
244                 self.updateTitle()
245
246                 # set window icon
247                 icon_bmp_data = wx.BitmapFromXPMData(cPickle.loads(zlib.decompress(icon_serpent)))
248                 icon = wx.EmptyIcon()
249                 icon.CopyFromBitmap(icon_bmp_data)
250                 self.SetIcon(icon)
251
252                 self.__register_events()
253
254                 self.LayoutMgr = gmHorstSpace.cHorstSpaceLayoutMgr(self, -1)
255                 self.vbox = wx.BoxSizer(wx.VERTICAL)
256                 self.vbox.Add(self.LayoutMgr, 10, wx.EXPAND | wx.ALL, 1)
257
258                 self.SetAutoLayout(True)
259                 self.SetSizerAndFit(self.vbox)
260
261                 # don't allow the window to get too small
262                 # setsizehints only allows minimum size, therefore window can't become small enough
263                 # effectively we need the font size to be configurable according to screen size
264                 #self.vbox.SetSizeHints(self)
265                 self.__set_GUI_size()
266
267                 self.Centre(wx.BOTH)
268                 self.Show(True)
269         #----------------------------------------------
270         def __set_GUI_size(self):
271                 """Try to get previous window size from backend."""
272
273                 cfg = gmCfg.cCfgSQL()
274
275                 # width
276                 width = int(cfg.get2 (
277                         option = 'main.window.width',
278                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
279                         bias = 'workplace',
280                         default = 800
281                 ))
282
283                 # height
284                 height = int(cfg.get2 (
285                         option = 'main.window.height',
286                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
287                         bias = 'workplace',
288                         default = 600
289                 ))
290
291                 _log.debug('setting GUI size to [%s:%s]' % (width, height))
292                 self.SetClientSize(wx.Size(width, height))
293         #----------------------------------------------
294         def __setup_main_menu(self):
295                 """Create the main menu entries.
296
297                 Individual entries are farmed out to the modules.
298                 """
299                 global wx
300                 self.mainmenu = wx.MenuBar()
301                 self.__gb['main.mainmenu'] = self.mainmenu
302
303                 # -- menu "GNUmed" -----------------
304                 menu_gnumed = wx.Menu()
305
306                 menu_config = wx.Menu()
307                 menu_gnumed.AppendMenu(wx.NewId(), _('Options ...'), menu_config)
308
309                 # -- submenu gnumed-config-db
310                 menu_cfg_db = wx.Menu()
311                 menu_config.AppendMenu(wx.NewId(), _('Database ...'), menu_cfg_db)
312
313                 ID = wx.NewId()
314                 menu_cfg_db.Append(ID, _('Language'), _('Configure the database language'))
315                 wx.EVT_MENU(self, ID, self.__on_set_db_lang)
316
317                 ID = wx.NewId()
318                 menu_cfg_db.Append(ID, _('Welcome message'), _('Configure the database welcome message (all users).'))
319                 wx.EVT_MENU(self, ID, self.__on_set_db_welcome)
320
321                 menu_cfg_client = wx.Menu()
322                 menu_config.AppendMenu(wx.NewId(), _('Client parameters ...'), menu_cfg_client)
323
324                 ID = wx.NewId()
325                 menu_cfg_client.Append(ID, _('Export chunk size'), _('Configure the chunk size used when exporting BLOBs from the database.'))
326                 wx.EVT_MENU(self, ID, self.__on_set_export_chunk_size)
327
328                 ID = wx.NewId()
329                 menu_cfg_client.Append(ID, _('Temporary directory'), _('Configure the directory to use as scratch space for temporary files.'))
330                 wx.EVT_MENU(self, ID, self.__on_set_temp_dir)
331
332                 item = menu_cfg_client.Append(-1, _('Email address'), _('The email address of the user for sending bug reports, etc.'))
333                 self.Bind(wx.EVT_MENU, self.__on_set_user_email, item)
334
335                 # -- submenu gnumed / config / ui
336                 menu_cfg_ui = wx.Menu()
337                 menu_config.AppendMenu(wx.NewId(), _('User interface ...'), menu_cfg_ui)
338
339 #               ID = wx.NewId()
340 #               menu_cfg_ui.Append(ID, _('Initial plugin'), _('Configure which plugin to show right after client startup.'))
341 #               wx.EVT_MENU(self, ID, self.__on_set_startup_plugin)
342
343                 ID = wx.NewId()
344                 menu_cfg_ui.Append(ID, _('Workplaces'), _('Choose the plugins to load per workplace.'))
345                 wx.EVT_MENU(self, ID, self.__on_configure_workplace)
346
347                 # -- submenu gnumed / config / ui / docs
348                 menu_cfg_doc = wx.Menu()
349                 menu_cfg_ui.AppendMenu(wx.NewId(), _('Document handling ...'), menu_cfg_doc)
350
351                 ID = wx.NewId()
352                 menu_cfg_doc.Append(ID, _('Review dialog'), _('Configure review dialog after document display.'))
353                 wx.EVT_MENU(self, ID, self.__on_configure_doc_review_dialog)
354
355                 ID = wx.NewId()
356                 menu_cfg_doc.Append(ID, _('UUID display'), _('Configure unique ID dialog on document import.'))
357                 wx.EVT_MENU(self, ID, self.__on_configure_doc_uuid_dialog)
358
359                 ID = wx.NewId()
360                 menu_cfg_doc.Append(ID, _('Empty documents'), _('Whether to allow saving documents without parts.'))
361                 wx.EVT_MENU(self, ID, self.__on_configure_partless_docs)
362
363                 # -- submenu gnumed / config / ui / updates
364                 menu_cfg_update = wx.Menu()
365                 menu_cfg_ui.AppendMenu(wx.NewId(), _('Update handling ...'), menu_cfg_update)
366
367                 ID = wx.NewId()
368                 menu_cfg_update.Append(ID, _('Auto-check'), _('Whether to auto-check for updates at startup.'))
369                 wx.EVT_MENU(self, ID, self.__on_configure_update_check)
370
371                 ID = wx.NewId()
372                 menu_cfg_update.Append(ID, _('Check scope'), _('When checking for updates, consider latest branch, too ?'))
373                 wx.EVT_MENU(self, ID, self.__on_configure_update_check_scope)
374
375                 ID = wx.NewId()
376                 menu_cfg_update.Append(ID, _('URL'), _('The URL to retrieve version information from.'))
377                 wx.EVT_MENU(self, ID, self.__on_configure_update_url)
378
379                 # -- submenu gnumed / config / ui / patient search
380                 menu_cfg_pat_search = wx.Menu()
381                 menu_cfg_ui.AppendMenu(wx.NewId(), _('Patient search ...'), menu_cfg_pat_search)
382
383                 ID = wx.NewId()
384                 menu_cfg_pat_search.Append(ID, _('Birthday reminder'), _('Configure birthday reminder proximity interval.'))
385                 wx.EVT_MENU(self, ID, self.__on_set_dob_reminder_proximity)
386
387                 ID = wx.NewId()
388                 menu_cfg_pat_search.Append(ID, _('Immediate source activation'), _('Configure immediate activation of single external patient.'))
389                 wx.EVT_MENU(self, ID, self.__on_set_quick_pat_search)
390
391                 ID = wx.NewId()
392                 menu_cfg_pat_search.Append(ID, _('Initial plugin'), _('Configure which plugin to show right after patient activation.'))
393                 wx.EVT_MENU(self, ID, self.__on_set_initial_pat_plugin)
394
395                 # -- submenu gnumed / config / ui / soap handling
396                 menu_cfg_soap_editing = wx.Menu()
397                 menu_cfg_ui.AppendMenu(wx.NewId(), _('Progress notes handling ...'), menu_cfg_soap_editing)
398
399                 ID = wx.NewId()
400                 menu_cfg_soap_editing.Append(ID, _('Multiple new episodes'), _('Configure opening multiple new episodes on a patient at once.'))
401                 wx.EVT_MENU(self, ID, self.__on_allow_multiple_new_episodes)
402
403                 # -- submenu gnumed / config / external tools
404                 menu_cfg_ext_tools = wx.Menu()
405                 menu_config.AppendMenu(wx.NewId(), _('External tools ...'), menu_cfg_ext_tools)
406
407                 ID = wx.NewId()
408                 menu_cfg_ext_tools.Append(ID, _('IFAP command'), _('Set the command to start IFAP.'))
409                 wx.EVT_MENU(self, ID, self.__on_set_ifap_cmd)
410
411                 ID = wx.NewId()
412                 menu_cfg_ext_tools.Append(ID, _('OOo startup time'), _('Set the time to wait for OpenOffice to settle after startup.'))
413                 wx.EVT_MENU(self, ID, self.__on_set_ooo_settle_time)
414
415                 # -- submenu gnumed / config / emr
416                 menu_cfg_emr = wx.Menu()
417                 menu_config.AppendMenu(wx.NewId(), _('EMR ...'), menu_cfg_emr)
418
419                 # -- submenu gnumed / config / emr / encounter
420                 menu_cfg_encounter = wx.Menu()
421                 menu_cfg_emr.AppendMenu(wx.NewId(), _('Encounter ...'), menu_cfg_encounter)
422
423                 ID = wx.NewId()
424                 menu_cfg_encounter.Append(ID, _('Edit on patient change'), _('Edit encounter details on changing of patients.'))
425                 wx.EVT_MENU(self, ID, self.__on_cfg_enc_pat_change)
426
427                 ID = wx.NewId()
428                 menu_cfg_encounter.Append(ID, _('Minimum duration'), _('Minimum duration of an encounter.'))
429                 wx.EVT_MENU(self, ID, self.__on_cfg_enc_min_ttl)
430
431                 ID = wx.NewId()
432                 menu_cfg_encounter.Append(ID, _('Maximum duration'), _('Maximum duration of an encounter.'))
433                 wx.EVT_MENU(self, ID, self.__on_cfg_enc_max_ttl)
434
435                 ID = wx.NewId()
436                 menu_cfg_encounter.Append(ID, _('Minimum empty age'), _('Minimum age of an empty encounter before considering for deletion.'))
437                 wx.EVT_MENU(self, ID, self.__on_cfg_enc_empty_ttl)
438
439                 ID = wx.NewId()
440                 menu_cfg_encounter.Append(ID, _('Default type'), _('Default type for new encounters.'))
441                 wx.EVT_MENU(self, ID, self.__on_cfg_enc_default_type)
442
443                 # -- submenu gnumed / config / emr / episode
444                 menu_cfg_episode = wx.Menu()
445                 menu_cfg_emr.AppendMenu(wx.NewId(), _('Episode ...'), menu_cfg_episode)
446
447                 ID = wx.NewId()
448                 menu_cfg_episode.Append(ID, _('Dormancy'), _('Maximum length of dormancy after which an episode will be considered closed.'))
449                 wx.EVT_MENU(self, ID, self.__on_cfg_epi_ttl)
450
451                 # --
452                 menu_gnumed.AppendSeparator()
453
454                 ID = wx.NewId()
455                 menu_gnumed.Append(ID, _('Check for updates'), _('Check for new releases of the GNUmed client.'))
456                 wx.EVT_MENU(self, ID, self.__on_check_for_updates)
457
458                 menu_gnumed.Append(wx.ID_EXIT, _('E&xit\tAlt-X'), _('Close this GNUmed client.'))
459                 wx.EVT_MENU(self, wx.ID_EXIT, self.__on_exit_gnumed)
460
461                 self.mainmenu.Append(menu_gnumed, '&GNUmed')
462
463                 # -- menu "Office" --------------------
464                 self.menu_office = wx.Menu()
465
466                 menu_master_data = wx.Menu()
467                 self.menu_office.AppendMenu(wx.NewId(), _('Manage &master data ...'), menu_master_data)
468
469                 menu_staff = wx.Menu()
470                 menu_master_data.AppendMenu(wx.NewId(), _('&Staff ...'), menu_staff)
471
472                 item = menu_staff.Append(-1, _('&Add staff member'), _('Add a new staff member'))
473                 self.Bind(wx.EVT_MENU, self.__on_add_new_staff, item)
474
475                 item = menu_staff.Append(-1, _('&Edit staff list'), _('Edit the list of staff'))
476                 self.Bind(wx.EVT_MENU, self.__on_edit_staff_list, item)
477
478                 item = menu_master_data.Append(-1, _('&Document types'), _('Manage the document types available in the system.'))
479                 self.Bind(wx.EVT_MENU, self.__on_edit_doc_types, item)
480
481                 item = menu_master_data.Append(-1, _('&Text expansions'), _('Manage keyword based text expansion macros.'))
482                 self.Bind(wx.EVT_MENU, self.__on_manage_text_expansion, item)
483
484                 item = menu_master_data.Append(-1, _('&Form templates'), _('Manage templates for forms and letters.'))
485                 self.Bind(wx.EVT_MENU, self.__on_edit_templates, item)
486
487                 item = menu_master_data.Append(-1, _('&Encounter types'), _('Manage encounter (consultation) types.'))
488                 self.Bind(wx.EVT_MENU, self.__on_manage_encounter_types, item)
489
490                 item = menu_master_data.Append(-1, _('&Provinces'), _('Manage provinces (counties, territories, ...).'))
491                 self.Bind(wx.EVT_MENU, self.__on_manage_provinces, item)
492
493                 self.__gb['main.officemenu'] = self.menu_office
494                 self.mainmenu.Append(self.menu_office, _('&Office'))
495
496                 # -- menu "Patient" ---------------------------
497                 menu_patient = wx.Menu()
498
499                 ID_CREATE_PATIENT = wx.NewId()
500                 menu_patient.Append(ID_CREATE_PATIENT, _('Register new'), _("Register a new patient with this practice"))
501                 wx.EVT_MENU(self, ID_CREATE_PATIENT, self.__on_create_patient)
502
503                 ID_LOAD_EXT_PAT = wx.NewId()
504                 menu_patient.Append(ID_LOAD_EXT_PAT, _('Load external'), _('Load and possibly create patient from an external source.'))
505                 wx.EVT_MENU(self, ID_LOAD_EXT_PAT, self.__on_load_external_patient)
506
507                 ID_DEL_PAT = wx.NewId()
508                 menu_patient.Append(ID_DEL_PAT, _('Deactivate record'), _('Deactivate (exclude from search) patient record in database.'))
509                 wx.EVT_MENU(self, ID_DEL_PAT, self.__on_delete_patient)
510
511                 menu_patient.AppendSeparator()
512
513                 ID_ENLIST_PATIENT_AS_STAFF = wx.NewId()
514                 menu_patient.Append(ID_ENLIST_PATIENT_AS_STAFF, _('Enlist as staff'), _('Enlist current patient as staff member'))
515                 wx.EVT_MENU(self, ID_ENLIST_PATIENT_AS_STAFF, self.__on_enlist_patient_as_staff)
516
517                 # FIXME: temporary until external program framework is active
518                 ID = wx.NewId()
519                 menu_patient.Append(ID, _('Export to GDT'), _('Export demographics of current patient into GDT file.'))
520                 wx.EVT_MENU(self, ID, self.__on_export_as_gdt)
521
522                 menu_patient.AppendSeparator()
523
524                 self.mainmenu.Append(menu_patient, '&Patient')
525                 self.__gb['main.patientmenu'] = menu_patient
526
527                 # -- menu "EMR" ---------------------------
528                 menu_emr = wx.Menu()
529                 self.mainmenu.Append(menu_emr, _("&EMR"))
530                 self.__gb['main.emrmenu'] = menu_emr
531
532                 # - submenu "export as"
533                 menu_emr_export = wx.Menu()
534                 menu_emr.AppendMenu(wx.NewId(), _('Export as ...'), menu_emr_export)
535                 #   1) ASCII
536                 ID_EXPORT_EMR_ASCII = wx.NewId()
537                 menu_emr_export.Append (
538                         ID_EXPORT_EMR_ASCII,
539                         _('Text document'),
540                         _("Export the EMR of the active patient into a text file")
541                 )
542                 wx.EVT_MENU(self, ID_EXPORT_EMR_ASCII, self.OnExportEMR)
543                 #   2) journal format
544                 ID_EXPORT_EMR_JOURNAL = wx.NewId()
545                 menu_emr_export.Append (
546                         ID_EXPORT_EMR_JOURNAL,
547                         _('Journal'),
548                         _("Export the EMR of the active patient as a chronological journal into a text file")
549                 )
550                 wx.EVT_MENU(self, ID_EXPORT_EMR_JOURNAL, self.__on_export_emr_as_journal)
551                 #   3) Medistar import format
552                 ID_EXPORT_MEDISTAR = wx.NewId()
553                 menu_emr_export.Append (
554                         ID_EXPORT_MEDISTAR,
555                         _('MEDISTAR import format'),
556                         _("GNUmed -> MEDISTAR. Export progress notes of active patient's active encounter into a text file.")
557                 )
558                 wx.EVT_MENU(self, ID_EXPORT_MEDISTAR, self.__on_export_for_medistar)
559
560                 # - summary
561                 ID_EMR_SUMMARY = wx.NewId()
562                 menu_emr.Append (
563                         ID_EMR_SUMMARY,
564                         _('Show Summary'),
565                         _('Show a summary of the EMR of the active patient')
566                 )
567                 wx.EVT_MENU(self, ID_EMR_SUMMARY, self.__on_show_emr_summary)
568
569                 # - submenu "show as"
570                 menu_emr_show = wx.Menu()
571                 menu_emr.AppendMenu(wx.NewId(), _('Show as ...'), menu_emr_show)
572                 self.__gb['main.emr_showmenu'] = menu_emr_show
573
574                 # - draw a line
575                 menu_emr.AppendSeparator()
576
577                 # - search
578                 ID_SEARCH_EMR = wx.NewId()
579                 menu_emr.Append (
580                         ID_SEARCH_EMR,
581                         _('Search'),
582                         _('Search for data in the EMR of the active patient')
583                 )
584                 wx.EVT_MENU(self, ID_SEARCH_EMR, self.__on_search_emr)
585
586                 # - start new encounter
587                 ID = wx.NewId()
588                 menu_emr.Append (
589                         ID,
590                         _('Start new encounter'),
591                         _('Start a new encounter for the active patient right now.')
592                 )
593                 wx.EVT_MENU(self, ID, self.__on_start_new_encounter)
594
595                 # - submenu EMR / History taking
596                 menu_history = wx.Menu()
597                 menu_emr.AppendMenu(wx.NewId(), _('&History taking ...'), menu_history)
598                 # - add health issue
599                 ID_ADD_HEALTH_ISSUE_TO_EMR = wx.NewId()
600                 menu_history.Append (
601                         ID_ADD_HEALTH_ISSUE_TO_EMR,
602                         _('&Past history (foundational issue / PMH)'),
603                         _('Add a past/previous medical history item (foundational health issue) to the EMR of the active patient')
604                 )
605                 wx.EVT_MENU(self, ID_ADD_HEALTH_ISSUE_TO_EMR, self.__on_add_health_issue)
606                 # - document current medication
607                 ID_ADD_DRUGS_TO_EMR = wx.NewId()
608                 menu_history.Append (
609                         ID_ADD_DRUGS_TO_EMR,
610                         _('Current &medication'),
611                         _('Select current medication from drug database and save into progress notes.')
612                 )
613                 wx.EVT_MENU(self, ID_ADD_DRUGS_TO_EMR, self.__on_add_medication)
614                 # - add allergy
615                 ID = wx.NewId()
616                 menu_history.Append (
617                         ID,
618                         _('&Allergies'),
619                         _('Manage documentation of allergies for the current patient.')
620                 )
621                 wx.EVT_MENU(self, ID, self.__on_manage_allergies)
622                 # - edit occupation
623                 ID = wx.NewId()
624                 menu_history.Append(ID, _('&Occupation'), _('Edit occupation details for the current patient.'))
625                 wx.EVT_MENU(self, ID, self.__on_edit_occupation)
626
627                 # - submenu EMR / Observations
628                 menu_obs = wx.Menu()
629                 menu_emr.AppendMenu(wx.NewId(), _('&Observations ...'), menu_obs)
630
631                 # - add measurement
632                 ID = wx.NewId()
633                 menu_obs.Append(ID, _('Add &Measurement(s)'), _('Add (a) measurement result(s) for the current patient.'))
634                 wx.EVT_MENU(self, ID, self.__on_add_measurement)
635
636                 # - draw a line
637                 menu_emr.AppendSeparator()
638
639                 # -- menu "paperwork" ---------------------
640                 menu_paperwork = wx.Menu()
641
642                 # submenu "Documents"
643 #               menu_docs = wx.Menu()
644 #               item = menu_docs.Append(-1, _('&Show docs'), _('Switch to document collection'))
645 #               self.Bind(wx.EVT_MENU, self__on_show_docs, item)
646 #               item = menu_docs.Append(-1, _('&Add document'), _('Add a new document'))
647
648                 item = menu_paperwork.Append(-1, _('&Write letter'), _('Write a letter for the current patient.'))
649                 self.Bind(wx.EVT_MENU, self.__on_new_letter, item)
650
651                 self.mainmenu.Append(menu_paperwork, _('&Correspondence'))
652
653                 # menu "View" ---------------------------
654 #               self.menu_view = wx.Menu()
655 #               self.__gb['main.viewmenu'] = self.menu_view
656 #               self.mainmenu.Append(self.menu_view, _("&View"));
657
658                 # menu "Tools"
659                 self.menu_tools = wx.Menu()
660                 self.__gb['main.toolsmenu'] = self.menu_tools
661                 self.mainmenu.Append(self.menu_tools, _("&Tools"))
662
663                 ID_DICOM_VIEWER = wx.NewId()
664                 viewer = _('no viewer installed')
665                 if os.access('/Applications/OsiriX.app/Contents/MacOS/OsiriX', os.X_OK):
666                         viewer = u'OsiriX'
667                 elif gmShellAPI.detect_external_binary(binary = 'aeskulap')[0]:
668                         viewer = u'Aeskulap'
669                 elif gmShellAPI.detect_external_binary(binary = 'amide')[0]:
670                         viewer = u'AMIDE'
671                 elif gmShellAPI.detect_external_binary(binary = 'xmedcon')[0]:
672                         viewer = u'(x)medcon'
673                 self.menu_tools.Append(ID_DICOM_VIEWER, _('DICOM viewer'), _('Start DICOM viewer (%s) for CD-ROM (X-Ray, CT, MR, etc). On Windows just insert CD.') % viewer)
674                 wx.EVT_MENU(self, ID_DICOM_VIEWER, self.__on_dicom_viewer)
675                 if viewer == _('no viewer installed'):
676                         _log.info('neither of OsiriX / Aeskulap / AMIDE / xmedcon found, disabling "DICOM viewer" menu item')
677                         self.menu_tools.Enable(id=ID_DICOM_VIEWER, enable=False)
678
679 #               ID_DERMTOOL = wx.NewId()
680 #               self.menu_tools.Append(ID_DERMTOOL, _("Dermatology"), _("A tool to aid dermatology diagnosis"))
681 #               wx.EVT_MENU (self, ID_DERMTOOL, self.__dermtool)
682
683                 ID = wx.NewId()
684                 self.menu_tools.Append(ID, _('Snellen chart'), _('Display fullscreen snellen chart.'))
685                 wx.EVT_MENU(self, ID, self.__on_snellen)
686
687                 self.menu_tools.AppendSeparator()
688
689                 # menu "Knowledge" ---------------------
690                 menu_knowledge = wx.Menu()
691                 self.__gb['main.knowledgemenu'] = menu_knowledge
692                 self.mainmenu.Append(menu_knowledge, _('&Knowledge'))
693
694                 menu_drug_dbs = wx.Menu()
695                 menu_knowledge.AppendMenu(wx.NewId(), _('&Drug Resources'), menu_drug_dbs)
696
697                 # - IFAP drug DB
698                 ID_IFAP = wx.NewId()
699                 menu_drug_dbs.Append(ID_IFAP, u'ifap', _('Start "ifap index PRAXIS" %s drug browser (Windows/Wine, Germany)') % gmTools.u_registered_trademark)
700                 wx.EVT_MENU(self, ID_IFAP, self.__on_ifap)
701
702                 menu_id = wx.NewId()
703                 menu_drug_dbs.Append(menu_id, u'kompendium.ch', _('Show "kompendium.ch" drug database (online, Switzerland)'))
704                 wx.EVT_MENU(self, menu_id, self.__on_kompendium_ch)
705
706 #               menu_knowledge.AppendSeparator()
707
708                 # - "recommended" medical links in the Wiki
709                 ID_MEDICAL_LINKS = wx.NewId()
710                 menu_knowledge.Append(ID_MEDICAL_LINKS, _('Medical links (www)'), _('Show a page of links to useful medical content.'))
711                 wx.EVT_MENU(self, ID_MEDICAL_LINKS, self.__on_medical_links)
712
713                 # -- menu "Help" --------------
714                 help_menu = wx.Menu()
715
716                 ID = wx.NewId()
717                 help_menu.Append(ID, _('GNUmed wiki'), _('Go to the GNUmed wiki on the web.'))
718                 wx.EVT_MENU(self, ID, self.__on_display_wiki)
719
720                 ID = wx.NewId()
721                 help_menu.Append(ID, _('User manual (www)'), _('Go to the User Manual on the web.'))
722                 wx.EVT_MENU(self, ID, self.__on_display_user_manual_online)
723
724                 menu_debugging = wx.Menu()
725                 help_menu.AppendMenu(wx.NewId(), _('Debugging ...'), menu_debugging)
726
727                 ID_SCREENSHOT = wx.NewId()
728                 menu_debugging.Append(ID_SCREENSHOT, _('Screenshot'), _('Save a screenshot of this GNUmed client.'))
729                 wx.EVT_MENU(self, ID_SCREENSHOT, self.__on_save_screenshot)
730
731                 ID = wx.NewId()
732                 menu_debugging.Append(ID, _('Backup log file'), _('Backup the content of the log to another file.'))
733                 wx.EVT_MENU(self, ID, self.__on_backup_log_file)
734
735                 ID = wx.NewId()
736                 menu_debugging.Append(ID, _('Bug tracker'), _('Go to the GNUmed bug tracker on the web.'))
737                 wx.EVT_MENU(self, ID, self.__on_display_bugtracker)
738
739                 ID_UNBLOCK = wx.NewId()
740                 menu_debugging.Append(ID_UNBLOCK, _('Unlock mouse'), _('Unlock mouse pointer in case it got stuck in hourglass mode.'))
741                 wx.EVT_MENU(self, ID_UNBLOCK, self.__on_unblock_cursor)
742
743                 item = menu_debugging.Append(-1, _('pgAdmin III'), _('pgAdmin III: Browse GNUmed database(s) in PostgreSQL server.'))
744                 self.Bind(wx.EVT_MENU, self.__on_pgadmin3, item)
745
746                 if _cfg.get(option = 'debug'):
747                         ID_TOGGLE_PAT_LOCK = wx.NewId()
748                         menu_debugging.Append(ID_TOGGLE_PAT_LOCK, _('Lock/unlock patient'), _('Lock/unlock patient - USE ONLY IF YOU KNOW WHAT YOU ARE DOING !'))
749                         wx.EVT_MENU(self, ID_TOGGLE_PAT_LOCK, self.__on_toggle_patient_lock)
750
751                         ID_TEST_EXCEPTION = wx.NewId()
752                         menu_debugging.Append(ID_TEST_EXCEPTION, _('Test error handling'), _('Throw an exception to test error handling.'))
753                         wx.EVT_MENU(self, ID_TEST_EXCEPTION, self.__on_test_exception)
754
755                         ID = wx.NewId()
756                         menu_debugging.Append(ID, _('Invoke inspector'), _('Invoke the widget hierarchy inspector (needs wxPython 2.8).'))
757                         wx.EVT_MENU(self, ID, self.__on_invoke_inspector)
758                         try:
759                                 import wx.lib.inspection
760                         except ImportError:
761                                 menu_debugging.Enable(id = ID, enable = False)
762
763                 help_menu.AppendSeparator()
764
765                 help_menu.Append(wx.ID_ABOUT, _('About GNUmed'), "")
766                 wx.EVT_MENU (self, wx.ID_ABOUT, self.OnAbout)
767
768                 ID_CONTRIBUTORS = wx.NewId()
769                 help_menu.Append(ID_CONTRIBUTORS, _('GNUmed contributors'), _('show GNUmed contributors'))
770                 wx.EVT_MENU(self, ID_CONTRIBUTORS, self.__on_show_contributors)
771
772                 item = help_menu.Append(-1, _('About database'), _('Show information about the current database.'))
773                 self.Bind(wx.EVT_MENU, self.__on_about_database, item)
774
775                 help_menu.AppendSeparator()
776
777                 # among other things the Manual is added from a plugin
778                 self.__gb['main.helpmenu'] = help_menu
779                 self.mainmenu.Append(help_menu, _("&Help"))
780
781
782                 # and activate menu structure
783                 self.SetMenuBar(self.mainmenu)
784         #----------------------------------------------
785         def __load_plugins(self):
786                 pass
787         #----------------------------------------------
788         # event handling
789         #----------------------------------------------
790         def __register_events(self):
791                 """register events we want to react to"""
792
793                 wx.EVT_CLOSE(self, self.OnClose)
794                 wx.EVT_ICONIZE(self, self.OnIconize)
795                 wx.EVT_MAXIMIZE(self, self.OnMaximize)
796                 wx.EVT_QUERY_END_SESSION(self, self._on_query_end_session)
797                 wx.EVT_END_SESSION(self, self._on_end_session)
798
799                 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
800                 gmDispatcher.connect(signal = u'name_mod_db', receiver = self._on_pat_name_changed)
801                 gmDispatcher.connect(signal = u'identity_mod_db', receiver = self._on_pat_name_changed)
802                 gmDispatcher.connect(signal = u'statustext', receiver = self._on_set_statustext)
803                 gmDispatcher.connect(signal = u'request_user_attention', receiver = self._on_request_user_attention)
804                 gmDispatcher.connect(signal = u'db_maintenance_warning', receiver = self._on_db_maintenance_warning)
805                 gmDispatcher.connect(signal = u'register_pre_exit_callback', receiver = self._register_pre_exit_callback)
806
807                 gmPerson.gmCurrentPatient().register_pre_selection_callback(callback = self._pre_selection_callback)
808         #----------------------------------------------
809         def _on_query_end_session(self, *args, **kwargs):
810                 wx.Bell()
811                 wx.Bell()
812                 wx.Bell()
813                 _log.warning('unhandled event detected: QUERY_END_SESSION')
814                 _log.info('we should be saving ourselves from here')
815                 gmLog2.flush()
816                 print "unhandled event detected: QUERY_END_SESSION"
817         #----------------------------------------------
818         def _on_end_session(self, *args, **kwargs):
819                 wx.Bell()
820                 wx.Bell()
821                 wx.Bell()
822                 _log.warning('unhandled event detected: END_SESSION')
823                 gmLog2.flush()
824                 print "unhandled event detected: END_SESSION"
825         #-----------------------------------------------
826         def _register_pre_exit_callback(self, callback=None):
827                 if not callable(callback):
828                         raise TypeError(u'callback [%s] not callable' % callback)
829
830                 self.__pre_exit_callbacks.append(callback)
831         #-----------------------------------------------
832         def _on_set_statustext(self, msg=None, loglevel=None, beep=True):
833
834                 if msg is None:
835                         msg = _('programmer forgot to specify status message')
836
837                 if loglevel is not None:
838                         _log.log(loglevel, msg.replace('\015', ' ').replace('\012', ' '))
839
840                 wx.CallAfter(self.SetStatusText, msg)
841
842                 if beep:
843                         wx.Bell()
844         #-----------------------------------------------
845         def _on_db_maintenance_warning(self):
846                 wx.CallAfter(self.__on_db_maintenance_warning)
847         #-----------------------------------------------
848         def __on_db_maintenance_warning(self):
849
850                 self.SetStatusText(_('The database will be shut down for maintenance in a few minutes.'))
851                 wx.Bell()
852                 if not wx.GetApp().IsActive():
853                         self.RequestUserAttention(flags = wx.USER_ATTENTION_ERROR)
854
855                 gmHooks.run_hook_script(hook = u'db_maintenance_warning')
856
857                 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
858                         None,
859                         -1,
860                         caption = _('Database shutdown warning'),
861                         question = _(
862                                 'The database will be shut down for maintenance\n'
863                                 'in a few minutes.\n'
864                                 '\n'
865                                 'In order to not suffer any loss of data you\n'
866                                 'will need to save your current work and log\n'
867                                 'out of this GNUmed client.\n'
868                         ),
869                         button_defs = [
870                                 {
871                                         u'label': _('Close now'),
872                                         u'tooltip': _('Close this GNUmed client immediately.'),
873                                         u'default': False
874                                 },
875                                 {
876                                         u'label': _('Finish work'),
877                                         u'tooltip': _('Finish and save current work first, then manually close this GNUmed client.'),
878                                         u'default': True
879                                 }
880                         ]
881                 )
882                 decision = dlg.ShowModal()
883                 if decision == wx.ID_YES:
884                         top_win = wx.GetApp().GetTopWindow()
885                         wx.CallAfter(top_win.Close)
886         #-----------------------------------------------
887         def _on_request_user_attention(self, msg=None, urgent=False):
888                 wx.CallAfter(self.__on_request_user_attention, msg, urgent)
889         #-----------------------------------------------
890         def __on_request_user_attention(self, msg=None, urgent=False):
891                 # already in the foreground ?
892                 if not wx.GetApp().IsActive():
893                         if urgent:
894                                 self.RequestUserAttention(flags = wx.USER_ATTENTION_ERROR)
895                         else:
896                                 self.RequestUserAttention(flags = wx.USER_ATTENTION_INFO)
897
898                 if msg is not None:
899                         self.SetStatusText(msg)
900
901                 if urgent:
902                         wx.Bell()
903
904                 gmHooks.run_hook_script(hook = u'request_user_attention')
905         #-----------------------------------------------
906         def _on_pat_name_changed(self):
907                 wx.CallAfter(self.__on_pat_name_changed)
908         #-----------------------------------------------
909         def __on_pat_name_changed(self):
910                 self.updateTitle()
911         #-----------------------------------------------
912         def _on_post_patient_selection(self, **kwargs):
913                 wx.CallAfter(self.__on_post_patient_selection, **kwargs)
914         #----------------------------------------------
915         def __on_post_patient_selection(self, **kwargs):
916                 self.updateTitle()
917                 try:
918                         gmHooks.run_hook_script(hook = u'post_patient_activation')
919                 except:
920                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot run script after patient activation.'))
921                         raise
922         #----------------------------------------------
923         def _pre_selection_callback(self):
924                 self.__sanity_check_encounter()
925         #----------------------------------------------
926         def __sanity_check_encounter(self):
927
928                 dbcfg = gmCfg.cCfgSQL()
929                 check_enc = bool(dbcfg.get2 (
930                         option = 'encounter.show_editor_before_patient_change',
931                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
932                         bias = 'user',
933                         default = True                                  # True: if needed, not always unconditionally
934                 ))
935
936                 if not check_enc:
937                         return True
938
939                 pat = gmPerson.gmCurrentPatient()
940                 emr = pat.get_emr()
941                 enc = emr.get_active_encounter()
942
943                 # did we add anything to the EMR ?
944                 has_narr = enc.has_narrative()
945                 has_docs = enc.has_documents()
946
947                 if (not has_narr) and (not has_docs):
948                         return True
949
950                 empty_aoe = (gmTools.coalesce(enc['assessment_of_encounter'], '').strip() == u'')
951                 zero_duration = (enc['last_affirmed'] == enc['started'])
952
953                 # all is well anyway
954                 if (not empty_aoe) and (not zero_duration):
955                         return True
956
957                 if zero_duration:
958                         enc['last_affirmed'] = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
959
960                 # no narrative, presumably only import of docs and done
961                 if not has_narr:
962                         if empty_aoe:
963                                 enc['assessment_of_encounter'] = _('only documents added')
964                         enc['pk_type'] = gmEMRStructItems.get_encounter_type(description = 'chart review')[0]['pk']
965                         # "last_affirmed" should be latest modified_at of relevant docs but that's a lot more involved
966                         enc.save_payload()
967                         return True
968
969                 # does have narrative
970                 if empty_aoe:
971                         # - work out suitable default
972                         epis = emr.get_episodes_by_encounter()
973                         if len(epis) > 0:
974                                 enc_summary = ''
975                                 for epi in epis:
976                                         enc_summary += '%s; ' % epi['description']
977                                 enc['assessment_of_encounter'] = enc_summary
978
979                 dlg = gmEMRStructWidgets.cEncounterEditAreaDlg(parent=self, encounter=enc)
980                 dlg.ShowModal()
981
982                 return True
983         #----------------------------------------------
984         # menu "paperwork"
985         #----------------------------------------------
986         def __on_show_docs(self, evt):
987                 gmDispatcher.send(signal='show_document_viewer')
988         #----------------------------------------------
989         def __on_new_letter(self, evt):
990                 pat = gmPerson.gmCurrentPatient()
991                 if not pat.connected:
992                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot write letter. No active patient.'), beep = True)
993                         return True
994                 gmFormWidgets.create_new_letter(parent = self)
995         #----------------------------------------------
996         def __on_edit_templates(self, evt):
997                 gmFormWidgets.let_user_select_form_template(parent = self)
998         #----------------------------------------------
999         # help menu
1000         #----------------------------------------------
1001         def OnAbout(self, event):
1002                 from Gnumed.wxpython import gmAbout
1003                 gmAbout = gmAbout.AboutFrame (
1004                         self,
1005                         -1,
1006                         _("About GNUmed"),
1007                         size=wx.Size(350, 300),
1008                         style = wx.MAXIMIZE_BOX,
1009                         version = current_client_ver
1010                 )
1011                 gmAbout.Centre(wx.BOTH)
1012                 gmTopLevelFrame.otherWin = gmAbout
1013                 gmAbout.Show(True)
1014                 del gmAbout
1015         #----------------------------------------------
1016         def __on_about_database(self, evt):
1017                 praxis = gmSurgery.gmCurrentPractice()
1018                 msg = praxis.db_logon_banner
1019                 if msg != u'':
1020                         gmGuiHelpers.gm_show_info(msg, _('About database'))
1021         #----------------------------------------------
1022         def __on_show_contributors(self, event):
1023                 from Gnumed.wxpython import gmAbout
1024                 contribs = gmAbout.cContributorsDlg (
1025                         parent = self,
1026                         id = -1,
1027                         title = _('GNUmed contributors'),
1028                         size = wx.Size(400,600),
1029                         style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
1030                 )
1031                 contribs.ShowModal()
1032                 del contribs
1033                 del gmAbout
1034         #----------------------------------------------
1035         # GNUmed menu
1036         #----------------------------------------------
1037         def __on_exit_gnumed(self, event):
1038                 """Invoked from Menu->Exit (calls ID_EXIT handler)."""
1039                 self.Close()    # -> calls wx.EVT_CLOSE handler
1040         #----------------------------------------------
1041         def __on_check_for_updates(self, evt):
1042                 check_for_updates()
1043         #----------------------------------------------
1044         # submenu GNUmed / options / client
1045         #----------------------------------------------
1046         def __on_set_temp_dir(self, evt):
1047
1048                 cfg = gmCfg.cCfgSQL()
1049
1050                 tmp_dir = gmTools.coalesce (
1051                         cfg.get2 (
1052                                 option = "horstspace.tmp_dir",
1053                                 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1054                                 bias = 'workplace'
1055                         ),
1056                         os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
1057                 )
1058
1059                 dlg = wx.DirDialog (
1060                         parent = self,
1061                         message = _('Choose temporary directory ...'),
1062                         defaultPath = tmp_dir,
1063                         style = wx.DD_DEFAULT_STYLE
1064                 )
1065                 result = dlg.ShowModal()
1066                 tmp_dir = dlg.GetPath()
1067                 dlg.Destroy()
1068
1069                 if result != wx.ID_OK:
1070                         return
1071
1072                 cfg.set (
1073                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
1074                         option = "horstspace.tmp_dir",
1075                         value = tmp_dir
1076                 )
1077         #----------------------------------------------
1078         def __on_set_export_chunk_size(self, evt):
1079
1080                 def is_valid(value):
1081                         try:
1082                                 i = int(value)
1083                         except:
1084                                 return False, value
1085                         if i < 0:
1086                                 return False, value
1087                         if i > (1024 * 1024 * 1024 * 10):               # 10 GB
1088                                 return False, value
1089                         return True, i
1090
1091                 gmCfgWidgets.configure_string_option (
1092                         message = _(
1093                                 'Some network installations cannot cope with loading\n'
1094                                 'documents of arbitrary size in one piece from the\n'
1095                                 'database (mainly observed on older Windows versions)\n.'
1096                                 '\n'
1097                                 'Under such circumstances documents need to be retrieved\n'
1098                                 'in chunks and reassembled on the client.\n'
1099                                 '\n'
1100                                 'Here you can set the size (in Bytes) above which\n'
1101                                 'GNUmed will retrieve documents in chunks. Setting this\n'
1102                                 'value to 0 will disable the chunking protocol.'
1103                         ),
1104                         option = 'horstspace.blob_export_chunk_size',
1105                         bias = 'workplace',
1106                         default_value = 1024 * 1024,
1107                         validator = is_valid
1108                 )
1109         #----------------------------------------------
1110         # submenu GNUmed / database
1111         #----------------------------------------------
1112         def __on_set_db_lang(self, event):
1113
1114                 langs = [
1115                         gmI18N.system_locale_level['language'],
1116                         gmI18N.system_locale_level['country'],
1117                         gmI18N.system_locale_level['full']
1118                 ]
1119
1120                 rows, idx = gmPG2.run_ro_queries (
1121                         queries = [{'cmd': u'select distinct lang from i18n.translations'}]
1122                 )
1123                 langs.extend([ r[0] for r in rows ])
1124
1125                 language = gmListWidgets.get_choices_from_list (
1126                         parent = self,
1127                         msg = _(
1128                                 'Please select the database language from the list below.\n'
1129                                 '\n'
1130                                 'This setting will not affect the language the user interface is\n'
1131                                 'displayed in but rather that of the data returned from the database\n'
1132                                 'such as encounter types, document types, and EMR formatting.\n'
1133                                 '\n'
1134                         ),
1135                         caption = _('Configuring database language'),
1136                         choices = langs,
1137                         columns = [_('Language')],
1138                         data = langs,
1139                         single_selection = True
1140                 )
1141
1142                 if language is None:
1143                         return
1144
1145                 _log.info('setting database language to [%s]', language)
1146                 rows, idx = gmPG2.run_rw_queries (
1147                         queries = [{'cmd': u'select i18n.set_curr_lang(%(lang)s)', 'args': {'lang': language}}],
1148                         return_data = True
1149                 )
1150
1151                 if rows[0][0]:
1152                         return
1153
1154                 force_language = gmGuiHelpers.gm_show_question (
1155                         _('The database currently holds no translations for\n'
1156                           'language [%s]. However, you can add translations\n'
1157                           'for things like document or encounter types yourself.\n'
1158                           '\n'
1159                           'Do you want to force the language setting to [%s] ?'
1160                         ) % (language, language),
1161                         _('Configuring database language')
1162                 )
1163                 if not force_language:
1164                         return
1165
1166                 _log.info('forcing database language to [%s]', language)
1167                 gmPG2.run_rw_queries(queries = [{
1168                         'cmd': u'select i18n.force_curr_lang(%s)',
1169                         'args': [gmI18N.system_locale_level['country']]
1170                 }])
1171         #----------------------------------------------
1172         def __on_set_db_welcome(self, event):
1173                 dlg = gmGuiHelpers.cGreetingEditorDlg(self, -1)
1174                 dlg.ShowModal()
1175         #----------------------------------------------
1176         # submenu GNUmed - config - external tools
1177         #----------------------------------------------
1178         def __on_set_ooo_settle_time(self, event):
1179
1180                 def is_valid(value):
1181                         try:
1182                                 return True, float(value)
1183                         except:
1184                                 return False, value
1185
1186                 gmCfgWidgets.configure_string_option (
1187                         message = _(
1188                                 'When GNUmed cannot find an OpenOffice server it\n'
1189                                 'will try to start one. OpenOffice, however, needs\n'
1190                                 'some time to fully start up.\n'
1191                                 '\n'
1192                                 'Here you can set the time for GNUmed to wait for OOo.\n'
1193                         ),
1194                         option = 'external.ooo.startup_settle_time',
1195                         bias = 'workplace',
1196                         default_value = 2.0,
1197                         validator = is_valid
1198                 )
1199         #----------------------------------------------
1200         def __on_set_ifap_cmd(self, event):
1201
1202                 def is_valid(value):
1203                         found, binary = gmShellAPI.detect_external_binary(value)
1204                         if not found:
1205                                 gmDispatcher.send (
1206                                         signal = 'statustext',
1207                                         msg = _('The command [%s] is not found. This may or may not be a problem.') % value,
1208                                         beep = True
1209                                 )
1210                                 return False, value
1211                         return True, binary
1212
1213                 gmCfgWidgets.configure_string_option (
1214                         message = _(
1215                                 'Enter the shell command with which to start the\n'
1216                                 'the IFAP drug database.\n'
1217                                 '\n'
1218                                 'GNUmed will try to verify the path which may,\n'
1219                                 'however, fail if you are using an emulator such\n'
1220                                 'as Wine. Nevertheless, starting IFAP will work\n'
1221                                 'as long as the shell command is correct despite\n'
1222                                 'the failing test.'
1223                         ),
1224                         option = 'external.ifap-win.shell_command',
1225                         bias = 'workplace',
1226                         default_value = 'C:\Ifapwin\WIAMDB.EXE',
1227                         validator = is_valid
1228                 )
1229         #----------------------------------------------
1230         # submenu GNUmed / config / ui
1231         #----------------------------------------------
1232         def __on_set_startup_plugin(self, evt):
1233
1234                 dbcfg = gmCfg.cCfgSQL()
1235                 # get list of possible plugins
1236                 plugin_list = gmTools.coalesce(dbcfg.get2 (
1237                         option = u'horstspace.notebook.plugin_load_order',
1238                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
1239                         bias = 'user'
1240                 ), [])
1241
1242                 # get current setting
1243                 initial_plugin = gmTools.coalesce(dbcfg.get2 (
1244                         option = u'horstspace.plugin_to_raise_after_startup',
1245                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
1246                         bias = 'user'
1247                 ), u'gmEMRBrowserPlugin')
1248                 try:
1249                         selections = [plugin_list.index(initial_plugin)]
1250                 except ValueError:
1251                         selections = None
1252
1253                 # now let user decide
1254                 plugin = gmListWidgets.get_choices_from_list (
1255                         parent = self,
1256                         msg = _(
1257                                 'Here you can choose which plugin you want\n'
1258                                 'GNUmed to display after initial startup.\n'
1259                                 '\n'
1260                                 'Note that the plugin must not require any\n'
1261                                 'patient to be activated.\n'
1262                                 '\n'
1263                                 'Select the desired plugin below:'
1264                         ),
1265                         caption = _('Configuration'),
1266                         choices = plugin_list,
1267                         selections = selections,
1268                         columns = [_('GNUmed Plugin')],
1269                         single_selection = True
1270                 )
1271
1272                 if plugin is None:
1273                         return
1274
1275                 dbcfg.set (
1276                         option = u'patient_search.plugin_to_raise_after_startup',
1277                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
1278                         value = plugin
1279                 )
1280         #----------------------------------------------
1281         # submenu GNUmed / config / ui / patient search
1282         #----------------------------------------------
1283         def __on_set_quick_pat_search(self, evt):
1284                 gmCfgWidgets.configure_boolean_option (
1285                         parent = self,
1286                         question = _(
1287                                 'If there is only one external patient\n'
1288                                 'source available do you want GNUmed\n'
1289                                 'to immediately go ahead and search for\n'
1290                                 'matching patient records ?\n\n'
1291                                 'If not GNUmed will let you confirm the source.'
1292                         ),
1293                         option = 'patient_search.external_sources.immediately_search_if_single_source',
1294                         button_tooltips = [
1295                                 _('Yes, search for matches immediately.'),
1296                                 _('No, let me confirm the external patient first.')
1297                         ]
1298                 )
1299         #----------------------------------------------
1300         def __on_set_dob_reminder_proximity(self, evt):
1301
1302                 def is_valid(value):
1303                         return gmPG2.is_pg_interval(candidate=value), value
1304
1305                 gmCfgWidgets.configure_string_option (
1306                         message = _(
1307                                 'When a patient is activated GNUmed checks the\n'
1308                                 "proximity of the patient's birthday.\n"
1309                                 '\n'
1310                                 'If the birthday falls within the range of\n'
1311                                 ' "today %s <the interval you set here>"\n'
1312                                 'GNUmed will remind you of the recent or\n'
1313                                 'imminent anniversary.'
1314                         ) % u'\u2213',
1315                         option = u'patient_search.dob_warn_interval',
1316                         bias = 'user',
1317                         default_value = '1 week',
1318                         validator = is_valid
1319                 )
1320         #----------------------------------------------
1321         def __on_allow_multiple_new_episodes(self, evt):
1322
1323                 gmCfgWidgets.configure_boolean_option (
1324                         parent = self,
1325                         question = _(
1326                                 'When adding progress notes do you want to\n'
1327                                 'allow opening several unassociated, new\n'
1328                                 'episodes for a patient at once ?\n'
1329                                 '\n'
1330                                 'This can be particularly helpful when entering\n'
1331                                 'progress notes on entirely new patients presenting\n'
1332                                 'with a multitude of problems on their first visit.'
1333                         ),
1334                         option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1335                         button_tooltips = [
1336                                 _('Yes, allow for multiple new episodes concurrently.'),
1337                                 _('No, only allow editing one new episode at a time.')
1338                         ]
1339                 )
1340         #----------------------------------------------
1341         def __on_set_initial_pat_plugin(self, evt):
1342
1343                 dbcfg = gmCfg.cCfgSQL()
1344                 # get list of possible plugins
1345                 plugin_list = gmTools.coalesce(dbcfg.get2 (
1346                         option = u'horstspace.notebook.plugin_load_order',
1347                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
1348                         bias = 'user'
1349                 ), [])
1350
1351                 # get current setting
1352                 initial_plugin = gmTools.coalesce(dbcfg.get2 (
1353                         option = u'patient_search.plugin_to_raise_after_search',
1354                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
1355                         bias = 'user'
1356                 ), u'gmEMRBrowserPlugin')
1357                 try:
1358                         selections = [plugin_list.index(initial_plugin)]
1359                 except ValueError:
1360                         selections = None
1361
1362                 # now let user decide
1363                 plugin = gmListWidgets.get_choices_from_list (
1364                         parent = self,
1365                         msg = _(
1366                                 'When a patient is activated GNUmed can\n'
1367                                 'be told to switch to a specific plugin.\n'
1368                                 '\n'
1369                                 'Select the desired plugin below:'
1370                         ),
1371                         caption = _('Configuration'),
1372                         choices = plugin_list,
1373                         selections = selections,
1374                         columns = [_('GNUmed Plugin')],
1375                         single_selection = True
1376                 )
1377
1378                 if plugin is None:
1379                         return
1380
1381                 dbcfg.set (
1382                         option = u'patient_search.plugin_to_raise_after_search',
1383                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
1384                         value = plugin
1385                 )
1386         #----------------------------------------------
1387         # submenu GNUmed / config / encounter
1388         #----------------------------------------------
1389         def __on_cfg_enc_default_type(self, evt):
1390                 enc_types = gmEMRStructItems.get_encounter_types()
1391
1392                 gmCfgWidgets.configure_string_from_list_option (
1393                         parent = self,
1394                         message = _('Select the default type for new encounters.\n'),
1395                         option = 'encounter.default_type',
1396                         bias = 'user',
1397                         default_value = u'in surgery',
1398                         choices = [ e[0] for e in enc_types ],
1399                         columns = [_('Encounter type')],
1400                         data = [ e[1] for e in enc_types ]
1401                 )
1402         #----------------------------------------------
1403         def __on_cfg_enc_pat_change(self, event):
1404                 gmCfgWidgets.configure_boolean_option (
1405                         parent = self,
1406                         question = _(
1407                                 'Do you want GNUmed to show the consultation\n'
1408                                 'details editor when changing the active patient ?'
1409                         ),
1410                         option = 'encounter.show_editor_before_patient_change',
1411                         button_tooltips = [
1412                                 _('Yes, show the consultation editor if it seems appropriate.'),
1413                                 _('No, never show the consultation editor even if it would seem useful.')
1414                         ]
1415                 )
1416         #----------------------------------------------
1417         def __on_cfg_enc_empty_ttl(self, evt):
1418
1419                 def is_valid(value):
1420                         return gmPG2.is_pg_interval(candidate=value), value
1421
1422                 gmCfgWidgets.configure_string_option (
1423                         message = _(
1424                                 'When a patient is activated GNUmed checks the\n'
1425                                 'chart for encounters lacking any entries.\n'
1426                                 '\n'
1427                                 'Any such encounters older than what you set\n'
1428                                 'here will be removed from the medical record.\n'
1429                                 '\n'
1430                                 'To effectively disable removal of such encounters\n'
1431                                 'set this option to an improbable value.\n'
1432                         ),
1433                         option = 'encounter.ttl_if_empty',
1434                         bias = 'user',
1435                         default_value = '1 week',
1436                         validator = is_valid
1437                 )
1438         #----------------------------------------------
1439         def __on_cfg_enc_min_ttl(self, evt):
1440
1441                 def is_valid(value):
1442                         return gmPG2.is_pg_interval(candidate=value), value
1443
1444                 gmCfgWidgets.configure_string_option (
1445                         message = _(
1446                                 'When a patient is activated GNUmed checks the\n'
1447                                 'age of the most recent consultation.\n'
1448                                 '\n'
1449                                 'If that consultation is younger than this age\n'
1450                                 'the existing consultation will be continued.\n'
1451                                 '\n'
1452                                 '(If it is really old a new consultation is\n'
1453                                 ' started, or else GNUmed will ask you.)\n'
1454                         ),
1455                         option = 'encounter.minimum_ttl',
1456                         bias = 'user',
1457                         default_value = '1 hour 30 minutes',
1458                         validator = is_valid
1459                 )
1460         #----------------------------------------------
1461         def __on_cfg_enc_max_ttl(self, evt):
1462
1463                 def is_valid(value):
1464                         return gmPG2.is_pg_interval(candidate=value), value
1465
1466                 gmCfgWidgets.configure_string_option (
1467                         message = _(
1468                                 'When a patient is activated GNUmed checks the\n'
1469                                 'age of the most recent consultation.\n'
1470                                 '\n'
1471                                 'If that consultation is older than this age\n'
1472                                 'GNUmed will always start a new consultation.\n'
1473                                 '\n'
1474                                 '(If it is very recent the existing consultation\n'
1475                                 ' is continued, or else GNUmed will ask you.)\n'
1476                         ),
1477                         option = 'encounter.maximum_ttl',
1478                         bias = 'user',
1479                         default_value = '6 hours',
1480                         validator = is_valid
1481                 )
1482         #----------------------------------------------
1483         def __on_cfg_epi_ttl(self, evt):
1484
1485                 def is_valid(value):
1486                         try:
1487                                 value = int(value)
1488                         except:
1489                                 return False, value
1490                         return gmPG2.is_pg_interval(candidate=value), value
1491
1492                 gmCfgWidgets.configure_string_option (
1493                         message = _(
1494                                 'At any time there can only be one open (ongoing) episode\n'
1495                                 'for each foundational health issue.\n'
1496                                 '\n'
1497                                 'When you try to open (add data to) an episode on a health\n'
1498                                 'issue GNUmed will check for an existing open episode on\n'
1499                                 'that issue. If there is any it will check the age of that\n'
1500                                 'episode. The episode is closed if it has been dormant (no\n'
1501                                 'data added, that is) for the period of time (in days) you\n'
1502                                 'set here.\n'
1503                                 '\n'
1504                                 "If the existing episode hasn't been dormant long enough\n"
1505                                 'GNUmed will consult you what to do.\n'
1506                                 '\n'
1507                                 'Enter maximum episode dormancy in DAYS:'
1508                         ),
1509                         option = 'episode.ttl',
1510                         bias = 'user',
1511                         default_value = 60,
1512                         validator = is_valid
1513                 )
1514         #----------------------------------------------
1515         def __on_set_user_email(self, evt):
1516                 email = gmSurgery.gmCurrentPractice().user_email
1517
1518                 dlg = wx.TextEntryDialog (
1519                         parent = self,
1520                         message = _(
1521                                 'This email address will be used when GNUmed\n'
1522                                 'is sending email on your behalf such as when\n'
1523                                 'reporting bugs or when you choose to contribute\n'
1524                                 'reference material to the GNUmed community.\n'
1525                                 '\n'
1526                                 'The developers will then be able to get back to you\n'
1527                                 'directly with advice. Otherwise you would have to\n'
1528                                 'follow the mailing list discussion for help.\n'
1529                                 '\n'
1530                                 'Leave this blank if you wish to stay anonymous.'
1531                         ),
1532                         caption = _('Please enter your email address.'),
1533                         defaultValue = gmTools.coalesce(email, u''),
1534                         style = wx.OK | wx.CANCEL | wx.CENTRE
1535                 )
1536                 decision = dlg.ShowModal()
1537                 if decision == wx.ID_CANCEL:
1538                         dlg.Destroy()
1539                         return
1540
1541                 email = dlg.GetValue().strip()
1542                 gmSurgery.gmCurrentPractice().user_email = email
1543                 gmExceptionHandlingWidgets.set_sender_email(email)
1544                 dlg.Destroy()
1545         #----------------------------------------------
1546         def __on_configure_workplace(self, evt):
1547                 gmProviderInboxWidgets.configure_workplace_plugins(parent = self)
1548         #----------------------------------------------
1549         def __on_configure_update_check(self, evt):
1550                 gmCfgWidgets.configure_boolean_option (
1551                         question = _(
1552                                 'Do you want GNUmed to check for updates at startup ?\n'
1553                                 '\n'
1554                                 'You will still need your system administrator to\n'
1555                                 'actually install any updates for you.\n'
1556                         ),
1557                         option = u'horstspace.update.autocheck_at_startup',
1558                         button_tooltips = [
1559                                 _('Yes, check for updates at startup.'),
1560                                 _('No, do not check for updates at startup.')
1561                         ]
1562                 )
1563         #----------------------------------------------
1564         def __on_configure_update_check_scope(self, evt):
1565                 gmCfgWidgets.configure_boolean_option (
1566                         question = _(
1567                                 'When checking for updates do you want GNUmed to\n'
1568                                 'look for bug fix updates only or do you want to\n'
1569                                 'know about features updates, too ?\n'
1570                                 '\n'
1571                                 'Minor updates (x.y.z.a -> x.y.z.b) contain bug fixes\n'
1572                                 'only. They can usually be installed without much\n'
1573                                 'preparation. They never require a database upgrade.\n'
1574                                 '\n'
1575                                 'Major updates (x.y.a -> x..y.b or y.a -> x.b) come\n'
1576                                 'with new features. They need more preparation and\n'
1577                                 'often require a database upgrade.\n'
1578                                 '\n'
1579                                 'You will still need your system administrator to\n'
1580                                 'actually install any updates for you.\n'
1581                         ),
1582                         option = u'horstspace.update.consider_latest_branch',
1583                         button_tooltips = [
1584                                 _('Yes, check for feature updates, too.'),
1585                                 _('No, check for bug-fix updates only.')
1586                         ]
1587                 )
1588         #----------------------------------------------
1589         def __on_configure_update_url(self, evt):
1590
1591                 import urllib2 as url
1592
1593                 def is_valid(value):
1594                         try:
1595                                 url.urlopen(value)
1596                         except:
1597                                 return False, value
1598
1599                         return True, value
1600
1601                 gmCfgWidgets.configure_string_option (
1602                         message = _(
1603                                 'GNUmed can check for new releases being available. To do\n'
1604                                 'so it needs to load version information from an URL.\n'
1605                                 '\n'
1606                                 'The default URL is:\n'
1607                                 '\n'
1608                                 ' http://www.gnumed.de/downloads/gnumed-versions.txt\n'
1609                                 '\n'
1610                                 'but you can configure any other URL locally. Note\n'
1611                                 'that you must enter the location as a valid URL.\n'
1612                                 'Depending on the URL the client will need online\n'
1613                                 'access when checking for updates.'
1614                         ),
1615                         option = u'horstspace.update.url',
1616                         bias = u'workplace',
1617                         default_value = u'http://www.gnumed.de/downloads/gnumed-versions.txt',
1618                         validator = is_valid
1619                 )
1620         #----------------------------------------------
1621         def __on_configure_partless_docs(self, evt):
1622                 gmCfgWidgets.configure_boolean_option (
1623                         question = _(
1624                                 'Do you want to allow saving of new documents without\n'
1625                                 'any parts or do you want GNUmed to enforce that they\n'
1626                                 'contain at least one part before they can be saved ?\n'
1627                                 '\n'
1628                                 'Part-less documents can be useful if you want to build\n'
1629                                 'up an index of, say, archived documents but do not\n'
1630                                 'want to scan in all the pages contained therein.'
1631                         ),
1632                         option = u'horstspace.scan_index.allow_partless_documents',
1633                         button_tooltips = [
1634                                 _('Yes, allow saving documents without any parts.'),
1635                                 _('No, require documents to have at least one part.')
1636                         ]
1637                 )
1638         #----------------------------------------------
1639         def __on_configure_doc_uuid_dialog(self, evt):
1640                 gmCfgWidgets.configure_boolean_option (
1641                         question = _(
1642                                 'After importing a new document do you\n'
1643                                 'want GNUmed to display the unique ID\n'
1644                                 'it auto-generated for that document ?\n'
1645                                 '\n'
1646                                 'This can be useful if you want to label the\n'
1647                                 'originals with that ID for later identification.'
1648                         ),
1649                         option = u'horstspace.scan_index.show_doc_id',
1650                         button_tooltips = [
1651                                 _('Yes, display the ID generated for the new document after importing.'),
1652                                 _('No, do not display the ID generated for the new document after importing.')
1653                         ]
1654                 )
1655         #----------------------------------------------
1656         def __on_configure_doc_review_dialog(self, evt):
1657
1658                 def is_valid(value):
1659                         try:
1660                                 value = int(value)
1661                         except:
1662                                 return False, value
1663                         if value not in [0, 1, 2]:
1664                                 return False, value
1665                         return True, value
1666
1667                 gmCfgWidgets.configure_string_option (
1668                         message = _(
1669                                 'GNUmed can show the document review dialog after\n'
1670                                 'calling the appropriate viewer for that document.\n'
1671                                 '\n'
1672                                 'Select the conditions under which you want\n'
1673                                 'GNUmed to do so:\n'
1674                                 '\n'
1675                                 ' 0: never display the review dialog\n'
1676                                 ' 1: always display the dialog\n'
1677                                 ' 2: only if there is no previous review by me\n'
1678                                 '\n'
1679                                 'Note that if a viewer is configured to not block\n'
1680                                 'GNUmed during document display the review dialog\n'
1681                                 'will actually appear in parallel to the viewer.'
1682                         ),
1683                         option = u'horstspace.document_viewer.review_after_display',
1684                         bias = u'user',
1685                         default_value = 2,
1686                         validator = is_valid
1687                 )
1688         #----------------------------------------------
1689         def __on_dicom_viewer(self, evt):
1690
1691                 if os.access('/Applications/OsiriX.app/Contents/MacOS/OsiriX', os.X_OK):
1692                         gmShellAPI.run_command_in_shell('/Applications/OsiriX.app/Contents/MacOS/OsiriX', blocking=False)
1693                         return
1694
1695                 for viewer in ['aeskulap', 'amide', 'xmedcon']:
1696                         found, cmd = gmShellAPI.detect_external_binary(binary = viewer)
1697                         if found:
1698                                 gmShellAPI.run_command_in_shell(cmd, blocking=False)
1699                                 return
1700
1701                 gmDispatcher.send(signal = 'statustext', msg = _('No DICOM viewer found.'), beep = True)
1702         #----------------------------------------------
1703         def __on_snellen(self, evt):
1704                 dlg = gmSnellen.cSnellenCfgDlg()
1705                 if dlg.ShowModal() != wx.ID_OK:
1706                         return
1707
1708                 frame = gmSnellen.cSnellenChart (
1709                         width = dlg.vals[0],
1710                         height = dlg.vals[1],
1711                         alpha = dlg.vals[2],
1712                         mirr = dlg.vals[3],
1713                         parent = None
1714                 )
1715                 frame.CentreOnScreen(wx.BOTH)
1716 #               self.SetTopWindow(frame)
1717 #               frame.Destroy = frame.DestroyWhenApp
1718                 frame.Show(True)
1719         #----------------------------------------------
1720         #----------------------------------------------
1721         def __on_medical_links(self, evt):
1722                 webbrowser.open (
1723                         url = 'http://wiki.gnumed.de/bin/view/Gnumed/MedicalContentLinks#AnchorLocaleI%s' % gmI18N.system_locale_level['language'],
1724                         new = False,
1725                         autoraise = True
1726                 )
1727         #----------------------------------------------
1728         def __on_ifap(self, evt):
1729                 jump_to_ifap()
1730         #----------------------------------------------
1731         def __on_kompendium_ch(self, evt):
1732                 webbrowser.open (
1733                         url = 'http://www.kompendium.ch',
1734                         new = False,
1735                         autoraise = True
1736                 )
1737         #----------------------------------------------
1738         # Help / Debugging
1739         #----------------------------------------------
1740         def __on_save_screenshot(self, evt):
1741
1742 #               src_rect = self.GetRect()
1743 #               sdc = wx.ScreenDC()                                             # whole screen area
1744 #               mdc = wx.MemoryDC()
1745 #               img = wx.EmptyBitmap(src_rect.width, src_rect.height)   # must be large enough for snapshot
1746 #               mdc.SelectObject(img)
1747 #               mdc.Blit (                                                              # copy ...
1748 #                       0, 0,                                                           # ... to here in the target ...
1749 #                       src_rect.width, src_rect.height,        # ... that much in ...
1750 #                       sdc,                                                            # ... the source (the screen) ...
1751 #                       0, 0                                                            # ... starting at
1752 #               )
1753
1754                 w, h = self.GetSize()
1755                 wdc = wx.WindowDC(self)
1756                 mdc = wx.MemoryDC()
1757                 img = wx.EmptyBitmap(w, h)
1758                 mdc.SelectObject(img)
1759                 mdc.Blit(0, 0, w, h, wdc, 0, 0)
1760
1761                 # FIXME: improve filename with patient/workplace/provider, allow user to select/change
1762                 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'gnumed-screenshot-%s.png')) % pyDT.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
1763                 img.SaveFile(fname, wx.BITMAP_TYPE_PNG)
1764                 gmDispatcher.send(signal = 'statustext', msg = _('Saved screenshot to file [%s].') % fname)
1765                 evt.Skip()
1766         #----------------------------------------------
1767         def __on_test_exception(self, evt):
1768                 #import nonexistant_module
1769                 raise ValueError('raised ValueError to test exception handling')
1770         #----------------------------------------------
1771         def __on_invoke_inspector(self, evt):
1772                 import wx.lib.inspection
1773                 wx.lib.inspection.InspectionTool().Show()
1774         #----------------------------------------------
1775         def __on_display_bugtracker(self, evt):
1776                 webbrowser.open (
1777                         url = 'http://savannah.gnu.org/bugs/?group=gnumed',
1778                         new = False,
1779                         autoraise = True
1780                 )
1781         #----------------------------------------------
1782         def __on_display_wiki(self, evt):
1783                 webbrowser.open (
1784                         url = 'http://wiki.gnumed.de',
1785                         new = False,
1786                         autoraise = True
1787                 )
1788         #----------------------------------------------
1789         def __on_display_user_manual_online(self, evt):
1790                 webbrowser.open (
1791                         url = 'http://wiki.gnumed.de/bin/view/Gnumed/GnumedManual#UserGuideInManual',
1792                         new = False,
1793                         autoraise = True
1794                 )
1795         #----------------------------------------------
1796         def __on_pgadmin3(self, evt):
1797                 found, cmd = gmShellAPI.detect_external_binary(binary = 'pgadmin3')
1798                 if found:
1799                         gmShellAPI.run_command_in_shell(cmd, blocking=False)
1800                         return
1801                 gmDispatcher.send(signal = 'statustext', msg = _('pgAdmin III not found.'), beep = True)
1802         #----------------------------------------------
1803         def __on_unblock_cursor(self, evt):
1804                 wx.EndBusyCursor()
1805         #----------------------------------------------
1806         def __on_toggle_patient_lock(self, evt):
1807                 curr_pat = gmPerson.gmCurrentPatient()
1808                 if curr_pat.locked:
1809                         curr_pat.force_unlock()
1810                 else:
1811                         curr_pat.locked = True
1812         #----------------------------------------------
1813         def __on_backup_log_file(self, evt):
1814                 name = os.path.basename(gmLog2._logfile_name)
1815                 name, ext = os.path.splitext(name)
1816                 new_name = '%s_%s%s' % (name, pyDT.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), ext)
1817                 new_path = os.path.expanduser(os.path.join('~', 'gnumed', 'logs'))
1818
1819                 dlg = wx.FileDialog (
1820                         parent = self,
1821                         message = _("Save current log as..."),
1822                         defaultDir = new_path,
1823                         defaultFile = new_name,
1824                         wildcard = "%s (*.log)|*.log" % _("log files"),
1825                         style = wx.SAVE
1826                 )
1827                 choice = dlg.ShowModal()
1828                 new_name = dlg.GetPath()
1829                 dlg.Destroy()
1830                 if choice != wx.ID_OK:
1831                         return True
1832
1833                 _log.warning('syncing log file for backup to [%s]', new_name)
1834                 gmLog2.flush()
1835                 shutil.copy2(gmLog2._logfile_name, new_name)
1836                 gmDispatcher.send('statustext', msg = _('Log file backed up as [%s].') % new_name)
1837         #----------------------------------------------
1838         # GNUmed /
1839         #----------------------------------------------
1840         def OnClose(self, event):
1841                 """This is the wx.EVT_CLOSE handler.
1842
1843                 - framework still functional
1844                 """
1845                 self._clean_exit()
1846                 self.Destroy()
1847         #----------------------------------------------
1848         def OnExportEMR(self, event):
1849                 """
1850                 Export selected patient EMR to a file
1851                 """
1852                 gmEMRBrowser.export_emr_to_ascii(parent=self)
1853         #----------------------------------------------
1854         def __dermtool (self, event):
1855                 import Gnumed.wxpython.gmDermTool as DT
1856                 frame = DT.DermToolDialog(None, -1)
1857                 frame.Show(True)
1858         #----------------------------------------------
1859         def __on_start_new_encounter(self, evt):
1860                 pat = gmPerson.gmCurrentPatient()
1861                 if not pat.connected:
1862                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot start new encounter. No active patient.'))
1863                         return False
1864                 emr = pat.get_emr()
1865                 emr.start_new_encounter()
1866                 gmDispatcher.send(signal = 'statustext', msg = _('Started a new encounter for the active patient.'))
1867         #----------------------------------------------
1868         def __on_add_health_issue(self, event):
1869                 pat = gmPerson.gmCurrentPatient()
1870                 if not pat.connected:
1871                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot add health issue. No active patient.'))
1872                         return False
1873                 ea = gmEMRStructWidgets.cHealthIssueEditAreaDlg(parent=self, id=-1)
1874                 ea.ShowModal()
1875         #----------------------------------------------
1876         def __on_add_medication(self, evt):
1877                 pat = gmPerson.gmCurrentPatient()
1878                 if not pat.connected:
1879                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot add medication. No active patient.'))
1880                         return False
1881
1882                 jump_to_ifap(import_drugs = True)
1883
1884                 evt.Skip()
1885         #----------------------------------------------
1886         def __on_manage_allergies(self, evt):
1887                 pat = gmPerson.gmCurrentPatient()
1888                 if not pat.connected:
1889                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot add allergy. No active patient.'))
1890                         return False
1891                 dlg = gmAllergyWidgets.cAllergyManagerDlg(parent=self, id=-1)
1892                 dlg.ShowModal()
1893         #----------------------------------------------
1894         def __on_edit_occupation(self, evt):
1895                 pat = gmPerson.gmCurrentPatient()
1896                 if not pat.connected:
1897                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit occupation. No active patient.'))
1898                         return False
1899                 gmDemographicsWidgets.edit_occupation()
1900                 evt.Skip()
1901         #----------------------------------------------
1902         def __on_add_measurement(self, evt):
1903                 pat = gmPerson.gmCurrentPatient()
1904                 if not pat.connected:
1905                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot add measurement. No active patient.'))
1906                         return False
1907                 gmMeasurementWidgets.edit_measurement(parent = self, measurement = None)
1908                 evt.Skip()
1909         #----------------------------------------------
1910         def __on_show_emr_summary(self, event):
1911                 pat = gmPerson.gmCurrentPatient()
1912                 if not pat.connected:
1913                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot show EMR summary. No active patient.'))
1914                         return False
1915
1916                 emr = pat.get_emr()
1917                 dlg = wx.MessageDialog (
1918                         parent = self,
1919                         message = emr.format_statistics(),
1920                         caption = _('EMR Summary'),
1921                         style = wx.OK | wx.STAY_ON_TOP
1922                 )
1923                 dlg.ShowModal()
1924                 dlg.Destroy()
1925                 return True
1926         #----------------------------------------------
1927         def __on_search_emr(self, event):
1928                 pat = gmPerson.gmCurrentPatient()
1929                 if not pat.connected:
1930                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
1931                         return False
1932
1933                 searcher = wx.TextEntryDialog (
1934                         parent = self,
1935                         message = _('Enter search term:'),
1936                         caption = _('Text search of entire EMR'),
1937                         style = wx.OK | wx.CANCEL | wx.CENTRE,
1938                         pos = wx.DefaultPosition
1939                 )
1940                 result = searcher.ShowModal()
1941                 if result == wx.ID_OK:
1942                         val = searcher.GetValue()
1943                         wx.BeginBusyCursor()
1944                         emr = pat.get_emr()
1945                         rows = emr.search_narrative_simple(val)
1946                         wx.EndBusyCursor()
1947                         txt = ''
1948                         for row in rows:
1949                                 txt += '%s - %s\n%s\n\n' % (row[1], row[4], row[2])
1950                         msg = _(
1951 """Search term was: "%s"
1952
1953 Search results:
1954 %s
1955 """) % (val, txt)
1956                         dlg = wx.MessageDialog (
1957                                 parent = None,
1958                                 message = msg,
1959                                 caption = _('search results'),
1960                                 style = wx.OK | wx.STAY_ON_TOP
1961                         )
1962                         dlg.ShowModal()
1963                         dlg.Destroy()
1964                         return True
1965         #----------------------------------------------
1966         def __on_export_emr_as_journal(self, event):
1967                 # sanity checks
1968                 pat = gmPerson.gmCurrentPatient()
1969                 if not pat.connected:
1970                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR journal. No active patient.'))
1971                         return False
1972                 # get file name
1973                 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
1974                 # FIXME: make configurable
1975                 aDefDir = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'EMR', pat['dirname']))
1976                 gmTools.mkdir(aDefDir)
1977                 # FIXME: make configurable
1978                 fname = '%s-%s_%s.txt' % (_('emr-journal'), pat['lastnames'], pat['firstnames'])
1979                 dlg = wx.FileDialog (
1980                         parent = self,
1981                         message = _("Save patient's EMR journal as..."),
1982                         defaultDir = aDefDir,
1983                         defaultFile = fname,
1984                         wildcard = aWildcard,
1985                         style = wx.SAVE
1986                 )
1987                 choice = dlg.ShowModal()
1988                 fname = dlg.GetPath()
1989                 dlg.Destroy()
1990                 if choice != wx.ID_OK:
1991                         return True
1992
1993                 _log.debug('exporting EMR journal to [%s]' % fname)
1994                 # instantiate exporter
1995                 exporter = gmPatientExporter.cEMRJournalExporter()
1996
1997                 wx.BeginBusyCursor()
1998                 try:
1999                         fname = exporter.export_to_file(filename = fname)
2000                 except:
2001                         wx.EndBusyCursor()
2002                         gmGuiHelpers.gm_show_error (
2003                                 _('Error exporting patient EMR as chronological journal.'),
2004                                 _('EMR journal export')
2005                         )
2006                         raise
2007                 wx.EndBusyCursor()
2008
2009                 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported EMR as chronological journal into file [%s].') % fname, beep=False)
2010
2011                 return True
2012         #----------------------------------------------
2013         def __on_export_for_medistar(self, event):
2014                 gmNarrativeWidgets.export_narrative_for_medistar_import (
2015                         parent = self,
2016                         soap_cats = u'soap',
2017                         encounter = None                        # IOW, the current one
2018                 )
2019         #----------------------------------------------
2020         def __on_load_external_patient(self, event):
2021                 dbcfg = gmCfg.cCfgSQL()
2022                 search_immediately = bool(dbcfg.get2 (
2023                         option = 'patient_search.external_sources.immediately_search_if_single_source',
2024                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
2025                         bias = 'user',
2026                         default = 0
2027                 ))
2028                 gmPatSearchWidgets.get_person_from_external_sources(parent=self, search_immediately=search_immediately, activate_immediately=True)
2029         #----------------------------------------------
2030         def __on_export_as_gdt(self, event):
2031                 curr_pat = gmPerson.gmCurrentPatient()
2032                 if not curr_pat.connected:
2033                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot export patient as GDT. No active patient.'))
2034                         return False
2035                 # FIXME: configurable
2036                 enc = 'cp850'
2037                 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'xDT', 'current-patient.gdt'))
2038                 curr_pat.export_as_gdt(filename = fname, encoding = enc)
2039                 gmDispatcher.send(signal = 'statustext', msg = _('Exported demographics to GDT file [%s].') % fname)
2040         #----------------------------------------------
2041         def __on_create_patient(self, event):
2042                 """Launch create patient wizard.
2043                 """
2044                 wiz = gmDemographicsWidgets.cNewPatientWizard(parent=self)
2045                 wiz.RunWizard(activate=True)
2046         #----------------------------------------------
2047         def __on_enlist_patient_as_staff(self, event):
2048                 pat = gmPerson.gmCurrentPatient()
2049                 if not pat.connected:
2050                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot add staff member. No active patient.'))
2051                         return False
2052                 dlg = gmStaffWidgets.cAddPatientAsStaffDlg(parent=self, id=-1)
2053                 dlg.ShowModal()
2054         #----------------------------------------------
2055         def __on_delete_patient(self, event):
2056                 pat = gmPerson.gmCurrentPatient()
2057                 if not pat.connected:
2058                         gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete patient. No patient active.'))
2059                         return False
2060                 gmDemographicsWidgets.disable_identity(identity=pat)
2061                 return True
2062         #----------------------------------------------
2063         def __on_add_new_staff(self, event):
2064                 """Create new person and add it as staff."""
2065                 wiz = gmDemographicsWidgets.cNewPatientWizard(parent=self)
2066                 if not wiz.RunWizard(activate=True):
2067                         return False
2068                 dlg = gmStaffWidgets.cAddPatientAsStaffDlg(parent=self, id=-1)
2069                 dlg.ShowModal()
2070         #----------------------------------------------
2071         def __on_edit_staff_list(self, event):
2072                 dlg = gmStaffWidgets.cEditStaffListDlg(parent=self, id=-1)
2073                 dlg.ShowModal()
2074         #----------------------------------------------
2075         def __on_edit_doc_types(self, event):
2076                 dlg = gmMedDocWidgets.cEditDocumentTypesDlg(parent=self, id=-1)
2077                 dlg.ShowModal()
2078         #----------------------------------------------
2079         def __on_manage_text_expansion(self, evt):
2080                 gmProviderInboxWidgets.configure_keyword_text_expansion(parent=self)
2081         #----------------------------------------------
2082         def __on_manage_encounter_types(self, evt):
2083                 gmEMRStructWidgets.manage_encounter_types(parent=self)
2084         #----------------------------------------------
2085         def __on_manage_provinces(self, evt):
2086                 gmDemographicsWidgets.manage_provinces(parent=self)
2087         #----------------------------------------------
2088         def _clean_exit(self):
2089                 """Cleanup helper.
2090
2091                 - should ALWAYS be called when this program is
2092                   to be terminated
2093                 - ANY code that should be executed before a
2094                   regular shutdown should go in here
2095                 - framework still functional
2096                 """
2097                 # run synchronous pre-exit callback
2098                 for call_back in self.__pre_exit_callbacks:
2099                         try:
2100                                 call_back()
2101                         except:
2102                                 print "*** pre-exit callback failed ***"
2103                                 _log.exception('callback [%s] failed', call_back)
2104
2105                 # shut down backend notifications listener
2106                 listener = gmBackendListener.gmBackendListener()
2107                 listener.stop_thread()
2108
2109                 # shutdown application scripting listener
2110                 if _scripting_listener is not None:
2111                         try:
2112                                 _scripting_listener.shutdown()
2113                         except:
2114                                 _log.exception('cannot stop scripting listener thread')
2115
2116                 gmDispatcher.disconnect(self._on_set_statustext, 'statustext')
2117
2118                 # signal imminent demise to plugins
2119                 gmDispatcher.send(u'application_closing')
2120
2121                 # remember GUI size
2122                 curr_width, curr_height = self.GetClientSizeTuple()
2123                 _log.info('GUI size at shutdown: [%s:%s]' % (curr_width, curr_height))
2124                 dbcfg = gmCfg.cCfgSQL()
2125                 dbcfg.set (
2126                         option = 'main.window.width',
2127                         value = curr_width,
2128                         workplace = gmSurgery.gmCurrentPractice().active_workplace
2129                 )
2130                 dbcfg.set (
2131                         option = 'main.window.height',
2132                         value = curr_height,
2133                         workplace = gmSurgery.gmCurrentPractice().active_workplace
2134                 )
2135
2136                 self.timer.Stop()
2137
2138                 sys.stdin = sys.__stdin__
2139                 sys.stdout = sys.__stdout__
2140                 sys.stderr = sys.__stderr__
2141         #----------------------------------------------
2142 #       def OnIdle(self, event):
2143 #               """Here we can process any background tasks
2144 #               """
2145 #               pass
2146         #----------------------------------------------
2147         def OnIconize(self, event):
2148                 # FIXME: we should maximize the amount of title bar information here
2149                 #_log.info('OnIconify')
2150                 event.Skip()
2151         #----------------------------------------------
2152         def OnMaximize(self, event):
2153                 # FIXME: we should change the amount of title bar information here
2154                 #_log.info('OnMaximize')
2155                 event.Skip()
2156         #----------------------------------------------
2157         # internal API
2158         #----------------------------------------------
2159         def updateTitle(self):
2160                 """Update title of main window based on template.
2161
2162                 This gives nice tooltips on iconified GNUmed instances.
2163                 User research indicates that in the title bar people want
2164                 the date of birth, not the age, so please stick to this
2165                 convention.
2166                 """
2167                 pat = gmPerson.gmCurrentPatient()
2168                 if pat.connected:
2169                         title = pat['title']
2170                         if title is None:
2171                                 title = ''
2172                         else:
2173                                 title = title[:4] + '.'
2174                         pat_str = "%s%s %s (%s) #%d" % (title, pat['firstnames'], pat['lastnames'], pat['dob'].strftime('%x').decode(gmI18N.get_encoding()), pat['pk_identity'])
2175                 else:
2176                         pat_str = _('no patient')
2177
2178                 title = self.__title_template % (
2179                         gmTools.coalesce(_provider['title'], ''),
2180                         _provider['firstnames'][:1],
2181                         _provider['lastnames'],
2182                         gmSurgery.gmCurrentPractice().active_workplace,
2183                         pat_str
2184                 )
2185                 self.SetTitle(title)
2186         #----------------------------------------------
2187         #----------------------------------------------
2188         def SetupStatusBar(self):
2189                 sb = self.CreateStatusBar(2, wx.ST_SIZEGRIP)
2190                 sb.SetStatusWidths([-1, 150])
2191                 #add time and date display to the right corner of the status bar
2192                 self.timer = wx.PyTimer(self._cb_update_clock)
2193                 self._cb_update_clock()
2194                 #update every second
2195                 self.timer.Start(milliseconds=1000)
2196         #----------------------------------------------
2197         def _cb_update_clock(self):
2198                 """Displays date and local time in the second slot of the status bar"""
2199                 t = time.localtime(time.time())
2200                 st = time.strftime('%c', t).decode(gmI18N.get_encoding())
2201                 self.SetStatusText(st,1)
2202         #------------------------------------------------
2203         def Lock(self):
2204                 """Lock GNUmed client against unauthorized access"""
2205                 # FIXME
2206 #               for i in range(1, self.nb.GetPageCount()):
2207 #                       self.nb.GetPage(i).Enable(False)
2208                 return
2209         #----------------------------------------------
2210         def Unlock(self):
2211                 """Unlock the main notebook widgets
2212                 As long as we are not logged into the database backend,
2213                 all pages but the 'login' page of the main notebook widget
2214                 are locked; i.e. not accessible by the user
2215                 """
2216                 #unlock notebook pages
2217 #               for i in range(1, self.nb.GetPageCount()):
2218 #                       self.nb.GetPage(i).Enable(True)
2219                 # go straight to patient selection
2220 #               self.nb.AdvanceSelection()
2221                 return
2222         #-----------------------------------------------
2223         def OnPanelSize (self, event):
2224                 wx.LayoutAlgorithm().LayoutWindow (self.LayoutMgr, self.nb)
2225         #------------------------------------------------
2226 #       def OnSashDrag (self, event):
2227 #               if event.GetDragStatus() == wx.SASH_STATUS_OUT_OF_RANGE:
2228 #                       return
2229 #               self.leftbox.SetDefaultSize(wx.Size(event.GetDragRect().width, 1000))
2230 #               self.bar_width = event.GetDragRect().width
2231 #               wx.LayoutAlgorithm().LayoutWindow(self.LayoutMgr, self.nb)
2232 #               self.nb.Refresh()
2233
2234 #==============================================================================
2235 class gmApp(wx.App):
2236
2237         def OnInit(self):
2238
2239                 self.__starting_up = True
2240
2241                 gmExceptionHandlingWidgets.install_wx_exception_handler()
2242                 # set this so things like "wx.StandardPaths.GetDataDir()" work as expected
2243                 self.SetAppName(u'gnumed')
2244 #               self.SetVendor(u'The GNUmed Development Community.')
2245                 paths = gmTools.gmPaths(app_name = 'gnumed', wx = wx)
2246                 paths.init_paths(wx = wx, app_name = 'gnumed')
2247
2248                 if not self.__setup_cfg():
2249                         return False
2250
2251                 gmExceptionHandlingWidgets.set_sender_email(gmSurgery.gmCurrentPractice().user_email)
2252
2253                 self.__guibroker = gmGuiBroker.GuiBroker()
2254                 self.__setup_platform()
2255
2256                 if not self.__establish_backend_connection():
2257                         return False
2258
2259                 self.__check_for_updates()
2260
2261                 # FIXME: load last position from backend
2262                 frame = gmTopLevelFrame(None, -1, _('GNUmed client'), (640,440))
2263                 frame.CentreOnScreen(wx.BOTH)
2264                 frame.Show(True)
2265                 self.SetTopWindow(frame)
2266
2267                 if _cfg.get(option = 'slave'):
2268                         if not self.__setup_scripting_listener():
2269                                 return False
2270
2271                 if _cfg.get(option = 'debug'):
2272                         self.RedirectStdio()
2273                         print "***** Redirecting STDOUT/STDERR to this log window *****"
2274
2275                 self.user_activity_detected = True
2276                 self.elapsed_inactivity_slices = 0
2277                 # FIXME: make configurable
2278                 self.max_user_inactivity_slices = 15    # 15 * 2000ms == 30 seconds
2279                 self.user_activity_timer = gmTimer.cTimer (
2280                         callback = self._on_user_activity_timer_expired,
2281                         delay = 2000                    # hence a minimum of 2 and max of 3.999... seconds after which inactivity is detected
2282                 )
2283                 self.user_activity_timer.Start(oneShot=True)
2284
2285                 self.__register_events()
2286
2287                 wx.CallAfter(self._do_after_init)
2288
2289                 return True
2290         #----------------------------------------------
2291         def OnExit(self):
2292                 """Called internally by wxPython after EVT_CLOSE has been handled on last frame.
2293
2294                 - after destroying all application windows and controls
2295                 - before wx.Windows internal cleanup
2296                 """
2297                 self.user_activity_timer.Stop()
2298                 gmTimer.shutdown()
2299                 gmExceptionHandlingWidgets.uninstall_wx_exception_handler()
2300         #----------------------------------------------
2301         def _on_query_end_session(self, *args, **kwargs):
2302                 wx.Bell()
2303                 wx.Bell()
2304                 wx.Bell()
2305                 _log.warning('unhandled event detected: QUERY_END_SESSION')
2306                 _log.info('we should be saving ourselves from here')
2307                 gmLog2.flush()
2308                 print "unhandled event detected: QUERY_END_SESSION"
2309         #----------------------------------------------
2310         def _on_end_session(self, *args, **kwargs):
2311                 wx.Bell()
2312                 wx.Bell()
2313                 wx.Bell()
2314                 _log.warning('unhandled event detected: END_SESSION')
2315                 gmLog2.flush()
2316                 print "unhandled event detected: END_SESSION"
2317         #----------------------------------------------
2318         def _on_app_activated(self, evt):
2319                 if evt.GetActive():
2320                         if self.__starting_up:
2321                                 gmHooks.run_hook_script(hook = u'app_activated_startup')
2322                         else:
2323                                 gmHooks.run_hook_script(hook = u'app_activated')
2324                 else:
2325                         gmHooks.run_hook_script(hook = u'app_deactivated')
2326
2327                 evt.Skip()
2328         #----------------------------------------------
2329         def _on_user_activity(self, evt):
2330                 self.user_activity_detected = True
2331                 evt.Skip()
2332         #----------------------------------------------
2333         def _on_user_activity_timer_expired(self, cookie=None):
2334
2335                 if self.user_activity_detected:
2336                         self.elapsed_inactivity_slices = 0
2337                         self.user_activity_detected = False
2338                         self.elapsed_inactivity_slices += 1
2339                 else:
2340                         if self.elapsed_inactivity_slices >= self.max_user_inactivity_slices:
2341 #                               print "User was inactive for 30 seconds."
2342                                 pass
2343
2344                 self.user_activity_timer.Start(oneShot = True)
2345         #----------------------------------------------
2346         # internal helpers
2347         #----------------------------------------------
2348         def _signal_debugging_monitor(*args, **kwargs):
2349                 try:
2350                         kwargs['originated_in_database']
2351                         print '==> got notification from database "%s":' % kwargs['signal']
2352                 except KeyError:
2353                         print '==> received signal from client: "%s"' % kwargs['signal']
2354
2355                 del kwargs['signal']
2356                 for key in kwargs.keys():
2357                         print '    [%s]: %s' % (key, kwargs[key])
2358         #----------------------------------------------
2359         def _do_after_init(self):
2360                 self.__starting_up = False
2361                 gmClinicalRecord.set_func_ask_user(a_func = gmEMRStructWidgets.ask_for_encounter_continuation)
2362                 self.__guibroker['horstspace.top_panel'].patient_selector.SetFocus()
2363                 gmHooks.run_hook_script(hook = u'startup-after-GUI-init')
2364         #----------------------------------------------
2365         def __register_events(self):
2366                 wx.EVT_QUERY_END_SESSION(self, self._on_query_end_session)
2367                 wx.EVT_END_SESSION(self, self._on_end_session)
2368
2369                 # You can bind your app to wx.EVT_ACTIVATE_APP which will fire when your
2370                 # app gets/looses focus, or you can wx.EVT_ACTIVATE with any of your
2371                 # toplevel windows and call evt.GetActive() in the handler to see whether
2372                 # it is gaining or loosing focus.
2373                 self.Bind(wx.EVT_ACTIVATE_APP, self._on_app_activated)
2374
2375                 self.Bind(wx.EVT_MOUSE_EVENTS, self._on_user_activity)
2376                 self.Bind(wx.EVT_KEY_DOWN, self._on_user_activity)
2377
2378                 if _cfg.get(option = 'debug'):
2379                         gmDispatcher.connect(receiver = self._signal_debugging_monitor)
2380         #----------------------------------------------
2381         def __check_for_updates(self):
2382
2383                 dbcfg = gmCfg.cCfgSQL()
2384
2385                 do_check = bool(dbcfg.get2 (
2386                         option = u'horstspace.update.autocheck_at_startup',
2387                         workplace = gmSurgery.gmCurrentPractice().active_workplace,
2388                         bias = 'workplace',
2389                         default = True
2390                 ))
2391
2392                 if not do_check:
2393                         return
2394
2395                 check_for_updates()
2396         #----------------------------------------------
2397         def __establish_backend_connection(self):
2398                 """Handle all the database related tasks necessary for startup."""
2399
2400                 # log on
2401                 from Gnumed.wxpython import gmAuthWidgets
2402                 override = _cfg.get(option = '--override-schema-check', source_order = [('cli', 'return')])
2403                 connected = gmAuthWidgets.connect_to_database (
2404                         expected_version = expected_db_ver,
2405                         require_version = not override,
2406                         client_version = current_client_ver
2407                 )
2408                 if not connected:
2409                         _log.warning("Login attempt unsuccessful. Can't run GNUmed without database connection")
2410                         return False
2411
2412                 # check account <-> staff member association
2413                 try:
2414                         global _provider
2415                         _provider = gmPerson.gmCurrentProvider(provider = gmPerson.cStaff())
2416                 except gmExceptions.ConstructorError, ValueError:
2417                         account = gmPG2.get_current_user()
2418                         _log.exception('DB account [%s] cannot be used as a GNUmed staff login', account)
2419                         msg = _(
2420                                 'The database account [%s] cannot be used as a\n'
2421                                 'staff member login for GNUmed. There was an\n'
2422                                 'error retrieving staff details for it.\n\n'
2423                                 'Please ask your administrator for help.\n'
2424                         ) % account
2425                         gmGuiHelpers.gm_show_error(msg, _('Checking access permissions'))
2426                         return False
2427
2428                 # improve exception handler setup
2429                 tmp = '%s%s %s (%s = %s)' % (
2430                         gmTools.coalesce(_provider['title'], ''),
2431                         _provider['firstnames'],
2432                         _provider['lastnames'],
2433                         _provider['short_alias'],
2434                         _provider['db_user']
2435                 )
2436                 gmExceptionHandlingWidgets.set_staff_name(staff_name = tmp)
2437
2438                 # display database banner
2439                 surgery = gmSurgery.gmCurrentPractice()
2440                 msg = surgery.db_logon_banner
2441                 if msg != u'':
2442                         gmGuiHelpers.gm_show_info(msg, _('Verifying database'))
2443
2444                 # check database language settings
2445                 self.__check_db_lang()
2446
2447                 return True
2448         #----------------------------------------------
2449         def __setup_cfg(self):
2450
2451                 paths = gmTools.gmPaths(app_name = u'gnumed', wx = wx)
2452
2453                 candidates = []
2454                 explicit_file = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
2455                 if explicit_file is not None:
2456                         candidates.append(explicit_file)
2457                 # provide a few fallbacks in the event the --conf-file isn't writable
2458                 candidates.append(os.path.join(paths.user_config_dir, 'gnumed.conf'))
2459                 candidates.append(os.path.join(paths.local_base_dir, 'gnumed.conf'))
2460                 candidates.append(os.path.join(paths.working_dir, 'gnumed.conf'))
2461
2462                 for candidate in candidates:
2463                         try:
2464                                 open(candidate, 'a+').close()
2465                                 _cfg.set_option(option = u'user_preferences_file', value = candidate)
2466                                 break
2467                         except IOError:
2468                                 continue
2469
2470                 if _cfg.get(option = u'user_preferences_file') is None:
2471                         msg = _(
2472                                 'Cannot find configuration file in any of:\n'
2473                                 '\n'
2474                                 ' %s\n'
2475                                 'You may need to use the comand line option\n'
2476                                 '\n'
2477                                 '       --conf-file=<FILE>'
2478                         ) % '\n '.join(candidates)
2479                         gmGuiHelpers.gm_show_error(msg, _('Checking configuration files'))
2480                         return False
2481
2482                 return True
2483         #----------------------------------------------
2484         def __setup_scripting_listener(self):
2485
2486                 from socket import error as SocketError
2487                 from Gnumed.pycommon import gmScriptingListener
2488                 from Gnumed.wxpython import gmMacro
2489
2490                 slave_personality = gmTools.coalesce (
2491                         _cfg.get (
2492                                 group = u'workplace',
2493                                 option = u'slave personality',
2494                                 source_order = [
2495                                         ('explicit', 'return'),
2496                                         ('workbase', 'return'),
2497                                         ('user', 'return'),
2498                                         ('system', 'return')
2499                                 ]
2500                         ),
2501                         u'gnumed-client'
2502                 )
2503
2504                 # FIXME: handle port via /var/run/
2505                 port = gmTools.coalesce (
2506                         int(_cfg.get (
2507                                 group = u'workplace',
2508                                 option = u'xml-rpc port',
2509                                 source_order = [
2510                                         ('explicit', 'return'),
2511                                         ('workbase', 'return'),
2512                                         ('user', 'return'),
2513                                         ('system', 'return')
2514                                 ]
2515                         )),
2516                         9999
2517                 )
2518
2519                 macro_executor = gmMacro.cMacroPrimitives(personality = slave_personality)
2520                 global _scripting_listener
2521                 try:
2522                         _scripting_listener = gmScriptingListener.cScriptingListener(port = port, macro_executor = macro_executor)
2523                 except SocketError, e:
2524                         _log.exception('cannot start GNUmed XML-RPC server')
2525                         gmGuiHelpers.gm_show_error (
2526                                 aMessage = (
2527                                         'Cannot start the GNUmed server:\n'
2528                                         '\n'
2529                                         ' [%s]'
2530                                 ) % e,
2531                                 aTitle = _('GNUmed startup')
2532                         )
2533                         return False
2534
2535                 _log.info('slave mode personality is [%s]' % slave_personality)
2536                 return True
2537         #----------------------------------------------
2538         def __setup_platform(self):
2539
2540                 import wx.lib.colourdb
2541                 wx.lib.colourdb.updateColourDB()
2542
2543                 traits = self.GetTraits()
2544                 try:
2545                         _log.info('desktop environment: [%s]', traits.GetDesktopEnvironment())
2546                 except:
2547                         pass
2548
2549                 if wx.Platform == '__WXMSW__':
2550                         _log.info('running on MS Windows')
2551                 elif wx.Platform == '__WXGTK__':
2552                         _log.info('running on GTK (probably Linux)')
2553                 elif wx.Platform == '__WXMAC__':
2554                         _log.info('running on Mac OS')
2555                 else:
2556                         _log.info('running on an unknown platform (%s)' % wx.Platform)
2557         #----------------------------------------------
2558         def __check_db_lang(self):
2559                 if gmI18N.system_locale is None or gmI18N.system_locale == '':
2560                         _log.warning("system locale is undefined (probably meaning 'C')")
2561                         return True
2562
2563                 db_lang = None
2564                 # get current database locale
2565                 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': u"select i18n.get_curr_lang() as lang"}])
2566                 if len(rows) == 0:
2567                         _log.debug("database locale currently not set")
2568                         msg = _(
2569                                 "There is no language selected in the database for user [%s].\n"
2570                                 "Your system language is currently set to [%s].\n\n"
2571                                 "Do you want to set the database language to '%s' ?\n\n"
2572                         )  % (_provider['db_user'], gmI18N.system_locale, gmI18N.system_locale)
2573                         checkbox_msg = _('Remember to ignore missing language')
2574                 else:
2575                         db_lang = rows[0]['lang']
2576                         _log.debug("current database locale: [%s]" % db_lang)
2577                         msg = _(
2578                                 "The currently selected database language ('%s') does\n"
2579                                 "not match the current system language ('%s').\n"
2580                                 "\n"
2581                                 "Do you want to set the database language to '%s' ?\n"
2582                         ) % (db_lang, gmI18N.system_locale, gmI18N.system_locale)
2583                         checkbox_msg = _('Remember to ignore language mismatch')
2584
2585                         # check if we can match up system and db language somehow
2586                         if db_lang == gmI18N.system_locale_level['full']:
2587                                 _log.debug('Database locale (%s) up to date.' % db_lang)
2588                                 return True
2589                         if db_lang == gmI18N.system_locale_level['country']:
2590                                 _log.debug('Database locale (%s) matches system locale (%s) at country level.' % (db_lang, gmI18N.system_locale))
2591                                 return True
2592                         if db_lang == gmI18N.system_locale_level['language']:
2593                                 _log.debug('Database locale (%s) matches system locale (%s) at language level.' % (db_lang, gmI18N.system_locale))
2594                                 return True
2595                         # no match
2596                         _log.warning('database locale [%s] does not match system locale [%s]' % (db_lang, gmI18N.system_locale))
2597
2598                 # returns either None or a locale string
2599                 ignored_sys_lang = _cfg.get (
2600                         group = u'backend',
2601                         option = u'ignored mismatching system locale',
2602                         source_order = [('explicit', 'return'), ('user', 'return'), ('local', 'return')]
2603                 )
2604
2605                 # are we to ignore *this* mismatch ?
2606                 if gmI18N.system_locale == ignored_sys_lang:
2607                         _log.info('configured to ignore system-to-database locale mismatch')
2608                         return True
2609
2610                 # no, so ask user
2611                 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
2612                         None,
2613                         -1,
2614                         caption = _('Checking database language settings'),
2615                         question = msg,
2616                         button_defs = [
2617                                 {'label': _('Set'), 'tooltip': _('Set your database language to [%s].') % gmI18N.system_locale, 'default': True},
2618                                 {'label': _("Don't set"), 'tooltip': _('Do not set your database language now.'), 'default': False}
2619                         ],
2620                         show_checkbox = True,
2621                         checkbox_msg = checkbox_msg,
2622                         checkbox_tooltip = _(
2623                                 'Checking this will make GNUmed remember your decision\n'
2624                                 'until the system language is changed.\n'
2625                                 '\n'
2626                                 'You can also reactivate this inquiry by removing the\n'
2627                                 'corresponding "ignore" option from the configuration file\n'
2628                                 '\n'
2629                                 ' [%s]'
2630                         ) % _cfg.get(option = 'user_preferences_file')
2631                 )
2632                 decision = dlg.ShowModal()
2633                 remember_ignoring_problem = dlg._CHBOX_dont_ask_again.GetValue()
2634                 dlg.Destroy()
2635
2636                 if decision == wx.ID_NO:
2637                         if not remember_ignoring_problem:
2638                                 return True
2639                         _log.info('User did not want to set database locale. Ignoring mismatch next time.')
2640                         gmCfg2.set_option_in_INI_file (
2641                                 filename = _cfg.get(option = 'user_preferences_file'),
2642                                 group = 'backend',
2643                                 option = 'ignored mismatching system locale',
2644                                 value = gmI18N.system_locale
2645                         )
2646                         return True
2647
2648                 # try setting database language (only possible if translation exists)
2649                 for lang in [gmI18N.system_locale_level['full'], gmI18N.system_locale_level['country'], gmI18N.system_locale_level['language']]:
2650                         if len(lang) > 0:
2651                                 # users are getting confused, so don't show these "errors",
2652                                 # they really are just notices about us being nice
2653                                 rows, idx = gmPG2.run_rw_queries (
2654                                         link_obj = None,
2655                                         queries = [{'cmd': u'select i18n.set_curr_lang(%s)', 'args': [lang]}],
2656                                         return_data = True
2657                                 )
2658                                 if rows[0][0]:
2659                                         _log.debug("Successfully set database language to [%s]." % lang)
2660                                 else:
2661                                         _log.error('Cannot set database language to [%s].' % lang)
2662                                         continue
2663                                 return True
2664