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 < 40) 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 4.0+ with unicode support is required.')
56 print('CRITICAL ERROR: Proper wxPython version not found. Halted.')
57 raise ValueError('wxPython 4.0+ 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 date_generated = gmDateTime.pydt_now_here()
2207 )
2208
2209 try: os.remove(arriba.pdf_result)
2210 except Exception: _log.exception('cannot remove [%s]', arriba.pdf_result)
2211
2212 if doc is None:
2213 return
2214
2215 doc['comment'] = 'arriba: %s' % _('cardiovascular risk assessment')
2216 doc.save()
2217
2218 try:
2219 open(arriba.xml_result).close()
2220 part = doc.add_part(file = arriba.xml_result)
2221 except Exception:
2222 _log.exception('error accessing [%s]', arriba.xml_result)
2223 gmDispatcher.send(signal = 'statustext', msg = _('[arriba] XML result not found in [%s]') % arriba.xml_result, beep = False)
2224
2225 if part is None:
2226 return
2227
2228 part['obj_comment'] = 'XML-Daten'
2229 part['filename'] = 'arriba-result.xml'
2230 part.save()
2231
2233
2234 dbcfg = gmCfg.cCfgSQL()
2235 cmd = dbcfg.get2 (
2236 option = 'external.tools.acs_risk_calculator_cmd',
2237 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2238 bias = 'user'
2239 )
2240
2241 if cmd is None:
2242 gmDispatcher.send(signal = 'statustext', msg = _('ACS risk assessment calculator not configured.'), beep = True)
2243 return
2244
2245 cwd = os.path.expanduser(os.path.join('~', '.gnumed'))
2246 try:
2247 subprocess.check_call (
2248 args = (cmd,),
2249 close_fds = True,
2250 cwd = cwd
2251 )
2252 except (OSError, ValueError, subprocess.CalledProcessError):
2253 _log.exception('there was a problem executing [%s]', cmd)
2254 gmDispatcher.send(signal = 'statustext', msg = _('Cannot run [%s] !') % cmd, beep = True)
2255 return
2256
2257 pdfs = glob.glob(os.path.join(cwd, 'arriba-%s-*.pdf' % gmDateTime.pydt_now_here().strftime('%Y-%m-%d')))
2258 for pdf in pdfs:
2259 try:
2260 open(pdf).close()
2261 except Exception:
2262 _log.exception('error accessing [%s]', pdf)
2263 gmDispatcher.send(signal = 'statustext', msg = _('There was a problem accessing the [arriba] result in [%s] !') % pdf, beep = True)
2264 continue
2265
2266 doc = gmDocumentWidgets.save_file_as_new_document (
2267 parent = self,
2268 filename = pdf,
2269 document_type = 'risk assessment',
2270 pk_org_unit = gmPraxis.gmCurrentPraxisBranch()['pk_org_unit'],
2271 date_generated = gmDateTime.pydt_now_here()
2272 )
2273
2274 try:
2275 os.remove(pdf)
2276 except Exception:
2277 _log.exception('cannot remove [%s]', pdf)
2278
2279 if doc is None:
2280 continue
2281 doc['comment'] = 'arriba: %s' % _('cardiovascular risk assessment')
2282 doc.save()
2283
2284 return
2285
2286
2294
2297
2300
2303
2320
2321
2324
2325
2331
2332
2335
2336
2337
2338
2341
2342
2345
2346
2349
2350
2351
2352
2361
2362
2364 raise ValueError('raised ValueError to test exception handling')
2365
2366
2368 import faulthandler
2369 _log.debug('testing faulthandler via SIGSEGV')
2370 faulthandler._sigsegv()
2371
2372
2376
2377
2379 raise gmExceptions.AccessDenied (
2380 _('[-9999]: <access violation test error>'),
2381 source = 'GNUmed code',
2382 code = -9999,
2383 details = _('This is a deliberate AccessDenied exception thrown to test the handling of access violations by means of a decorator.')
2384 )
2385
2386 @gmAccessPermissionWidgets.verify_minimum_required_role('admin', activity = _('testing access check for non-existant <admin> role'))
2388 raise gmExceptions.AccessDenied (
2389 _('[-9999]: <access violation test error>'),
2390 source = 'GNUmed code',
2391 code = -9999,
2392 details = _('This is a deliberate AccessDenied exception. You should not see this message because the role is checked in a decorator.')
2393 )
2394
2396 import wx.lib.inspection
2397 wx.lib.inspection.InspectionTool().Show()
2398
2401
2404
2407
2410
2417
2421
2424
2428
2429
2431 self.StatusBar.show_history()
2432
2433
2436
2437
2440
2441
2448
2452
2454 name = os.path.basename(gmLog2._logfile_name)
2455 name, ext = os.path.splitext(name)
2456 new_name = '%s_%s%s' % (name, pyDT.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), ext)
2457 new_path = os.path.expanduser(os.path.join('~', 'gnumed'))
2458
2459 dlg = wx.FileDialog (
2460 parent = self,
2461 message = _("Save current log as..."),
2462 defaultDir = new_path,
2463 defaultFile = new_name,
2464 wildcard = "%s (*.log)|*.log" % _("log files"),
2465 style = wx.FD_SAVE
2466 )
2467 choice = dlg.ShowModal()
2468 new_name = dlg.GetPath()
2469 dlg.DestroyLater()
2470 if choice != wx.ID_OK:
2471 return True
2472
2473 _log.warning('syncing log file for backup to [%s]', new_name)
2474 gmLog2.flush()
2475 shutil.copy2(gmLog2._logfile_name, new_name)
2476 gmDispatcher.send('statustext', msg = _('Log file backed up as [%s].') % new_name)
2477
2480
2481
2484
2485
2488
2489
2492
2493
2494
2495
2497 """This is the wx.EVT_CLOSE handler.
2498
2499 - framework still functional
2500 """
2501 _log.debug('gmTopLevelFrame.OnClose() start')
2502 self._clean_exit()
2503 self.DestroyLater()
2504 _log.debug('gmTopLevelFrame.OnClose() end')
2505 return True
2506
2507
2512
2513
2521
2528
2535
2542
2552
2560
2568
2576
2584
2592
2593
2594 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2603
2604
2605 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2614
2615
2616 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2625
2626
2627 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage family history'))
2636
2637 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
2644
2645 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('calculate EDC'))
2649
2650
2651 @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage suppressed hints'))
2658
2659
2666
2667
2684
2687
2690
2691
2693 pat = gmPerson.gmCurrentPatient()
2694 if not pat.connected:
2695 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR. No active patient.'))
2696 return False
2697 from Gnumed.exporters import gmPatientExporter
2698 exporter = gmPatientExporter.cEmrExport(patient = pat)
2699 fname = gmTools.get_unique_filename(prefix = 'gm-exp-', suffix = '.txt')
2700 output_file = io.open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
2701 exporter.set_output_file(output_file)
2702 exporter.dump_constraints()
2703 exporter.dump_demographic_record(True)
2704 exporter.dump_clinical_record()
2705 exporter.dump_med_docs()
2706 output_file.close()
2707 pat.export_area.add_file(filename = fname, hint = _('EMR as text document'))
2708
2709
2727
2728
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
2836
2837
2862
2863
2888
2889
2896
2897
2899 curr_pat = gmPerson.gmCurrentPatient()
2900 if not curr_pat.connected:
2901 gmDispatcher.send(signal = 'statustext', msg = _('Cannot add tag to person. No active patient.'))
2902 return
2903
2904 tag = gmDemographicsWidgets.manage_tag_images(parent = self)
2905 if tag is None:
2906 return
2907
2908 tag = curr_pat.add_tag(tag['pk_tag_image'])
2909 msg = _('Edit the comment on tag [%s]') % tag['l10n_description']
2910 comment = wx.GetTextFromUser (
2911 message = msg,
2912 caption = _('Editing tag comment'),
2913 default_value = gmTools.coalesce(tag['comment'], ''),
2914 parent = self
2915 )
2916
2917 if comment == '':
2918 return
2919
2920 if comment.strip() == tag['comment']:
2921 return
2922
2923 if comment == ' ':
2924 tag['comment'] = None
2925 else:
2926 tag['comment'] = comment.strip()
2927
2928 tag.save()
2929
2930
2940
2941
2951
2952
2961
2962
2971
2972
2974 curr_pat = gmPerson.gmCurrentPatient()
2975 if not curr_pat.connected:
2976 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export patient as GDT. No active patient.'))
2977 return False
2978 enc = 'cp850'
2979 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'current-patient.gdt'))
2980 curr_pat.export_as_gdt(filename = fname, encoding = enc)
2981 gmDispatcher.send(signal = 'statustext', msg = _('Exported demographics to GDT file [%s].') % fname)
2982
2983
2985 curr_pat = gmPerson.gmCurrentPatient()
2986 if not curr_pat.connected:
2987 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export patient as VCARD. No active patient.'))
2988 return False
2989 fname = os.path.expanduser(os.path.join('~', 'gnumed', 'current-patient.vcf'))
2990 curr_pat.export_as_vcard(filename = fname)
2991 gmDispatcher.send(signal = 'statustext', msg = _('Exported demographics to VCARD file [%s].') % fname)
2992
2993
2996
2997
3000
3001
3004
3005
3008
3011
3019
3027
3030
3037
3041
3044
3047
3048
3051
3052
3055
3056
3059
3060
3062 """Cleanup helper.
3063
3064 - should ALWAYS be called when this program is
3065 to be terminated
3066 - ANY code that should be executed before a
3067 regular shutdown should go in here
3068 - framework still functional
3069 """
3070 _log.debug('gmTopLevelFrame._clean_exit() start')
3071
3072
3073 listener = gmBackendListener.gmBackendListener()
3074 try:
3075 listener.shutdown()
3076 except Exception:
3077 _log.exception('cannot stop backend notifications listener thread')
3078
3079
3080 if _scripting_listener is not None:
3081 try:
3082 _scripting_listener.shutdown()
3083 except Exception:
3084 _log.exception('cannot stop scripting listener thread')
3085
3086
3087 self.StatusBar.clock_update_timer.Stop()
3088 gmTimer.shutdown()
3089 gmPhraseWheel.shutdown()
3090
3091
3092 for call_back in self.__pre_exit_callbacks:
3093 try:
3094 call_back()
3095 except Exception:
3096 print('*** pre-exit callback failed ***')
3097 print('%s' % call_back)
3098 _log.exception('callback [%s] failed', call_back)
3099
3100
3101 gmDispatcher.send('application_closing')
3102
3103
3104 gmDispatcher.disconnect(self._on_set_statustext, 'statustext')
3105
3106
3107
3108 curr_width, curr_height = self.GetSize()
3109 _log.info('GUI size at shutdown: [%s:%s]' % (curr_width, curr_height))
3110 curr_pos_x, curr_pos_y = self.GetScreenPosition()
3111 _log.info('GUI position at shutdown: [%s:%s]' % (curr_pos_x, curr_pos_y))
3112 if 0 not in [curr_width, curr_height]:
3113 dbcfg = gmCfg.cCfgSQL()
3114 try:
3115 dbcfg.set (
3116 option = 'main.window.width',
3117 value = curr_width,
3118 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3119 )
3120 dbcfg.set (
3121 option = 'main.window.height',
3122 value = curr_height,
3123 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3124 )
3125 dbcfg.set (
3126 option = 'main.window.position.x',
3127 value = curr_pos_x,
3128 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3129 )
3130 dbcfg.set (
3131 option = 'main.window.position.y',
3132 value = curr_pos_y,
3133 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace
3134 )
3135 except Exception:
3136 _log.exception('cannot save current client window size and/or position')
3137
3138 if _cfg.get(option = 'debug'):
3139 print('---=== GNUmed shutdown ===---')
3140 try:
3141 print(_('You have to manually close this window to finalize shutting down GNUmed.'))
3142 print(_('This is so that you can inspect the console output at your leisure.'))
3143 except UnicodeEncodeError:
3144 print('You have to manually close this window to finalize shutting down GNUmed.')
3145 print('This is so that you can inspect the console output at your leisure.')
3146 print('---=== GNUmed shutdown ===---')
3147
3148
3149 gmExceptionHandlingWidgets.uninstall_wx_exception_handler()
3150
3151
3152 import threading
3153 _log.debug("%s active threads", threading.activeCount())
3154 for t in threading.enumerate():
3155 _log.debug('thread %s', t)
3156 if t.name == 'MainThread':
3157 continue
3158 print('GNUmed: waiting for thread [%s] to finish' % t.name)
3159
3160 _log.debug('gmTopLevelFrame._clean_exit() end')
3161
3162
3163
3164
3166 if _cfg.get(option = 'slave'):
3167 self.__title_template = '%s%s: %%(pat)s [%%(prov)s@%%(wp)s in %%(site)s of %%(prax)s] (%s:%s)' % (
3168 gmTools._GM_TITLE_PREFIX,
3169 gmTools.u_chain,
3170 _cfg.get(option = 'slave personality'),
3171 _cfg.get(option = 'xml-rpc port')
3172 )
3173 else:
3174 self.__title_template = '%s: %%(pat)s [%%(prov)s@%%(wp)s in %%(site)s of %%(prax)s]' % gmTools._GM_TITLE_PREFIX
3175
3176
3178 """Update title of main window based on template.
3179
3180 This gives nice tooltips on iconified GNUmed instances.
3181
3182 User research indicates that in the title bar people want
3183 the date of birth, not the age, so please stick to this
3184 convention.
3185 """
3186 args = {}
3187
3188 pat = gmPerson.gmCurrentPatient()
3189 if pat.connected:
3190 args['pat'] = '%s %s %s (%s) #%d' % (
3191 gmTools.coalesce(pat['title'], '', '%.4s'),
3192 pat['firstnames'],
3193 pat['lastnames'],
3194 pat.get_formatted_dob(format = '%Y %b %d'),
3195 pat['pk_identity']
3196 )
3197 else:
3198 args['pat'] = _('no patient')
3199
3200 args['prov'] = '%s%s.%s' % (
3201 gmTools.coalesce(_provider['title'], '', '%s '),
3202 _provider['firstnames'][:1],
3203 _provider['lastnames']
3204 )
3205
3206 praxis = gmPraxis.gmCurrentPraxisBranch()
3207 args['wp'] = praxis.active_workplace
3208 args['site'] = praxis['branch']
3209 args['prax'] = praxis['praxis']
3210
3211 self.SetTitle(self.__title_template % args)
3212
3213
3221
3222
3227
3228
3230 """Lock GNUmed client against unauthorized access"""
3231
3232
3233
3234 return
3235
3236
3238 """Unlock the main notebook widgets
3239 As long as we are not logged into the database backend,
3240 all pages but the 'login' page of the main notebook widget
3241 are locked; i.e. not accessible by the user
3242 """
3243
3244
3245
3246
3247
3248 return
3249
3251 wx.LayoutAlgorithm().LayoutWindow(self.LayoutMgr, self.nb)
3252
3255
3257 try:
3258 kwargs['style'] = kwargs['style'] | wx.STB_SIZEGRIP | wx.STB_SHOW_TIPS
3259 except KeyError:
3260 kwargs['style'] = wx.STB_SIZEGRIP | wx.STB_SHOW_TIPS
3261 super().__init__(*args, **kwargs)
3262
3263 self.FieldsCount = 2
3264 self.SetStatusWidths([-1, 225])
3265
3266 self.__msg_fifo = []
3267 self.__normal_background_colour = self.GetBackgroundColour()
3268 self.__blink_background_color = 'yellow'
3269 self.__times_to_blink = 0
3270 self.__blink_counter = 0
3271
3272 self.clock_update_timer = wx.PyTimer(self._cb_update_clock)
3273 self.clock_update_timer.Start(milliseconds = 1000)
3274
3275 self.Bind(wx.EVT_LEFT_DCLICK, self._on_show_history)
3276
3277
3278 - def SetStatusText(self, text, i=0, beep=False):
3279 prev = self.previous_text
3280 msg = self.__update_history(text, i)
3281 super().SetStatusText(msg, i)
3282 if beep:
3283 wx.Bell()
3284 self.__initiate_blinking(text, i, prev)
3285
3286
3287 - def PushStatusText(self, text, field=0):
3288 prev = self.previous_text
3289 msg = self.__update_history(text, field)
3290 super().PushStatusText(msg, field)
3291 self.__initiate_blinking(text, i, prev)
3292
3293
3296
3297
3298 - def show_history(self):
3299 lines = []
3300 for entry in self.__msg_fifo:
3301 lines.append('%s (%s)' % (entry['text'], ','.join(entry['timestamps'])))
3302 gmGuiHelpers.gm_show_info (
3303 title = _('Statusbar history'),
3304 info = _(
3305 '%s - now\n'
3306 '\n'
3307 '%s'
3308 ) % (
3309 gmDateTime.pydt_now_here().strftime('%H:%M'),
3310 '\n'.join(lines)
3311 )
3312 )
3313
3314
3315
3316
3318 """Advances date and local time in the second slot.
3319
3320 Also drives blinking activity.
3321 """
3322 t = time.localtime(time.time())
3323 st = time.strftime('%Y %b %d %H:%M:%S', t)
3324 self.SetStatusText(st, 1)
3325 if self.__times_to_blink > 0:
3326 self.__perhaps_blink()
3327
3328
3330 if self.__blink_counter > self.__times_to_blink:
3331 self.set_normal_color()
3332 self.__times_to_blink = 0
3333 self.__blink_counter = 0
3334 return
3335
3336 if self.SetBackgroundColour(self.__blink_background_color):
3337 self.__blink_counter += 1
3338 return
3339
3340 self.set_normal_color()
3341
3342
3344 if field != 0:
3345 return
3346 text = text.strip()
3347 if text == '':
3348 return
3349 if text == previous_text:
3350 return
3351 self.__blink_counter = 0
3352 self.__times_to_blink = 2
3353
3354
3356 if len(self.__msg_fifo) == 0:
3357 return None
3358 return self.__msg_fifo[0]['text']
3359
3360 previous_text = property(_get_previous_text)
3361
3362
3363 - def __update_history(self, text, field):
3364 if field > 0:
3365 return text
3366
3367 text = text.strip()
3368 if text == '':
3369 return text
3370
3371 now = gmDateTime.pydt_now_here().strftime('%H:%M')
3372 if len(self.__msg_fifo) == 0:
3373 self.__msg_fifo.append({'text': text, 'timestamps': [now]})
3374 return '%s %s' % (now, text)
3375
3376 last = self.__msg_fifo[0]
3377 if text == last['text']:
3378 last['timestamps'].insert(0, now)
3379 return '%s %s (#%s)' % (now, text, len(last['timestamps']))
3380
3381 self.__msg_fifo.insert(0, {'text': text, 'timestamps': [now]})
3382 if len(self.__msg_fifo) > 20:
3383 self.__msg_fifo = self.__msg_fifo[:20]
3384 return '%s %s' % (now, text)
3385
3386
3387 - def _on_show_history(self, evt):
3389
3390
3392 print('----------------------------------')
3393 print('Statusbar history @ [%s]:' % gmDateTime.pydt_now_here().strftime('%H:%M'))
3394 print('\n'.join(self.__msg_fifo))
3395 print('----------------------------------')
3396
3397
3398 - def _on_print_history(self, evt):
3399 evt.Skip()
3400 self.__print_msg_fifo()
3401
3402
3403 -class gmApp(wx.App):
3404
3406
3407 if _cfg.get(option = 'debug'):
3408 self.SetAssertMode(wx.APP_ASSERT_EXCEPTION | wx.APP_ASSERT_LOG)
3409 else:
3410 self.SetAssertMode(wx.APP_ASSERT_SUPPRESS)
3411
3412 self.__starting_up = True
3413
3414
3415 wx.ToolTip.SetAutoPop(4000)
3416
3417 gmExceptionHandlingWidgets.install_wx_exception_handler()
3418 gmExceptionHandlingWidgets.set_client_version(_cfg.get(option = 'client_version'))
3419
3420 self.SetAppName('gnumed')
3421 self.SetVendorName('gnumed_community')
3422 try:
3423 self.SetAppDisplayName('GNUmed %s' % _cfg.get(option = 'client_version'))
3424 except AttributeError:
3425 _log.info('SetAppDisplayName() not supported')
3426 try:
3427 self.SetVendorDisplayName('The GNUmed Development Community.')
3428 except AttributeError:
3429 _log.info('SetVendorDisplayName() not supported')
3430 paths = gmTools.gmPaths(app_name = 'gnumed', wx = wx)
3431 paths.init_paths(wx = wx, app_name = 'gnumed')
3432
3433
3434 dw, dh = wx.DisplaySize()
3435 _log.info('display size: %s:%s' % (wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X), wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)))
3436 _log.debug('display size: %s:%s %s mm', dw, dh, wx.DisplaySizeMM())
3437 for disp_idx in range(wx.Display.GetCount()):
3438 disp = wx.Display(disp_idx)
3439 disp_mode = disp.CurrentMode
3440 _log.debug('display [%s] "%s": primary=%s, client_area=%s, geom=%s, vid_mode=[%sbpp across %sx%spx @%sHz]',
3441 disp_idx, disp.Name, disp.IsPrimary(), disp.ClientArea, disp.Geometry,
3442 disp_mode.bpp, disp_mode.Width, disp_mode.Height, disp_mode.refresh
3443 )
3444
3445 if not self.__setup_prefs_file():
3446 return False
3447
3448 gmExceptionHandlingWidgets.set_sender_email(gmPraxis.gmCurrentPraxisBranch().user_email)
3449
3450 self.__guibroker = gmGuiBroker.GuiBroker()
3451 self.__setup_platform()
3452
3453 if not self.__establish_backend_connection():
3454 return False
3455 if not self.__verify_db_account():
3456 return False
3457 if not self.__verify_praxis_branch():
3458 return False
3459
3460 self.__check_db_lang()
3461 self.__update_workplace_list()
3462
3463 if not _cfg.get(option = 'skip-update-check'):
3464 self.__check_for_updates()
3465
3466 if _cfg.get(option = 'slave'):
3467 if not self.__setup_scripting_listener():
3468 return False
3469
3470 frame = gmTopLevelFrame(None, id = -1, title = _('GNUmed client'), size = (640, 440))
3471 frame.CentreOnScreen(wx.BOTH)
3472 self.SetTopWindow(frame)
3473 frame.Show(True)
3474
3475 if _cfg.get(option = 'debug'):
3476 self.RedirectStdio()
3477 self.SetOutputWindowAttributes(title = _('GNUmed stdout/stderr window'))
3478
3479
3480 print('---=== GNUmed startup ===---')
3481 print(_('redirecting STDOUT/STDERR to this log window'))
3482 print('---=== GNUmed startup ===---')
3483
3484 self.__setup_user_activity_timer()
3485 self.__register_events()
3486
3487 wx.CallAfter(self._do_after_init)
3488
3489 return True
3490
3492 """Called internally by wxPython after EVT_CLOSE has been handled on last frame.
3493
3494 - after destroying all application windows and controls
3495 - before wx.Windows internal cleanup
3496 """
3497 _log.debug('gmApp.OnExit() start')
3498
3499 self.__shutdown_user_activity_timer()
3500
3501 if _cfg.get(option = 'debug'):
3502 self.RestoreStdio()
3503 sys.stdin = sys.__stdin__
3504 sys.stdout = sys.__stdout__
3505 sys.stderr = sys.__stderr__
3506
3507 top_wins = wx.GetTopLevelWindows()
3508 if len(top_wins) > 0:
3509 _log.debug('%s top level windows still around in <app>.OnExit()', len(top_wins))
3510 _log.debug(top_wins)
3511 for win in top_wins:
3512 _log.debug('destroying: %s', win)
3513 win.DestroyLater()
3514
3515 _log.debug('gmApp.OnExit() end')
3516 return 0
3517
3518
3520 wx.Bell()
3521 wx.Bell()
3522 wx.Bell()
3523 _log.warning('unhandled event detected: QUERY_END_SESSION')
3524 _log.info('we should be saving ourselves from here')
3525 gmLog2.flush()
3526 print('unhandled event detected: QUERY_END_SESSION')
3527
3529 wx.Bell()
3530 wx.Bell()
3531 wx.Bell()
3532 _log.warning('unhandled event detected: END_SESSION')
3533 gmLog2.flush()
3534 print('unhandled event detected: END_SESSION')
3535
3546
3548 self.user_activity_detected = True
3549 evt.Skip()
3550
3552
3553 if self.user_activity_detected:
3554 self.elapsed_inactivity_slices = 0
3555 self.user_activity_detected = False
3556 self.elapsed_inactivity_slices += 1
3557 else:
3558 if self.elapsed_inactivity_slices >= self.max_user_inactivity_slices:
3559
3560 pass
3561
3562 self.user_activity_timer.Start(oneShot = True)
3563
3564
3565
3567 self.__starting_up = False
3568
3569 self.__guibroker['horstspace.top_panel']._TCTRL_patient_selector.SetFocus()
3570 gmHooks.run_hook_script(hook = 'startup-after-GUI-init')
3571
3572
3574 self.user_activity_detected = True
3575 self.elapsed_inactivity_slices = 0
3576
3577 self.max_user_inactivity_slices = 15
3578 self.user_activity_timer = gmTimer.cTimer (
3579 callback = self._on_user_activity_timer_expired,
3580 delay = 2000
3581 )
3582 self.user_activity_timer.Start(oneShot=True)
3583
3584
3586 try:
3587 self.user_activity_timer.Stop()
3588 del self.user_activity_timer
3589 except Exception:
3590 pass
3591
3592
3594 self.Bind(wx.EVT_QUERY_END_SESSION, self._on_query_end_session)
3595 self.Bind(wx.EVT_END_SESSION, self._on_end_session)
3596
3597
3598
3599
3600
3601 self.Bind(wx.EVT_ACTIVATE_APP, self._on_app_activated)
3602
3603 self.Bind(wx.EVT_MOUSE_EVENTS, self._on_user_activity)
3604 self.Bind(wx.EVT_KEY_DOWN, self._on_user_activity)
3605
3606
3620
3621
3635
3636
3665
3666
3668
3669 if not gmPraxisWidgets.set_active_praxis_branch(no_parent = True):
3670 _log.debug('failed to activate branch')
3671 return False
3672
3673 _log.debug('activated branch')
3674
3675 creds = gmConnectionPool.gmConnectionPool().credentials
3676 msg = '\n'
3677 msg += _('Database <%s> on <%s>') % (
3678 creds.database,
3679 gmTools.coalesce(creds.host, 'localhost')
3680 )
3681 msg += '\n\n'
3682
3683 _log.debug('getting branch')
3684
3685 praxis = gmPraxis.gmCurrentPraxisBranch()
3686 msg += _('Branch "%s" of praxis "%s"\n') % (
3687 praxis['branch'],
3688 praxis['praxis']
3689 )
3690 msg += '\n\n'
3691
3692 _log.debug('getting banner')
3693
3694 banner = praxis.db_logon_banner
3695 _log.debug('got banner')
3696 if banner.strip() == '':
3697 _log.debug('no banner')
3698 return True
3699 msg += banner
3700 msg += '\n\n'
3701
3702 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
3703 None,
3704 -1,
3705 caption = _('Verifying database'),
3706 question = gmTools.wrap(msg, 60, initial_indent = ' ', subsequent_indent = ' '),
3707 button_defs = [
3708 {'label': _('Connect'), 'tooltip': _('Yes, connect to this database.'), 'default': True},
3709 {'label': _('Disconnect'), 'tooltip': _('No, do not connect to this database.'), 'default': False}
3710 ]
3711 )
3712
3713 _log.debug('showing banner')
3714
3715 log_on = dlg.ShowModal()
3716
3717 _log.debug('scheduling dlg destruction')
3718
3719 dlg.DestroyLater()
3720
3721 if log_on == wx.ID_YES:
3722 _log.debug('returning for logon')
3723 return True
3724
3725 _log.info('user decided to not connect to this database')
3726 return False
3727
3728
3742
3743
3745 """Setup access to a config file for storing preferences."""
3746
3747 paths = gmTools.gmPaths(app_name = 'gnumed', wx = wx)
3748
3749 candidates = []
3750 explicit_file = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
3751 if explicit_file is not None:
3752 candidates.append(explicit_file)
3753
3754 candidates.append(os.path.join(paths.user_config_dir, 'gnumed.conf'))
3755 candidates.append(os.path.join(paths.local_base_dir, 'gnumed.conf'))
3756 candidates.append(os.path.join(paths.working_dir, 'gnumed.conf'))
3757
3758 prefs_file = None
3759 for candidate in candidates:
3760 try:
3761 open(candidate, 'a+').close()
3762 prefs_file = candidate
3763 break
3764 except IOError:
3765 continue
3766
3767 if prefs_file is None:
3768 msg = _(
3769 'Cannot find configuration file in any of:\n'
3770 '\n'
3771 ' %s\n'
3772 'You may need to use the comand line option\n'
3773 '\n'
3774 ' --conf-file=<FILE>'
3775 ) % '\n '.join(candidates)
3776 gmGuiHelpers.gm_show_error(msg, _('Checking configuration files'))
3777 return False
3778
3779 _cfg.set_option(option = 'user_preferences_file', value = prefs_file)
3780 _log.info('user preferences file: %s', prefs_file)
3781
3782 return True
3783
3784
3786
3787 from socket import error as SocketError
3788 from Gnumed.pycommon import gmScriptingListener
3789 from Gnumed.wxpython import gmMacro
3790
3791 slave_personality = gmTools.coalesce (
3792 _cfg.get (
3793 group = 'workplace',
3794 option = 'slave personality',
3795 source_order = [
3796 ('explicit', 'return'),
3797 ('workbase', 'return'),
3798 ('user', 'return'),
3799 ('system', 'return')
3800 ]
3801 ),
3802 'gnumed-client'
3803 )
3804 _cfg.set_option(option = 'slave personality', value = slave_personality)
3805
3806
3807 port = int (
3808 gmTools.coalesce (
3809 _cfg.get (
3810 group = 'workplace',
3811 option = 'xml-rpc port',
3812 source_order = [
3813 ('explicit', 'return'),
3814 ('workbase', 'return'),
3815 ('user', 'return'),
3816 ('system', 'return')
3817 ]
3818 ),
3819 9999
3820 )
3821 )
3822 _cfg.set_option(option = 'xml-rpc port', value = port)
3823
3824 macro_executor = gmMacro.cMacroPrimitives(personality = slave_personality)
3825 global _scripting_listener
3826 try:
3827 _scripting_listener = gmScriptingListener.cScriptingListener(port = port, macro_executor = macro_executor)
3828 except SocketError as e:
3829 _log.exception('cannot start GNUmed XML-RPC server')
3830 gmGuiHelpers.gm_show_error (
3831 aMessage = (
3832 'Cannot start the GNUmed server:\n'
3833 '\n'
3834 ' [%s]'
3835 ) % e,
3836 aTitle = _('GNUmed startup')
3837 )
3838 return False
3839
3840 return True
3841
3842
3863
3864
3866 if gmI18N.system_locale is None or gmI18N.system_locale == '':
3867 _log.warning("system locale is undefined (probably meaning 'C')")
3868 return True
3869
3870 curr_db_lang = gmPG2.get_current_user_language()
3871 _log.debug('current user language in DB: %s', curr_db_lang)
3872 if curr_db_lang is None:
3873 _log.info('no language selected in DB, trying to set')
3874 for lang in [gmI18N.system_locale_level['full'], gmI18N.system_locale_level['country'], gmI18N.system_locale_level['language']]:
3875 if len(lang) == 0:
3876 continue
3877 if gmPG2.set_user_language(language = lang):
3878 _log.debug("Successfully set database language to [%s]." % lang)
3879 return True
3880
3881 _log.error('Cannot set database language to [%s].' % lang)
3882 return True
3883
3884 if curr_db_lang == gmI18N.system_locale_level['full']:
3885 _log.debug('Database locale (%s) up to date.' % curr_db_lang)
3886 return True
3887
3888 if curr_db_lang == gmI18N.system_locale_level['country']:
3889 _log.debug('Database locale (%s) matches system locale (%s) at country level.' % (curr_db_lang, gmI18N.system_locale))
3890 return True
3891
3892 if curr_db_lang == gmI18N.system_locale_level['language']:
3893 _log.debug('Database locale (%s) matches system locale (%s) at language level.' % (curr_db_lang, gmI18N.system_locale))
3894 return True
3895
3896 _log.warning('database locale [%s] does not match system locale [%s]' % (curr_db_lang, gmI18N.system_locale))
3897 sys_lang2ignore = _cfg.get (
3898 group = 'backend',
3899 option = 'ignored mismatching system locale',
3900 source_order = [('explicit', 'return'), ('local', 'return'), ('user', 'return'), ('system', 'return')]
3901 )
3902 if gmI18N.system_locale == sys_lang2ignore:
3903 _log.info('configured to ignore system-to-database locale mismatch')
3904 return True
3905
3906
3907 msg = _(
3908 "The currently selected database language ('%s') does\n"
3909 "not match the current system language ('%s').\n"
3910 "\n"
3911 "Do you want to set the database language to '%s' ?\n"
3912 ) % (curr_db_lang, gmI18N.system_locale, gmI18N.system_locale)
3913 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
3914 None,
3915 -1,
3916 caption = _('Checking database language settings'),
3917 question = msg,
3918 button_defs = [
3919 {'label': _('Set'), 'tooltip': _('Set your database language to [%s].') % gmI18N.system_locale, 'default': True},
3920 {'label': _("Don't set"), 'tooltip': _('Do not set your database language now.'), 'default': False}
3921 ],
3922 show_checkbox = True,
3923 checkbox_msg = _('Remember to ignore language mismatch'),
3924 checkbox_tooltip = _(
3925 'Checking this will make GNUmed remember your decision\n'
3926 'until the system language is changed.\n'
3927 '\n'
3928 'You can also reactivate this inquiry by removing the\n'
3929 'corresponding "ignore" option from the configuration file\n'
3930 '\n'
3931 ' [%s]'
3932 ) % _cfg.get(option = 'user_preferences_file')
3933 )
3934 decision = dlg.ShowModal()
3935 remember2ignore_this_mismatch = dlg._CHBOX_dont_ask_again.GetValue()
3936 dlg.DestroyLater()
3937 if decision == wx.ID_NO:
3938 if not remember2ignore_this_mismatch:
3939 return True
3940 _log.info('User did not want to set database locale. Ignoring mismatch next time.')
3941 gmCfg2.set_option_in_INI_file (
3942 filename = _cfg.get(option = 'user_preferences_file'),
3943 group = 'backend',
3944 option = 'ignored mismatching system locale',
3945 value = gmI18N.system_locale
3946 )
3947 return True
3948
3949 for lang in [gmI18N.system_locale_level['full'], gmI18N.system_locale_level['country'], gmI18N.system_locale_level['language']]:
3950 if len(lang) == 0:
3951 continue
3952 if gmPG2.set_user_language(language = lang):
3953 _log.debug("Successfully set database language to [%s]." % lang)
3954 return True
3955
3956 _log.error('Cannot set database language to [%s].' % lang)
3957
3958 _log.info('forcing database language to [%s]', gmI18N.system_locale_level['country'])
3959 gmPG2.force_user_language(language = gmI18N.system_locale_level['country'])
3960 return True
3961
3964 try:
3965 kwargs['originated_in_database']
3966 print('==> got notification from database "%s":' % kwargs['signal'])
3967 except KeyError:
3968 print('==> received signal from client: "%s"' % kwargs['signal'])
3969
3970 del kwargs['signal']
3971 for key in kwargs:
3972
3973 try: print(' [%s]: %s' % (key, kwargs[key]))
3974 except Exception: print('cannot print signal information')
3975
3980
3992
3997
4000
4001
4002
4003 gmDispatcher.set_main_thread_caller(wx.CallAfter)
4004
4005 if _cfg.get(option = 'debug'):
4006 gmDispatcher.connect(receiver = _signal_debugging_monitor)
4007 _log.debug('gmDispatcher signal monitor activated')
4008
4009 setup_safe_wxEndBusyCursor()
4010
4011 setup_callbacks()
4012
4013
4014
4015
4016 app = gmApp(redirect = False, clearSigInt = False)
4017 app.MainLoop()
4018
4019
4020
4021
4022 if __name__ == '__main__':
4023
4024 from GNUmed.pycommon import gmI18N
4025 gmI18N.activate_locale()
4026 gmI18N.install_domain()
4027
4028 _log.info('Starting up as main module.')
4029 main()
4030
4031
4032