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