1
2
3 __doc__ = """GNUmed GUI client.
4
5 This contains the GUI application framework and main window
6 of the all signing all dancing GNUmed Python Reference
7 client. It relies on the <gnumed.py> launcher having set up
8 the non-GUI-related runtime environment.
9
10 copyright: authors
11 """
12
13 __author__ = "H. Herb <hherb@gnumed.net>,\
14 K. Hilbert <Karsten.Hilbert@gmx.net>,\
15 I. Haywood <i.haywood@ugrad.unimelb.edu.au>"
16 __license__ = 'GPL v2 or later (details at http://www.gnu.org)'
17
18
19 import sys
20 import time
21 import os
22 import os.path
23 import datetime as pyDT
24 import shutil
25 import logging
26 import urllib.request
27 import subprocess
28 import glob
29 import io
30
31 _log = logging.getLogger('gm.main')
32
33
34
35 from Gnumed.pycommon import gmCfg2
36 _cfg = gmCfg2.gmCfgData()
37
38
39
40 try:
41 import wx
42 _log.info('wxPython version loaded: %s %s' % (wx.VERSION_STRING, wx.PlatformInfo))
43 except ImportError:
44 _log.exception('cannot import wxPython')
45 print('GNUmed startup: Cannot import wxPython library.')
46 print('GNUmed startup: Make sure wxPython is installed.')
47 print('CRITICAL ERROR: Error importing wxPython. Halted.')
48 raise
49
50
51
52 version = int('%s%s' % (wx.MAJOR_VERSION, wx.MINOR_VERSION))
53 if (version < 28) or ('unicode' not in wx.PlatformInfo):
54 print('GNUmed startup: Unsupported wxPython version (%s: %s).' % (wx.VERSION_STRING, wx.PlatformInfo))
55 print('GNUmed startup: wxPython 2.8+ with unicode support is required.')
56 print('CRITICAL ERROR: Proper wxPython version not found. Halted.')
57 raise ValueError('wxPython 2.8+ with unicode support not found')
58
59
60
61 from Gnumed.pycommon import gmCfg
62 from Gnumed.pycommon import gmPG2
63 from Gnumed.pycommon import gmDispatcher
64 from Gnumed.pycommon import gmGuiBroker
65 from Gnumed.pycommon import gmI18N
66 from Gnumed.pycommon import gmExceptions
67 from Gnumed.pycommon import gmShellAPI
68 from Gnumed.pycommon import gmTools
69 from Gnumed.pycommon import gmDateTime
70 from Gnumed.pycommon import gmHooks
71 from Gnumed.pycommon import gmBackendListener
72 from Gnumed.pycommon import gmLog2
73 from Gnumed.pycommon import gmNetworkTools
74 from Gnumed.pycommon import gmMimeLib
75
76 from Gnumed.business import gmPerson
77 from Gnumed.business import gmClinicalRecord
78 from Gnumed.business import gmPraxis
79 from Gnumed.business import gmEMRStructItems
80 from Gnumed.business import gmArriba
81 from Gnumed.business import gmStaff
82
83 from Gnumed.exporters import gmPatientExporter
84
85 from Gnumed.wxpython import gmGuiHelpers
86 from Gnumed.wxpython import gmHorstSpace
87 from Gnumed.wxpython import gmDemographicsWidgets
88 from Gnumed.wxpython import gmPersonCreationWidgets
89 from Gnumed.wxpython import gmEMRStructWidgets
90 from Gnumed.wxpython import gmPatSearchWidgets
91 from Gnumed.wxpython import gmAllergyWidgets
92 from Gnumed.wxpython import gmListWidgets
93 from Gnumed.wxpython import gmProviderInboxWidgets
94 from Gnumed.wxpython import gmCfgWidgets
95 from Gnumed.wxpython import gmExceptionHandlingWidgets
96 from Gnumed.wxpython import gmNarrativeWorkflows
97 from Gnumed.wxpython import gmPhraseWheel
98 from Gnumed.wxpython import gmMedicationWidgets
99 from Gnumed.wxpython import gmStaffWidgets
100 from Gnumed.wxpython import gmDocumentWidgets
101 from Gnumed.wxpython import gmTimer
102 from Gnumed.wxpython import gmMeasurementWidgets
103 from Gnumed.wxpython import gmFormWidgets
104 from Gnumed.wxpython import gmSnellen
105 from Gnumed.wxpython import gmVaccWidgets
106 from Gnumed.wxpython import gmPersonContactWidgets
107 from Gnumed.wxpython import gmI18nWidgets
108 from Gnumed.wxpython import gmCodingWidgets
109 from Gnumed.wxpython import gmOrganizationWidgets
110 from Gnumed.wxpython import gmAuthWidgets
111 from Gnumed.wxpython import gmFamilyHistoryWidgets
112 from Gnumed.wxpython import gmDataPackWidgets
113 from Gnumed.wxpython import gmContactWidgets
114 from Gnumed.wxpython import gmAddressWidgets
115 from Gnumed.wxpython import gmBillingWidgets
116 from Gnumed.wxpython import gmKeywordExpansionWidgets
117 from Gnumed.wxpython import gmAccessPermissionWidgets
118 from Gnumed.wxpython import gmPraxisWidgets
119 from Gnumed.wxpython import gmEncounterWidgets
120 from Gnumed.wxpython import gmAutoHintWidgets
121 from Gnumed.wxpython import gmPregWidgets
122 from Gnumed.wxpython import gmExternalCareWidgets
123 from Gnumed.wxpython import gmHabitWidgets
124 from Gnumed.wxpython import gmSubstanceMgmtWidgets
125 from Gnumed.wxpython import gmATCWidgets
126 from Gnumed.wxpython import gmLOINCWidgets
127 from Gnumed.wxpython import gmVisualProgressNoteWidgets
128 from Gnumed.wxpython import gmHospitalStayWidgets
129 from Gnumed.wxpython import gmProcedureWidgets
130
131
132 _provider = None
133 _scripting_listener = None
134 _original_wxEndBusyCursor = None
141
142 __wxlog = cLog_wx2gm()
143 _log.info('redirecting wx.Log to [%s]', __wxlog)
144 wx.Log.SetActiveTarget(__wxlog)
149 """GNUmed client's main windows frame.
150
151 This is where it all happens. Avoid popping up any other windows.
152 Most user interaction should happen to and from widgets within this frame
153 """
154
155 - def __init__(self, parent, id, title, size=wx.DefaultSize):
156 """You'll have to browse the source to understand what the constructor does
157 """
158 wx.Frame.__init__(self, parent, id, title, size, style = wx.DEFAULT_FRAME_STYLE)
159
160 self.__setup_font()
161
162 self.__gb = gmGuiBroker.GuiBroker()
163 self.__pre_exit_callbacks = []
164 self.bar_width = -1
165 self.menu_id2plugin = {}
166
167 _log.info('workplace is >>>%s<<<', gmPraxis.gmCurrentPraxisBranch().active_workplace)
168
169 self.setup_statusbar()
170 self.SetStatusText(_('You are logged in as %s%s.%s (%s). DB account <%s>.') % (
171 gmTools.coalesce(_provider['title'], ''),
172 _provider['firstnames'][:1],
173 _provider['lastnames'],
174 _provider['short_alias'],
175 _provider['db_user']
176 ))
177 self.__setup_main_menu()
178
179 self.__set_window_title_template()
180 self.__update_window_title()
181
182
183
184
185
186 self.SetIcon(gmTools.get_icon(wx = wx))
187
188 self.__register_events()
189
190 self.LayoutMgr = gmHorstSpace.cHorstSpaceLayoutMgr(self, -1)
191 self.vbox = wx.BoxSizer(wx.VERTICAL)
192 self.vbox.Add(self.LayoutMgr, 10, wx.EXPAND | wx.ALL, 1)
193
194 self.SetAutoLayout(True)
195 self.SetSizerAndFit(self.vbox)
196
197
198
199
200
201 self.__set_GUI_size()
202
203
205
206 font = self.GetFont()
207 _log.debug('system default font is [%s] (%s)', font.GetNativeFontInfoUserDesc(), font.GetNativeFontInfoDesc())
208
209 desired_font_face = _cfg.get (
210 group = 'workplace',
211 option = 'client font',
212 source_order = [
213 ('explicit', 'return'),
214 ('workbase', 'return'),
215 ('local', 'return'),
216 ('user', 'return'),
217 ('system', 'return')
218 ]
219 )
220
221 fonts2try = []
222 if desired_font_face is not None:
223 _log.info('client is configured to use font [%s]', desired_font_face)
224 fonts2try.append(desired_font_face)
225
226 if wx.Platform == '__WXMSW__':
227 sane_font_face = 'Noto Sans'
228 _log.info('MS Windows: appending fallback font candidate [%s]', sane_font_face)
229 fonts2try.append(sane_font_face)
230 sane_font_face = 'DejaVu Sans'
231 _log.info('MS Windows: appending fallback font candidate [%s]', sane_font_face)
232 fonts2try.append(sane_font_face)
233
234 if len(fonts2try) == 0:
235 return
236
237 for font_face in fonts2try:
238 success = font.SetFaceName(font_face)
239 if success:
240 self.SetFont(font)
241 _log.debug('switched font to [%s] (%s)', font.GetNativeFontInfoUserDesc(), font.GetNativeFontInfoDesc())
242 return
243 font = self.GetFont()
244 _log.error('cannot switch font from [%s] (%s) to [%s]', font.GetNativeFontInfoUserDesc(), font.GetNativeFontInfoDesc(), font_face)
245
246 return
247
248
250 """Try to get previous window size from backend."""
251
252 cfg = gmCfg.cCfgSQL()
253 width = int(cfg.get2 (
254 option = 'main.window.width',
255 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
256 bias = 'workplace',
257 default = 800
258 ))
259 height = int(cfg.get2 (
260 option = 'main.window.height',
261 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
262 bias = 'workplace',
263 default = 600
264 ))
265 _log.debug('previous GUI size [%sx%s]', width, height)
266 pos_x = int(cfg.get2 (
267 option = 'main.window.position.x',
268 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
269 bias = 'workplace',
270 default = 0
271 ))
272 pos_y = int(cfg.get2 (
273 option = 'main.window.position.y',
274 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
275 bias = 'workplace',
276 default = 0
277 ))
278 _log.debug('previous GUI position [%s:%s]', pos_x, pos_y)
279
280 curr_disp_width = wx.DisplaySize()[0]
281 curr_disp_height = wx.DisplaySize()[1]
282
283 if width > curr_disp_width:
284 _log.debug('adjusting GUI width from %s to display width %s', width, curr_disp_width)
285 width = curr_disp_width
286 if height > curr_disp_height:
287 _log.debug('adjusting GUI height from %s to display height %s', height, curr_disp_height)
288 height = curr_disp_height
289
290 if width < 100:
291 _log.debug('adjusting GUI width to minimum of 100 pixel')
292 width = 100
293 if height < 100:
294 _log.debug('adjusting GUI height to minimum of 100 pixel')
295 height = 100
296 _log.info('setting GUI geom to [%sx%s] @ [%s:%s]', width, height, pos_x, pos_y)
297
298
299 self.SetSize(wx.Size(width, height))
300 self.SetPosition(wx.Point(pos_x, pos_y))
301
302
304 """Create the main menu entries.
305
306 Individual entries are farmed out to the modules.
307
308 menu item template:
309
310 item = menu_*.Append(-1)
311 self.Bind(wx.EVT_MENU, self.__on_*, item)
312 """
313 global wx
314 self.mainmenu = wx.MenuBar()
315 self.__gb['main.mainmenu'] = self.mainmenu
316
317
318 menu_gnumed = wx.Menu()
319 self.menu_plugins = wx.Menu()
320 menu_gnumed.Append(wx.NewId(), _('&Go to plugin ...'), self.menu_plugins)
321 item = menu_gnumed.Append(-1, _('Check for updates'), _('Check for new releases of the GNUmed client.'))
322 self.Bind(wx.EVT_MENU, self.__on_check_for_updates, item)
323 item = menu_gnumed.Append(-1, _('Announce downtime'), _('Announce database maintenance downtime to all connected clients.'))
324 self.Bind(wx.EVT_MENU, self.__on_announce_maintenance, item)
325 menu_gnumed.AppendSeparator()
326
327
328 menu_config = wx.Menu()
329
330 item = menu_config.Append(-1, _('All options'), _('List all options as configured in the database.'))
331 self.Bind(wx.EVT_MENU, self.__on_list_configuration, item)
332
333
334 menu_cfg_db = wx.Menu()
335 item = menu_cfg_db.Append(-1, _('Language'), _('Configure the database language'))
336 self.Bind(wx.EVT_MENU, self.__on_configure_db_lang, item)
337 item = menu_cfg_db.Append(-1, _('Welcome message'), _('Configure the database welcome message (all users).'))
338 self.Bind(wx.EVT_MENU, self.__on_configure_db_welcome, item)
339 menu_config.Append(wx.NewId(), _('Database ...'), menu_cfg_db)
340
341
342 menu_cfg_client = wx.Menu()
343 item = menu_cfg_client.Append(-1, _('Export chunk size'), _('Configure the chunk size used when exporting BLOBs from the database.'))
344 self.Bind(wx.EVT_MENU, self.__on_configure_export_chunk_size, item)
345 item = menu_cfg_client.Append(-1, _('Email address'), _('The email address of the user for sending bug reports, etc.'))
346 self.Bind(wx.EVT_MENU, self.__on_configure_user_email, item)
347 menu_config.Append(wx.NewId(), _('Client parameters ...'), menu_cfg_client)
348
349
350 menu_cfg_ui = wx.Menu()
351 item = menu_cfg_ui.Append(-1, _('Medication measurements'), _('Select the measurements panel to show in the medications plugin.'))
352 self.Bind(wx.EVT_MENU, self.__on_cfg_meds_lab_pnl, item)
353 item = menu_cfg_ui.Append(-1, _('General measurements'), _('Select the measurements panel to show in the top pane.'))
354 self.Bind(wx.EVT_MENU, self.__on_cfg_top_lab_pnl, item)
355
356
357 menu_cfg_doc = wx.Menu()
358 item = menu_cfg_doc.Append(-1, _('Review dialog'), _('Configure review dialog after document display.'))
359 self.Bind(wx.EVT_MENU, self.__on_configure_doc_review_dialog, item)
360 item = menu_cfg_doc.Append(-1, _('UUID display'), _('Configure unique ID dialog on document import.'))
361 self.Bind(wx.EVT_MENU, self.__on_configure_doc_uuid_dialog, item)
362 item = menu_cfg_doc.Append(-1, _('Empty documents'), _('Whether to allow saving documents without parts.'))
363 self.Bind(wx.EVT_MENU, self.__on_configure_partless_docs, item)
364 item = menu_cfg_doc.Append(-1, _('Generate UUID'), _('Whether to generate UUIDs for new documents.'))
365 self.Bind(wx.EVT_MENU, self.__on_configure_generate_doc_uuid, item)
366 menu_cfg_ui.Append(wx.NewId(), _('Document handling ...'), menu_cfg_doc)
367
368
369 menu_cfg_update = wx.Menu()
370 item = menu_cfg_update.Append(-1, _('Auto-check'), _('Whether to auto-check for updates at startup.'))
371 self.Bind(wx.EVT_MENU, self.__on_configure_update_check, item)
372 item = menu_cfg_update.Append(-1, _('Check scope'), _('When checking for updates, consider latest branch, too ?'))
373 self.Bind(wx.EVT_MENU, self.__on_configure_update_check_scope, item)
374 item = menu_cfg_update.Append(-1, _('URL'), _('The URL to retrieve version information from.'))
375 self.Bind(wx.EVT_MENU, self.__on_configure_update_url, item)
376 menu_cfg_ui.Append(wx.NewId(), _('Update handling ...'), menu_cfg_update)
377
378
379 menu_cfg_pat_search = wx.Menu()
380 item = menu_cfg_pat_search.Append(-1, _('Birthday reminder'), _('Configure birthday reminder proximity interval.'))
381 self.Bind(wx.EVT_MENU, self.__on_configure_dob_reminder_proximity, item)
382 item = menu_cfg_pat_search.Append(-1, _('Immediate source activation'), _('Configure immediate activation of single external person.'))
383 self.Bind(wx.EVT_MENU, self.__on_configure_quick_pat_search, item)
384 item = menu_cfg_pat_search.Append(-1, _('Initial plugin'), _('Configure which plugin to show right after person activation.'))
385 self.Bind(wx.EVT_MENU, self.__on_configure_initial_pat_plugin, item)
386 item = menu_cfg_pat_search.Append(-1, _('Default region'), _('Configure the default region for person creation.'))
387 self.Bind(wx.EVT_MENU, self.__on_cfg_default_region, item)
388 item = menu_cfg_pat_search.Append(-1, _('Default country'), _('Configure the default country for person creation.'))
389 self.Bind(wx.EVT_MENU, self.__on_cfg_default_country, item)
390 menu_cfg_ui.Append(wx.NewId(), _('Person ...'), menu_cfg_pat_search)
391
392
393 menu_cfg_soap_editing = wx.Menu()
394 item = menu_cfg_soap_editing.Append(-1, _('Multiple new episodes'), _('Configure opening multiple new episodes on a patient at once.'))
395 self.Bind(wx.EVT_MENU, self.__on_allow_multiple_new_episodes, item)
396 item = menu_cfg_soap_editing.Append(-1, _('Auto-open editors'), _('Configure auto-opening editors for recent problems.'))
397 self.Bind(wx.EVT_MENU, self.__on_allow_auto_open_episodes, item)
398 item = menu_cfg_soap_editing.Append(-1, _('SOAP fields'), _('Configure SOAP editor - individual SOAP fields vs text editor like'))
399 self.Bind(wx.EVT_MENU, self.__on_use_fields_in_soap_editor, item)
400 menu_cfg_ui.Append(wx.NewId(), _('Progress notes handling ...'), menu_cfg_soap_editing)
401
402
403 menu_cfg_ext_tools = wx.Menu()
404
405
406 item = menu_cfg_ext_tools.Append(-1, _('MI/stroke risk calc cmd'), _('Set the command to start the CV risk calculator.'))
407 self.Bind(wx.EVT_MENU, self.__on_configure_acs_risk_calculator_cmd, item)
408 item = menu_cfg_ext_tools.Append(-1, _('OOo startup time'), _('Set the time to wait for OpenOffice to settle after startup.'))
409 self.Bind(wx.EVT_MENU, self.__on_configure_ooo_settle_time, item)
410 item = menu_cfg_ext_tools.Append(-1, _('Measurements URL'), _('URL for measurements encyclopedia.'))
411 self.Bind(wx.EVT_MENU, self.__on_configure_measurements_url, item)
412 item = menu_cfg_ext_tools.Append(-1, _('Drug data source'), _('Select the drug data source.'))
413 self.Bind(wx.EVT_MENU, self.__on_configure_drug_data_source, item)
414
415
416 item = menu_cfg_ext_tools.Append(-1, _('ADR URL'), _('URL for reporting Adverse Drug Reactions.'))
417 self.Bind(wx.EVT_MENU, self.__on_configure_adr_url, item)
418 item = menu_cfg_ext_tools.Append(-1, _('vaccADR URL'), _('URL for reporting Adverse Drug Reactions to *vaccines*.'))
419 self.Bind(wx.EVT_MENU, self.__on_configure_vaccine_adr_url, item)
420 item = menu_cfg_ext_tools.Append(-1, _('Vacc plans URL'), _('URL for vaccination plans.'))
421 self.Bind(wx.EVT_MENU, self.__on_configure_vaccination_plans_url, item)
422 item = menu_cfg_ext_tools.Append(-1, _('Visual SOAP editor'), _('Set the command for calling the visual progress note editor.'))
423 self.Bind(wx.EVT_MENU, self.__on_configure_visual_soap_cmd, item)
424 menu_config.Append(wx.NewId(), _('External tools ...'), menu_cfg_ext_tools)
425
426
427 menu_cfg_bill = wx.Menu()
428 item = menu_cfg_bill.Append(-1, _('Invoice ID template'), _('Set the template for generating invoice IDs.'))
429 self.Bind(wx.EVT_MENU, self.__on_cfg_invoice_id_template, item)
430 item = menu_cfg_bill.Append(-1, _('Invoice template (no VAT)'), _('Select the template for printing an invoice without VAT.'))
431 self.Bind(wx.EVT_MENU, self.__on_cfg_invoice_template_no_vat, item)
432 item = menu_cfg_bill.Append(-1, _('Invoice template (with VAT)'), _('Select the template for printing an invoice with VAT.'))
433 self.Bind(wx.EVT_MENU, self.__on_cfg_invoice_template_with_vat, item)
434 item = menu_cfg_bill.Append(-1, _('Catalogs URL'), _('URL for billing catalogs (schedules of fees).'))
435 self.Bind(wx.EVT_MENU, self.__on_configure_billing_catalogs_url, item)
436
437
438 menu_cfg_emr = wx.Menu()
439 item = menu_cfg_emr.Append(-1, _('Medication list template'), _('Select the template for printing a medication list.'))
440 self.Bind(wx.EVT_MENU, self.__on_cfg_medication_list_template, item)
441 item = menu_cfg_emr.Append(-1, _('Prescription mode'), _('Select the default mode for creating a prescription.'))
442 self.Bind(wx.EVT_MENU, self.__on_cfg_prescription_mode, item)
443 item = menu_cfg_emr.Append(-1, _('Prescription template'), _('Select the template for printing a prescription.'))
444 self.Bind(wx.EVT_MENU, self.__on_cfg_prescription_template, item)
445 item = menu_cfg_emr.Append(-1, _('Default Gnuplot template'), _('Select the default template for plotting test results.'))
446 self.Bind(wx.EVT_MENU, self.__on_cfg_default_gnuplot_template, item)
447 item = menu_cfg_emr.Append(-1, _('Fallback provider'), _('Select the doctor to fall back to for patients without a primary provider.'))
448 self.Bind(wx.EVT_MENU, self.__on_cfg_fallback_primary_provider, item)
449
450
451 menu_cfg_encounter = wx.Menu()
452 item = menu_cfg_encounter.Append(-1, _('Edit before patient change'), _('Edit encounter details before change of patient.'))
453 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_pat_change, item)
454 item = menu_cfg_encounter.Append(-1, _('Minimum duration'), _('Minimum duration of an encounter.'))
455 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_min_ttl, item)
456 item = menu_cfg_encounter.Append(-1, _('Maximum duration'), _('Maximum duration of an encounter.'))
457 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_max_ttl, item)
458 item = menu_cfg_encounter.Append(-1, _('Minimum empty age'), _('Minimum age of an empty encounter before considering for deletion.'))
459 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_empty_ttl, item)
460 item = menu_cfg_encounter.Append(-1, _('Default type'), _('Default type for new encounters.'))
461 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_default_type, item)
462 menu_cfg_emr.Append(wx.NewId(), _('Encounter ...'), menu_cfg_encounter)
463
464
465 menu_cfg_episode = wx.Menu()
466 item = menu_cfg_episode.Append(-1, _('Dormancy'), _('Maximum length of dormancy after which an episode will be considered closed.'))
467 self.Bind(wx.EVT_MENU, self.__on_cfg_epi_ttl, item)
468 menu_cfg_emr.Append(wx.NewId(), _('Episode ...'), menu_cfg_episode)
469
470 menu_config.Append(wx.NewId(), _('User interface ...'), menu_cfg_ui)
471 menu_config.Append(wx.NewId(), _('EMR ...'), menu_cfg_emr)
472 menu_config.Append(wx.NewId(), _('Billing ...'), menu_cfg_bill)
473 menu_gnumed.Append(wx.NewId(), _('Preferences ...'), menu_config)
474
475
476 menu_master_data = wx.Menu()
477 item = menu_master_data.Append(-1, _('Manage lists'), _('Manage various lists of master data.'))
478 self.Bind(wx.EVT_MENU, self.__on_manage_master_data, item)
479 item = menu_master_data.Append(-1, _('Manage praxis'), _('Manage your praxis branches.'))
480 self.Bind(wx.EVT_MENU, self.__on_manage_praxis, item)
481 item = menu_master_data.Append(-1, _('Install data packs'), _('Install reference data from data packs.'))
482 self.Bind(wx.EVT_MENU, self.__on_install_data_packs, item)
483 item = menu_master_data.Append(-1, _('Update ATC'), _('Install ATC reference data.'))
484 self.Bind(wx.EVT_MENU, self.__on_update_atc, item)
485 item = menu_master_data.Append(-1, _('Update LOINC'), _('Download and install LOINC reference data.'))
486 self.Bind(wx.EVT_MENU, self.__on_update_loinc, item)
487 item = menu_master_data.Append(-1, _('Create fake vaccines'), _('Re-create fake generic vaccines.'))
488 self.Bind(wx.EVT_MENU, self.__on_generate_vaccines, item)
489 menu_gnumed.Append(wx.NewId(), _('&Master data ...'), menu_master_data)
490
491
492 menu_users = wx.Menu()
493 item = menu_users.Append(-1, _('&Add user'), _('Add a new GNUmed user'))
494 self.Bind(wx.EVT_MENU, self.__on_add_new_staff, item)
495 item = menu_users.Append(-1, _('&Edit users'), _('Edit the list of GNUmed users'))
496 self.Bind(wx.EVT_MENU, self.__on_edit_staff_list, item)
497 item = menu_users.Append(-1, _('&Change DB owner PWD'), _('Change the password of the GNUmed database owner'))
498 self.Bind(wx.EVT_MENU, self.__on_edit_gmdbowner_password, item)
499 menu_gnumed.Append(wx.NewId(), _('&Users ...'), menu_users)
500
501 menu_gnumed.AppendSeparator()
502
503 item = menu_gnumed.Append(wx.ID_EXIT, _('E&xit\tAlt-X'), _('Close this GNUmed client.'))
504 self.Bind(wx.EVT_MENU, self.__on_exit_gnumed, item)
505
506 self.mainmenu.Append(menu_gnumed, '&GNUmed')
507
508
509 menu_person = wx.Menu()
510
511 item = menu_person.Append(-1, _('Search'), _('Search for a person.'))
512 self.Bind(wx.EVT_MENU, self.__on_search_person, item)
513 acc_tab = wx.AcceleratorTable([(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, item.GetId())])
514 self.SetAcceleratorTable(acc_tab)
515 item = menu_person.Append(-1, _('&Register person'), _("Register a new person with GNUmed"))
516 self.Bind(wx.EVT_MENU, self.__on_create_new_patient, item)
517
518 menu_person_import = wx.Menu()
519 item = menu_person_import.Append(-1, _('From &External sources'), _('Load and possibly create person from available external sources.'))
520 self.Bind(wx.EVT_MENU, self.__on_load_external_patient, item)
521 item = menu_person_import.Append(-1, _('&vCard file \u2192 patient'), _('Import demographics from .vcf vCard file as patient'))
522 self.Bind(wx.EVT_MENU, self.__on_import_vcard_from_file, item)
523 item = menu_person_import.Append(-1, _('Clipboard (&XML) \u2192 patient'), _('Import demographics from clipboard (LinuxMedNews XML) as patient'))
524 self.Bind(wx.EVT_MENU, self.__on_import_xml_linuxmednews, item)
525 item = menu_person_import.Append(-1, _('Clipboard (&vCard) \u2192 patient'), _('Import demographics from clipboard (vCard) as patient'))
526 self.Bind(wx.EVT_MENU, self.__on_import_vcard_from_clipboard, item)
527 menu_person.Append(wx.NewId(), '&Import\u2026', menu_person_import)
528
529 menu_person_export = wx.Menu()
530 menu_person_export_clipboard = wx.Menu()
531 item = menu_person_export_clipboard.Append(-1, '&GDT', _('Export demographics of currently active person as GDT into clipboard.'))
532 self.Bind(wx.EVT_MENU, self.__on_export_gdt2clipboard, item)
533 item = menu_person_export_clipboard.Append(-1, '&XML (LinuxMedNews)', _('Export demographics of currently active person as XML (LinuxMedNews) into clipboard'))
534 self.Bind(wx.EVT_MENU, self.__on_export_linuxmednews_xml2clipboard, item)
535 item = menu_person_export_clipboard.Append(-1, '&vCard', _('Export demographics of currently active person as vCard into clipboard'))
536 self.Bind(wx.EVT_MENU, self.__on_export_vcard2clipboard, item)
537 menu_person_export.Append(wx.NewId(), _('\u2192 &Clipboard as\u2026'), menu_person_export_clipboard)
538
539 menu_person_export_file = wx.Menu()
540 item = menu_person_export_file.Append(-1, '&GDT', _('Export demographics of currently active person into GDT file.'))
541 self.Bind(wx.EVT_MENU, self.__on_export_as_gdt, item)
542 item = menu_person_export_file.Append(-1, '&vCard', _('Export demographics of currently active person into vCard file.'))
543 self.Bind(wx.EVT_MENU, self.__on_export_as_vcard, item)
544 menu_person_export.Append(wx.NewId(), _('\u2192 &File as\u2026'), menu_person_export_file)
545
546 menu_person.Append(wx.NewId(), 'E&xport\u2026', menu_person_export)
547
548 item = menu_person.Append(-1, _('&Merge persons'), _('Merge two persons into one.'))
549 self.Bind(wx.EVT_MENU, self.__on_merge_patients, item)
550 item = menu_person.Append(-1, _('Deactivate record'), _('Deactivate (exclude from search) person record in database.'))
551 self.Bind(wx.EVT_MENU, self.__on_delete_patient, item)
552 menu_person.AppendSeparator()
553 item = menu_person.Append(-1, _('Add &tag'), _('Add a text/image tag to this person.'))
554 self.Bind(wx.EVT_MENU, self.__on_add_tag2person, item)
555 item = menu_person.Append(-1, _('Enlist as user'), _('Enlist current person as GNUmed user'))
556 self.Bind(wx.EVT_MENU, self.__on_enlist_patient_as_staff, item)
557 menu_person.AppendSeparator()
558
559 self.mainmenu.Append(menu_person, '&Person')
560 self.__gb['main.patientmenu'] = menu_person
561
562
563 menu_emr = wx.Menu()
564
565
566 menu_emr_manage = wx.Menu()
567 item = menu_emr_manage.Append(-1, _('&Past history (health issue / PMH)'), _('Add a past/previous medical history item (health issue) to the EMR of the active patient'))
568 self.Bind(wx.EVT_MENU, self.__on_add_health_issue, item)
569 item = menu_emr_manage.Append(-1, _('&Episode'), _('Add an episode of illness to the EMR of the active patient'))
570 self.Bind(wx.EVT_MENU, self.__on_add_episode, item)
571 item = menu_emr_manage.Append(-1, _('&Medication'), _('Add medication / substance use entry.'))
572 self.Bind(wx.EVT_MENU, self.__on_add_medication, item)
573 item = menu_emr_manage.Append(-1, _('&Allergies'), _('Manage documentation of allergies for the current patient.'))
574 self.Bind(wx.EVT_MENU, self.__on_manage_allergies, item)
575 item = menu_emr_manage.Append(-1, _('&Occupation'), _('Edit occupation details for the current patient.'))
576 self.Bind(wx.EVT_MENU, self.__on_edit_occupation, item)
577 item = menu_emr_manage.Append(-1, _('&Hospitalizations'), _('Manage hospitalizations.'))
578 self.Bind(wx.EVT_MENU, self.__on_manage_hospital_stays, item)
579 item = menu_emr_manage.Append(-1, _('&External care'), _('Manage external care.'))
580 self.Bind(wx.EVT_MENU, self.__on_manage_external_care, item)
581 item = menu_emr_manage.Append(-1, _('&Procedures'), _('Manage procedures performed on the patient.'))
582 self.Bind(wx.EVT_MENU, self.__on_manage_performed_procedures, item)
583 item = menu_emr_manage.Append(-1, _('&Measurements'), _('Manage measurement results for the current patient.'))
584 self.Bind(wx.EVT_MENU, self.__on_manage_measurements, item)
585 item = menu_emr_manage.Append(-1, _('&Vaccinations: by shot'), _('Manage vaccinations for the current patient (by shots given).'))
586 self.Bind(wx.EVT_MENU, self.__on_manage_vaccination, item)
587 item = menu_emr_manage.Append(-1, _('&Vaccinations: by indication'), _('Manage vaccinations for the current patient (by indication).'))
588 self.Bind(wx.EVT_MENU, self.__on_show_all_vaccinations_by_indication, item)
589 item = menu_emr_manage.Append(-1, _('&Vaccinations: latest'), _('List latest vaccinations for the current patient.'))
590 self.Bind(wx.EVT_MENU, self.__on_show_latest_vaccinations, item)
591 item = menu_emr_manage.Append(-1, _('&Family history (FHx)'), _('Manage family history.'))
592 self.Bind(wx.EVT_MENU, self.__on_manage_fhx, item)
593 item = menu_emr_manage.Append(-1, _('&Encounters'), _('List all encounters including empty ones.'))
594 self.Bind(wx.EVT_MENU, self.__on_list_encounters, item)
595 item = menu_emr_manage.Append(-1, _('&Pregnancy'), _('Calculate EDC.'))
596 self.Bind(wx.EVT_MENU, self.__on_calc_edc, item)
597 item = menu_emr_manage.Append(-1, _('Suppressed hints'), _('Manage dynamic hints suppressed in this patient.'))
598 self.Bind(wx.EVT_MENU, self.__on_manage_suppressed_hints, item)
599 item = menu_emr_manage.Append(-1, _('Substance abuse'), _('Manage substance abuse documentation of this patient.'))
600 self.Bind(wx.EVT_MENU, self.__on_manage_substance_abuse, item)
601 menu_emr.Append(wx.NewId(), _('&Manage ...'), menu_emr_manage)
602
603
604 item = menu_emr.Append(-1, _('Search this EMR'), _('Search for data in the EMR of the active patient'))
605 self.Bind(wx.EVT_MENU, self.__on_search_emr, item)
606
607 item = menu_emr.Append(-1, _('Start new encounter'), _('Start a new encounter for the active patient right now.'))
608 self.Bind(wx.EVT_MENU, self.__on_start_new_encounter, item)
609
610
611
612
613 item = menu_emr.Append(-1, _('Statistics'), _('Show a high-level statistic summary of the EMR.'))
614 self.Bind(wx.EVT_MENU, self.__on_show_emr_summary, item)
615
616
617
618
619 menu_emr.AppendSeparator()
620
621
622 menu_emr_export = wx.Menu()
623 item = menu_emr_export.Append(-1, _('Journal (encounters)'), _("Copy EMR of the active patient as a chronological journal into export area"))
624 self.Bind(wx.EVT_MENU, self.__on_export_emr_as_journal, item)
625 item = menu_emr_export.Append(-1, _('Journal (mod time)'), _("Copy EMR of active patient as journal (by last modification time) into export area"))
626 self.Bind(wx.EVT_MENU, self.__on_export_emr_by_last_mod, item)
627 item = menu_emr_export.Append(-1, _('Text document'), _("Copy EMR of active patient as text document into export area"))
628 self.Bind(wx.EVT_MENU, self.__export_emr_as_textfile, item)
629 item = menu_emr_export.Append(-1, _('Timeline file'), _("Copy EMR of active patient as timeline file (XML) into export area"))
630 self.Bind(wx.EVT_MENU, self.__export_emr_as_timeline_xml, item)
631 item = menu_emr_export.Append(-1, _('Care structure'), _("Copy EMR of active patient as care structure text file into export area"))
632 self.Bind(wx.EVT_MENU, self.__export_emr_as_care_structure, item)
633
634 item = menu_emr_export.Append(-1, _('MEDISTAR import format (as file)'), _("GNUmed -> MEDISTAR. Save progress notes of active patient's active encounter into a text file."))
635 self.Bind(wx.EVT_MENU, self.__on_export_for_medistar, item)
636 menu_emr.Append(wx.NewId(), _('Put into export area as ...'), menu_emr_export)
637
638 menu_emr.AppendSeparator()
639
640 self.mainmenu.Append(menu_emr, _("&EMR"))
641 self.__gb['main.emrmenu'] = menu_emr
642
643
644 menu_paperwork = wx.Menu()
645 item = menu_paperwork.Append(-1, _('&Write letter'), _('Write a letter for the current patient.'))
646 self.Bind(wx.EVT_MENU, self.__on_new_letter, item)
647 item = menu_paperwork.Append(-1, _('Screenshot -> export area'), _('Put a screenshot into the patient export area.'))
648 self.Bind(wx.EVT_MENU, self.__on_save_screenshot_into_export_area, item)
649 menu_paperwork.AppendSeparator()
650 item = menu_paperwork.Append(-1, _('List Placeholders'), _('Show a list of all placeholders.'))
651 self.Bind(wx.EVT_MENU, self.__on_show_placeholders, item)
652
653
654 self.mainmenu.Append(menu_paperwork, _('&Correspondence'))
655 self.__gb['main.paperworkmenu'] = menu_paperwork
656
657
658 self.menu_tools = wx.Menu()
659 item = self.menu_tools.Append(-1, _('Search all EMRs'), _('Search for data across the EMRs of all patients'))
660 self.Bind(wx.EVT_MENU, self.__on_search_across_emrs, item)
661 viewer = _('no viewer installed')
662 if gmShellAPI.detect_external_binary(binary = 'ginkgocadx')[0]:
663 viewer = 'Ginkgo CADx'
664 elif os.access('/Applications/OsiriX.app/Contents/MacOS/OsiriX', os.X_OK):
665 viewer = 'OsiriX'
666 elif gmShellAPI.detect_external_binary(binary = 'aeskulap')[0]:
667 viewer = 'Aeskulap'
668 elif gmShellAPI.detect_external_binary(binary = 'amide')[0]:
669 viewer = 'AMIDE'
670 elif gmShellAPI.detect_external_binary(binary = 'dicomscope')[0]:
671 viewer = 'DicomScope'
672 elif gmShellAPI.detect_external_binary(binary = 'xmedcon')[0]:
673 viewer = '(x)medcon'
674 item = self.menu_tools.Append(-1, _('DICOM viewer'), _('Start DICOM viewer (%s) for CD-ROM (X-Ray, CT, MR, etc). On Windows just insert CD.') % viewer)
675 self.Bind(wx.EVT_MENU, self.__on_dicom_viewer, item)
676 if viewer == _('no viewer installed'):
677 _log.info('neither of Ginkgo CADx / OsiriX / Aeskulap / AMIDE / DicomScope / xmedcon found, disabling "DICOM viewer" menu item')
678 self.menu_tools.Enable(id = item.Id, enable=False)
679
680
681 item = self.menu_tools.Append(-1, _('Snellen chart'), _('Display fullscreen snellen chart.'))
682 self.Bind(wx.EVT_MENU, self.__on_snellen, item)
683 item = self.menu_tools.Append(-1, _('MI/stroke risk'), _('Acute coronary syndrome/stroke risk assessment.'))
684 self.Bind(wx.EVT_MENU, self.__on_acs_risk_assessment, item)
685 item = self.menu_tools.Append(-1, 'arriba', _('arriba: cardiovascular risk assessment (%s).') % 'www.arriba-hausarzt.de')
686 self.Bind(wx.EVT_MENU, self.__on_arriba, item)
687 if not gmShellAPI.detect_external_binary(binary = 'arriba')[0]:
688 _log.info('<arriba> not found, disabling "arriba" menu item')
689 self.menu_tools.Enable(id = item.Id, enable = False)
690
691 menu_lab = wx.Menu()
692 item = menu_lab.Append(-1, _('Show HL7'), _('Show formatted data from HL7 file'))
693 self.Bind(wx.EVT_MENU, self.__on_show_hl7, item)
694 item = menu_lab.Append(-1, _('Unwrap XML'), _('Unwrap HL7 data from XML file (Excelleris, ...)'))
695 self.Bind(wx.EVT_MENU, self.__on_unwrap_hl7_from_xml, item)
696 item = menu_lab.Append(-1, _('Stage HL7'), _('Stage HL7 data from file'))
697 self.Bind(wx.EVT_MENU, self.__on_stage_hl7, item)
698 item = menu_lab.Append(-1, _('Browse pending'), _('Browse pending (staged) incoming data'))
699 self.Bind(wx.EVT_MENU, self.__on_incoming, item)
700
701 self.menu_tools.Append(wx.NewId(), _('Lab results ...'), menu_lab)
702
703 self.menu_tools.AppendSeparator()
704
705 self.mainmenu.Append(self.menu_tools, _("&Tools"))
706 self.__gb['main.toolsmenu'] = self.menu_tools
707
708
709 menu_knowledge = wx.Menu()
710
711
712 menu_drug_dbs = wx.Menu()
713 item = menu_drug_dbs.Append(-1, _('&Database'), _('Jump to the drug database configured as the default.'))
714 self.Bind(wx.EVT_MENU, self.__on_jump_to_drug_db, item)
715
716
717
718 item = menu_drug_dbs.Append(-1, 'kompendium.ch', _('Show "kompendium.ch" drug database (online, Switzerland)'))
719 self.Bind(wx.EVT_MENU, self.__on_kompendium_ch, item)
720 menu_knowledge.Append(wx.NewId(), _('&Drug Resources'), menu_drug_dbs)
721
722
723
724 item = menu_knowledge.Append(-1, _('Medical links (www)'), _('Show a page of links to useful medical content.'))
725 self.Bind(wx.EVT_MENU, self.__on_medical_links, item)
726
727 self.mainmenu.Append(menu_knowledge, _('&Knowledge'))
728 self.__gb['main.knowledgemenu'] = menu_knowledge
729
730
731 self.menu_office = wx.Menu()
732
733 item = self.menu_office.Append(-1, _('&Audit trail'), _('Display database audit trail.'))
734 self.Bind(wx.EVT_MENU, self.__on_display_audit_trail, item)
735
736 self.menu_office.AppendSeparator()
737
738 item = self.menu_office.Append(-1, _('&Bills'), _('List all bills across all patients.'))
739 self.Bind(wx.EVT_MENU, self.__on_show_all_bills, item)
740
741 item = self.menu_office.Append(-1, _('&Organizations'), _('Manage organizations.'))
742 self.Bind(wx.EVT_MENU, self.__on_manage_orgs, item)
743
744 self.mainmenu.Append(self.menu_office, _('&Office'))
745 self.__gb['main.officemenu'] = self.menu_office
746
747
748 help_menu = wx.Menu()
749 help_menu.Append(-1, _('GNUmed wiki'), _('Go to the GNUmed wiki on the web.'))
750 self.Bind(wx.EVT_MENU, self.__on_display_wiki, item)
751 help_menu.Append(-1, _('User manual (www)'), _('Go to the User Manual on the web.'))
752 self.Bind(wx.EVT_MENU, self.__on_display_user_manual_online, item)
753 item = help_menu.Append(-1, _('Menu reference (www)'), _('View the reference for menu items on the web.'))
754 self.Bind(wx.EVT_MENU, self.__on_menu_reference, item)
755 item = help_menu.Append(-1, _('Browse work dir'), _('Browse user working directory [%s].') % os.path.join(gmTools.gmPaths().home_dir, 'gnumed'))
756 self.Bind(wx.EVT_MENU, self.__on_browse_work_dir, item)
757
758 menu_debugging = wx.Menu()
759 item = menu_debugging.Append(-1, _('Status line: &Clear'), _('Clear the status line.'))
760 self.Bind(wx.EVT_MENU, self.__on_clear_status_line, item)
761 item = menu_debugging.Append(-1, _('Status line: History'), _('Show status line history.'))
762 self.Bind(wx.EVT_MENU, self.__on_show_status_line_history, item)
763 item = menu_debugging.Append(-1, _('Tooltips on'), _('Globally enable tooltips.'))
764 self.Bind(wx.EVT_MENU, self.__on_enable_tooltips, item)
765 item = menu_debugging.Append(-1, _('Tooltips off'), _('Globally (attempt to) disable tooltips.'))
766 self.Bind(wx.EVT_MENU, self.__on_disable_tooltips, item)
767 item = menu_debugging.Append(-1, _('Screenshot'), _('Save a screenshot of this GNUmed client.'))
768 self.Bind(wx.EVT_MENU, self.__on_save_screenshot, item)
769 item = menu_debugging.Append(-1, _('Show log file'), _('Show log file in text viewer.'))
770 self.Bind(wx.EVT_MENU, self.__on_show_log_file, item)
771 item = menu_debugging.Append(-1, _('Backup log file'), _('Backup content of the log to another file.'))
772 self.Bind(wx.EVT_MENU, self.__on_backup_log_file, item)
773 item = menu_debugging.Append(-1, _('Email log file'), _('Send log file to the authors for help.'))
774 self.Bind(wx.EVT_MENU, self.__on_email_log_file, item)
775 item = menu_debugging.Append(-1, _('Browse tmp dir'), _('Browse temporary directory [%s].') % gmTools.gmPaths().tmp_dir)
776 self.Bind(wx.EVT_MENU, self.__on_browse_tmp_dir, item)
777 item = menu_debugging.Append(-1, _('Browse internal work dir'), _('Browse internal working directory [%s].') % os.path.join(gmTools.gmPaths().home_dir, '.gnumed'))
778 self.Bind(wx.EVT_MENU, self.__on_browse_internal_work_dir, item)
779 item = menu_debugging.Append(-1, _('Bug tracker'), _('Go to the GNUmed bug tracker on the web.'))
780 self.Bind(wx.EVT_MENU, self.__on_display_bugtracker, item)
781 item = menu_debugging.Append(-1, _('Unlock mouse'), _('Unlock mouse pointer in case it got stuck in hourglass mode.'))
782 self.Bind(wx.EVT_MENU, self.__on_unblock_cursor, item)
783 item = menu_debugging.Append(-1, _('pgAdmin III'), _('pgAdmin III: Browse GNUmed database(s) in PostgreSQL server.'))
784 self.Bind(wx.EVT_MENU, self.__on_pgadmin3, item)
785
786
787 if _cfg.get(option = 'debug'):
788 item = menu_debugging.Append(-1, _('Lock/unlock patient search'), _('Lock/unlock patient search - USE ONLY IF YOU KNOW WHAT YOU ARE DOING !'))
789 self.Bind(wx.EVT_MENU, self.__on_toggle_patient_lock, item)
790 item = menu_debugging.Append(-1, _('Test error handling'), _('Throw an exception to test error handling.'))
791 self.Bind(wx.EVT_MENU, self.__on_test_exception, item)
792 item = menu_debugging.Append(-1, _('Test access violation exception'), _('Simulate an access violation exception.'))
793 self.Bind(wx.EVT_MENU, self.__on_test_access_violation, item)
794 item = menu_debugging.Append(-1, _('Test access checking'), _('Simulate a failing access check.'))
795 self.Bind(wx.EVT_MENU, self.__on_test_access_checking, item)
796 item = menu_debugging.Append(-1, _('Invoke inspector'), _('Invoke the widget hierarchy inspector (needs wxPython 2.8).'))
797 self.Bind(wx.EVT_MENU, self.__on_invoke_inspector, item)
798 try:
799 import wx.lib.inspection
800 except ImportError:
801 menu_debugging.Enable(id = ID, enable = False)
802 try:
803 import faulthandler
804 item = menu_debugging.Append(-1, _('Test fault handler'), _('Simulate a catastrophic fault (SIGSEGV).'))
805 self.Bind(wx.EVT_MENU, self.__on_test_segfault, item)
806 except ImportError:
807 pass
808 item = menu_debugging.Append(-1, _('Test placeholder'), _('Manually test placeholders'))
809 self.Bind(wx.EVT_MENU, self.__on_test_placeholders, item)
810
811
812 help_menu.Append(wx.NewId(), _('Debugging ...'), menu_debugging)
813 help_menu.AppendSeparator()
814
815 item = help_menu.Append(wx.ID_ABOUT, _('About GNUmed'), '')
816 self.Bind(wx.EVT_MENU, self.OnAbout, item)
817 item = help_menu.Append(-1, _('About database'), _('Show information about the current database.'))
818 self.Bind(wx.EVT_MENU, self.__on_about_database, item)
819 item = help_menu.Append(-1, _('About contributors'), _('Show GNUmed contributors'))
820 self.Bind(wx.EVT_MENU, self.__on_show_contributors, item)
821 help_menu.AppendSeparator()
822
823 self.mainmenu.Append(help_menu, _("&Help"))
824
825 self.__gb['main.helpmenu'] = help_menu
826
827
828 self.SetMenuBar(self.mainmenu)
829
830
833
834
835
837 """register events we want to react to"""
838
839 self.Bind(wx.EVT_CLOSE, self.OnClose)
840 self.Bind(wx.EVT_QUERY_END_SESSION, self._on_query_end_session)
841 self.Bind(wx.EVT_END_SESSION, self._on_end_session)
842
843 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
844 gmDispatcher.connect(signal = 'statustext', receiver = self._on_set_statustext)
845 gmDispatcher.connect(signal = 'request_user_attention', receiver = self._on_request_user_attention)
846 gmDispatcher.connect(signal = 'register_pre_exit_callback', receiver = self._register_pre_exit_callback)
847 gmDispatcher.connect(signal = 'plugin_loaded', receiver = self._on_plugin_loaded)
848
849 gmDispatcher.connect(signal = 'db_maintenance_warning', receiver = self._on_db_maintenance_warning)
850 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
851
852
853
854 gmPerson.gmCurrentPatient().register_before_switching_from_patient_callback(callback = self._before_switching_from_patient_callback)
855
856
858
859 if kwds['table'] == 'dem.praxis_branch':
860 if kwds['operation'] != 'UPDATE':
861 return True
862 branch = gmPraxis.gmCurrentPraxisBranch()
863 if branch['pk_praxis_branch'] != kwds['pk_of_row']:
864 return True
865 self.__update_window_title()
866 return True
867
868 if kwds['table'] == 'dem.names':
869 pat = gmPerson.gmCurrentPatient()
870 if pat.connected:
871 if pat.ID != kwds['pk_identity']:
872 return True
873 self.__update_window_title()
874 return True
875
876 if kwds['table'] == 'dem.identity':
877 if kwds['operation'] != 'UPDATE':
878 return True
879 pat = gmPerson.gmCurrentPatient()
880 if pat.connected:
881 if pat.ID != kwds['pk_identity']:
882 return True
883 self.__update_window_title()
884 return True
885
886 return True
887
888
889 - def _on_plugin_loaded(self, plugin_name=None, class_name=None, menu_name=None, menu_item_name=None, menu_help_string=None):
890
891 _log.debug('registering plugin with menu system')
892 _log.debug(' generic name: %s', plugin_name)
893 _log.debug(' class name: %s', class_name)
894 _log.debug(' specific menu: %s', menu_name)
895 _log.debug(' menu item: %s', menu_item_name)
896
897
898 item = self.menu_plugins.Append(-1, plugin_name, _('Raise plugin [%s].') % plugin_name)
899 self.Bind(wx.EVT_MENU, self.__on_raise_a_plugin, item)
900 self.menu_id2plugin[item.Id] = class_name
901
902
903 if menu_name is not None:
904 menu = self.__gb['main.%smenu' % menu_name]
905 item = menu.Append(-1, menu_item_name, menu_help_string)
906 self.Bind(wx.EVT_MENU, self.__on_raise_a_plugin, item)
907 self.menu_id2plugin[item.Id] = class_name
908
909 return True
910
916
918 wx.Bell()
919 wx.Bell()
920 wx.Bell()
921 _log.warning('unhandled event detected: QUERY_END_SESSION')
922 _log.info('we should be saving ourselves from here')
923 gmLog2.flush()
924 print('unhandled event detected: QUERY_END_SESSION')
925
927 wx.Bell()
928 wx.Bell()
929 wx.Bell()
930 _log.warning('unhandled event detected: END_SESSION')
931 gmLog2.flush()
932 print('unhandled event detected: END_SESSION')
933
934
936 if not callable(callback):
937 raise TypeError('callback [%s] not callable' % callback)
938
939 self.__pre_exit_callbacks.append(callback)
940
941
942 - def _on_set_statustext_pubsub(self, context=None):
943 try:
944 beep = context.data['beep']
945 except KeyError:
946 beep = False
947 wx.CallAfter(self.SetStatusText, '%s' % context.data['msg'], beep = beep)
948
949
950 - def _on_set_statustext(self, msg=None, loglevel=None, beep=True):
951 if msg is None:
952 msg = _('programmer forgot to specify status message')
953 if loglevel is not None:
954 _log.log(loglevel, msg.replace('\015', ' ').replace('\012', ' '))
955 wx.CallAfter(self.SetStatusText, msg, beep = beep)
956
957
959
960 self.SetStatusText(_('The database will be shut down for maintenance in a few minutes.'))
961 wx.Bell()
962 if not wx.GetApp().IsActive():
963 self.RequestUserAttention(flags = wx.USER_ATTENTION_ERROR)
964
965 gmHooks.run_hook_script(hook = 'db_maintenance_warning')
966
967 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
968 None,
969 -1,
970 caption = _('Database shutdown warning'),
971 question = _(
972 'The database will be shut down for maintenance\n'
973 'in a few minutes.\n'
974 '\n'
975 'In order to not suffer any loss of data you\n'
976 'will need to save your current work and log\n'
977 'out of this GNUmed client.\n'
978 ),
979 button_defs = [
980 {
981 'label': _('Close now'),
982 'tooltip': _('Close this GNUmed client immediately.'),
983 'default': False
984 },
985 {
986 'label': _('Finish work'),
987 'tooltip': _('Finish and save current work first, then manually close this GNUmed client.'),
988 'default': True
989 }
990 ]
991 )
992 decision = dlg.ShowModal()
993 if decision == wx.ID_YES:
994 top_win = wx.GetApp().GetTopWindow()
995 wx.CallAfter(top_win.Close)
996
997
999
1000 if not wx.GetApp().IsActive():
1001 if urgent:
1002 self.RequestUserAttention(flags = wx.USER_ATTENTION_ERROR)
1003 else:
1004 self.RequestUserAttention(flags = wx.USER_ATTENTION_INFO)
1005
1006 if msg is not None:
1007 self.SetStatusText(msg)
1008
1009 if urgent:
1010 wx.Bell()
1011
1012 gmHooks.run_hook_script(hook = 'request_user_attention')
1013
1014 - def _on_post_patient_selection(self, **kwargs):
1015 self.__update_window_title()
1016 gmDispatcher.send(signal = 'statustext', msg = '')
1017 try:
1018 gmHooks.run_hook_script(hook = 'post_patient_activation')
1019 except Exception:
1020 _log.exception('error running hook [post_patient_activation]')
1021 gmDispatcher.send(signal = 'statustext', msg = _('Cannot run script after patient activation.'))
1022
1023
1031
1032
1033
1036
1043
1044
1048
1049
1051 evt.Skip()
1052 pat = gmPerson.gmCurrentPatient()
1053 if not pat.connected:
1054 gmDispatcher.send(signal = 'statustext', msg = _('Cannot put screenshot into export area. No active patient.'), beep = True)
1055 return False
1056
1057 screenshot_file = self.__save_screenshot_to_file()
1058 if not os.path.exists(screenshot_file):
1059 gmDispatcher.send(signal = 'statustext', msg = _('Cannot put screenshot into export area. No screenshot found.'), beep = True)
1060 return False
1061
1062 pat.export_area.add_file(filename = screenshot_file, hint = _('GMd screenshot'))
1063 return True
1064
1065
1079
1080
1081
1082
1084
1085 return
1086
1087
1088 from Gnumed.wxpython import gmAbout
1089 frame_about = gmAbout.AboutFrame (
1090 self,
1091 -1,
1092 _("About GNUmed"),
1093 size=wx.Size(350, 300),
1094 style = wx.MAXIMIZE_BOX,
1095 version = _cfg.get(option = 'client_version'),
1096 debug = _cfg.get(option = 'debug')
1097 )
1098 frame_about.Centre(wx.BOTH)
1099 gmTopLevelFrame.otherWin = frame_about
1100 frame_about.Show(True)
1101 frame_about.DestroyLater()
1102
1103
1105 praxis = gmPraxis.gmCurrentPraxisBranch()
1106 msg = praxis.db_logon_banner
1107 login = gmPG2.get_default_login()
1108 auth = _(
1109 '\n\n'
1110 ' praxis: %s\n'
1111 ' branch: %s\n'
1112 ' workplace: %s\n'
1113 ' account: %s\n'
1114 ' locale: %s\n'
1115 ' access: %s\n'
1116 ' database: %s\n'
1117 ' server: %s\n'
1118 ' PostgreSQL: %s\n'
1119 ) % (
1120 praxis['praxis'],
1121 praxis['branch'],
1122 praxis.active_workplace,
1123 login.user,
1124 gmPG2.get_current_user_language(),
1125 _provider['role'],
1126 login.database,
1127 gmTools.coalesce(login.host, '<localhost>'),
1128 gmPG2.postgresql_version_string
1129 )
1130 msg += auth
1131 gmGuiHelpers.gm_show_info(msg, _('About database and server'))
1132
1133
1135 from Gnumed.wxpython import gmAbout
1136 contribs = gmAbout.cContributorsDlg (
1137 parent = self,
1138 id = -1,
1139 title = _('GNUmed contributors'),
1140 size = wx.Size(400,600),
1141 style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
1142 )
1143 contribs.ShowModal()
1144 contribs.DestroyLater()
1145
1146
1147
1148
1150 """Invoked from Menu GNUmed / Exit (which calls this ID_EXIT handler)."""
1151 _log.debug('gmTopLevelFrame._on_exit_gnumed() start')
1152 self.Close(True)
1153 _log.debug('gmTopLevelFrame._on_exit_gnumed() end')
1154
1155
1158
1159
1161 send = gmGuiHelpers.gm_show_question (
1162 _('This will send a notification about database downtime\n'
1163 'to all GNUmed clients connected to your database.\n'
1164 '\n'
1165 'Do you want to send the notification ?\n'
1166 ),
1167 _('Announcing database maintenance downtime')
1168 )
1169 if not send:
1170 return
1171 gmPG2.send_maintenance_notification()
1172
1173
1176
1177
1178
1191
1192 gmCfgWidgets.configure_string_option (
1193 message = _(
1194 'Some network installations cannot cope with loading\n'
1195 'documents of arbitrary size in one piece from the\n'
1196 'database (mainly observed on older Windows versions)\n.'
1197 '\n'
1198 'Under such circumstances documents need to be retrieved\n'
1199 'in chunks and reassembled on the client.\n'
1200 '\n'
1201 'Here you can set the size (in Bytes) above which\n'
1202 'GNUmed will retrieve documents in chunks. Setting this\n'
1203 'value to 0 will disable the chunking protocol.'
1204 ),
1205 option = 'horstspace.blob_export_chunk_size',
1206 bias = 'workplace',
1207 default_value = 1024 * 1024,
1208 validator = is_valid
1209 )
1210
1211
1212
1280
1284
1285
1286
1295
1296 gmCfgWidgets.configure_string_option (
1297 message = _(
1298 'When GNUmed cannot find an OpenOffice server it\n'
1299 'will try to start one. OpenOffice, however, needs\n'
1300 'some time to fully start up.\n'
1301 '\n'
1302 'Here you can set the time for GNUmed to wait for OOo.\n'
1303 ),
1304 option = 'external.ooo.startup_settle_time',
1305 bias = 'workplace',
1306 default_value = 2.0,
1307 validator = is_valid
1308 )
1309
1312
1313
1316
1317
1320
1321
1324
1325
1340
1341 gmCfgWidgets.configure_string_option (
1342 message = _(
1343 'GNUmed will use this URL to access an encyclopedia of\n'
1344 'measurement/lab methods from within the measurments grid.\n'
1345 '\n'
1346 'You can leave this empty but to set it to a specific\n'
1347 'address the URL must be accessible now.'
1348 ),
1349 option = 'external.urls.measurements_encyclopedia',
1350 bias = 'user',
1351 default_value = german_default,
1352 validator = is_valid
1353 )
1354
1355
1368
1369 gmCfgWidgets.configure_string_option (
1370 message = _(
1371 'Enter the shell command with which to start the\n'
1372 'the ACS risk assessment calculator.\n'
1373 '\n'
1374 'GNUmed will try to verify the path which may,\n'
1375 'however, fail if you are using an emulator such\n'
1376 'as Wine. Nevertheless, starting the calculator\n'
1377 'will work as long as the shell command is correct\n'
1378 'despite the failing test.'
1379 ),
1380 option = 'external.tools.acs_risk_calculator_cmd',
1381 bias = 'user',
1382 validator = is_valid
1383 )
1384
1387
1400
1401 gmCfgWidgets.configure_string_option (
1402 message = _(
1403 'Enter the shell command with which to start\n'
1404 'the FreeDiams drug database frontend.\n'
1405 '\n'
1406 'GNUmed will try to verify that path.'
1407 ),
1408 option = 'external.tools.freediams_cmd',
1409 bias = 'workplace',
1410 default_value = None,
1411 validator = is_valid
1412 )
1413
1426
1427 gmCfgWidgets.configure_string_option (
1428 message = _(
1429 'Enter the shell command with which to start the\n'
1430 'the IFAP drug database.\n'
1431 '\n'
1432 'GNUmed will try to verify the path which may,\n'
1433 'however, fail if you are using an emulator such\n'
1434 'as Wine. Nevertheless, starting IFAP will work\n'
1435 'as long as the shell command is correct despite\n'
1436 'the failing test.'
1437 ),
1438 option = 'external.ifap-win.shell_command',
1439 bias = 'workplace',
1440 default_value = 'C:\Ifapwin\WIAMDB.EXE',
1441 validator = is_valid
1442 )
1443
1444
1445
1494
1495
1496
1513
1516
1519
1524
1525 gmCfgWidgets.configure_string_option (
1526 message = _(
1527 'When a patient is activated GNUmed checks the\n'
1528 "proximity of the patient's birthday.\n"
1529 '\n'
1530 'If the birthday falls within the range of\n'
1531 ' "today %s <the interval you set here>"\n'
1532 'GNUmed will remind you of the recent or\n'
1533 'imminent anniversary.'
1534 ) % '\u2213',
1535 option = 'patient_search.dob_warn_interval',
1536 bias = 'user',
1537 default_value = '1 week',
1538 validator = is_valid
1539 )
1540
1542
1543 gmCfgWidgets.configure_boolean_option (
1544 parent = self,
1545 question = _(
1546 'When adding progress notes do you want to\n'
1547 'allow opening several unassociated, new\n'
1548 'episodes for a patient at once ?\n'
1549 '\n'
1550 'This can be particularly helpful when entering\n'
1551 'progress notes on entirely new patients presenting\n'
1552 'with a multitude of problems on their first visit.'
1553 ),
1554 option = 'horstspace.soap_editor.allow_same_episode_multiple_times',
1555 button_tooltips = [
1556 _('Yes, allow for multiple new episodes concurrently.'),
1557 _('No, only allow editing one new episode at a time.')
1558 ]
1559 )
1560
1562
1563 gmCfgWidgets.configure_boolean_option (
1564 parent = self,
1565 question = _(
1566 'When activating a patient, do you want GNUmed to\n'
1567 'auto-open editors for all active problems that were\n'
1568 'touched upon during the current and the most recent\n'
1569 'encounter ?'
1570 ),
1571 option = 'horstspace.soap_editor.auto_open_latest_episodes',
1572 button_tooltips = [
1573 _('Yes, auto-open editors for all problems of the most recent encounter.'),
1574 _('No, only auto-open one editor for a new, unassociated problem.')
1575 ]
1576 )
1577
1578
1580 gmCfgWidgets.configure_boolean_option (
1581 parent = self,
1582 question = _(
1583 'When editing progress notes, do you want GNUmed to\n'
1584 'show individual fields for each of the SOAP categories\n'
1585 'or do you want to use a text-editor like field for\n'
1586 'all SOAP categories which can then be set per line\n'
1587 'of input ?'
1588 ),
1589 option = 'horstspace.soap_editor.use_one_field_per_soap_category',
1590 button_tooltips = [
1591 _('Yes, show a dedicated field per SOAP category.'),
1592 _('No, use one field for all SOAP categories.')
1593 ]
1594 )
1595
1596
1642
1643
1644
1647
1650
1663
1664 gmCfgWidgets.configure_string_option (
1665 message = _(
1666 'GNUmed will use this URL to let you browse\n'
1667 'billing catalogs (schedules of fees).\n'
1668 '\n'
1669 'You can leave this empty but to set it to a specific\n'
1670 'address the URL must be accessible now.'
1671 ),
1672 option = 'external.urls.schedules_of_fees',
1673 bias = 'user',
1674 default_value = german_default,
1675 validator = is_valid
1676 )
1677
1678
1680 from Gnumed.business.gmBilling import DEFAULT_INVOICE_ID_TEMPLATE
1681 gmCfgWidgets.configure_string_option (
1682 message = _(
1683 'GNUmed will use this template to generate invoice IDs.\n'
1684 '\n'
1685 'If unset GNUmed will use the builtin format\n'
1686 ' >>>%s<<<\n'
1687 '\n'
1688 'The template is processed according to Python\n'
1689 'String Formatting rules with dictionary data.\n'
1690 '\n'
1691 'The following placeholders can be used:\n'
1692 ' %%(pk_pat)s - primary key of the patient\n'
1693 ' %%(date)s - current date\n'
1694 ' %%(time)s - current time\n'
1695 ' %%(firstname)s - first names of patient\n'
1696 ' %%(lastname)s - last names of patient\n'
1697 ' %%(dob)s - date of birth of patient\n'
1698 ' #counter# - replaced by a counter\n'
1699 ' - counting up from 1 to 999999 until the invoice ID is unique\n'
1700 ' - optional if %%(time)s is included\n'
1701 '\n'
1702 'Length modifiers are respected so that\n'
1703 '%%(lastname)4.4s will work as expected.\n'
1704 ) % DEFAULT_INVOICE_ID_TEMPLATE,
1705 option = u'billing.invoice_id_template'
1706 )
1707
1708
1709
1710
1713
1716
1718 gmCfgWidgets.configure_string_from_list_option (
1719 parent = self,
1720 message = _('Select the default prescription mode.\n'),
1721 option = 'horst_space.default_prescription_mode',
1722 bias = 'user',
1723 default_value = 'form',
1724 choices = [ _('Formular'), _('Datenbank') ],
1725 columns = [_('Prescription mode')],
1726 data = [ 'form', 'database' ]
1727 )
1728
1731
1734
1737
1740
1742 enc_types = gmEMRStructItems.get_encounter_types()
1743 msg = _(
1744 'Select the default type for new encounters.\n'
1745 '\n'
1746 'Leaving this unset will make GNUmed apply the most commonly used type.\n'
1747 )
1748 gmCfgWidgets.configure_string_from_list_option (
1749 parent = self,
1750 message = msg,
1751 option = 'encounter.default_type',
1752 bias = 'user',
1753
1754 choices = [ e[0] for e in enc_types ],
1755 columns = [_('Encounter type')],
1756 data = [ e[1] for e in enc_types ]
1757 )
1758
1760 gmCfgWidgets.configure_boolean_option (
1761 parent = self,
1762 question = _(
1763 'Do you want GNUmed to show the encounter\n'
1764 'details editor when changing the active patient ?'
1765 ),
1766 option = 'encounter.show_editor_before_patient_change',
1767 button_tooltips = [
1768 _('Yes, show the encounter editor if it seems appropriate.'),
1769 _('No, never show the encounter editor even if it would seem useful.')
1770 ]
1771 )
1772
1777
1778 gmCfgWidgets.configure_string_option (
1779 message = _(
1780 'When a patient is activated GNUmed checks the\n'
1781 'chart for encounters lacking any entries.\n'
1782 '\n'
1783 'Any such encounters older than what you set\n'
1784 'here will be removed from the medical record.\n'
1785 '\n'
1786 'To effectively disable removal of such encounters\n'
1787 'set this option to an improbable value.\n'
1788 ),
1789 option = 'encounter.ttl_if_empty',
1790 bias = 'user',
1791 default_value = '1 week',
1792 validator = is_valid
1793 )
1794
1799
1800 gmCfgWidgets.configure_string_option (
1801 message = _(
1802 'When a patient is activated GNUmed checks the\n'
1803 'age of the most recent encounter.\n'
1804 '\n'
1805 'If that encounter is younger than this age\n'
1806 'the existing encounter will be continued.\n'
1807 '\n'
1808 '(If it is really old a new encounter is\n'
1809 ' started, or else GNUmed will ask you.)\n'
1810 ),
1811 option = 'encounter.minimum_ttl',
1812 bias = 'user',
1813 default_value = '1 hour 30 minutes',
1814 validator = is_valid
1815 )
1816
1821
1822 gmCfgWidgets.configure_string_option (
1823 message = _(
1824 'When a patient is activated GNUmed checks the\n'
1825 'age of the most recent encounter.\n'
1826 '\n'
1827 'If that encounter is older than this age\n'
1828 'GNUmed will always start a new encounter.\n'
1829 '\n'
1830 '(If it is very recent the existing encounter\n'
1831 ' is continued, or else GNUmed will ask you.)\n'
1832 ),
1833 option = 'encounter.maximum_ttl',
1834 bias = 'user',
1835 default_value = '6 hours',
1836 validator = is_valid
1837 )
1838
1847
1848 gmCfgWidgets.configure_string_option (
1849 message = _(
1850 'At any time there can only be one open (ongoing)\n'
1851 'episode for each health issue.\n'
1852 '\n'
1853 'When you try to open (add data to) an episode on a health\n'
1854 'issue GNUmed will check for an existing open episode on\n'
1855 'that issue. If there is any it will check the age of that\n'
1856 'episode. The episode is closed if it has been dormant (no\n'
1857 'data added, that is) for the period of time (in days) you\n'
1858 'set here.\n'
1859 '\n'
1860 "If the existing episode hasn't been dormant long enough\n"
1861 'GNUmed will consult you what to do.\n'
1862 '\n'
1863 'Enter maximum episode dormancy in DAYS:'
1864 ),
1865 option = 'episode.ttl',
1866 bias = 'user',
1867 default_value = 60,
1868 validator = is_valid
1869 )
1870
1901
1916
1941
1951
1952 gmCfgWidgets.configure_string_option (
1953 message = _(
1954 'GNUmed can check for new releases being available. To do\n'
1955 'so it needs to load version information from an URL.\n'
1956 '\n'
1957 'The default URL is:\n'
1958 '\n'
1959 ' http://www.gnumed.de/downloads/gnumed-versions.txt\n'
1960 '\n'
1961 'but you can configure any other URL locally. Note\n'
1962 'that you must enter the location as a valid URL.\n'
1963 'Depending on the URL the client will need online\n'
1964 'access when checking for updates.'
1965 ),
1966 option = 'horstspace.update.url',
1967 bias = 'workplace',
1968 default_value = 'http://www.gnumed.de/downloads/gnumed-versions.txt',
1969 validator = is_valid
1970 )
1971
1989
2006
2023
2034
2035 gmCfgWidgets.configure_string_option (
2036 message = _(
2037 'GNUmed can show the document review dialog after\n'
2038 'calling the appropriate viewer for that document.\n'
2039 '\n'
2040 'Select the conditions under which you want\n'
2041 'GNUmed to do so:\n'
2042 '\n'
2043 ' 0: never display the review dialog\n'
2044 ' 1: always display the dialog\n'
2045 ' 2: only if there is no previous review by me\n'
2046 ' 3: only if there is no previous review at all\n'
2047 ' 4: only if there is no review by the responsible reviewer\n'
2048 '\n'
2049 'Note that if a viewer is configured to not block\n'
2050 'GNUmed during document display the review dialog\n'
2051 'will actually appear in parallel to the viewer.'
2052 ),
2053 option = 'horstspace.document_viewer.review_after_display',
2054 bias = 'user',
2055 default_value = 3,
2056 validator = is_valid
2057 )
2058
2060
2061
2062 master_data_lists = [
2063 'adr',
2064 'provinces',
2065 'codes',
2066 'billables',
2067 'ref_data_sources',
2068 'meds_substances',
2069 'meds_doses',
2070 'meds_components',
2071 'meds_drugs',
2072 'meds_vaccines',
2073 'orgs',
2074 'labs',
2075 'meta_test_types',
2076 'test_types',
2077 'test_panels',
2078 'form_templates',
2079 'doc_types',
2080 'enc_types',
2081 'communication_channel_types',
2082 'text_expansions',
2083 'patient_tags',
2084 'hints',
2085 'db_translations',
2086 'workplaces'
2087 ]
2088
2089 master_data_list_names = {
2090 'adr': _('Addresses (likely slow)'),
2091 'hints': _('Dynamic automatic hints'),
2092 'codes': _('Codes and their respective terms'),
2093 'communication_channel_types': _('Communication channel types'),
2094 'orgs': _('Organizations with their units, addresses, and comm channels'),
2095 'labs': _('Measurements: diagnostic organizations (path labs, ...)'),
2096 'test_types': _('Measurements: test types'),
2097 'test_panels': _('Measurements: test panels/profiles/batteries'),
2098 'form_templates': _('Document templates (forms, letters, plots, ...)'),
2099 'doc_types': _('Document types'),
2100 'enc_types': _('Encounter types'),
2101 'text_expansions': _('Keyword based text expansion macros'),
2102 'meta_test_types': _('Measurements: aggregate test types'),
2103 'patient_tags': _('Patient tags'),
2104 'provinces': _('Provinces (counties, territories, states, regions, ...)'),
2105 'db_translations': _('String translations in the database'),
2106 'meds_vaccines': _('Medications: vaccines'),
2107 'meds_substances': _('Medications: base substances'),
2108 'meds_doses': _('Medications: substance dosage'),
2109 'meds_components': _('Medications: drug components'),
2110 'meds_drugs': _('Medications: drug products and generic drugs'),
2111 'workplaces': _('Workplace profiles (which plugins to load)'),
2112 'billables': _('Billable items'),
2113 'ref_data_sources': _('Reference data sources')
2114 }
2115
2116 map_list2handler = {
2117 'form_templates': gmFormWidgets.manage_form_templates,
2118 'doc_types': gmDocumentWidgets.manage_document_types,
2119 'text_expansions': gmKeywordExpansionWidgets.configure_keyword_text_expansion,
2120 'db_translations': gmI18nWidgets.manage_translations,
2121 'codes': gmCodingWidgets.browse_coded_terms,
2122 'enc_types': gmEncounterWidgets.manage_encounter_types,
2123 'provinces': gmAddressWidgets.manage_regions,
2124 'workplaces': gmPraxisWidgets.configure_workplace_plugins,
2125 'labs': gmMeasurementWidgets.manage_measurement_orgs,
2126 'test_types': gmMeasurementWidgets.manage_measurement_types,
2127 'meta_test_types': gmMeasurementWidgets.manage_meta_test_types,
2128 'orgs': gmOrganizationWidgets.manage_orgs,
2129 'adr': gmAddressWidgets.manage_addresses,
2130 'meds_substances': gmSubstanceMgmtWidgets.manage_substances,
2131 'meds_doses': gmSubstanceMgmtWidgets.manage_substance_doses,
2132 'meds_components': gmSubstanceMgmtWidgets.manage_drug_components,
2133 'meds_drugs': gmSubstanceMgmtWidgets.manage_drug_products,
2134 'meds_vaccines': gmVaccWidgets.manage_vaccines,
2135 'patient_tags': gmDemographicsWidgets.manage_tag_images,
2136 'communication_channel_types': gmContactWidgets.manage_comm_channel_types,
2137 'billables': gmBillingWidgets.manage_billables,
2138 'ref_data_sources': gmCodingWidgets.browse_data_sources,
2139 'hints': gmAutoHintWidgets.manage_dynamic_hints,
2140 'test_panels': gmMeasurementWidgets.manage_test_panels
2141 }
2142
2143
2144 def edit(item):
2145 try: map_list2handler[item](parent = self)
2146 except KeyError: pass
2147 return False
2148
2149
2150 gmListWidgets.get_choices_from_list (
2151 parent = self,
2152 caption = _('Master data management'),
2153 choices = [ master_data_list_names[lst] for lst in master_data_lists],
2154 data = master_data_lists,
2155 columns = [_('Select the list you want to manage:')],
2156 edit_callback = edit,
2157 single_selection = True,
2158 ignore_OK_button = True
2159 )
2160
2163
2164
2166
2167 found, cmd = gmShellAPI.detect_external_binary(binary = 'ginkgocadx')
2168 if found:
2169 gmShellAPI.run_command_in_shell(cmd, blocking=False)
2170 return
2171
2172 if os.access('/Applications/OsiriX.app/Contents/MacOS/OsiriX', os.X_OK):
2173 gmShellAPI.run_command_in_shell('/Applications/OsiriX.app/Contents/MacOS/OsiriX', blocking = False)
2174 return
2175
2176 for viewer in ['aeskulap', 'amide', 'dicomscope', 'xmedcon']:
2177 found, cmd = gmShellAPI.detect_external_binary(binary = viewer)
2178 if found:
2179 gmShellAPI.run_command_in_shell(cmd, blocking = False)
2180 return
2181
2182 gmDispatcher.send(signal = 'statustext', msg = _('No DICOM viewer found.'), beep = True)
2183
2185
2186 curr_pat = gmPerson.gmCurrentPatient()
2187
2188 arriba = gmArriba.cArriba()
2189 pat = gmTools.bool2subst(curr_pat.connected, curr_pat, None)
2190 if not arriba.run(patient = pat, debug = _cfg.get(option = 'debug')):
2191 return
2192
2193
2194 if curr_pat is None:
2195 return
2196
2197 if arriba.pdf_result is None:
2198 return
2199
2200 doc = gmDocumentWidgets.save_file_as_new_document (
2201 parent = self,
2202 filename = arriba.pdf_result,
2203 document_type = _('risk assessment'),
2204 pk_org_unit = gmPraxis.gmCurrentPraxisBranch()['pk_org_unit']
2205 )
2206
2207 try: os.remove(arriba.pdf_result)
2208 except Exception: _log.exception('cannot remove [%s]', arriba.pdf_result)
2209
2210 if doc is None:
2211 return
2212
2213 doc['comment'] = 'arriba: %s' % _('cardiovascular risk assessment')
2214 doc.save()
2215
2216 try:
2217 open(arriba.xml_result).close()
2218 part = doc.add_part(file = arriba.xml_result)
2219 except Exception:
2220 _log.exception('error accessing [%s]', arriba.xml_result)
2221 gmDispatcher.send(signal = 'statustext', msg = _('[arriba] XML result not found in [%s]') % arriba.xml_result, beep = False)
2222
2223 if part is None:
2224 return
2225
2226 part['obj_comment'] = 'XML-Daten'
2227 part['filename'] = 'arriba-result.xml'
2228 part.save()
2229
2231
2232 dbcfg = gmCfg.cCfgSQL()
2233 cmd = dbcfg.get2 (
2234 option = 'external.tools.acs_risk_calculator_cmd',
2235 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2236 bias = 'user'
2237 )
2238
2239 if cmd is None:
2240 gmDispatcher.send(signal = 'statustext', msg = _('ACS risk assessment calculator not configured.'), beep = True)
2241 return
2242
2243 cwd = os.path.expanduser(os.path.join('~', '.gnumed'))
2244 try:
2245 subprocess.check_call (
2246 args = (cmd,),
2247 close_fds = True,
2248 cwd = cwd
2249 )
2250 except (OSError, ValueError, subprocess.CalledProcessError):
2251 _log.exception('there was a problem executing [%s]', cmd)
2252 gmDispatcher.send(signal = 'statustext', msg = _('Cannot run [%s] !') % cmd, beep = True)
2253 return
2254
2255 pdfs = glob.glob(os.path.join(cwd, 'arriba-%s-*.pdf' % gmDateTime.pydt_now_here().strftime('%Y-%m-%d')))
2256 for pdf in pdfs:
2257 try:
2258 open(pdf).close()
2259 except Exception:
2260 _log.exception('error accessing [%s]', pdf)
2261 gmDispatcher.send(signal = 'statustext', msg = _('There was a problem accessing the [arriba] result in [%s] !') % pdf, beep = True)
2262 continue
2263
2264 doc = gmDocumentWidgets.save_file_as_new_document (
2265 parent = self,
2266 filename = pdf,
2267 document_type = 'risk assessment',
2268 pk_org_unit = gmPraxis.gmCurrentPraxisBranch()['pk_org_unit']
2269 )
2270
2271 try:
2272 os.remove(pdf)
2273 except Exception:
2274 _log.exception('cannot remove [%s]', pdf)
2275
2276 if doc is None:
2277 continue
2278 doc['comment'] = 'arriba: %s' % _('cardiovascular risk assessment')
2279 doc.save()
2280
2281 return
2282
2283
2291
2294
2297
2300
2317
2318
2321
2322
2328
2329
2332
2333
2334
2335
2338
2339
2342
2343
2346
2347
2348
2349
2358
2359
2361 raise ValueError('raised ValueError to test exception handling')
2362
2363
2365 import faulthandler
2366 _log.debug('testing faulthandler via SIGSEGV')
2367 faulthandler._sigsegv()
2368
2369
2373
2374
2376 raise gmExceptions.AccessDenied (
2377 _('[-9999]: <access violation test error>'),
2378 source = 'GNUmed code',
2379 code = -9999,
2380 details = _('This is a deliberate AccessDenied exception thrown to test the handling of access violations by means of a decorator.')
2381 )
2382
2383 @gmAccessPermissionWidgets.verify_minimum_required_role('admin', activity = _('testing access check for non-existant <admin> role'))
2385 raise gmExceptions.AccessDenied (
2386 _('[-9999]: <access violation test error>'),
2387 source = 'GNUmed code',
2388 code = -9999,
2389 details = _('This is a deliberate AccessDenied exception. You should not see this message because the role is checked in a decorator.')
2390 )
2391
2393 import wx.lib.inspection
2394 wx.lib.inspection.InspectionTool().Show()
2395
2398
2401
2404
2407
2414
2418
2421
2425
2426
2428 self.StatusBar.show_history()
2429
2430
2433
2434
2437
2438
2445
2449
2451 name = os.path.basename(gmLog2._logfile_name)
2452 name, ext = os.path.splitext(name)
2453 new_name = '%s_%s%s' % (name, pyDT.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), ext)
2454 new_path = os.path.expanduser(os.path.join('~', 'gnumed'))
2455
2456 dlg = wx.FileDialog (
2457 parent = self,
2458 message = _("Save current log as..."),
2459 defaultDir = new_path,
2460 defaultFile = new_name,
2461 wildcard = "%s (*.log)|*.log" % _("log files"),
2462 style = wx.FD_SAVE
2463 )
2464 choice = dlg.ShowModal()
2465 new_name = dlg.GetPath()
2466 dlg.DestroyLater()
2467 if choice != wx.ID_OK:
2468 return True
2469
2470 _log.warning('syncing log file for backup to [%s]', new_name)
2471 gmLog2.flush()
2472 shutil.copy2(gmLog2._logfile_name, new_name)
2473 gmDispatcher.send('statustext', msg = _('Log file backed up as [%s].') % new_name)
2474
2477
2478
2481
2482
2485
2486
2489
2490
2491
2492
2494 """This is the wx.EVT_CLOSE handler.
2495
2496 - framework still functional
2497 """
2498 _log.debug('gmTopLevelFrame.OnClose() start')
2499 self._clean_exit()
2500 self.DestroyLater()
2501 _log.debug('gmTopLevelFrame.OnClose() end')
2502 return True
2503
2504
2509
2510
2518
2525
2532
2539
2549
2557
2565
2573
2581
2589
2590
2591 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2600
2601
2602 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2611
2612
2613 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2622
2623
2624 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage family history'))
2633
2634 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2641
2642 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('calculate EDC'))
2646
2647
2648 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage suppressed hints'))
2655
2656
2663
2664
2681
2684
2687
2688
2690 pat = gmPerson.gmCurrentPatient()
2691 if not pat.connected:
2692 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR. No active patient.'))
2693 return False
2694 from Gnumed.exporters import gmPatientExporter
2695 exporter = gmPatientExporter.cEmrExport(patient = pat)
2696 fname = gmTools.get_unique_filename(prefix = 'gm-exp-', suffix = '.txt')
2697 output_file = io.open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
2698 exporter.set_output_file(output_file)
2699 exporter.dump_constraints()
2700 exporter.dump_demographic_record(True)
2701 exporter.dump_clinical_record()
2702 exporter.dump_med_docs()
2703 output_file.close()
2704 pat.export_area.add_file(filename = fname, hint = _('EMR as text document'))
2705
2706
2724
2725
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2859
2860
2885
2886
2893
2894
2896 curr_pat = gmPerson.gmCurrentPatient()
2897 if not curr_pat.connected:
2898 gmDispatcher.send(signal = 'statustext', msg = _('Cannot add tag to person. No active patient.'))
2899 return
2900
2901 tag = gmDemographicsWidgets.manage_tag_images(parent = self)
2902 if tag is None:
2903 return
2904
2905 tag = curr_pat.add_tag(tag['pk_tag_image'])
2906 msg = _('Edit the comment on tag [%s]') % tag['l10n_description']
2907 comment = wx.GetTextFromUser (
2908 message = msg,
2909 caption = _('Editing tag comment'),
2910 default_value = gmTools.coalesce(tag['comment'], ''),
2911 parent = self
2912 )
2913
2914 if comment == '':
2915 return
2916
2917 if comment.strip() == tag['comment']:
2918 return
2919
2920 if comment == ' ':
2921 tag['comment'] = None
2922 else:
2923 tag['comment'] = comment.strip()
2924
2925 tag.save()
2926
2927
2937
2938
2948
2949
2958
2959
2968
2969
2971 curr_pat = gmPerson.gmCurrentPatient()
2972 if not curr_pat.connected:
2973 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export patient as GDT. No active patient.'))
2974 return False
2975 enc = 'cp850'
2976 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'current-patient.gdt'))
2977 curr_pat.export_as_gdt(filename = fname, encoding = enc)
2978 gmDispatcher.send(signal = 'statustext', msg = _('Exported demographics to GDT file [%s].') % fname)
2979
2980
2982 curr_pat = gmPerson.gmCurrentPatient()
2983 if not curr_pat.connected:
2984 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export patient as VCARD. No active patient.'))
2985 return False
2986 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'current-patient.vcf'))
2987 curr_pat.export_as_vcard(filename = fname)
2988 gmDispatcher.send(signal = 'statustext', msg = _('Exported demographics to VCARD file [%s].') % fname)
2989
2990
2993
2994
2997
2998
3001
3002
3005
3008
3016
3024
3027
3034
3038
3041
3044
3045
3048
3049
3052
3053
3056
3057
3059 """Cleanup helper.
3060
3061 - should ALWAYS be called when this program is
3062 to be terminated
3063 - ANY code that should be executed before a
3064 regular shutdown should go in here
3065 - framework still functional
3066 """
3067 _log.debug('gmTopLevelFrame._clean_exit() start')
3068
3069
3070 listener = gmBackendListener.gmBackendListener()
3071 try:
3072 listener.shutdown()
3073 except Exception:
3074 _log.exception('cannot stop backend notifications listener thread')
3075
3076
3077 if _scripting_listener is not None:
3078 try:
3079 _scripting_listener.shutdown()
3080 except Exception:
3081 _log.exception('cannot stop scripting listener thread')
3082
3083
3084 self.StatusBar.clock_update_timer.Stop()
3085 gmTimer.shutdown()
3086 gmPhraseWheel.shutdown()
3087
3088
3089 for call_back in self.__pre_exit_callbacks:
3090 try:
3091 call_back()
3092 except Exception:
3093 print('*** pre-exit callback failed ***')
3094 print('%s' % call_back)
3095 _log.exception('callback [%s] failed', call_back)
3096
3097
3098 gmDispatcher.send('application_closing')
3099
3100
3101 gmDispatcher.disconnect(self._on_set_statustext, 'statustext')
3102
3103
3104
3105 curr_width, curr_height = self.GetSize()
3106 _log.info('GUI size at shutdown: [%s:%s]' % (curr_width, curr_height))
3107 curr_pos_x, curr_pos_y = self.GetScreenPosition()
3108 _log.info('GUI position at shutdown: [%s:%s]' % (curr_pos_x, curr_pos_y))
3109 if 0 not in [curr_width, curr_height]:
3110 dbcfg = gmCfg.cCfgSQL()
3111 try:
3112 dbcfg.set (
3113 option = 'main.window.width',
3114 value = curr_width,
3115 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3116 )
3117 dbcfg.set (
3118 option = 'main.window.height',
3119 value = curr_height,
3120 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3121 )
3122 dbcfg.set (
3123 option = 'main.window.position.x',
3124 value = curr_pos_x,
3125 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3126 )
3127 dbcfg.set (
3128 option = 'main.window.position.y',
3129 value = curr_pos_y,
3130 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3131 )
3132 except Exception:
3133 _log.exception('cannot save current client window size and/or position')
3134
3135 if _cfg.get(option = 'debug'):
3136 print('---=== GNUmed shutdown ===---')
3137 try:
3138 print(_('You have to manually close this window to finalize shutting down GNUmed.'))
3139 print(_('This is so that you can inspect the console output at your leisure.'))
3140 except UnicodeEncodeError:
3141 print('You have to manually close this window to finalize shutting down GNUmed.')
3142 print('This is so that you can inspect the console output at your leisure.')
3143 print('---=== GNUmed shutdown ===---')
3144
3145
3146 gmExceptionHandlingWidgets.uninstall_wx_exception_handler()
3147
3148
3149 import threading
3150 _log.debug("%s active threads", threading.activeCount())
3151 for t in threading.enumerate():
3152 _log.debug('thread %s', t)
3153 if t.name == 'MainThread':
3154 continue
3155 print('GNUmed: waiting for thread [%s] to finish' % t.name)
3156
3157 _log.debug('gmTopLevelFrame._clean_exit() end')
3158
3159
3160
3161
3163 if _cfg.get(option = 'slave'):
3164 self.__title_template = '%s%s: %%(pat)s [%%(prov)s@%%(wp)s in %%(site)s of %%(prax)s] (%s:%s)' % (
3165 gmTools._GM_TITLE_PREFIX,
3166 gmTools.u_chain,
3167 _cfg.get(option = 'slave personality'),
3168 _cfg.get(option = 'xml-rpc port')
3169 )
3170 else:
3171 self.__title_template = '%s: %%(pat)s [%%(prov)s@%%(wp)s in %%(site)s of %%(prax)s]' % gmTools._GM_TITLE_PREFIX
3172
3173
3175 """Update title of main window based on template.
3176
3177 This gives nice tooltips on iconified GNUmed instances.
3178
3179 User research indicates that in the title bar people want
3180 the date of birth, not the age, so please stick to this
3181 convention.
3182 """
3183 args = {}
3184
3185 pat = gmPerson.gmCurrentPatient()
3186 if pat.connected:
3187 args['pat'] = '%s %s %s (%s) #%d' % (
3188 gmTools.coalesce(pat['title'], '', '%.4s'),
3189 pat['firstnames'],
3190 pat['lastnames'],
3191 pat.get_formatted_dob(format = '%Y %b %d'),
3192 pat['pk_identity']
3193 )
3194 else:
3195 args['pat'] = _('no patient')
3196
3197 args['prov'] = '%s%s.%s' % (
3198 gmTools.coalesce(_provider['title'], '', '%s '),
3199 _provider['firstnames'][:1],
3200 _provider['lastnames']
3201 )
3202
3203 praxis = gmPraxis.gmCurrentPraxisBranch()
3204 args['wp'] = praxis.active_workplace
3205 args['site'] = praxis['branch']
3206 args['prax'] = praxis['praxis']
3207
3208 self.SetTitle(self.__title_template % args)
3209
3210
3218
3219
3224
3225
3227 """Lock GNUmed client against unauthorized access"""
3228
3229
3230
3231 return
3232
3233
3235 """Unlock the main notebook widgets
3236 As long as we are not logged into the database backend,
3237 all pages but the 'login' page of the main notebook widget
3238 are locked; i.e. not accessible by the user
3239 """
3240
3241
3242
3243
3244
3245 return
3246
3248 wx.LayoutAlgorithm().LayoutWindow(self.LayoutMgr, self.nb)
3249
3252
3254 try:
3255 kwargs['style'] = kwargs['style'] | wx.STB_SIZEGRIP | wx.STB_SHOW_TIPS
3256 except KeyError:
3257 kwargs['style'] = wx.STB_SIZEGRIP | wx.STB_SHOW_TIPS
3258 super().__init__(*args, **kwargs)
3259
3260 self.FieldsCount = 2
3261 self.SetStatusWidths([-1, 225])
3262
3263 self.__msg_fifo = []
3264 self.__normal_background_colour = self.GetBackgroundColour()
3265 self.__blink_background_color = 'yellow'
3266 self.__times_to_blink = 0
3267 self.__blink_counter = 0
3268
3269 self.clock_update_timer = wx.PyTimer(self._cb_update_clock)
3270 self.clock_update_timer.Start(milliseconds = 1000)
3271
3272 self.Bind(wx.EVT_LEFT_DCLICK, self._on_show_history)
3273
3274
3275 - def SetStatusText(self, text, i=0, beep=False):
3276 prev = self.previous_text
3277 msg = self.__update_history(text, i)
3278 super().SetStatusText(msg, i)
3279 if beep:
3280 wx.Bell()
3281 self.__initiate_blinking(text, i, prev)
3282
3283
3284 - def PushStatusText(self, text, field=0):
3285 prev = self.previous_text
3286 msg = self.__update_history(text, field)
3287 super().PushStatusText(msg, field)
3288 self.__initiate_blinking(text, i, prev)
3289
3290
3293
3294
3295 - def show_history(self):
3296 lines = []
3297 for entry in self.__msg_fifo:
3298 lines.append('%s (%s)' % (entry['text'], ','.join(entry['timestamps'])))
3299 gmGuiHelpers.gm_show_info (
3300 title = _('Statusbar history'),
3301 info = _(
3302 '%s - now\n'
3303 '\n'
3304 '%s'
3305 ) % (
3306 gmDateTime.pydt_now_here().strftime('%H:%M'),
3307 '\n'.join(lines)
3308 )
3309 )
3310
3311
3312
3313
3315 """Advances date and local time in the second slot.
3316
3317 Also drives blinking activity.
3318 """
3319 t = time.localtime(time.time())
3320 st = time.strftime('%Y %b %d %H:%M:%S', t)
3321 self.SetStatusText(st, 1)
3322 if self.__times_to_blink > 0:
3323 self.__perhaps_blink()
3324
3325
3327 if self.__blink_counter > self.__times_to_blink:
3328 self.set_normal_color()
3329 self.__times_to_blink = 0
3330 self.__blink_counter = 0
3331 return
3332
3333 if self.SetBackgroundColour(self.__blink_background_color):
3334 self.__blink_counter += 1
3335 return
3336
3337 self.set_normal_color()
3338
3339
3341 if field != 0:
3342 return
3343 text = text.strip()
3344 if text == '':
3345 return
3346 if text == previous_text:
3347 return
3348 self.__blink_counter = 0
3349 self.__times_to_blink = 2
3350
3351
3353 if len(self.__msg_fifo) == 0:
3354 return None
3355 return self.__msg_fifo[0]['text']
3356
3357 previous_text = property(_get_previous_text)
3358
3359
3360 - def __update_history(self, text, field):
3361 if field > 0:
3362 return text
3363
3364 text = text.strip()
3365 if text == '':
3366 return text
3367
3368 now = gmDateTime.pydt_now_here().strftime('%H:%M')
3369 if len(self.__msg_fifo) == 0:
3370 self.__msg_fifo.append({'text': text, 'timestamps': [now]})
3371 return '%s %s' % (now, text)
3372
3373 last = self.__msg_fifo[0]
3374 if text == last['text']:
3375 last['timestamps'].insert(0, now)
3376 return '%s %s (#%s)' % (now, text, len(last['timestamps']))
3377
3378 self.__msg_fifo.insert(0, {'text': text, 'timestamps': [now]})
3379 if len(self.__msg_fifo) > 20:
3380 self.__msg_fifo = self.__msg_fifo[:20]
3381 return '%s %s' % (now, text)
3382
3383
3384 - def _on_show_history(self, evt):
3386
3387
3389 print('----------------------------------')
3390 print('Statusbar history @ [%s]:' % gmDateTime.pydt_now_here().strftime('%H:%M'))
3391 print('\n'.join(self.__msg_fifo))
3392 print('----------------------------------')
3393
3394
3395 - def _on_print_history(self, evt):
3396 evt.Skip()
3397 self.__print_msg_fifo()
3398
3399
3400 -class gmApp(wx.App):
3401
3403
3404 if _cfg.get(option = 'debug'):
3405 self.SetAssertMode(wx.APP_ASSERT_EXCEPTION | wx.APP_ASSERT_LOG)
3406 else:
3407 self.SetAssertMode(wx.APP_ASSERT_SUPPRESS)
3408
3409 self.__starting_up = True
3410
3411
3412 wx.ToolTip.SetAutoPop(4000)
3413
3414 gmExceptionHandlingWidgets.install_wx_exception_handler()
3415 gmExceptionHandlingWidgets.set_client_version(_cfg.get(option = 'client_version'))
3416
3417 self.SetAppName('gnumed')
3418 self.SetVendorName('gnumed_community')
3419 try:
3420 self.SetAppDisplayName('GNUmed %s' % _cfg.get(option = 'client_version'))
3421 except AttributeError:
3422 _log.info('SetAppDisplayName() not supported')
3423 try:
3424 self.SetVendorDisplayName('The GNUmed Development Community.')
3425 except AttributeError:
3426 _log.info('SetVendorDisplayName() not supported')
3427 paths = gmTools.gmPaths(app_name = 'gnumed', wx = wx)
3428 paths.init_paths(wx = wx, app_name = 'gnumed')
3429
3430
3431 dw, dh = wx.DisplaySize()
3432 _log.info('display size: %s:%s' % (wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X), wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)))
3433 _log.debug('display size: %s:%s %s mm', dw, dh, wx.DisplaySizeMM())
3434 for disp_idx in range(wx.Display.GetCount()):
3435 disp = wx.Display(disp_idx)
3436 disp_mode = disp.CurrentMode
3437 _log.debug('display [%s] "%s": primary=%s, client_area=%s, geom=%s, vid_mode=[%sbpp across %sx%spx @%sHz]',
3438 disp_idx, disp.Name, disp.IsPrimary(), disp.ClientArea, disp.Geometry,
3439 disp_mode.bpp, disp_mode.Width, disp_mode.Height, disp_mode.refresh
3440 )
3441
3442 if not self.__setup_prefs_file():
3443 return False
3444
3445 gmExceptionHandlingWidgets.set_sender_email(gmPraxis.gmCurrentPraxisBranch().user_email)
3446
3447 self.__guibroker = gmGuiBroker.GuiBroker()
3448 self.__setup_platform()
3449
3450 if not self.__establish_backend_connection():
3451 return False
3452 if not self.__verify_db_account():
3453 return False
3454 if not self.__verify_praxis_branch():
3455 return False
3456
3457 self.__check_db_lang()
3458 self.__update_workplace_list()
3459
3460 if not _cfg.get(option = 'skip-update-check'):
3461 self.__check_for_updates()
3462
3463 if _cfg.get(option = 'slave'):
3464 if not self.__setup_scripting_listener():
3465 return False
3466
3467 frame = gmTopLevelFrame(None, id = -1, title = _('GNUmed client'), size = (640, 440))
3468 frame.CentreOnScreen(wx.BOTH)
3469 self.SetTopWindow(frame)
3470 frame.Show(True)
3471
3472 if _cfg.get(option = 'debug'):
3473 self.RedirectStdio()
3474 self.SetOutputWindowAttributes(title = _('GNUmed stdout/stderr window'))
3475
3476
3477 print('---=== GNUmed startup ===---')
3478 print(_('redirecting STDOUT/STDERR to this log window'))
3479 print('---=== GNUmed startup ===---')
3480
3481 self.__setup_user_activity_timer()
3482 self.__register_events()
3483
3484 wx.CallAfter(self._do_after_init)
3485
3486 return True
3487
3489 """Called internally by wxPython after EVT_CLOSE has been handled on last frame.
3490
3491 - after destroying all application windows and controls
3492 - before wx.Windows internal cleanup
3493 """
3494 _log.debug('gmApp.OnExit() start')
3495
3496 self.__shutdown_user_activity_timer()
3497
3498 if _cfg.get(option = 'debug'):
3499 self.RestoreStdio()
3500 sys.stdin = sys.__stdin__
3501 sys.stdout = sys.__stdout__
3502 sys.stderr = sys.__stderr__
3503
3504 top_wins = wx.GetTopLevelWindows()
3505 if len(top_wins) > 0:
3506 _log.debug('%s top level windows still around in <app>.OnExit()', len(top_wins))
3507 _log.debug(top_wins)
3508 for win in top_wins:
3509 _log.debug('destroying: %s', win)
3510 win.DestroyLater()
3511
3512 _log.debug('gmApp.OnExit() end')
3513 return 0
3514
3515
3517 wx.Bell()
3518 wx.Bell()
3519 wx.Bell()
3520 _log.warning('unhandled event detected: QUERY_END_SESSION')
3521 _log.info('we should be saving ourselves from here')
3522 gmLog2.flush()
3523 print('unhandled event detected: QUERY_END_SESSION')
3524
3526 wx.Bell()
3527 wx.Bell()
3528 wx.Bell()
3529 _log.warning('unhandled event detected: END_SESSION')
3530 gmLog2.flush()
3531 print('unhandled event detected: END_SESSION')
3532
3543
3545 self.user_activity_detected = True
3546 evt.Skip()
3547
3549
3550 if self.user_activity_detected:
3551 self.elapsed_inactivity_slices = 0
3552 self.user_activity_detected = False
3553 self.elapsed_inactivity_slices += 1
3554 else:
3555 if self.elapsed_inactivity_slices >= self.max_user_inactivity_slices:
3556
3557 pass
3558
3559 self.user_activity_timer.Start(oneShot = True)
3560
3561
3562
3564 self.__starting_up = False
3565
3566 self.__guibroker['horstspace.top_panel']._TCTRL_patient_selector.SetFocus()
3567 gmHooks.run_hook_script(hook = 'startup-after-GUI-init')
3568
3569
3571 self.user_activity_detected = True
3572 self.elapsed_inactivity_slices = 0
3573
3574 self.max_user_inactivity_slices = 15
3575 self.user_activity_timer = gmTimer.cTimer (
3576 callback = self._on_user_activity_timer_expired,
3577 delay = 2000
3578 )
3579 self.user_activity_timer.Start(oneShot=True)
3580
3581
3583 try:
3584 self.user_activity_timer.Stop()
3585 del self.user_activity_timer
3586 except Exception:
3587 pass
3588
3589
3591 self.Bind(wx.EVT_QUERY_END_SESSION, self._on_query_end_session)
3592 self.Bind(wx.EVT_END_SESSION, self._on_end_session)
3593
3594
3595
3596
3597
3598 self.Bind(wx.EVT_ACTIVATE_APP, self._on_app_activated)
3599
3600 self.Bind(wx.EVT_MOUSE_EVENTS, self._on_user_activity)
3601 self.Bind(wx.EVT_KEY_DOWN, self._on_user_activity)
3602
3603
3617
3618
3632
3633
3662
3663
3665
3666 if not gmPraxisWidgets.set_active_praxis_branch(no_parent = True):
3667 return False
3668
3669 login = gmPG2.get_default_login()
3670 msg = '\n'
3671 msg += _('Database <%s> on <%s>') % (
3672 login.database,
3673 gmTools.coalesce(login.host, 'localhost')
3674 )
3675 msg += '\n\n'
3676
3677 praxis = gmPraxis.gmCurrentPraxisBranch()
3678 msg += _('Branch "%s" of praxis "%s"\n') % (
3679 praxis['branch'],
3680 praxis['praxis']
3681 )
3682 msg += '\n\n'
3683
3684 banner = praxis.db_logon_banner
3685 if banner.strip() == '':
3686 return True
3687 msg += banner
3688 msg += '\n\n'
3689
3690 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
3691 None,
3692 -1,
3693 caption = _('Verifying database'),
3694 question = gmTools.wrap(msg, 60, initial_indent = ' ', subsequent_indent = ' '),
3695 button_defs = [
3696 {'label': _('Connect'), 'tooltip': _('Yes, connect to this database.'), 'default': True},
3697 {'label': _('Disconnect'), 'tooltip': _('No, do not connect to this database.'), 'default': False}
3698 ]
3699 )
3700 log_on = dlg.ShowModal()
3701 dlg.DestroyLater()
3702 if log_on == wx.ID_YES:
3703 return True
3704 _log.info('user decided to not connect to this database')
3705 return False
3706
3707
3721
3722
3724 """Setup access to a config file for storing preferences."""
3725
3726 paths = gmTools.gmPaths(app_name = 'gnumed', wx = wx)
3727
3728 candidates = []
3729 explicit_file = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
3730 if explicit_file is not None:
3731 candidates.append(explicit_file)
3732
3733 candidates.append(os.path.join(paths.user_config_dir, 'gnumed.conf'))
3734 candidates.append(os.path.join(paths.local_base_dir, 'gnumed.conf'))
3735 candidates.append(os.path.join(paths.working_dir, 'gnumed.conf'))
3736
3737 prefs_file = None
3738 for candidate in candidates:
3739 try:
3740 open(candidate, 'a+').close()
3741 prefs_file = candidate
3742 break
3743 except IOError:
3744 continue
3745
3746 if prefs_file is None:
3747 msg = _(
3748 'Cannot find configuration file in any of:\n'
3749 '\n'
3750 ' %s\n'
3751 'You may need to use the comand line option\n'
3752 '\n'
3753 ' --conf-file=<FILE>'
3754 ) % '\n '.join(candidates)
3755 gmGuiHelpers.gm_show_error(msg, _('Checking configuration files'))
3756 return False
3757
3758 _cfg.set_option(option = 'user_preferences_file', value = prefs_file)
3759 _log.info('user preferences file: %s', prefs_file)
3760
3761 return True
3762
3763
3765
3766 from socket import error as SocketError
3767 from Gnumed.pycommon import gmScriptingListener
3768 from Gnumed.wxpython import gmMacro
3769
3770 slave_personality = gmTools.coalesce (
3771 _cfg.get (
3772 group = 'workplace',
3773 option = 'slave personality',
3774 source_order = [
3775 ('explicit', 'return'),
3776 ('workbase', 'return'),
3777 ('user', 'return'),
3778 ('system', 'return')
3779 ]
3780 ),
3781 'gnumed-client'
3782 )
3783 _cfg.set_option(option = 'slave personality', value = slave_personality)
3784
3785
3786 port = int (
3787 gmTools.coalesce (
3788 _cfg.get (
3789 group = 'workplace',
3790 option = 'xml-rpc port',
3791 source_order = [
3792 ('explicit', 'return'),
3793 ('workbase', 'return'),
3794 ('user', 'return'),
3795 ('system', 'return')
3796 ]
3797 ),
3798 9999
3799 )
3800 )
3801 _cfg.set_option(option = 'xml-rpc port', value = port)
3802
3803 macro_executor = gmMacro.cMacroPrimitives(personality = slave_personality)
3804 global _scripting_listener
3805 try:
3806 _scripting_listener = gmScriptingListener.cScriptingListener(port = port, macro_executor = macro_executor)
3807 except SocketError as e:
3808 _log.exception('cannot start GNUmed XML-RPC server')
3809 gmGuiHelpers.gm_show_error (
3810 aMessage = (
3811 'Cannot start the GNUmed server:\n'
3812 '\n'
3813 ' [%s]'
3814 ) % e,
3815 aTitle = _('GNUmed startup')
3816 )
3817 return False
3818
3819 return True
3820
3821
3842
3843
3845 if gmI18N.system_locale is None or gmI18N.system_locale == '':
3846 _log.warning("system locale is undefined (probably meaning 'C')")
3847 return True
3848
3849 curr_db_lang = gmPG2.get_current_user_language()
3850 _log.debug('current user language in DB: %s', curr_db_lang)
3851 if curr_db_lang is None:
3852 _log.info('no language selected in DB, trying to set')
3853 for lang in [gmI18N.system_locale_level['full'], gmI18N.system_locale_level['country'], gmI18N.system_locale_level['language']]:
3854 if len(lang) == 0:
3855 continue
3856 if gmPG2.set_user_language(language = lang):
3857 _log.debug("Successfully set database language to [%s]." % lang)
3858 return True
3859
3860 _log.error('Cannot set database language to [%s].' % lang)
3861 return True
3862
3863 if curr_db_lang == gmI18N.system_locale_level['full']:
3864 _log.debug('Database locale (%s) up to date.' % curr_db_lang)
3865 return True
3866
3867 if curr_db_lang == gmI18N.system_locale_level['country']:
3868 _log.debug('Database locale (%s) matches system locale (%s) at country level.' % (curr_db_lang, gmI18N.system_locale))
3869 return True
3870
3871 if curr_db_lang == gmI18N.system_locale_level['language']:
3872 _log.debug('Database locale (%s) matches system locale (%s) at language level.' % (curr_db_lang, gmI18N.system_locale))
3873 return True
3874
3875 _log.warning('database locale [%s] does not match system locale [%s]' % (curr_db_lang, gmI18N.system_locale))
3876 sys_lang2ignore = _cfg.get (
3877 group = 'backend',
3878 option = 'ignored mismatching system locale',
3879 source_order = [('explicit', 'return'), ('local', 'return'), ('user', 'return'), ('system', 'return')]
3880 )
3881 if gmI18N.system_locale == sys_lang2ignore:
3882 _log.info('configured to ignore system-to-database locale mismatch')
3883 return True
3884
3885
3886 msg = _(
3887 "The currently selected database language ('%s') does\n"
3888 "not match the current system language ('%s').\n"
3889 "\n"
3890 "Do you want to set the database language to '%s' ?\n"
3891 ) % (curr_db_lang, gmI18N.system_locale, gmI18N.system_locale)
3892 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
3893 None,
3894 -1,
3895 caption = _('Checking database language settings'),
3896 question = msg,
3897 button_defs = [
3898 {'label': _('Set'), 'tooltip': _('Set your database language to [%s].') % gmI18N.system_locale, 'default': True},
3899 {'label': _("Don't set"), 'tooltip': _('Do not set your database language now.'), 'default': False}
3900 ],
3901 show_checkbox = True,
3902 checkbox_msg = _('Remember to ignore language mismatch'),
3903 checkbox_tooltip = _(
3904 'Checking this will make GNUmed remember your decision\n'
3905 'until the system language is changed.\n'
3906 '\n'
3907 'You can also reactivate this inquiry by removing the\n'
3908 'corresponding "ignore" option from the configuration file\n'
3909 '\n'
3910 ' [%s]'
3911 ) % _cfg.get(option = 'user_preferences_file')
3912 )
3913 decision = dlg.ShowModal()
3914 remember2ignore_this_mismatch = dlg._CHBOX_dont_ask_again.GetValue()
3915 dlg.DestroyLater()
3916 if decision == wx.ID_NO:
3917 if not remember2ignore_this_mismatch:
3918 return True
3919 _log.info('User did not want to set database locale. Ignoring mismatch next time.')
3920 gmCfg2.set_option_in_INI_file (
3921 filename = _cfg.get(option = 'user_preferences_file'),
3922 group = 'backend',
3923 option = 'ignored mismatching system locale',
3924 value = gmI18N.system_locale
3925 )
3926 return True
3927
3928 for lang in [gmI18N.system_locale_level['full'], gmI18N.system_locale_level['country'], gmI18N.system_locale_level['language']]:
3929 if len(lang) == 0:
3930 continue
3931 if gmPG2.set_user_language(language = lang):
3932 _log.debug("Successfully set database language to [%s]." % lang)
3933 return True
3934
3935 _log.error('Cannot set database language to [%s].' % lang)
3936
3937 _log.info('forcing database language to [%s]', gmI18N.system_locale_level['country'])
3938 gmPG2.force_user_language(language = gmI18N.system_locale_level['country'])
3939 return True
3940
3943 try:
3944 kwargs['originated_in_database']
3945 print('==> got notification from database "%s":' % kwargs['signal'])
3946 except KeyError:
3947 print('==> received signal from client: "%s"' % kwargs['signal'])
3948
3949 del kwargs['signal']
3950 for key in kwargs:
3951
3952 try: print(' [%s]: %s' % (key, kwargs[key]))
3953 except Exception: print('cannot print signal information')
3954
3959
3971
3976
3979
3980
3981
3982 gmDispatcher.set_main_thread_caller(wx.CallAfter)
3983
3984 if _cfg.get(option = 'debug'):
3985 gmDispatcher.connect(receiver = _signal_debugging_monitor)
3986 _log.debug('gmDispatcher signal monitor activated')
3987
3988 setup_safe_wxEndBusyCursor()
3989
3990 setup_callbacks()
3991
3992
3993
3994
3995 app = gmApp(redirect = False, clearSigInt = False)
3996 app.MainLoop()
3997
3998
3999
4000
4001 if __name__ == '__main__':
4002
4003 from GNUmed.pycommon import gmI18N
4004 gmI18N.activate_locale()
4005 gmI18N.install_domain()
4006
4007 _log.info('Starting up as main module.')
4008 main()
4009
4010
4011