1 """GNUmed hooks framework.
2
3 This module provides convenience functions and definitions
4 for accessing the GNUmed hooks framework.
5
6 This framework calls the script
7
8 ~/.gnumed/scripts/hook_script.py
9
10 at various times during client execution. The script must
11 contain a function
12
13 def run_script(hook=None):
14 pass
15
16 which accepts a single argument <hook>. That argument will
17 contain the hook that is being activated.
18 """
19
20 __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>"
21 __license__ = "GPL v2 or later (details at http://www.gnu.org)"
22
23
24 import os
25 import sys
26 import stat
27 import logging
28 import io
29
30
31
32 if __name__ == '__main__':
33 sys.path.insert(0, '../../')
34 from Gnumed.pycommon import gmDispatcher
35 from Gnumed.pycommon import gmTools
36
37
38 _log = logging.getLogger('gm.hook')
39
40
41 known_hooks = [
42 'post_patient_activation',
43 'post_person_creation',
44
45 'after_waiting_list_modified',
46
47 'shutdown-post-GUI',
48 'startup-after-GUI-init',
49 'startup-before-GUI',
50
51 'request_user_attention',
52 'app_activated_startup',
53 'app_activated',
54 'app_deactivated',
55
56 'after_substance_intake_modified',
57 'after_test_result_modified',
58 'after_soap_modified',
59 'after_code_link_modified',
60
61 'after_new_doc_created',
62 'before_print_doc',
63 'before_fax_doc',
64 'before_mail_doc',
65 'before_print_doc_part',
66 'before_fax_doc_part',
67 'before_mail_doc_part',
68 'before_external_doc_access',
69
70 'db_maintenance_warning'
71 ]
72
73
74 README_pat_dir = """Directory for data files containing the current patient.
75
76 Whenever the patient is changed GNUmed will export
77 formatted demographics into files in this directory
78 for use by 3rd party software.
79
80 Currently exported formats:
81
82 GDT, VCF (vcard), MCF (mecard), XML (linuxmednews)"""
83
84
85 HOOK_SCRIPT_EXAMPLE = """#!/usr/bin/python3
86 # -*- coding: utf-8 -*-
87 #
88 #===========================================================================
89 #
90 # Example script to be run off GNUmed hooks.
91 #
92 # It can print a message to stdout whenever any hook is invoked.
93 #
94 # Copy this file to ~/.gnumed/scripts/hook_script.py and modify as needed.
95 #
96 # Known hooks:
97 #
98 # %s
99 #
100 #===========================================================================
101 # SPDX-License-Identifier: GPL-2.0-or-later
102 __license__ = "GPL v2 or later (details at http://www.gnu.org)"
103 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
104
105
106 import os
107
108
109 from Gnumed.pycommon import gmWorkerThread
110 from Gnumed.pycommon import gmTools
111
112 from Gnumed.business import gmPerson
113
114 from Gnumed.wxpython import gmGuiHelpers
115 from Gnumed.wxpython import gmPatSearchWidgets
116
117
118 PAT_DIR = os.path.expanduser(os.path.join('~', '.gnumed', 'current_patient'))
119 README_pat_dir = \"\"\"%s
120 \"\"\"
121 CURR_PAT = None
122
123 #===========================================================================
124 def on_startup_after_GUI_init():
125 # examine external patient sources
126 gmPatSearchWidgets.get_person_from_external_sources(search_immediately = False, activate_immediately = True)
127
128
129 def request_user_attention():
130 # signal user to look at GNUmed
131 gmGuiHelpers.gm_show_info(_('Hey, GNUmed wants you to take a look at it !'))
132
133
134 #def on_app_activated_startup():
135 #pass
136
137
138 def on_app_activated():
139 # might want to look at external sources again
140 gmPatSearchWidgets.get_person_from_external_sources(search_immediately = False, activate_immediately = True)
141
142
143 def _export_patient_demographics():
144 if CURR_PAT is None:
145 return
146
147 fname = os.path.join(PAT_DIR, 'patient')
148 CURR_PAT.export_as_gdt(filename = fname + '.gdt', encoding = 'cp850')
149 CURR_PAT.export_as_xml_linuxmednews(filename = fname + '.xml')
150 CURR_PAT.export_as_vcard(filename = fname + '.vcf')
151 CURR_PAT.export_as_mecard(filename = fname + '.mcf')
152
153
154 #def on_app_deactivated():
155 def on_post_patient_activation():
156 # might want to export the active patient into an xDT file
157 global CURR_PAT
158 curr_pat = gmPerson.gmCurrentPatient()
159 if not curr_pat.connected:
160 CURR_PAT = None
161 return
162
163 CURR_PAT = curr_pat
164 gmWorkerThread.execute_in_worker_thread (
165 payload_function = _export_patient_demographics,
166 worker_name = 'current_patient_demographics_exporter'
167 )
168
169
170 #===========================================================================
171 gmTools.mkdir(PAT_DIR)
172 gmTools.create_directory_description_file(directory = PAT_DIR, readme = README_pat_dir)
173
174 # main entry point
175 def run_script(hook=None):
176
177 if hook is None:
178 hook = _('no hook specified, please report bug')
179 print('hook_script.py::run_script():', hook)
180
181 #print('GNUmed invoked the hook:', hook)
182
183 # a few examples:
184
185 #if hook == 'startup-after-GUI-init':
186 # on_startup_after_GUI_init()
187
188 #if hook == 'request_user_attention':
189 # on_request_user_attention()
190
191 #if hook == 'app_activated_startup':
192 # on_app_activated_startup()
193
194 #if hook == 'app_activated':
195 # on_app_activated()
196
197 #if hook == 'app_deactivated':
198 # on_app_deactivated()
199
200 if hook == 'post_patient_activation':
201 on_post_patient_activation()
202
203 return
204 """ % (
205 '\n#\t'.join(known_hooks),
206 README_pat_dir
207 )
208
209
210
211
212
213 HOOK_SCRIPT_NAME = 'hook_script.py'
214 HOOK_SCRIPT_DIR = os.path.expanduser(os.path.join('~', '.gnumed', 'scripts'))
215 HOOK_SCRIPT_FULL_NAME = os.path.join(HOOK_SCRIPT_DIR, HOOK_SCRIPT_NAME)
216
217
218 README_hook_dir = """Directory for scripts called from hooks at client runtime.
219
220 Known hooks:
221
222 %s
223
224 You can use <%s.example> as a script template.
225 """ % (
226 '\n\t'.join(known_hooks),
227 HOOK_SCRIPT_NAME
228 )
229
230
231
244
245
246 hook_module = None
247
249
250 global hook_module
251 if not reimport:
252 if hook_module is not None:
253 return True
254
255 if not os.access(HOOK_SCRIPT_FULL_NAME, os.F_OK):
256 _log.warning('creating default hook script')
257 f = io.open(HOOK_SCRIPT_FULL_NAME, mode = 'wt', encoding = 'utf8')
258 f.write("""
259 # known hooks:
260 # %s
261
262 def run_script(hook=None):
263 pass
264 """ % '\n#\t'.join(known_hooks))
265 f.close()
266 os.chmod(HOOK_SCRIPT_FULL_NAME, 384)
267
268 if os.path.islink(HOOK_SCRIPT_FULL_NAME):
269 gmDispatcher.send (
270 signal = 'statustext',
271 msg = _('Script must not be a link: [%s].') % HOOK_SCRIPT_FULL_NAME
272 )
273 return False
274
275 if not os.access(HOOK_SCRIPT_FULL_NAME, os.R_OK):
276 gmDispatcher.send (
277 signal = 'statustext',
278 msg = _('Script must be readable by the calling user: [%s].') % HOOK_SCRIPT_FULL_NAME
279 )
280 return False
281
282 script_stat_val = os.stat(HOOK_SCRIPT_FULL_NAME)
283 _log.debug('hook script stat(): %s', script_stat_val)
284 script_perms = stat.S_IMODE(script_stat_val.st_mode)
285 _log.debug('hook script mode: %s (oktal: %s)', script_perms, oct(script_perms))
286 if script_perms != 384:
287 if os.name in ['nt']:
288 _log.warning('this platform does not support os.stat() file permission checking')
289 else:
290 gmDispatcher.send (
291 signal = 'statustext',
292 msg = _('Script must be readable by the calling user only (permissions "0600"): [%s].') % HOOK_SCRIPT_FULL_NAME
293 )
294 return False
295
296 try:
297 tmp = gmTools.import_module_from_directory(HOOK_SCRIPT_DIR, HOOK_SCRIPT_NAME)
298 except Exception:
299 _log.exception('cannot import hook script')
300 return False
301
302 hook_module = tmp
303
304
305
306 _log.info('hook script: %s', HOOK_SCRIPT_FULL_NAME)
307 return True
308
309
310 __current_hook_stack = []
311
313
314
315 _log.info('told to pull hook [%s]', hook)
316
317 if hook not in known_hooks:
318 raise ValueError('run_hook_script(): unknown hook [%s]' % hook)
319
320 if not import_hook_module(reimport = False):
321 _log.debug('cannot import hook module, not pulling hook')
322 return False
323
324 if hook in __current_hook_stack:
325 _log.error('hook-code cycle detected, aborting')
326 _log.error('current hook stack: %s', __current_hook_stack)
327 return False
328
329 __current_hook_stack.append(hook)
330
331 try:
332 hook_module.run_script(hook = hook)
333 except Exception:
334 _log.exception('error running hook script for [%s]', hook)
335 gmDispatcher.send (
336 signal = 'statustext',
337 msg = _('Error running hook [%s] script.') % hook,
338 beep = True
339 )
340 if __current_hook_stack[-1] != hook:
341 _log.error('hook nesting errror detected')
342 _log.error('latest hook: expected [%s], found [%s]', hook, __current_hook_stack[-1])
343 _log.error('current hook stack: %s', __current_hook_stack)
344 else:
345 __current_hook_stack.pop()
346 return False
347
348 if __current_hook_stack[-1] != hook:
349 _log.error('hook nesting errror detected')
350 _log.error('latest hook: expected [%s], found [%s]', hook, __current_hook_stack[-1])
351 _log.error('current hook stack: %s', __current_hook_stack)
352 else:
353 __current_hook_stack.pop()
354
355 return True
356
357
358
359 setup_hook_dir()
360
361 if __name__ == '__main__':
362
363 if len(sys.argv) < 2:
364 sys.exit()
365
366 if sys.argv[1] != 'test':
367 sys.exit()
368
369 run_hook_script(hook = 'shutdown-post-GUI')
370 run_hook_script(hook = 'post_patient_activation')
371 run_hook_script(hook = 'invalid hook')
372
373
374