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 template (no VAT)'), _('Select the template for printing an invoice without VAT.'))
429 self.Bind(wx.EVT_MENU, self.__on_cfg_invoice_template_no_vat, item)
430 item = menu_cfg_bill.Append(-1, _('Invoice template (with VAT)'), _('Select the template for printing an invoice with VAT.'))
431 self.Bind(wx.EVT_MENU, self.__on_cfg_invoice_template_with_vat, item)
432 item = menu_cfg_bill.Append(-1, _('Catalogs URL'), _('URL for billing catalogs (schedules of fees).'))
433 self.Bind(wx.EVT_MENU, self.__on_configure_billing_catalogs_url, item)
434
435
436 menu_cfg_emr = wx.Menu()
437 item = menu_cfg_emr.Append(-1, _('Medication list template'), _('Select the template for printing a medication list.'))
438 self.Bind(wx.EVT_MENU, self.__on_cfg_medication_list_template, item)
439 item = menu_cfg_emr.Append(-1, _('Prescription mode'), _('Select the default mode for creating a prescription.'))
440 self.Bind(wx.EVT_MENU, self.__on_cfg_prescription_mode, item)
441 item = menu_cfg_emr.Append(-1, _('Prescription template'), _('Select the template for printing a prescription.'))
442 self.Bind(wx.EVT_MENU, self.__on_cfg_prescription_template, item)
443 item = menu_cfg_emr.Append(-1, _('Default Gnuplot template'), _('Select the default template for plotting test results.'))
444 self.Bind(wx.EVT_MENU, self.__on_cfg_default_gnuplot_template, item)
445 item = menu_cfg_emr.Append(-1, _('Fallback provider'), _('Select the doctor to fall back to for patients without a primary provider.'))
446 self.Bind(wx.EVT_MENU, self.__on_cfg_fallback_primary_provider, item)
447
448
449 menu_cfg_encounter = wx.Menu()
450 item = menu_cfg_encounter.Append(-1, _('Edit before patient change'), _('Edit encounter details before change of patient.'))
451 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_pat_change, item)
452 item = menu_cfg_encounter.Append(-1, _('Minimum duration'), _('Minimum duration of an encounter.'))
453 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_min_ttl, item)
454 item = menu_cfg_encounter.Append(-1, _('Maximum duration'), _('Maximum duration of an encounter.'))
455 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_max_ttl, item)
456 item = menu_cfg_encounter.Append(-1, _('Minimum empty age'), _('Minimum age of an empty encounter before considering for deletion.'))
457 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_empty_ttl, item)
458 item = menu_cfg_encounter.Append(-1, _('Default type'), _('Default type for new encounters.'))
459 self.Bind(wx.EVT_MENU, self.__on_cfg_enc_default_type, item)
460 menu_cfg_emr.Append(wx.NewId(), _('Encounter ...'), menu_cfg_encounter)
461
462
463 menu_cfg_episode = wx.Menu()
464 item = menu_cfg_episode.Append(-1, _('Dormancy'), _('Maximum length of dormancy after which an episode will be considered closed.'))
465 self.Bind(wx.EVT_MENU, self.__on_cfg_epi_ttl, item)
466 menu_cfg_emr.Append(wx.NewId(), _('Episode ...'), menu_cfg_episode)
467
468 menu_config.Append(wx.NewId(), _('User interface ...'), menu_cfg_ui)
469 menu_config.Append(wx.NewId(), _('EMR ...'), menu_cfg_emr)
470 menu_config.Append(wx.NewId(), _('Billing ...'), menu_cfg_bill)
471 menu_gnumed.Append(wx.NewId(), _('Preferences ...'), menu_config)
472
473
474 menu_master_data = wx.Menu()
475 item = menu_master_data.Append(-1, _('Manage lists'), _('Manage various lists of master data.'))
476 self.Bind(wx.EVT_MENU, self.__on_manage_master_data, item)
477 item = menu_master_data.Append(-1, _('Manage praxis'), _('Manage your praxis branches.'))
478 self.Bind(wx.EVT_MENU, self.__on_manage_praxis, item)
479 item = menu_master_data.Append(-1, _('Install data packs'), _('Install reference data from data packs.'))
480 self.Bind(wx.EVT_MENU, self.__on_install_data_packs, item)
481 item = menu_master_data.Append(-1, _('Update ATC'), _('Install ATC reference data.'))
482 self.Bind(wx.EVT_MENU, self.__on_update_atc, item)
483 item = menu_master_data.Append(-1, _('Update LOINC'), _('Download and install LOINC reference data.'))
484 self.Bind(wx.EVT_MENU, self.__on_update_loinc, item)
485 item = menu_master_data.Append(-1, _('Create fake vaccines'), _('Re-create fake generic vaccines.'))
486 self.Bind(wx.EVT_MENU, self.__on_generate_vaccines, item)
487 menu_gnumed.Append(wx.NewId(), _('&Master data ...'), menu_master_data)
488
489
490 menu_users = wx.Menu()
491 item = menu_users.Append(-1, _('&Add user'), _('Add a new GNUmed user'))
492 self.Bind(wx.EVT_MENU, self.__on_add_new_staff, item)
493 item = menu_users.Append(-1, _('&Edit users'), _('Edit the list of GNUmed users'))
494 self.Bind(wx.EVT_MENU, self.__on_edit_staff_list, item)
495 item = menu_users.Append(-1, _('&Change DB owner PWD'), _('Change the password of the GNUmed database owner'))
496 self.Bind(wx.EVT_MENU, self.__on_edit_gmdbowner_password, item)
497 menu_gnumed.Append(wx.NewId(), _('&Users ...'), menu_users)
498
499 menu_gnumed.AppendSeparator()
500
501 item = menu_gnumed.Append(wx.ID_EXIT, _('E&xit\tAlt-X'), _('Close this GNUmed client.'))
502 self.Bind(wx.EVT_MENU, self.__on_exit_gnumed, item)
503
504 self.mainmenu.Append(menu_gnumed, '&GNUmed')
505
506
507 menu_person = wx.Menu()
508
509 item = menu_person.Append(-1, _('Search'), _('Search for a person.'))
510 self.Bind(wx.EVT_MENU, self.__on_search_person, item)
511 acc_tab = wx.AcceleratorTable([(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, item.GetId())])
512 self.SetAcceleratorTable(acc_tab)
513 item = menu_person.Append(-1, _('&Register person'), _("Register a new person with GNUmed"))
514 self.Bind(wx.EVT_MENU, self.__on_create_new_patient, item)
515
516 menu_person_import = wx.Menu()
517 item = menu_person_import.Append(-1, _('From &External sources'), _('Load and possibly create person from available external sources.'))
518 self.Bind(wx.EVT_MENU, self.__on_load_external_patient, item)
519 item = menu_person_import.Append(-1, _('&vCard file \u2192 patient'), _('Import demographics from .vcf vCard file as patient'))
520 self.Bind(wx.EVT_MENU, self.__on_import_vcard_from_file, item)
521 item = menu_person_import.Append(-1, _('Clipboard (&XML) \u2192 patient'), _('Import demographics from clipboard (LinuxMedNews XML) as patient'))
522 self.Bind(wx.EVT_MENU, self.__on_import_xml_linuxmednews, item)
523 item = menu_person_import.Append(-1, _('Clipboard (&vCard) \u2192 patient'), _('Import demographics from clipboard (vCard) as patient'))
524 self.Bind(wx.EVT_MENU, self.__on_import_vcard_from_clipboard, item)
525 menu_person.Append(wx.NewId(), '&Import\u2026', menu_person_import)
526
527 menu_person_export = wx.Menu()
528 menu_person_export_clipboard = wx.Menu()
529 item = menu_person_export_clipboard.Append(-1, '&GDT', _('Export demographics of currently active person as GDT into clipboard.'))
530 self.Bind(wx.EVT_MENU, self.__on_export_gdt2clipboard, item)
531 item = menu_person_export_clipboard.Append(-1, '&XML (LinuxMedNews)', _('Export demographics of currently active person as XML (LinuxMedNews) into clipboard'))
532 self.Bind(wx.EVT_MENU, self.__on_export_linuxmednews_xml2clipboard, item)
533 item = menu_person_export_clipboard.Append(-1, '&vCard', _('Export demographics of currently active person as vCard into clipboard'))
534 self.Bind(wx.EVT_MENU, self.__on_export_vcard2clipboard, item)
535 menu_person_export.Append(wx.NewId(), _('\u2192 &Clipboard as\u2026'), menu_person_export_clipboard)
536
537 menu_person_export_file = wx.Menu()
538 item = menu_person_export_file.Append(-1, '&GDT', _('Export demographics of currently active person into GDT file.'))
539 self.Bind(wx.EVT_MENU, self.__on_export_as_gdt, item)
540 item = menu_person_export_file.Append(-1, '&vCard', _('Export demographics of currently active person into vCard file.'))
541 self.Bind(wx.EVT_MENU, self.__on_export_as_vcard, item)
542 menu_person_export.Append(wx.NewId(), _('\u2192 &File as\u2026'), menu_person_export_file)
543
544 menu_person.Append(wx.NewId(), 'E&xport\u2026', menu_person_export)
545
546 item = menu_person.Append(-1, _('&Merge persons'), _('Merge two persons into one.'))
547 self.Bind(wx.EVT_MENU, self.__on_merge_patients, item)
548 item = menu_person.Append(-1, _('Deactivate record'), _('Deactivate (exclude from search) person record in database.'))
549 self.Bind(wx.EVT_MENU, self.__on_delete_patient, item)
550 menu_person.AppendSeparator()
551 item = menu_person.Append(-1, _('Add &tag'), _('Add a text/image tag to this person.'))
552 self.Bind(wx.EVT_MENU, self.__on_add_tag2person, item)
553 item = menu_person.Append(-1, _('Enlist as user'), _('Enlist current person as GNUmed user'))
554 self.Bind(wx.EVT_MENU, self.__on_enlist_patient_as_staff, item)
555 menu_person.AppendSeparator()
556
557 self.mainmenu.Append(menu_person, '&Person')
558 self.__gb['main.patientmenu'] = menu_person
559
560
561 menu_emr = wx.Menu()
562
563
564 menu_emr_manage = wx.Menu()
565 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'))
566 self.Bind(wx.EVT_MENU, self.__on_add_health_issue, item)
567 item = menu_emr_manage.Append(-1, _('&Episode'), _('Add an episode of illness to the EMR of the active patient'))
568 self.Bind(wx.EVT_MENU, self.__on_add_episode, item)
569 item = menu_emr_manage.Append(-1, _('&Medication'), _('Add medication / substance use entry.'))
570 self.Bind(wx.EVT_MENU, self.__on_add_medication, item)
571 item = menu_emr_manage.Append(-1, _('&Allergies'), _('Manage documentation of allergies for the current patient.'))
572 self.Bind(wx.EVT_MENU, self.__on_manage_allergies, item)
573 item = menu_emr_manage.Append(-1, _('&Occupation'), _('Edit occupation details for the current patient.'))
574 self.Bind(wx.EVT_MENU, self.__on_edit_occupation, item)
575 item = menu_emr_manage.Append(-1, _('&Hospitalizations'), _('Manage hospitalizations.'))
576 self.Bind(wx.EVT_MENU, self.__on_manage_hospital_stays, item)
577 item = menu_emr_manage.Append(-1, _('&External care'), _('Manage external care.'))
578 self.Bind(wx.EVT_MENU, self.__on_manage_external_care, item)
579 item = menu_emr_manage.Append(-1, _('&Procedures'), _('Manage procedures performed on the patient.'))
580 self.Bind(wx.EVT_MENU, self.__on_manage_performed_procedures, item)
581 item = menu_emr_manage.Append(-1, _('&Measurements'), _('Manage measurement results for the current patient.'))
582 self.Bind(wx.EVT_MENU, self.__on_manage_measurements, item)
583 item = menu_emr_manage.Append(-1, _('&Vaccinations: by shot'), _('Manage vaccinations for the current patient (by shots given).'))
584 self.Bind(wx.EVT_MENU, self.__on_manage_vaccination, item)
585 item = menu_emr_manage.Append(-1, _('&Vaccinations: by indication'), _('Manage vaccinations for the current patient (by indication).'))
586 self.Bind(wx.EVT_MENU, self.__on_show_all_vaccinations_by_indication, item)
587 item = menu_emr_manage.Append(-1, _('&Vaccinations: latest'), _('List latest vaccinations for the current patient.'))
588 self.Bind(wx.EVT_MENU, self.__on_show_latest_vaccinations, item)
589 item = menu_emr_manage.Append(-1, _('&Family history (FHx)'), _('Manage family history.'))
590 self.Bind(wx.EVT_MENU, self.__on_manage_fhx, item)
591 item = menu_emr_manage.Append(-1, _('&Encounters'), _('List all encounters including empty ones.'))
592 self.Bind(wx.EVT_MENU, self.__on_list_encounters, item)
593 item = menu_emr_manage.Append(-1, _('&Pregnancy'), _('Calculate EDC.'))
594 self.Bind(wx.EVT_MENU, self.__on_calc_edc, item)
595 item = menu_emr_manage.Append(-1, _('Suppressed hints'), _('Manage dynamic hints suppressed in this patient.'))
596 self.Bind(wx.EVT_MENU, self.__on_manage_suppressed_hints, item)
597 item = menu_emr_manage.Append(-1, _('Substance abuse'), _('Manage substance abuse documentation of this patient.'))
598 self.Bind(wx.EVT_MENU, self.__on_manage_substance_abuse, item)
599 menu_emr.Append(wx.NewId(), _('&Manage ...'), menu_emr_manage)
600
601
602 item = menu_emr.Append(-1, _('Search this EMR'), _('Search for data in the EMR of the active patient'))
603 self.Bind(wx.EVT_MENU, self.__on_search_emr, item)
604
605 item = menu_emr.Append(-1, _('Start new encounter'), _('Start a new encounter for the active patient right now.'))
606 self.Bind(wx.EVT_MENU, self.__on_start_new_encounter, item)
607
608
609
610
611 item = menu_emr.Append(-1, _('Statistics'), _('Show a high-level statistic summary of the EMR.'))
612 self.Bind(wx.EVT_MENU, self.__on_show_emr_summary, item)
613
614
615
616
617 menu_emr.AppendSeparator()
618
619
620 menu_emr_export = wx.Menu()
621 item = menu_emr_export.Append(-1, _('Journal (encounters)'), _("Copy EMR of the active patient as a chronological journal into export area"))
622 self.Bind(wx.EVT_MENU, self.__on_export_emr_as_journal, item)
623 item = menu_emr_export.Append(-1, _('Journal (mod time)'), _("Copy EMR of active patient as journal (by last modification time) into export area"))
624 self.Bind(wx.EVT_MENU, self.__on_export_emr_by_last_mod, item)
625 item = menu_emr_export.Append(-1, _('Text document'), _("Copy EMR of active patient as text document into export area"))
626 self.Bind(wx.EVT_MENU, self.__export_emr_as_textfile, item)
627 item = menu_emr_export.Append(-1, _('Timeline file'), _("Copy EMR of active patient as timeline file (XML) into export area"))
628 self.Bind(wx.EVT_MENU, self.__export_emr_as_timeline_xml, item)
629 item = menu_emr_export.Append(-1, _('Care structure'), _("Copy EMR of active patient as care structure text file into export area"))
630 self.Bind(wx.EVT_MENU, self.__export_emr_as_care_structure, item)
631
632 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."))
633 self.Bind(wx.EVT_MENU, self.__on_export_for_medistar, item)
634 menu_emr.Append(wx.NewId(), _('Put into export area as ...'), menu_emr_export)
635
636 menu_emr.AppendSeparator()
637
638 self.mainmenu.Append(menu_emr, _("&EMR"))
639 self.__gb['main.emrmenu'] = menu_emr
640
641
642 menu_paperwork = wx.Menu()
643 item = menu_paperwork.Append(-1, _('&Write letter'), _('Write a letter for the current patient.'))
644 self.Bind(wx.EVT_MENU, self.__on_new_letter, item)
645 item = menu_paperwork.Append(-1, _('Screenshot -> export area'), _('Put a screenshot into the patient export area.'))
646 self.Bind(wx.EVT_MENU, self.__on_save_screenshot_into_export_area, item)
647 menu_paperwork.AppendSeparator()
648 item = menu_paperwork.Append(-1, _('List Placeholders'), _('Show a list of all placeholders.'))
649 self.Bind(wx.EVT_MENU, self.__on_show_placeholders, item)
650
651
652 self.mainmenu.Append(menu_paperwork, _('&Correspondence'))
653 self.__gb['main.paperworkmenu'] = menu_paperwork
654
655
656 self.menu_tools = wx.Menu()
657 item = self.menu_tools.Append(-1, _('Search all EMRs'), _('Search for data across the EMRs of all patients'))
658 self.Bind(wx.EVT_MENU, self.__on_search_across_emrs, item)
659 viewer = _('no viewer installed')
660 if gmShellAPI.detect_external_binary(binary = 'ginkgocadx')[0]:
661 viewer = 'Ginkgo CADx'
662 elif os.access('/Applications/OsiriX.app/Contents/MacOS/OsiriX', os.X_OK):
663 viewer = 'OsiriX'
664 elif gmShellAPI.detect_external_binary(binary = 'aeskulap')[0]:
665 viewer = 'Aeskulap'
666 elif gmShellAPI.detect_external_binary(binary = 'amide')[0]:
667 viewer = 'AMIDE'
668 elif gmShellAPI.detect_external_binary(binary = 'dicomscope')[0]:
669 viewer = 'DicomScope'
670 elif gmShellAPI.detect_external_binary(binary = 'xmedcon')[0]:
671 viewer = '(x)medcon'
672 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)
673 self.Bind(wx.EVT_MENU, self.__on_dicom_viewer, item)
674 if viewer == _('no viewer installed'):
675 _log.info('neither of Ginkgo CADx / OsiriX / Aeskulap / AMIDE / DicomScope / xmedcon found, disabling "DICOM viewer" menu item')
676 self.menu_tools.Enable(id = item.Id, enable=False)
677
678
679 item = self.menu_tools.Append(-1, _('Snellen chart'), _('Display fullscreen snellen chart.'))
680 self.Bind(wx.EVT_MENU, self.__on_snellen, item)
681 item = self.menu_tools.Append(-1, _('MI/stroke risk'), _('Acute coronary syndrome/stroke risk assessment.'))
682 self.Bind(wx.EVT_MENU, self.__on_acs_risk_assessment, item)
683 item = self.menu_tools.Append(-1, 'arriba', _('arriba: cardiovascular risk assessment (%s).') % 'www.arriba-hausarzt.de')
684 self.Bind(wx.EVT_MENU, self.__on_arriba, item)
685 if not gmShellAPI.detect_external_binary(binary = 'arriba')[0]:
686 _log.info('<arriba> not found, disabling "arriba" menu item')
687 self.menu_tools.Enable(id = item.Id, enable = False)
688
689 menu_lab = wx.Menu()
690 item = menu_lab.Append(-1, _('Show HL7'), _('Show formatted data from HL7 file'))
691 self.Bind(wx.EVT_MENU, self.__on_show_hl7, item)
692 item = menu_lab.Append(-1, _('Unwrap XML'), _('Unwrap HL7 data from XML file (Excelleris, ...)'))
693 self.Bind(wx.EVT_MENU, self.__on_unwrap_hl7_from_xml, item)
694 item = menu_lab.Append(-1, _('Stage HL7'), _('Stage HL7 data from file'))
695 self.Bind(wx.EVT_MENU, self.__on_stage_hl7, item)
696 item = menu_lab.Append(-1, _('Browse pending'), _('Browse pending (staged) incoming data'))
697 self.Bind(wx.EVT_MENU, self.__on_incoming, item)
698
699 self.menu_tools.Append(wx.NewId(), _('Lab results ...'), menu_lab)
700
701 self.menu_tools.AppendSeparator()
702
703 self.mainmenu.Append(self.menu_tools, _("&Tools"))
704 self.__gb['main.toolsmenu'] = self.menu_tools
705
706
707 menu_knowledge = wx.Menu()
708
709
710 menu_drug_dbs = wx.Menu()
711 item = menu_drug_dbs.Append(-1, _('&Database'), _('Jump to the drug database configured as the default.'))
712 self.Bind(wx.EVT_MENU, self.__on_jump_to_drug_db, item)
713
714
715
716 item = menu_drug_dbs.Append(-1, 'kompendium.ch', _('Show "kompendium.ch" drug database (online, Switzerland)'))
717 self.Bind(wx.EVT_MENU, self.__on_kompendium_ch, item)
718 menu_knowledge.Append(wx.NewId(), _('&Drug Resources'), menu_drug_dbs)
719
720
721
722 item = menu_knowledge.Append(-1, _('Medical links (www)'), _('Show a page of links to useful medical content.'))
723 self.Bind(wx.EVT_MENU, self.__on_medical_links, item)
724
725 self.mainmenu.Append(menu_knowledge, _('&Knowledge'))
726 self.__gb['main.knowledgemenu'] = menu_knowledge
727
728
729 self.menu_office = wx.Menu()
730
731 item = self.menu_office.Append(-1, _('&Audit trail'), _('Display database audit trail.'))
732 self.Bind(wx.EVT_MENU, self.__on_display_audit_trail, item)
733
734 self.menu_office.AppendSeparator()
735
736 item = self.menu_office.Append(-1, _('&Bills'), _('List all bills across all patients.'))
737 self.Bind(wx.EVT_MENU, self.__on_show_all_bills, item)
738
739 item = self.menu_office.Append(-1, _('&Organizations'), _('Manage organizations.'))
740 self.Bind(wx.EVT_MENU, self.__on_manage_orgs, item)
741
742 self.mainmenu.Append(self.menu_office, _('&Office'))
743 self.__gb['main.officemenu'] = self.menu_office
744
745
746 help_menu = wx.Menu()
747 help_menu.Append(-1, _('GNUmed wiki'), _('Go to the GNUmed wiki on the web.'))
748 self.Bind(wx.EVT_MENU, self.__on_display_wiki, item)
749 help_menu.Append(-1, _('User manual (www)'), _('Go to the User Manual on the web.'))
750 self.Bind(wx.EVT_MENU, self.__on_display_user_manual_online, item)
751 item = help_menu.Append(-1, _('Menu reference (www)'), _('View the reference for menu items on the web.'))
752 self.Bind(wx.EVT_MENU, self.__on_menu_reference, item)
753 item = help_menu.Append(-1, _('Browse work dir'), _('Browse user working directory [%s].') % os.path.join(gmTools.gmPaths().home_dir, 'gnumed'))
754 self.Bind(wx.EVT_MENU, self.__on_browse_work_dir, item)
755
756 menu_debugging = wx.Menu()
757 item = menu_debugging.Append(-1, _('Status line: &Clear'), _('Clear the status line.'))
758 self.Bind(wx.EVT_MENU, self.__on_clear_status_line, item)
759 item = menu_debugging.Append(-1, _('Status line: History'), _('Show status line history.'))
760 self.Bind(wx.EVT_MENU, self.__on_show_status_line_history, item)
761 item = menu_debugging.Append(-1, _('Tooltips on'), _('Globally enable tooltips.'))
762 self.Bind(wx.EVT_MENU, self.__on_enable_tooltips, item)
763 item = menu_debugging.Append(-1, _('Tooltips off'), _('Globally (attempt to) disable tooltips.'))
764 self.Bind(wx.EVT_MENU, self.__on_disable_tooltips, item)
765 item = menu_debugging.Append(-1, _('Screenshot'), _('Save a screenshot of this GNUmed client.'))
766 self.Bind(wx.EVT_MENU, self.__on_save_screenshot, item)
767 item = menu_debugging.Append(-1, _('Show log file'), _('Show log file in text viewer.'))
768 self.Bind(wx.EVT_MENU, self.__on_show_log_file, item)
769 item = menu_debugging.Append(-1, _('Backup log file'), _('Backup content of the log to another file.'))
770 self.Bind(wx.EVT_MENU, self.__on_backup_log_file, item)
771 item = menu_debugging.Append(-1, _('Email log file'), _('Send log file to the authors for help.'))
772 self.Bind(wx.EVT_MENU, self.__on_email_log_file, item)
773 item = menu_debugging.Append(-1, _('Browse tmp dir'), _('Browse temporary directory [%s].') % gmTools.gmPaths().tmp_dir)
774 self.Bind(wx.EVT_MENU, self.__on_browse_tmp_dir, item)
775 item = menu_debugging.Append(-1, _('Browse internal work dir'), _('Browse internal working directory [%s].') % os.path.join(gmTools.gmPaths().home_dir, '.gnumed'))
776 self.Bind(wx.EVT_MENU, self.__on_browse_internal_work_dir, item)
777 item = menu_debugging.Append(-1, _('Bug tracker'), _('Go to the GNUmed bug tracker on the web.'))
778 self.Bind(wx.EVT_MENU, self.__on_display_bugtracker, item)
779 item = menu_debugging.Append(-1, _('Unlock mouse'), _('Unlock mouse pointer in case it got stuck in hourglass mode.'))
780 self.Bind(wx.EVT_MENU, self.__on_unblock_cursor, item)
781 item = menu_debugging.Append(-1, _('pgAdmin III'), _('pgAdmin III: Browse GNUmed database(s) in PostgreSQL server.'))
782 self.Bind(wx.EVT_MENU, self.__on_pgadmin3, item)
783
784
785 if _cfg.get(option = 'debug'):
786 item = menu_debugging.Append(-1, _('Lock/unlock patient search'), _('Lock/unlock patient search - USE ONLY IF YOU KNOW WHAT YOU ARE DOING !'))
787 self.Bind(wx.EVT_MENU, self.__on_toggle_patient_lock, item)
788 item = menu_debugging.Append(-1, _('Test error handling'), _('Throw an exception to test error handling.'))
789 self.Bind(wx.EVT_MENU, self.__on_test_exception, item)
790 item = menu_debugging.Append(-1, _('Test access violation exception'), _('Simulate an access violation exception.'))
791 self.Bind(wx.EVT_MENU, self.__on_test_access_violation, item)
792 item = menu_debugging.Append(-1, _('Test access checking'), _('Simulate a failing access check.'))
793 self.Bind(wx.EVT_MENU, self.__on_test_access_checking, item)
794 item = menu_debugging.Append(-1, _('Invoke inspector'), _('Invoke the widget hierarchy inspector (needs wxPython 2.8).'))
795 self.Bind(wx.EVT_MENU, self.__on_invoke_inspector, item)
796 try:
797 import wx.lib.inspection
798 except ImportError:
799 menu_debugging.Enable(id = ID, enable = False)
800 try:
801 import faulthandler
802 item = menu_debugging.Append(-1, _('Test fault handler'), _('Simulate a catastrophic fault (SIGSEGV).'))
803 self.Bind(wx.EVT_MENU, self.__on_test_segfault, item)
804 except ImportError:
805 pass
806 item = menu_debugging.Append(-1, _('Test placeholder'), _('Manually test placeholders'))
807 self.Bind(wx.EVT_MENU, self.__on_test_placeholders, item)
808
809 help_menu.Append(wx.NewId(), _('Debugging ...'), menu_debugging)
810 help_menu.AppendSeparator()
811
812 item = help_menu.Append(wx.ID_ABOUT, _('About GNUmed'), '')
813 self.Bind(wx.EVT_MENU, self.OnAbout, item)
814 item = help_menu.Append(-1, _('About database'), _('Show information about the current database.'))
815 self.Bind(wx.EVT_MENU, self.__on_about_database, item)
816 item = help_menu.Append(-1, _('About contributors'), _('Show GNUmed contributors'))
817 self.Bind(wx.EVT_MENU, self.__on_show_contributors, item)
818 help_menu.AppendSeparator()
819
820 self.mainmenu.Append(help_menu, _("&Help"))
821
822 self.__gb['main.helpmenu'] = help_menu
823
824
825 self.SetMenuBar(self.mainmenu)
826
827
830
831
832
834 """register events we want to react to"""
835
836 self.Bind(wx.EVT_CLOSE, self.OnClose)
837 self.Bind(wx.EVT_QUERY_END_SESSION, self._on_query_end_session)
838 self.Bind(wx.EVT_END_SESSION, self._on_end_session)
839
840 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
841 gmDispatcher.connect(signal = 'statustext', receiver = self._on_set_statustext)
842 gmDispatcher.connect(signal = 'request_user_attention', receiver = self._on_request_user_attention)
843 gmDispatcher.connect(signal = 'register_pre_exit_callback', receiver = self._register_pre_exit_callback)
844 gmDispatcher.connect(signal = 'plugin_loaded', receiver = self._on_plugin_loaded)
845
846 gmDispatcher.connect(signal = 'db_maintenance_warning', receiver = self._on_db_maintenance_warning)
847 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
848
849
850
851 gmPerson.gmCurrentPatient().register_before_switching_from_patient_callback(callback = self._before_switching_from_patient_callback)
852
853
855
856 if kwds['table'] == 'dem.praxis_branch':
857 if kwds['operation'] != 'UPDATE':
858 return True
859 branch = gmPraxis.gmCurrentPraxisBranch()
860 if branch['pk_praxis_branch'] != kwds['pk_of_row']:
861 return True
862 self.__update_window_title()
863 return True
864
865 if kwds['table'] == 'dem.names':
866 pat = gmPerson.gmCurrentPatient()
867 if pat.connected:
868 if pat.ID != kwds['pk_identity']:
869 return True
870 self.__update_window_title()
871 return True
872
873 if kwds['table'] == 'dem.identity':
874 if kwds['operation'] != 'UPDATE':
875 return True
876 pat = gmPerson.gmCurrentPatient()
877 if pat.connected:
878 if pat.ID != kwds['pk_identity']:
879 return True
880 self.__update_window_title()
881 return True
882
883 return True
884
885
886 - def _on_plugin_loaded(self, plugin_name=None, class_name=None, menu_name=None, menu_item_name=None, menu_help_string=None):
887
888 _log.debug('registering plugin with menu system')
889 _log.debug(' generic name: %s', plugin_name)
890 _log.debug(' class name: %s', class_name)
891 _log.debug(' specific menu: %s', menu_name)
892 _log.debug(' menu item: %s', menu_item_name)
893
894
895 item = self.menu_plugins.Append(-1, plugin_name, _('Raise plugin [%s].') % plugin_name)
896 self.Bind(wx.EVT_MENU, self.__on_raise_a_plugin, item)
897 self.menu_id2plugin[item.Id] = class_name
898
899
900 if menu_name is not None:
901 menu = self.__gb['main.%smenu' % menu_name]
902 item = menu.Append(-1, menu_item_name, menu_help_string)
903 self.Bind(wx.EVT_MENU, self.__on_raise_a_plugin, item)
904 self.menu_id2plugin[item.Id] = class_name
905
906 return True
907
913
915 wx.Bell()
916 wx.Bell()
917 wx.Bell()
918 _log.warning('unhandled event detected: QUERY_END_SESSION')
919 _log.info('we should be saving ourselves from here')
920 gmLog2.flush()
921 print('unhandled event detected: QUERY_END_SESSION')
922
924 wx.Bell()
925 wx.Bell()
926 wx.Bell()
927 _log.warning('unhandled event detected: END_SESSION')
928 gmLog2.flush()
929 print('unhandled event detected: END_SESSION')
930
931
933 if not callable(callback):
934 raise TypeError('callback [%s] not callable' % callback)
935
936 self.__pre_exit_callbacks.append(callback)
937
938
939 - def _on_set_statustext_pubsub(self, context=None):
940 try:
941 beep = context.data['beep']
942 except KeyError:
943 beep = False
944 wx.CallAfter(self.SetStatusText, '%s' % context.data['msg'], beep = beep)
945
946
947 - def _on_set_statustext(self, msg=None, loglevel=None, beep=True):
948 if msg is None:
949 msg = _('programmer forgot to specify status message')
950 if loglevel is not None:
951 _log.log(loglevel, msg.replace('\015', ' ').replace('\012', ' '))
952 wx.CallAfter(self.SetStatusText, msg, beep = beep)
953
954
956
957 self.SetStatusText(_('The database will be shut down for maintenance in a few minutes.'))
958 wx.Bell()
959 if not wx.GetApp().IsActive():
960 self.RequestUserAttention(flags = wx.USER_ATTENTION_ERROR)
961
962 gmHooks.run_hook_script(hook = 'db_maintenance_warning')
963
964 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
965 None,
966 -1,
967 caption = _('Database shutdown warning'),
968 question = _(
969 'The database will be shut down for maintenance\n'
970 'in a few minutes.\n'
971 '\n'
972 'In order to not suffer any loss of data you\n'
973 'will need to save your current work and log\n'
974 'out of this GNUmed client.\n'
975 ),
976 button_defs = [
977 {
978 'label': _('Close now'),
979 'tooltip': _('Close this GNUmed client immediately.'),
980 'default': False
981 },
982 {
983 'label': _('Finish work'),
984 'tooltip': _('Finish and save current work first, then manually close this GNUmed client.'),
985 'default': True
986 }
987 ]
988 )
989 decision = dlg.ShowModal()
990 if decision == wx.ID_YES:
991 top_win = wx.GetApp().GetTopWindow()
992 wx.CallAfter(top_win.Close)
993
994
996
997 if not wx.GetApp().IsActive():
998 if urgent:
999 self.RequestUserAttention(flags = wx.USER_ATTENTION_ERROR)
1000 else:
1001 self.RequestUserAttention(flags = wx.USER_ATTENTION_INFO)
1002
1003 if msg is not None:
1004 self.SetStatusText(msg)
1005
1006 if urgent:
1007 wx.Bell()
1008
1009 gmHooks.run_hook_script(hook = 'request_user_attention')
1010
1011 - def _on_post_patient_selection(self, **kwargs):
1012 self.__update_window_title()
1013 gmDispatcher.send(signal = 'statustext', msg = '')
1014 try:
1015 gmHooks.run_hook_script(hook = 'post_patient_activation')
1016 except:
1017 gmDispatcher.send(signal = 'statustext', msg = _('Cannot run script after patient activation.'))
1018 raise
1019
1027
1028
1029
1032
1039
1040
1044
1045
1054
1055
1069
1070
1071
1072
1074
1075 return
1076
1077
1078 from Gnumed.wxpython import gmAbout
1079 frame_about = gmAbout.AboutFrame (
1080 self,
1081 -1,
1082 _("About GNUmed"),
1083 size=wx.Size(350, 300),
1084 style = wx.MAXIMIZE_BOX,
1085 version = _cfg.get(option = 'client_version'),
1086 debug = _cfg.get(option = 'debug')
1087 )
1088 frame_about.Centre(wx.BOTH)
1089 gmTopLevelFrame.otherWin = frame_about
1090 frame_about.Show(True)
1091 frame_about.DestroyLater()
1092
1093
1124
1125
1127 from Gnumed.wxpython import gmAbout
1128 contribs = gmAbout.cContributorsDlg (
1129 parent = self,
1130 id = -1,
1131 title = _('GNUmed contributors'),
1132 size = wx.Size(400,600),
1133 style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
1134 )
1135 contribs.ShowModal()
1136 contribs.DestroyLater()
1137
1138
1139
1140
1142 """Invoked from Menu GNUmed / Exit (which calls this ID_EXIT handler)."""
1143 _log.debug('gmTopLevelFrame._on_exit_gnumed() start')
1144 self.Close(True)
1145 _log.debug('gmTopLevelFrame._on_exit_gnumed() end')
1146
1147
1150
1151
1153 send = gmGuiHelpers.gm_show_question (
1154 _('This will send a notification about database downtime\n'
1155 'to all GNUmed clients connected to your database.\n'
1156 '\n'
1157 'Do you want to send the notification ?\n'
1158 ),
1159 _('Announcing database maintenance downtime')
1160 )
1161 if not send:
1162 return
1163 gmPG2.send_maintenance_notification()
1164
1165
1168
1169
1170
1183
1184 gmCfgWidgets.configure_string_option (
1185 message = _(
1186 'Some network installations cannot cope with loading\n'
1187 'documents of arbitrary size in one piece from the\n'
1188 'database (mainly observed on older Windows versions)\n.'
1189 '\n'
1190 'Under such circumstances documents need to be retrieved\n'
1191 'in chunks and reassembled on the client.\n'
1192 '\n'
1193 'Here you can set the size (in Bytes) above which\n'
1194 'GNUmed will retrieve documents in chunks. Setting this\n'
1195 'value to 0 will disable the chunking protocol.'
1196 ),
1197 option = 'horstspace.blob_export_chunk_size',
1198 bias = 'workplace',
1199 default_value = 1024 * 1024,
1200 validator = is_valid
1201 )
1202
1203
1204
1272
1276
1277
1278
1287
1288 gmCfgWidgets.configure_string_option (
1289 message = _(
1290 'When GNUmed cannot find an OpenOffice server it\n'
1291 'will try to start one. OpenOffice, however, needs\n'
1292 'some time to fully start up.\n'
1293 '\n'
1294 'Here you can set the time for GNUmed to wait for OOo.\n'
1295 ),
1296 option = 'external.ooo.startup_settle_time',
1297 bias = 'workplace',
1298 default_value = 2.0,
1299 validator = is_valid
1300 )
1301
1304
1305
1308
1309
1312
1313
1316
1317
1332
1333 gmCfgWidgets.configure_string_option (
1334 message = _(
1335 'GNUmed will use this URL to access an encyclopedia of\n'
1336 'measurement/lab methods from within the measurments grid.\n'
1337 '\n'
1338 'You can leave this empty but to set it to a specific\n'
1339 'address the URL must be accessible now.'
1340 ),
1341 option = 'external.urls.measurements_encyclopedia',
1342 bias = 'user',
1343 default_value = german_default,
1344 validator = is_valid
1345 )
1346
1347
1360
1361 gmCfgWidgets.configure_string_option (
1362 message = _(
1363 'Enter the shell command with which to start the\n'
1364 'the ACS risk assessment calculator.\n'
1365 '\n'
1366 'GNUmed will try to verify the path which may,\n'
1367 'however, fail if you are using an emulator such\n'
1368 'as Wine. Nevertheless, starting the calculator\n'
1369 'will work as long as the shell command is correct\n'
1370 'despite the failing test.'
1371 ),
1372 option = 'external.tools.acs_risk_calculator_cmd',
1373 bias = 'user',
1374 validator = is_valid
1375 )
1376
1379
1392
1393 gmCfgWidgets.configure_string_option (
1394 message = _(
1395 'Enter the shell command with which to start\n'
1396 'the FreeDiams drug database frontend.\n'
1397 '\n'
1398 'GNUmed will try to verify that path.'
1399 ),
1400 option = 'external.tools.freediams_cmd',
1401 bias = 'workplace',
1402 default_value = None,
1403 validator = is_valid
1404 )
1405
1418
1419 gmCfgWidgets.configure_string_option (
1420 message = _(
1421 'Enter the shell command with which to start the\n'
1422 'the IFAP drug database.\n'
1423 '\n'
1424 'GNUmed will try to verify the path which may,\n'
1425 'however, fail if you are using an emulator such\n'
1426 'as Wine. Nevertheless, starting IFAP will work\n'
1427 'as long as the shell command is correct despite\n'
1428 'the failing test.'
1429 ),
1430 option = 'external.ifap-win.shell_command',
1431 bias = 'workplace',
1432 default_value = 'C:\Ifapwin\WIAMDB.EXE',
1433 validator = is_valid
1434 )
1435
1436
1437
1486
1487
1488
1505
1508
1511
1516
1517 gmCfgWidgets.configure_string_option (
1518 message = _(
1519 'When a patient is activated GNUmed checks the\n'
1520 "proximity of the patient's birthday.\n"
1521 '\n'
1522 'If the birthday falls within the range of\n'
1523 ' "today %s <the interval you set here>"\n'
1524 'GNUmed will remind you of the recent or\n'
1525 'imminent anniversary.'
1526 ) % '\u2213',
1527 option = 'patient_search.dob_warn_interval',
1528 bias = 'user',
1529 default_value = '1 week',
1530 validator = is_valid
1531 )
1532
1534
1535 gmCfgWidgets.configure_boolean_option (
1536 parent = self,
1537 question = _(
1538 'When adding progress notes do you want to\n'
1539 'allow opening several unassociated, new\n'
1540 'episodes for a patient at once ?\n'
1541 '\n'
1542 'This can be particularly helpful when entering\n'
1543 'progress notes on entirely new patients presenting\n'
1544 'with a multitude of problems on their first visit.'
1545 ),
1546 option = 'horstspace.soap_editor.allow_same_episode_multiple_times',
1547 button_tooltips = [
1548 _('Yes, allow for multiple new episodes concurrently.'),
1549 _('No, only allow editing one new episode at a time.')
1550 ]
1551 )
1552
1554
1555 gmCfgWidgets.configure_boolean_option (
1556 parent = self,
1557 question = _(
1558 'When activating a patient, do you want GNUmed to\n'
1559 'auto-open editors for all active problems that were\n'
1560 'touched upon during the current and the most recent\n'
1561 'encounter ?'
1562 ),
1563 option = 'horstspace.soap_editor.auto_open_latest_episodes',
1564 button_tooltips = [
1565 _('Yes, auto-open editors for all problems of the most recent encounter.'),
1566 _('No, only auto-open one editor for a new, unassociated problem.')
1567 ]
1568 )
1569
1570
1572 gmCfgWidgets.configure_boolean_option (
1573 parent = self,
1574 question = _(
1575 'When editing progress notes, do you want GNUmed to\n'
1576 'show individual fields for each of the SOAP categories\n'
1577 'or do you want to use a text-editor like field for\n'
1578 'all SOAP categories which can then be set per line\n'
1579 'of input ?'
1580 ),
1581 option = 'horstspace.soap_editor.use_one_field_per_soap_category',
1582 button_tooltips = [
1583 _('Yes, show a dedicated field per SOAP category.'),
1584 _('No, use one field for all SOAP categories.')
1585 ]
1586 )
1587
1588
1634
1635
1636
1639
1642
1655
1656 gmCfgWidgets.configure_string_option (
1657 message = _(
1658 'GNUmed will use this URL to let you browse\n'
1659 'billing catalogs (schedules of fees).\n'
1660 '\n'
1661 'You can leave this empty but to set it to a specific\n'
1662 'address the URL must be accessible now.'
1663 ),
1664 option = 'external.urls.schedules_of_fees',
1665 bias = 'user',
1666 default_value = german_default,
1667 validator = is_valid
1668 )
1669
1670
1671
1674
1677
1679 gmCfgWidgets.configure_string_from_list_option (
1680 parent = self,
1681 message = _('Select the default prescription mode.\n'),
1682 option = 'horst_space.default_prescription_mode',
1683 bias = 'user',
1684 default_value = 'form',
1685 choices = [ _('Formular'), _('Datenbank') ],
1686 columns = [_('Prescription mode')],
1687 data = [ 'form', 'database' ]
1688 )
1689
1692
1695
1698
1701
1703 enc_types = gmEMRStructItems.get_encounter_types()
1704 msg = _(
1705 'Select the default type for new encounters.\n'
1706 '\n'
1707 'Leaving this unset will make GNUmed apply the most commonly used type.\n'
1708 )
1709 gmCfgWidgets.configure_string_from_list_option (
1710 parent = self,
1711 message = msg,
1712 option = 'encounter.default_type',
1713 bias = 'user',
1714
1715 choices = [ e[0] for e in enc_types ],
1716 columns = [_('Encounter type')],
1717 data = [ e[1] for e in enc_types ]
1718 )
1719
1721 gmCfgWidgets.configure_boolean_option (
1722 parent = self,
1723 question = _(
1724 'Do you want GNUmed to show the encounter\n'
1725 'details editor when changing the active patient ?'
1726 ),
1727 option = 'encounter.show_editor_before_patient_change',
1728 button_tooltips = [
1729 _('Yes, show the encounter editor if it seems appropriate.'),
1730 _('No, never show the encounter editor even if it would seem useful.')
1731 ]
1732 )
1733
1738
1739 gmCfgWidgets.configure_string_option (
1740 message = _(
1741 'When a patient is activated GNUmed checks the\n'
1742 'chart for encounters lacking any entries.\n'
1743 '\n'
1744 'Any such encounters older than what you set\n'
1745 'here will be removed from the medical record.\n'
1746 '\n'
1747 'To effectively disable removal of such encounters\n'
1748 'set this option to an improbable value.\n'
1749 ),
1750 option = 'encounter.ttl_if_empty',
1751 bias = 'user',
1752 default_value = '1 week',
1753 validator = is_valid
1754 )
1755
1760
1761 gmCfgWidgets.configure_string_option (
1762 message = _(
1763 'When a patient is activated GNUmed checks the\n'
1764 'age of the most recent encounter.\n'
1765 '\n'
1766 'If that encounter is younger than this age\n'
1767 'the existing encounter will be continued.\n'
1768 '\n'
1769 '(If it is really old a new encounter is\n'
1770 ' started, or else GNUmed will ask you.)\n'
1771 ),
1772 option = 'encounter.minimum_ttl',
1773 bias = 'user',
1774 default_value = '1 hour 30 minutes',
1775 validator = is_valid
1776 )
1777
1782
1783 gmCfgWidgets.configure_string_option (
1784 message = _(
1785 'When a patient is activated GNUmed checks the\n'
1786 'age of the most recent encounter.\n'
1787 '\n'
1788 'If that encounter is older than this age\n'
1789 'GNUmed will always start a new encounter.\n'
1790 '\n'
1791 '(If it is very recent the existing encounter\n'
1792 ' is continued, or else GNUmed will ask you.)\n'
1793 ),
1794 option = 'encounter.maximum_ttl',
1795 bias = 'user',
1796 default_value = '6 hours',
1797 validator = is_valid
1798 )
1799
1808
1809 gmCfgWidgets.configure_string_option (
1810 message = _(
1811 'At any time there can only be one open (ongoing)\n'
1812 'episode for each health issue.\n'
1813 '\n'
1814 'When you try to open (add data to) an episode on a health\n'
1815 'issue GNUmed will check for an existing open episode on\n'
1816 'that issue. If there is any it will check the age of that\n'
1817 'episode. The episode is closed if it has been dormant (no\n'
1818 'data added, that is) for the period of time (in days) you\n'
1819 'set here.\n'
1820 '\n'
1821 "If the existing episode hasn't been dormant long enough\n"
1822 'GNUmed will consult you what to do.\n'
1823 '\n'
1824 'Enter maximum episode dormancy in DAYS:'
1825 ),
1826 option = 'episode.ttl',
1827 bias = 'user',
1828 default_value = 60,
1829 validator = is_valid
1830 )
1831
1862
1877
1902
1912
1913 gmCfgWidgets.configure_string_option (
1914 message = _(
1915 'GNUmed can check for new releases being available. To do\n'
1916 'so it needs to load version information from an URL.\n'
1917 '\n'
1918 'The default URL is:\n'
1919 '\n'
1920 ' http://www.gnumed.de/downloads/gnumed-versions.txt\n'
1921 '\n'
1922 'but you can configure any other URL locally. Note\n'
1923 'that you must enter the location as a valid URL.\n'
1924 'Depending on the URL the client will need online\n'
1925 'access when checking for updates.'
1926 ),
1927 option = 'horstspace.update.url',
1928 bias = 'workplace',
1929 default_value = 'http://www.gnumed.de/downloads/gnumed-versions.txt',
1930 validator = is_valid
1931 )
1932
1950
1967
1984
1995
1996 gmCfgWidgets.configure_string_option (
1997 message = _(
1998 'GNUmed can show the document review dialog after\n'
1999 'calling the appropriate viewer for that document.\n'
2000 '\n'
2001 'Select the conditions under which you want\n'
2002 'GNUmed to do so:\n'
2003 '\n'
2004 ' 0: never display the review dialog\n'
2005 ' 1: always display the dialog\n'
2006 ' 2: only if there is no previous review by me\n'
2007 ' 3: only if there is no previous review at all\n'
2008 ' 4: only if there is no review by the responsible reviewer\n'
2009 '\n'
2010 'Note that if a viewer is configured to not block\n'
2011 'GNUmed during document display the review dialog\n'
2012 'will actually appear in parallel to the viewer.'
2013 ),
2014 option = 'horstspace.document_viewer.review_after_display',
2015 bias = 'user',
2016 default_value = 3,
2017 validator = is_valid
2018 )
2019
2021
2022
2023 master_data_lists = [
2024 'adr',
2025 'provinces',
2026 'codes',
2027 'billables',
2028 'ref_data_sources',
2029 'meds_substances',
2030 'meds_doses',
2031 'meds_components',
2032 'meds_drugs',
2033 'meds_vaccines',
2034 'orgs',
2035 'labs',
2036 'meta_test_types',
2037 'test_types',
2038 'test_panels',
2039 'form_templates',
2040 'doc_types',
2041 'enc_types',
2042 'communication_channel_types',
2043 'text_expansions',
2044 'patient_tags',
2045 'hints',
2046 'db_translations',
2047 'workplaces'
2048 ]
2049
2050 master_data_list_names = {
2051 'adr': _('Addresses (likely slow)'),
2052 'hints': _('Dynamic automatic hints'),
2053 'codes': _('Codes and their respective terms'),
2054 'communication_channel_types': _('Communication channel types'),
2055 'orgs': _('Organizations with their units, addresses, and comm channels'),
2056 'labs': _('Measurements: diagnostic organizations (path labs, ...)'),
2057 'test_types': _('Measurements: test types'),
2058 'test_panels': _('Measurements: test panels/profiles/batteries'),
2059 'form_templates': _('Document templates (forms, letters, plots, ...)'),
2060 'doc_types': _('Document types'),
2061 'enc_types': _('Encounter types'),
2062 'text_expansions': _('Keyword based text expansion macros'),
2063 'meta_test_types': _('Measurements: aggregate test types'),
2064 'patient_tags': _('Patient tags'),
2065 'provinces': _('Provinces (counties, territories, states, regions, ...)'),
2066 'db_translations': _('String translations in the database'),
2067 'meds_vaccines': _('Medications: vaccines'),
2068 'meds_substances': _('Medications: base substances'),
2069 'meds_doses': _('Medications: substance dosage'),
2070 'meds_components': _('Medications: drug components'),
2071 'meds_drugs': _('Medications: drug products and generic drugs'),
2072 'workplaces': _('Workplace profiles (which plugins to load)'),
2073 'billables': _('Billable items'),
2074 'ref_data_sources': _('Reference data sources')
2075 }
2076
2077 map_list2handler = {
2078 'form_templates': gmFormWidgets.manage_form_templates,
2079 'doc_types': gmDocumentWidgets.manage_document_types,
2080 'text_expansions': gmKeywordExpansionWidgets.configure_keyword_text_expansion,
2081 'db_translations': gmI18nWidgets.manage_translations,
2082 'codes': gmCodingWidgets.browse_coded_terms,
2083 'enc_types': gmEncounterWidgets.manage_encounter_types,
2084 'provinces': gmAddressWidgets.manage_regions,
2085 'workplaces': gmPraxisWidgets.configure_workplace_plugins,
2086 'labs': gmMeasurementWidgets.manage_measurement_orgs,
2087 'test_types': gmMeasurementWidgets.manage_measurement_types,
2088 'meta_test_types': gmMeasurementWidgets.manage_meta_test_types,
2089 'orgs': gmOrganizationWidgets.manage_orgs,
2090 'adr': gmAddressWidgets.manage_addresses,
2091 'meds_substances': gmSubstanceMgmtWidgets.manage_substances,
2092 'meds_doses': gmSubstanceMgmtWidgets.manage_substance_doses,
2093 'meds_components': gmSubstanceMgmtWidgets.manage_drug_components,
2094 'meds_drugs': gmSubstanceMgmtWidgets.manage_drug_products,
2095 'meds_vaccines': gmVaccWidgets.manage_vaccines,
2096 'patient_tags': gmDemographicsWidgets.manage_tag_images,
2097 'communication_channel_types': gmContactWidgets.manage_comm_channel_types,
2098 'billables': gmBillingWidgets.manage_billables,
2099 'ref_data_sources': gmCodingWidgets.browse_data_sources,
2100 'hints': gmAutoHintWidgets.manage_dynamic_hints,
2101 'test_panels': gmMeasurementWidgets.manage_test_panels
2102 }
2103
2104
2105 def edit(item):
2106 try: map_list2handler[item](parent = self)
2107 except KeyError: pass
2108 return False
2109
2110
2111 gmListWidgets.get_choices_from_list (
2112 parent = self,
2113 caption = _('Master data management'),
2114 choices = [ master_data_list_names[lst] for lst in master_data_lists],
2115 data = master_data_lists,
2116 columns = [_('Select the list you want to manage:')],
2117 edit_callback = edit,
2118 single_selection = True,
2119 ignore_OK_button = True
2120 )
2121
2124
2126
2127 found, cmd = gmShellAPI.detect_external_binary(binary = 'ginkgocadx')
2128 if found:
2129 gmShellAPI.run_command_in_shell(cmd, blocking=False)
2130 return
2131
2132 if os.access('/Applications/OsiriX.app/Contents/MacOS/OsiriX', os.X_OK):
2133 gmShellAPI.run_command_in_shell('/Applications/OsiriX.app/Contents/MacOS/OsiriX', blocking = False)
2134 return
2135
2136 for viewer in ['aeskulap', 'amide', 'dicomscope', 'xmedcon']:
2137 found, cmd = gmShellAPI.detect_external_binary(binary = viewer)
2138 if found:
2139 gmShellAPI.run_command_in_shell(cmd, blocking = False)
2140 return
2141
2142 gmDispatcher.send(signal = 'statustext', msg = _('No DICOM viewer found.'), beep = True)
2143
2145
2146 curr_pat = gmPerson.gmCurrentPatient()
2147
2148 arriba = gmArriba.cArriba()
2149 pat = gmTools.bool2subst(curr_pat.connected, curr_pat, None)
2150 if not arriba.run(patient = pat, debug = _cfg.get(option = 'debug')):
2151 return
2152
2153
2154 if curr_pat is None:
2155 return
2156
2157 if arriba.pdf_result is None:
2158 return
2159
2160 doc = gmDocumentWidgets.save_file_as_new_document (
2161 parent = self,
2162 filename = arriba.pdf_result,
2163 document_type = _('risk assessment'),
2164 pk_org_unit = gmPraxis.gmCurrentPraxisBranch()['pk_org_unit']
2165 )
2166
2167 try: os.remove(arriba.pdf_result)
2168 except Exception: _log.exception('cannot remove [%s]', arriba.pdf_result)
2169
2170 if doc is None:
2171 return
2172
2173 doc['comment'] = 'arriba: %s' % _('cardiovascular risk assessment')
2174 doc.save()
2175
2176 try:
2177 open(arriba.xml_result).close()
2178 part = doc.add_part(file = arriba.xml_result)
2179 except Exception:
2180 _log.exception('error accessing [%s]', arriba.xml_result)
2181 gmDispatcher.send(signal = 'statustext', msg = _('[arriba] XML result not found in [%s]') % arriba.xml_result, beep = False)
2182
2183 if part is None:
2184 return
2185
2186 part['obj_comment'] = 'XML-Daten'
2187 part['filename'] = 'arriba-result.xml'
2188 part.save()
2189
2191
2192 dbcfg = gmCfg.cCfgSQL()
2193 cmd = dbcfg.get2 (
2194 option = 'external.tools.acs_risk_calculator_cmd',
2195 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2196 bias = 'user'
2197 )
2198
2199 if cmd is None:
2200 gmDispatcher.send(signal = 'statustext', msg = _('ACS risk assessment calculator not configured.'), beep = True)
2201 return
2202
2203 cwd = os.path.expanduser(os.path.join('~', '.gnumed'))
2204 try:
2205 subprocess.check_call (
2206 args = (cmd,),
2207 close_fds = True,
2208 cwd = cwd
2209 )
2210 except (OSError, ValueError, subprocess.CalledProcessError):
2211 _log.exception('there was a problem executing [%s]', cmd)
2212 gmDispatcher.send(signal = 'statustext', msg = _('Cannot run [%s] !') % cmd, beep = True)
2213 return
2214
2215 pdfs = glob.glob(os.path.join(cwd, 'arriba-%s-*.pdf' % gmDateTime.pydt_now_here().strftime('%Y-%m-%d')))
2216 for pdf in pdfs:
2217 try:
2218 open(pdf).close()
2219 except:
2220 _log.exception('error accessing [%s]', pdf)
2221 gmDispatcher.send(signal = 'statustext', msg = _('There was a problem accessing the [arriba] result in [%s] !') % pdf, beep = True)
2222 continue
2223
2224 doc = gmDocumentWidgets.save_file_as_new_document (
2225 parent = self,
2226 filename = pdf,
2227 document_type = 'risk assessment',
2228 pk_org_unit = gmPraxis.gmCurrentPraxisBranch()['pk_org_unit']
2229 )
2230
2231 try:
2232 os.remove(pdf)
2233 except Exception:
2234 _log.exception('cannot remove [%s]', pdf)
2235
2236 if doc is None:
2237 continue
2238 doc['comment'] = 'arriba: %s' % _('cardiovascular risk assessment')
2239 doc.save()
2240
2241 return
2242
2243
2251
2254
2257
2260
2277
2278
2281
2282
2288
2289
2292
2293
2294
2295
2298
2299
2302
2303
2306
2307
2308
2309
2311 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'gnumed-screenshot-%s.png')) % pyDT.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
2312 self.__save_screenshot_to_file(filename = fname)
2313
2314
2316 raise ValueError('raised ValueError to test exception handling')
2317
2318
2320 import faulthandler
2321 _log.debug('testing faulthandler via SIGSEGV')
2322 faulthandler._sigsegv()
2323
2324
2328
2329
2331 raise gmExceptions.AccessDenied (
2332 _('[-9999]: <access violation test error>'),
2333 source = 'GNUmed code',
2334 code = -9999,
2335 details = _('This is a deliberate AccessDenied exception thrown to test the handling of access violations by means of a decorator.')
2336 )
2337
2338 @gmAccessPermissionWidgets.verify_minimum_required_role('admin', activity = _('testing access check for non-existant <admin> role'))
2340 raise gmExceptions.AccessDenied (
2341 _('[-9999]: <access violation test error>'),
2342 source = 'GNUmed code',
2343 code = -9999,
2344 details = _('This is a deliberate AccessDenied exception. You should not see this message because the role is checked in a decorator.')
2345 )
2346
2348 import wx.lib.inspection
2349 wx.lib.inspection.InspectionTool().Show()
2350
2353
2356
2359
2362
2369
2373
2376
2380
2381
2383 self.StatusBar.show_history()
2384
2385
2388
2389
2392
2393
2400
2404
2406 name = os.path.basename(gmLog2._logfile_name)
2407 name, ext = os.path.splitext(name)
2408 new_name = '%s_%s%s' % (name, pyDT.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), ext)
2409 new_path = os.path.expanduser(os.path.join('~', 'gnumed'))
2410
2411 dlg = wx.FileDialog (
2412 parent = self,
2413 message = _("Save current log as..."),
2414 defaultDir = new_path,
2415 defaultFile = new_name,
2416 wildcard = "%s (*.log)|*.log" % _("log files"),
2417 style = wx.FD_SAVE
2418 )
2419 choice = dlg.ShowModal()
2420 new_name = dlg.GetPath()
2421 dlg.DestroyLater()
2422 if choice != wx.ID_OK:
2423 return True
2424
2425 _log.warning('syncing log file for backup to [%s]', new_name)
2426 gmLog2.flush()
2427 shutil.copy2(gmLog2._logfile_name, new_name)
2428 gmDispatcher.send('statustext', msg = _('Log file backed up as [%s].') % new_name)
2429
2432
2433
2436
2437
2440
2441
2444
2445
2446
2447
2449 """This is the wx.EVT_CLOSE handler.
2450
2451 - framework still functional
2452 """
2453 _log.debug('gmTopLevelFrame.OnClose() start')
2454 self._clean_exit()
2455 self.DestroyLater()
2456 _log.debug('gmTopLevelFrame.OnClose() end')
2457 return True
2458
2459
2464
2465
2473
2480
2487
2494
2504
2512
2520
2528
2536
2544
2545
2546 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2555
2556
2557 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2566
2567
2568 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2577
2578
2579 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage family history'))
2588
2589 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2596
2597 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('calculate EDC'))
2601
2602
2603 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage suppressed hints'))
2610
2611
2618
2619
2636
2639
2642
2643
2645 pat = gmPerson.gmCurrentPatient()
2646 if not pat.connected:
2647 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR. No active patient.'))
2648 return False
2649 from Gnumed.exporters import gmPatientExporter
2650 exporter = gmPatientExporter.cEmrExport(patient = pat)
2651 fname = gmTools.get_unique_filename(prefix = 'gm-exp-', suffix = '.txt')
2652 output_file = io.open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
2653 exporter.set_output_file(output_file)
2654 exporter.dump_constraints()
2655 exporter.dump_demographic_record(True)
2656 exporter.dump_clinical_record()
2657 exporter.dump_med_docs()
2658 output_file.close()
2659 pat.export_area.add_file(filename = fname, hint = _('EMR as text document'))
2660
2661
2679
2680
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
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
2813
2814
2839
2840
2847
2848
2850 curr_pat = gmPerson.gmCurrentPatient()
2851 if not curr_pat.connected:
2852 gmDispatcher.send(signal = 'statustext', msg = _('Cannot add tag to person. No active patient.'))
2853 return
2854
2855 tag = gmDemographicsWidgets.manage_tag_images(parent = self)
2856 if tag is None:
2857 return
2858
2859 tag = curr_pat.add_tag(tag['pk_tag_image'])
2860 msg = _('Edit the comment on tag [%s]') % tag['l10n_description']
2861 comment = wx.GetTextFromUser (
2862 message = msg,
2863 caption = _('Editing tag comment'),
2864 default_value = gmTools.coalesce(tag['comment'], ''),
2865 parent = self
2866 )
2867
2868 if comment == '':
2869 return
2870
2871 if comment.strip() == tag['comment']:
2872 return
2873
2874 if comment == ' ':
2875 tag['comment'] = None
2876 else:
2877 tag['comment'] = comment.strip()
2878
2879 tag.save()
2880
2881
2891
2892
2902
2903
2912
2913
2922
2923
2925 curr_pat = gmPerson.gmCurrentPatient()
2926 if not curr_pat.connected:
2927 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export patient as GDT. No active patient.'))
2928 return False
2929 enc = 'cp850'
2930 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'current-patient.gdt'))
2931 curr_pat.export_as_gdt(filename = fname, encoding = enc)
2932 gmDispatcher.send(signal = 'statustext', msg = _('Exported demographics to GDT file [%s].') % fname)
2933
2934
2936 curr_pat = gmPerson.gmCurrentPatient()
2937 if not curr_pat.connected:
2938 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export patient as VCARD. No active patient.'))
2939 return False
2940 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'current-patient.vcf'))
2941 curr_pat.export_as_vcard(filename = fname)
2942 gmDispatcher.send(signal = 'statustext', msg = _('Exported demographics to VCARD file [%s].') % fname)
2943
2944
2947
2948
2951
2952
2955
2956
2959
2962
2970
2978
2981
2988
2992
2995
2998
2999
3002
3003
3006
3007
3010
3011
3013 """Cleanup helper.
3014
3015 - should ALWAYS be called when this program is
3016 to be terminated
3017 - ANY code that should be executed before a
3018 regular shutdown should go in here
3019 - framework still functional
3020 """
3021 _log.debug('gmTopLevelFrame._clean_exit() start')
3022
3023
3024 listener = gmBackendListener.gmBackendListener()
3025 try:
3026 listener.shutdown()
3027 except:
3028 _log.exception('cannot stop backend notifications listener thread')
3029
3030
3031 if _scripting_listener is not None:
3032 try:
3033 _scripting_listener.shutdown()
3034 except:
3035 _log.exception('cannot stop scripting listener thread')
3036
3037
3038 self.StatusBar.clock_update_timer.Stop()
3039 gmTimer.shutdown()
3040 gmPhraseWheel.shutdown()
3041
3042
3043 for call_back in self.__pre_exit_callbacks:
3044 try:
3045 call_back()
3046 except:
3047 print('*** pre-exit callback failed ***')
3048 print('%s' % call_back)
3049 _log.exception('callback [%s] failed', call_back)
3050
3051
3052 gmDispatcher.send('application_closing')
3053
3054
3055 gmDispatcher.disconnect(self._on_set_statustext, 'statustext')
3056
3057
3058
3059 curr_width, curr_height = self.GetSize()
3060 _log.info('GUI size at shutdown: [%s:%s]' % (curr_width, curr_height))
3061 curr_pos_x, curr_pos_y = self.GetScreenPosition()
3062 _log.info('GUI position at shutdown: [%s:%s]' % (curr_pos_x, curr_pos_y))
3063 if 0 not in [curr_width, curr_height]:
3064 dbcfg = gmCfg.cCfgSQL()
3065 try:
3066 dbcfg.set (
3067 option = 'main.window.width',
3068 value = curr_width,
3069 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3070 )
3071 dbcfg.set (
3072 option = 'main.window.height',
3073 value = curr_height,
3074 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3075 )
3076 dbcfg.set (
3077 option = 'main.window.position.x',
3078 value = curr_pos_x,
3079 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3080 )
3081 dbcfg.set (
3082 option = 'main.window.position.y',
3083 value = curr_pos_y,
3084 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3085 )
3086 except Exception:
3087 _log.exception('cannot save current client window size and/or position')
3088
3089 if _cfg.get(option = 'debug'):
3090 print('---=== GNUmed shutdown ===---')
3091 try:
3092 print(_('You have to manually close this window to finalize shutting down GNUmed.'))
3093 print(_('This is so that you can inspect the console output at your leisure.'))
3094 except UnicodeEncodeError:
3095 print('You have to manually close this window to finalize shutting down GNUmed.')
3096 print('This is so that you can inspect the console output at your leisure.')
3097 print('---=== GNUmed shutdown ===---')
3098
3099
3100 gmExceptionHandlingWidgets.uninstall_wx_exception_handler()
3101
3102
3103 import threading
3104 _log.debug("%s active threads", threading.activeCount())
3105 for t in threading.enumerate():
3106 _log.debug('thread %s', t)
3107 if t.name == 'MainThread':
3108 continue
3109 print('GNUmed: waiting for thread [%s] to finish' % t.name)
3110
3111 _log.debug('gmTopLevelFrame._clean_exit() end')
3112
3113
3114
3115
3117
3118 if _cfg.get(option = 'slave'):
3119 self.__title_template = 'GMdS: %%(pat)s [%%(prov)s@%%(wp)s in %%(site)s of %%(prax)s] (%s:%s)' % (
3120 _cfg.get(option = 'slave personality'),
3121 _cfg.get(option = 'xml-rpc port')
3122 )
3123 else:
3124 self.__title_template = 'GMd: %(pat)s [%(prov)s@%(wp)s in %(site)s of %(prax)s]'
3125
3126
3128 """Update title of main window based on template.
3129
3130 This gives nice tooltips on iconified GNUmed instances.
3131
3132 User research indicates that in the title bar people want
3133 the date of birth, not the age, so please stick to this
3134 convention.
3135 """
3136 args = {}
3137
3138 pat = gmPerson.gmCurrentPatient()
3139 if pat.connected:
3140 args['pat'] = '%s %s %s (%s) #%d' % (
3141 gmTools.coalesce(pat['title'], '', '%.4s'),
3142 pat['firstnames'],
3143 pat['lastnames'],
3144 pat.get_formatted_dob(format = '%Y %b %d'),
3145 pat['pk_identity']
3146 )
3147 else:
3148 args['pat'] = _('no patient')
3149
3150 args['prov'] = '%s%s.%s' % (
3151 gmTools.coalesce(_provider['title'], '', '%s '),
3152 _provider['firstnames'][:1],
3153 _provider['lastnames']
3154 )
3155
3156 praxis = gmPraxis.gmCurrentPraxisBranch()
3157 args['wp'] = praxis.active_workplace
3158 args['site'] = praxis['branch']
3159 args['prax'] = praxis['praxis']
3160
3161 self.SetTitle(self.__title_template % args)
3162
3163
3165
3166 time.sleep(0.5)
3167
3168 rect = self.GetRect()
3169
3170
3171 if sys.platform == 'linux2':
3172 client_x, client_y = self.ClientToScreen((0, 0))
3173 border_width = client_x - rect.x
3174 title_bar_height = client_y - rect.y
3175
3176 if self.GetMenuBar():
3177 title_bar_height /= 2
3178 rect.width += (border_width * 2)
3179 rect.height += title_bar_height + border_width
3180
3181 scr_dc = wx.ScreenDC()
3182 mem_dc = wx.MemoryDC()
3183 img = wx.Bitmap(rect.width, rect.height)
3184 mem_dc.SelectObject(img)
3185 mem_dc.Blit (
3186 0, 0,
3187 rect.width, rect.height,
3188 scr_dc,
3189 rect.x, rect.y
3190 )
3191
3192
3193 if filename is None:
3194 filename = gmTools.get_unique_filename (
3195 prefix = 'gm-screenshot-%s-' % pyDT.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'),
3196 suffix = '.png'
3197 )
3198
3199 img.SaveFile(filename, wx.BITMAP_TYPE_PNG)
3200 gmDispatcher.send(signal = 'statustext', msg = _('Saved screenshot to file [%s].') % filename)
3201
3202 return filename
3203
3204
3209
3210
3212 """Lock GNUmed client against unauthorized access"""
3213
3214
3215
3216 return
3217
3218
3220 """Unlock the main notebook widgets
3221 As long as we are not logged into the database backend,
3222 all pages but the 'login' page of the main notebook widget
3223 are locked; i.e. not accessible by the user
3224 """
3225
3226
3227
3228
3229
3230 return
3231
3233 wx.LayoutAlgorithm().LayoutWindow(self.LayoutMgr, self.nb)
3234
3237
3239 try:
3240 kwargs['style'] = kwargs['style'] | wx.STB_SIZEGRIP | wx.STB_SHOW_TIPS
3241 except KeyError:
3242 kwargs['style'] = wx.STB_SIZEGRIP | wx.STB_SHOW_TIPS
3243 super().__init__(*args, **kwargs)
3244
3245 self.FieldsCount = 2
3246 self.SetStatusWidths([-1, 225])
3247
3248 self.__msg_fifo = []
3249 self.__normal_background_colour = self.GetBackgroundColour()
3250 self.__blink_background_color = 'yellow'
3251 self.__times_to_blink = 0
3252 self.__blink_counter = 0
3253
3254 self.clock_update_timer = wx.PyTimer(self._cb_update_clock)
3255 self.clock_update_timer.Start(milliseconds = 1000)
3256
3257 self.Bind(wx.EVT_LEFT_DCLICK, self._on_show_history)
3258
3259
3260 - def SetStatusText(self, text, i=0, beep=False):
3261 prev = self.previous_text
3262 msg = self.__update_history(text, i)
3263 super().SetStatusText(msg, i)
3264 if beep:
3265 wx.Bell()
3266 self.__initiate_blinking(text, i, prev)
3267
3268
3269 - def PushStatusText(self, text, field=0):
3270 prev = self.previous_text
3271 msg = self.__update_history(text, field)
3272 super().PushStatusText(msg, field)
3273 self.__initiate_blinking(text, i, prev)
3274
3275
3278
3279
3280 - def show_history(self):
3281 lines = []
3282 for entry in self.__msg_fifo:
3283 lines.append('%s (%s)' % (entry['text'], ','.join(entry['timestamps'])))
3284 gmGuiHelpers.gm_show_info (
3285 title = _('Statusbar history'),
3286 info = _(
3287 '%s - now\n'
3288 '\n'
3289 '%s'
3290 ) % (
3291 gmDateTime.pydt_now_here().strftime('%H:%M'),
3292 '\n'.join(lines)
3293 )
3294 )
3295
3296
3297
3298
3300 """Advances date and local time in the second slot.
3301
3302 Also drives blinking activity.
3303 """
3304 t = time.localtime(time.time())
3305 st = time.strftime('%Y %b %d %H:%M:%S', t)
3306 self.SetStatusText(st, 1)
3307 if self.__times_to_blink > 0:
3308 wx.CallAfter(self.__blink)
3309
3310
3312 if self.__blink_counter == self.__times_to_blink:
3313 self.set_normal_color()
3314 self.__times_to_blink = 0
3315 return
3316
3317 if self.SetBackgroundColour(self.__blink_background_color):
3318 self.__blink_counter += 1
3319 return
3320
3321 self.set_normal_color()
3322
3323
3325 if field != 0:
3326 return
3327 text = text.strip()
3328 if text == '':
3329 return
3330 if text == previous_text:
3331 return
3332 self.__blink_counter = 0
3333 self.__times_to_blink = 2
3334
3335
3337 if len(self.__msg_fifo) == 0:
3338 return None
3339 return self.__msg_fifo[0]['text']
3340
3341 previous_text = property(_get_previous_text)
3342
3343
3344 - def __update_history(self, text, field):
3345 if field > 0:
3346 return text
3347
3348 text = text.strip()
3349 if text == '':
3350 return text
3351
3352 now = gmDateTime.pydt_now_here().strftime('%H:%M')
3353 if len(self.__msg_fifo) == 0:
3354 self.__msg_fifo.append({'text': text, 'timestamps': [now]})
3355 return '%s %s' % (now, text)
3356
3357 last = self.__msg_fifo[0]
3358 if text == last['text']:
3359 last['timestamps'].insert(0, now)
3360 return '%s %s (#%s)' % (now, text, len(last['timestamps']))
3361
3362 self.__msg_fifo.insert(0, {'text': text, 'timestamps': [now]})
3363 if len(self.__msg_fifo) > 20:
3364 self.__msg_fifo = self.__msg_fifo[:20]
3365 return '%s %s' % (now, text)
3366
3367
3368 - def _on_show_history(self, evt):
3370
3371
3373 print('----------------------------------')
3374 print('Statusbar history @ [%s]:' % gmDateTime.pydt_now_here().strftime('%H:%M'))
3375 print('\n'.join(self.__msg_fifo))
3376 print('----------------------------------')
3377
3378
3379 - def _on_print_history(self, evt):
3380 evt.Skip()
3381 self.__print_msg_fifo()
3382
3383
3384 -class gmApp(wx.App):
3385
3387
3388 if _cfg.get(option = 'debug'):
3389 self.SetAssertMode(wx.APP_ASSERT_EXCEPTION | wx.APP_ASSERT_LOG)
3390 else:
3391 self.SetAssertMode(wx.APP_ASSERT_SUPPRESS)
3392
3393 self.__starting_up = True
3394
3395
3396 wx.ToolTip.SetAutoPop(4000)
3397
3398 gmExceptionHandlingWidgets.install_wx_exception_handler()
3399 gmExceptionHandlingWidgets.set_client_version(_cfg.get(option = 'client_version'))
3400
3401 self.SetAppName('gnumed')
3402 self.SetVendorName('gnumed_community')
3403 try:
3404 self.SetAppDisplayName('GNUmed %s' % _cfg.get(option = 'client_version'))
3405 except AttributeError:
3406 _log.info('SetAppDisplayName() not supported')
3407 try:
3408 self.SetVendorDisplayName('The GNUmed Development Community.')
3409 except AttributeError:
3410 _log.info('SetVendorDisplayName() not supported')
3411 paths = gmTools.gmPaths(app_name = 'gnumed', wx = wx)
3412 paths.init_paths(wx = wx, app_name = 'gnumed')
3413
3414
3415 dw, dh = wx.DisplaySize()
3416 _log.info('display size: %s:%s' % (wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X), wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)))
3417 _log.debug('display size: %s:%s %s mm', dw, dh, wx.DisplaySizeMM())
3418 for disp_idx in range(wx.Display.GetCount()):
3419 disp = wx.Display(disp_idx)
3420 disp_mode = disp.CurrentMode
3421 _log.debug('display [%s] "%s": primary=%s, client_area=%s, geom=%s, vid_mode=[%sbpp across %sx%spx @%sHz]',
3422 disp_idx, disp.Name, disp.IsPrimary(), disp.ClientArea, disp.Geometry,
3423 disp_mode.bpp, disp_mode.Width, disp_mode.Height, disp_mode.refresh
3424 )
3425
3426 if not self.__setup_prefs_file():
3427 return False
3428
3429 gmExceptionHandlingWidgets.set_sender_email(gmPraxis.gmCurrentPraxisBranch().user_email)
3430
3431 self.__guibroker = gmGuiBroker.GuiBroker()
3432 self.__setup_platform()
3433
3434 if not self.__establish_backend_connection():
3435 return False
3436 if not self.__verify_db_account():
3437 return False
3438 if not self.__verify_praxis_branch():
3439 return False
3440
3441 self.__check_db_lang()
3442 self.__update_workplace_list()
3443
3444 if not _cfg.get(option = 'skip-update-check'):
3445 self.__check_for_updates()
3446
3447 if _cfg.get(option = 'slave'):
3448 if not self.__setup_scripting_listener():
3449 return False
3450
3451 frame = gmTopLevelFrame(None, id = -1, title = _('GNUmed client'), size = (640, 440))
3452 frame.CentreOnScreen(wx.BOTH)
3453 self.SetTopWindow(frame)
3454 frame.Show(True)
3455
3456 if _cfg.get(option = 'debug'):
3457 self.RedirectStdio()
3458 self.SetOutputWindowAttributes(title = _('GNUmed stdout/stderr window'))
3459
3460
3461 print('---=== GNUmed startup ===---')
3462 print(_('redirecting STDOUT/STDERR to this log window'))
3463 print('---=== GNUmed startup ===---')
3464
3465 self.__setup_user_activity_timer()
3466 self.__register_events()
3467
3468 wx.CallAfter(self._do_after_init)
3469
3470 return True
3471
3473 """Called internally by wxPython after EVT_CLOSE has been handled on last frame.
3474
3475 - after destroying all application windows and controls
3476 - before wx.Windows internal cleanup
3477 """
3478 _log.debug('gmApp.OnExit() start')
3479
3480 self.__shutdown_user_activity_timer()
3481
3482 if _cfg.get(option = 'debug'):
3483 self.RestoreStdio()
3484 sys.stdin = sys.__stdin__
3485 sys.stdout = sys.__stdout__
3486 sys.stderr = sys.__stderr__
3487
3488 top_wins = wx.GetTopLevelWindows()
3489 if len(top_wins) > 0:
3490 _log.debug('%s top level windows still around in <app>.OnExit()', len(top_wins))
3491 _log.debug(top_wins)
3492 for win in top_wins:
3493 _log.debug('destroying: %s', win)
3494 win.DestroyLater()
3495
3496 _log.debug('gmApp.OnExit() end')
3497 return 0
3498
3499
3501 wx.Bell()
3502 wx.Bell()
3503 wx.Bell()
3504 _log.warning('unhandled event detected: QUERY_END_SESSION')
3505 _log.info('we should be saving ourselves from here')
3506 gmLog2.flush()
3507 print('unhandled event detected: QUERY_END_SESSION')
3508
3510 wx.Bell()
3511 wx.Bell()
3512 wx.Bell()
3513 _log.warning('unhandled event detected: END_SESSION')
3514 gmLog2.flush()
3515 print('unhandled event detected: END_SESSION')
3516
3527
3529 self.user_activity_detected = True
3530 evt.Skip()
3531
3533
3534 if self.user_activity_detected:
3535 self.elapsed_inactivity_slices = 0
3536 self.user_activity_detected = False
3537 self.elapsed_inactivity_slices += 1
3538 else:
3539 if self.elapsed_inactivity_slices >= self.max_user_inactivity_slices:
3540
3541 pass
3542
3543 self.user_activity_timer.Start(oneShot = True)
3544
3545
3546
3548 self.__starting_up = False
3549
3550 self.__guibroker['horstspace.top_panel']._TCTRL_patient_selector.SetFocus()
3551 gmHooks.run_hook_script(hook = 'startup-after-GUI-init')
3552
3553
3555 self.user_activity_detected = True
3556 self.elapsed_inactivity_slices = 0
3557
3558 self.max_user_inactivity_slices = 15
3559 self.user_activity_timer = gmTimer.cTimer (
3560 callback = self._on_user_activity_timer_expired,
3561 delay = 2000
3562 )
3563 self.user_activity_timer.Start(oneShot=True)
3564
3565
3567 try:
3568 self.user_activity_timer.Stop()
3569 del self.user_activity_timer
3570 except:
3571 pass
3572
3573
3575 self.Bind(wx.EVT_QUERY_END_SESSION, self._on_query_end_session)
3576 self.Bind(wx.EVT_END_SESSION, self._on_end_session)
3577
3578
3579
3580
3581
3582 self.Bind(wx.EVT_ACTIVATE_APP, self._on_app_activated)
3583
3584 self.Bind(wx.EVT_MOUSE_EVENTS, self._on_user_activity)
3585 self.Bind(wx.EVT_KEY_DOWN, self._on_user_activity)
3586
3587
3601
3602
3615
3644
3646
3647 if not gmPraxisWidgets.set_active_praxis_branch(no_parent = True):
3648 return False
3649
3650 login = gmPG2.get_default_login()
3651 msg = '\n'
3652 msg += _('Database <%s> on <%s>') % (
3653 login.database,
3654 gmTools.coalesce(login.host, 'localhost')
3655 )
3656 msg += '\n\n'
3657
3658 praxis = gmPraxis.gmCurrentPraxisBranch()
3659 msg += _('Branch "%s" of praxis "%s"\n') % (
3660 praxis['branch'],
3661 praxis['praxis']
3662 )
3663 msg += '\n\n'
3664
3665 banner = praxis.db_logon_banner
3666 if banner.strip() == '':
3667 return True
3668 msg += banner
3669 msg += '\n\n'
3670
3671 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
3672 None,
3673 -1,
3674 caption = _('Verifying database'),
3675 question = gmTools.wrap(msg, 60, initial_indent = ' ', subsequent_indent = ' '),
3676 button_defs = [
3677 {'label': _('Connect'), 'tooltip': _('Yes, connect to this database.'), 'default': True},
3678 {'label': _('Disconnect'), 'tooltip': _('No, do not connect to this database.'), 'default': False}
3679 ]
3680 )
3681 log_on = dlg.ShowModal()
3682 dlg.DestroyLater()
3683 if log_on == wx.ID_YES:
3684 return True
3685 _log.info('user decided to not connect to this database')
3686 return False
3687
3701
3703 """Setup access to a config file for storing preferences."""
3704
3705 paths = gmTools.gmPaths(app_name = 'gnumed', wx = wx)
3706
3707 candidates = []
3708 explicit_file = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
3709 if explicit_file is not None:
3710 candidates.append(explicit_file)
3711
3712 candidates.append(os.path.join(paths.user_config_dir, 'gnumed.conf'))
3713 candidates.append(os.path.join(paths.local_base_dir, 'gnumed.conf'))
3714 candidates.append(os.path.join(paths.working_dir, 'gnumed.conf'))
3715
3716 prefs_file = None
3717 for candidate in candidates:
3718 try:
3719 open(candidate, 'a+').close()
3720 prefs_file = candidate
3721 break
3722 except IOError:
3723 continue
3724
3725 if prefs_file is None:
3726 msg = _(
3727 'Cannot find configuration file in any of:\n'
3728 '\n'
3729 ' %s\n'
3730 'You may need to use the comand line option\n'
3731 '\n'
3732 ' --conf-file=<FILE>'
3733 ) % '\n '.join(candidates)
3734 gmGuiHelpers.gm_show_error(msg, _('Checking configuration files'))
3735 return False
3736
3737 _cfg.set_option(option = 'user_preferences_file', value = prefs_file)
3738 _log.info('user preferences file: %s', prefs_file)
3739
3740 return True
3741
3743
3744 from socket import error as SocketError
3745 from Gnumed.pycommon import gmScriptingListener
3746 from Gnumed.wxpython import gmMacro
3747
3748 slave_personality = gmTools.coalesce (
3749 _cfg.get (
3750 group = 'workplace',
3751 option = 'slave personality',
3752 source_order = [
3753 ('explicit', 'return'),
3754 ('workbase', 'return'),
3755 ('user', 'return'),
3756 ('system', 'return')
3757 ]
3758 ),
3759 'gnumed-client'
3760 )
3761 _cfg.set_option(option = 'slave personality', value = slave_personality)
3762
3763
3764 port = int (
3765 gmTools.coalesce (
3766 _cfg.get (
3767 group = 'workplace',
3768 option = 'xml-rpc port',
3769 source_order = [
3770 ('explicit', 'return'),
3771 ('workbase', 'return'),
3772 ('user', 'return'),
3773 ('system', 'return')
3774 ]
3775 ),
3776 9999
3777 )
3778 )
3779 _cfg.set_option(option = 'xml-rpc port', value = port)
3780
3781 macro_executor = gmMacro.cMacroPrimitives(personality = slave_personality)
3782 global _scripting_listener
3783 try:
3784 _scripting_listener = gmScriptingListener.cScriptingListener(port = port, macro_executor = macro_executor)
3785 except SocketError as e:
3786 _log.exception('cannot start GNUmed XML-RPC server')
3787 gmGuiHelpers.gm_show_error (
3788 aMessage = (
3789 'Cannot start the GNUmed server:\n'
3790 '\n'
3791 ' [%s]'
3792 ) % e,
3793 aTitle = _('GNUmed startup')
3794 )
3795 return False
3796
3797 return True
3798
3819
3821 if gmI18N.system_locale is None or gmI18N.system_locale == '':
3822 _log.warning("system locale is undefined (probably meaning 'C')")
3823 return True
3824
3825 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': "select i18n.get_curr_lang() as lang"}])
3826 curr_db_lang = rows[0]['lang']
3827 _log.debug("current database locale: [%s]" % curr_db_lang)
3828
3829 if curr_db_lang is None:
3830
3831 cmd = 'select i18n.set_curr_lang(%s)'
3832 for lang in [gmI18N.system_locale_level['full'], gmI18N.system_locale_level['country'], gmI18N.system_locale_level['language']]:
3833 if len(lang) == 0:
3834 continue
3835 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [lang]}], return_data = True)
3836 if rows[0][0]:
3837 _log.debug("Successfully set database language to [%s]." % lang)
3838 return True
3839 _log.error('Cannot set database language to [%s].' % lang)
3840
3841 return True
3842
3843 if curr_db_lang == gmI18N.system_locale_level['full']:
3844 _log.debug('Database locale (%s) up to date.' % curr_db_lang)
3845 return True
3846 if curr_db_lang == gmI18N.system_locale_level['country']:
3847 _log.debug('Database locale (%s) matches system locale (%s) at country level.' % (curr_db_lang, gmI18N.system_locale))
3848 return True
3849 if curr_db_lang == gmI18N.system_locale_level['language']:
3850 _log.debug('Database locale (%s) matches system locale (%s) at language level.' % (curr_db_lang, gmI18N.system_locale))
3851 return True
3852
3853 _log.warning('database locale [%s] does not match system locale [%s]' % (curr_db_lang, gmI18N.system_locale))
3854
3855 sys_lang2ignore = _cfg.get (
3856 group = 'backend',
3857 option = 'ignored mismatching system locale',
3858 source_order = [('explicit', 'return'), ('local', 'return'), ('user', 'return'), ('system', 'return')]
3859 )
3860 if gmI18N.system_locale == sys_lang2ignore:
3861 _log.info('configured to ignore system-to-database locale mismatch')
3862 return True
3863
3864
3865 msg = _(
3866 "The currently selected database language ('%s') does\n"
3867 "not match the current system language ('%s').\n"
3868 "\n"
3869 "Do you want to set the database language to '%s' ?\n"
3870 ) % (curr_db_lang, gmI18N.system_locale, gmI18N.system_locale)
3871 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
3872 None,
3873 -1,
3874 caption = _('Checking database language settings'),
3875 question = msg,
3876 button_defs = [
3877 {'label': _('Set'), 'tooltip': _('Set your database language to [%s].') % gmI18N.system_locale, 'default': True},
3878 {'label': _("Don't set"), 'tooltip': _('Do not set your database language now.'), 'default': False}
3879 ],
3880 show_checkbox = True,
3881 checkbox_msg = _('Remember to ignore language mismatch'),
3882 checkbox_tooltip = _(
3883 'Checking this will make GNUmed remember your decision\n'
3884 'until the system language is changed.\n'
3885 '\n'
3886 'You can also reactivate this inquiry by removing the\n'
3887 'corresponding "ignore" option from the configuration file\n'
3888 '\n'
3889 ' [%s]'
3890 ) % _cfg.get(option = 'user_preferences_file')
3891 )
3892 decision = dlg.ShowModal()
3893 remember2ignore_this_mismatch = dlg._CHBOX_dont_ask_again.GetValue()
3894 dlg.DestroyLater()
3895
3896 if decision == wx.ID_NO:
3897 if not remember2ignore_this_mismatch:
3898 return True
3899 _log.info('User did not want to set database locale. Ignoring mismatch next time.')
3900 gmCfg2.set_option_in_INI_file (
3901 filename = _cfg.get(option = 'user_preferences_file'),
3902 group = 'backend',
3903 option = 'ignored mismatching system locale',
3904 value = gmI18N.system_locale
3905 )
3906 return True
3907
3908
3909 cmd = 'select i18n.set_curr_lang(%s)'
3910 for lang in [gmI18N.system_locale_level['full'], gmI18N.system_locale_level['country'], gmI18N.system_locale_level['language']]:
3911 if len(lang) == 0:
3912 continue
3913 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [lang]}], return_data = True)
3914 if rows[0][0]:
3915 _log.debug("Successfully set database language to [%s]." % lang)
3916 return True
3917 _log.error('Cannot set database language to [%s].' % lang)
3918
3919
3920 _log.info('forcing database language to [%s]', gmI18N.system_locale_level['country'])
3921 gmPG2.run_rw_queries(queries = [{
3922 'cmd': 'select i18n.force_curr_lang(%s)',
3923 'args': [gmI18N.system_locale_level['country']]
3924 }])
3925
3926 return True
3927
3930 try:
3931 kwargs['originated_in_database']
3932 print('==> got notification from database "%s":' % kwargs['signal'])
3933 except KeyError:
3934 print('==> received signal from client: "%s"' % kwargs['signal'])
3935
3936 del kwargs['signal']
3937 for key in kwargs:
3938
3939 try: print(' [%s]: %s' % (key, kwargs[key]))
3940 except: print('cannot print signal information')
3941
3946
3958
3963
3966
3967
3968
3969 gmDispatcher.set_main_thread_caller(wx.CallAfter)
3970
3971 if _cfg.get(option = 'debug'):
3972 gmDispatcher.connect(receiver = _signal_debugging_monitor)
3973 _log.debug('gmDispatcher signal monitor activated')
3974
3975 setup_safe_wxEndBusyCursor()
3976
3977 setup_callbacks()
3978
3979
3980
3981
3982 app = gmApp(redirect = False, clearSigInt = False)
3983 app.MainLoop()
3984
3985
3986
3987
3988 if __name__ == '__main__':
3989
3990 from GNUmed.pycommon import gmI18N
3991 gmI18N.activate_locale()
3992 gmI18N.install_domain()
3993
3994 _log.info('Starting up as main module.')
3995 main()
3996
3997
3998