| Home | Trees | Indices | Help |
|
|---|
|
|
1 # coding: latin-1
2 """GNUmed quick person search widgets.
3
4 This widget allows to search for persons based on the
5 critera name, date of birth and person ID. It goes to
6 considerable lengths to understand the user's intent from
7 her input. For that to work well we need per-culture
8 query generators. However, there's always the fallback
9 generator.
10 """
11 #============================================================
12 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
13 __license__ = 'GPL v2 or later (for details see http://www.gnu.org/)'
14
15 import sys, os.path, glob, re as regex, logging
16
17
18 import wx
19
20
21 if __name__ == '__main__':
22 sys.path.insert(0, '../../')
23 from Gnumed.pycommon import gmLog2
24 from Gnumed.pycommon import gmDispatcher
25 from Gnumed.pycommon import gmDateTime
26 from Gnumed.pycommon import gmTools
27 from Gnumed.pycommon import gmPG2
28 from Gnumed.pycommon import gmI18N
29 from Gnumed.pycommon import gmCfg
30 from Gnumed.pycommon import gmMatchProvider
31 from Gnumed.pycommon import gmCfg2
32 from Gnumed.pycommon import gmNetworkTools
33
34 from Gnumed.business import gmPerson
35 from Gnumed.business import gmStaff
36 from Gnumed.business import gmKVK
37 from Gnumed.business import gmSurgery
38 from Gnumed.business import gmCA_MSVA
39 from Gnumed.business import gmPersonSearch
40 from Gnumed.business import gmProviderInbox
41
42 from Gnumed.wxpython import gmGuiHelpers, gmDemographicsWidgets, gmAuthWidgets
43 from Gnumed.wxpython import gmRegetMixin, gmPhraseWheel, gmEditArea
44
45
46 _log = logging.getLogger('gm.person')
47
48 _cfg = gmCfg2.gmCfgData()
49
50 ID_PatPickList = wx.NewId()
51 ID_BTN_AddNew = wx.NewId()
52
53 #============================================================
57 #============================================================
58 from Gnumed.wxGladeWidgets import wxgMergePatientsDlg
59
61
63 wxgMergePatientsDlg.wxgMergePatientsDlg.__init__(self, *args, **kwargs)
64
65 curr_pat = gmPerson.gmCurrentPatient()
66 if curr_pat.connected:
67 self._TCTRL_patient1.person = curr_pat
68 self._TCTRL_patient1._display_name()
69 self._RBTN_patient1.SetValue(True)
70 #--------------------------------------------------------
165 #============================================================
166 from Gnumed.wxGladeWidgets import wxgSelectPersonFromListDlg
167
169
171 wxgSelectPersonFromListDlg.wxgSelectPersonFromListDlg.__init__(self, *args, **kwargs)
172
173 self.__cols = [
174 _('Title'),
175 _('Lastname'),
176 _('Firstname'),
177 _('Nickname'),
178 _('DOB'),
179 _('Gender'),
180 _('last visit'),
181 _('found via')
182 ]
183 self.__init_ui()
184 #--------------------------------------------------------
188 #--------------------------------------------------------
190 self._LCTRL_persons.DeleteAllItems()
191
192 pos = len(persons) + 1
193 if pos == 1:
194 return False
195
196 for person in persons:
197 row_num = self._LCTRL_persons.InsertStringItem(pos, label = gmTools.coalesce(person['title'], ''))
198 self._LCTRL_persons.SetStringItem(index = row_num, col = 1, label = person['lastnames'])
199 self._LCTRL_persons.SetStringItem(index = row_num, col = 2, label = person['firstnames'])
200 self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = gmTools.coalesce(person['preferred'], ''))
201 self._LCTRL_persons.SetStringItem(index = row_num, col = 4, label = person.get_formatted_dob(format = '%x', encoding = gmI18N.get_encoding()))
202 self._LCTRL_persons.SetStringItem(index = row_num, col = 5, label = gmTools.coalesce(person['l10n_gender'], '?'))
203 label = u''
204 if person.is_patient:
205 enc = person.get_last_encounter()
206 if enc is not None:
207 label = u'%s (%s)' % (enc['started'].strftime('%x').decode(gmI18N.get_encoding()), enc['l10n_type'])
208 self._LCTRL_persons.SetStringItem(index = row_num, col = 6, label = label)
209 try:
210 self._LCTRL_persons.SetStringItem(index = row_num, col = 7, label = person['match_type'])
211 except KeyError:
212 _log.warning('cannot set match_type field')
213 self._LCTRL_persons.SetStringItem(index = row_num, col = 7, label = u'??')
214
215 for col in range(len(self.__cols)):
216 self._LCTRL_persons.SetColumnWidth(col=col, width=wx.LIST_AUTOSIZE)
217
218 self._BTN_select.Enable(False)
219 self._LCTRL_persons.SetFocus()
220 self._LCTRL_persons.Select(0)
221
222 self._LCTRL_persons.set_data(data=persons)
223 #--------------------------------------------------------
225 return self._LCTRL_persons.get_item_data(self._LCTRL_persons.GetFirstSelected())
226 #--------------------------------------------------------
227 # event handlers
228 #--------------------------------------------------------
232 #--------------------------------------------------------
239 #============================================================
240 from Gnumed.wxGladeWidgets import wxgSelectPersonDTOFromListDlg
241
242 -class cSelectPersonDTOFromListDlg(wxgSelectPersonDTOFromListDlg.wxgSelectPersonDTOFromListDlg):
243
245 wxgSelectPersonDTOFromListDlg.wxgSelectPersonDTOFromListDlg.__init__(self, *args, **kwargs)
246
247 self.__cols = [
248 _('Source'),
249 _('Lastname'),
250 _('Firstname'),
251 _('DOB'),
252 _('Gender')
253 ]
254 self.__init_ui()
255 #--------------------------------------------------------
259 #--------------------------------------------------------
261 self._LCTRL_persons.DeleteAllItems()
262
263 pos = len(dtos) + 1
264 if pos == 1:
265 return False
266
267 for rec in dtos:
268 row_num = self._LCTRL_persons.InsertStringItem(pos, label = rec['source'])
269 dto = rec['dto']
270 self._LCTRL_persons.SetStringItem(index = row_num, col = 1, label = dto.lastnames)
271 self._LCTRL_persons.SetStringItem(index = row_num, col = 2, label = dto.firstnames)
272 if dto.dob is None:
273 self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = u'')
274 else:
275 self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = dto.dob.strftime('%x').decode(gmI18N.get_encoding()))
276 self._LCTRL_persons.SetStringItem(index = row_num, col = 4, label = gmTools.coalesce(dto.gender, ''))
277
278 for col in range(len(self.__cols)):
279 self._LCTRL_persons.SetColumnWidth(col=col, width=wx.LIST_AUTOSIZE)
280
281 self._BTN_select.Enable(False)
282 self._LCTRL_persons.SetFocus()
283 self._LCTRL_persons.Select(0)
284
285 self._LCTRL_persons.set_data(data=dtos)
286 #--------------------------------------------------------
288 return self._LCTRL_persons.get_item_data(self._LCTRL_persons.GetFirstSelected())
289 #--------------------------------------------------------
290 # event handlers
291 #--------------------------------------------------------
295 #--------------------------------------------------------
302
303 #============================================================
305
306 group = u'CA Medical Manager MSVA'
307
308 src_order = [
309 ('explicit', 'append'),
310 ('workbase', 'append'),
311 ('local', 'append'),
312 ('user', 'append'),
313 ('system', 'append')
314 ]
315 msva_files = _cfg.get (
316 group = group,
317 option = 'filename',
318 source_order = src_order
319 )
320 if msva_files is None:
321 return []
322
323 dtos = []
324 for msva_file in msva_files:
325 try:
326 # FIXME: potentially return several persons per file
327 msva_dtos = gmCA_MSVA.read_persons_from_msva_file(filename = msva_file)
328 except StandardError:
329 gmGuiHelpers.gm_show_error (
330 _(
331 'Cannot load patient from Medical Manager MSVA file\n\n'
332 ' [%s]'
333 ) % msva_file,
334 _('Activating MSVA patient')
335 )
336 _log.exception('cannot read patient from MSVA file [%s]' % msva_file)
337 continue
338
339 dtos.extend([ {'dto': dto, 'source': dto.source} for dto in msva_dtos ])
340 #dtos.extend([ {'dto': dto} for dto in msva_dtos ])
341
342 return dtos
343
344 #============================================================
345
347
348 bdt_files = []
349
350 # some can be auto-detected
351 # MCS/Isynet: $DRIVE:\Winacs\TEMP\BDTxx.tmp where xx is the workplace
352 candidates = []
353 drives = 'cdefghijklmnopqrstuvwxyz'
354 for drive in drives:
355 candidate = drive + ':\Winacs\TEMP\BDT*.tmp'
356 candidates.extend(glob.glob(candidate))
357 for candidate in candidates:
358 path, filename = os.path.split(candidate)
359 # FIXME: add encoding !
360 bdt_files.append({'file': candidate, 'source': 'MCS/Isynet %s' % filename[-6:-4]})
361
362 # some need to be configured
363 # aggregate sources
364 src_order = [
365 ('explicit', 'return'),
366 ('workbase', 'append'),
367 ('local', 'append'),
368 ('user', 'append'),
369 ('system', 'append')
370 ]
371 xdt_profiles = _cfg.get (
372 group = 'workplace',
373 option = 'XDT profiles',
374 source_order = src_order
375 )
376 if xdt_profiles is None:
377 return []
378
379 # first come first serve
380 src_order = [
381 ('explicit', 'return'),
382 ('workbase', 'return'),
383 ('local', 'return'),
384 ('user', 'return'),
385 ('system', 'return')
386 ]
387 for profile in xdt_profiles:
388 name = _cfg.get (
389 group = 'XDT profile %s' % profile,
390 option = 'filename',
391 source_order = src_order
392 )
393 if name is None:
394 _log.error('XDT profile [%s] does not define a <filename>' % profile)
395 continue
396 encoding = _cfg.get (
397 group = 'XDT profile %s' % profile,
398 option = 'encoding',
399 source_order = src_order
400 )
401 if encoding is None:
402 _log.warning('xDT source profile [%s] does not specify an <encoding> for BDT file [%s]' % (profile, name))
403 source = _cfg.get (
404 group = 'XDT profile %s' % profile,
405 option = 'source',
406 source_order = src_order
407 )
408 dob_format = _cfg.get (
409 group = 'XDT profile %s' % profile,
410 option = 'DOB format',
411 source_order = src_order
412 )
413 if dob_format is None:
414 _log.warning('XDT profile [%s] does not define a date of birth format in <DOB format>' % profile)
415 bdt_files.append({'file': name, 'source': source, 'encoding': encoding, 'dob_format': dob_format})
416
417 dtos = []
418 for bdt_file in bdt_files:
419 try:
420 # FIXME: potentially return several persons per file
421 dto = gmPerson.get_person_from_xdt (
422 filename = bdt_file['file'],
423 encoding = bdt_file['encoding'],
424 dob_format = bdt_file['dob_format']
425 )
426
427 except IOError:
428 gmGuiHelpers.gm_show_info (
429 _(
430 'Cannot access BDT file\n\n'
431 ' [%s]\n\n'
432 'to import patient.\n\n'
433 'Please check your configuration.'
434 ) % bdt_file,
435 _('Activating xDT patient')
436 )
437 _log.exception('cannot access xDT file [%s]' % bdt_file['file'])
438 continue
439 except:
440 gmGuiHelpers.gm_show_error (
441 _(
442 'Cannot load patient from BDT file\n\n'
443 ' [%s]'
444 ) % bdt_file,
445 _('Activating xDT patient')
446 )
447 _log.exception('cannot read patient from xDT file [%s]' % bdt_file['file'])
448 continue
449
450 dtos.append({'dto': dto, 'source': gmTools.coalesce(bdt_file['source'], dto.source)})
451
452 return dtos
453
454 #============================================================
455
457
458 pracsoft_files = []
459
460 # try detecting PATIENTS.IN files
461 candidates = []
462 drives = 'cdefghijklmnopqrstuvwxyz'
463 for drive in drives:
464 candidate = drive + ':\MDW2\PATIENTS.IN'
465 candidates.extend(glob.glob(candidate))
466 for candidate in candidates:
467 drive, filename = os.path.splitdrive(candidate)
468 pracsoft_files.append({'file': candidate, 'source': 'PracSoft (AU): drive %s' % drive})
469
470 # add configured one(s)
471 src_order = [
472 ('explicit', 'append'),
473 ('workbase', 'append'),
474 ('local', 'append'),
475 ('user', 'append'),
476 ('system', 'append')
477 ]
478 fnames = _cfg.get (
479 group = 'AU PracSoft PATIENTS.IN',
480 option = 'filename',
481 source_order = src_order
482 )
483
484 src_order = [
485 ('explicit', 'return'),
486 ('user', 'return'),
487 ('system', 'return'),
488 ('local', 'return'),
489 ('workbase', 'return')
490 ]
491 source = _cfg.get (
492 group = 'AU PracSoft PATIENTS.IN',
493 option = 'source',
494 source_order = src_order
495 )
496
497 if source is not None:
498 for fname in fnames:
499 fname = os.path.abspath(os.path.expanduser(fname))
500 if os.access(fname, os.R_OK):
501 pracsoft_files.append({'file': os.path.expanduser(fname), 'source': source})
502 else:
503 _log.error('cannot read [%s] in AU PracSoft profile' % fname)
504
505 # and parse them
506 dtos = []
507 for pracsoft_file in pracsoft_files:
508 try:
509 tmp = gmPerson.get_persons_from_pracsoft_file(filename = pracsoft_file['file'])
510 except:
511 _log.exception('cannot parse PracSoft file [%s]' % pracsoft_file['file'])
512 continue
513 for dto in tmp:
514 dtos.append({'dto': dto, 'source': pracsoft_file['source']})
515
516 return dtos
517 #============================================================
519
520 dbcfg = gmCfg.cCfgSQL()
521 kvk_dir = os.path.abspath(os.path.expanduser(dbcfg.get2 (
522 option = 'DE.KVK.spool_dir',
523 workplace = gmSurgery.gmCurrentPractice().active_workplace,
524 bias = 'workplace',
525 default = u'/var/spool/kvkd/'
526 )))
527 dtos = []
528 for dto in gmKVK.get_available_kvks_as_dtos(spool_dir = kvk_dir):
529 dtos.append({'dto': dto, 'source': 'KVK'})
530
531 return dtos
532 #============================================================
533 -def get_person_from_external_sources(parent=None, search_immediately=False, activate_immediately=False):
534 """Load patient from external source.
535
536 - scan external sources for candidates
537 - let user select source
538 - if > 1 available: always
539 - if only 1 available: depending on search_immediately
540 - search for patients matching info from external source
541 - if more than one match:
542 - let user select patient
543 - if no match:
544 - create patient
545 - activate patient
546 """
547 # get DTOs from interfaces
548 dtos = []
549 dtos.extend(load_persons_from_xdt())
550 dtos.extend(load_persons_from_pracsoft_au())
551 dtos.extend(load_persons_from_kvks())
552 dtos.extend(load_persons_from_ca_msva())
553
554 # no external persons
555 if len(dtos) == 0:
556 gmDispatcher.send(signal='statustext', msg=_('No patients found in external sources.'))
557 return None
558
559 # one external patient with DOB - already active ?
560 if (len(dtos) == 1) and (dtos[0]['dto'].dob is not None):
561 dto = dtos[0]['dto']
562 # is it already the current patient ?
563 curr_pat = gmPerson.gmCurrentPatient()
564 if curr_pat.connected:
565 key_dto = dto.firstnames + dto.lastnames + dto.dob.strftime('%Y-%m-%d') + dto.gender
566 names = curr_pat.get_active_name()
567 key_pat = names['firstnames'] + names['lastnames'] + curr_pat.get_formatted_dob(format = '%Y-%m-%d') + curr_pat['gender']
568 _log.debug('current patient: %s' % key_pat)
569 _log.debug('dto patient : %s' % key_dto)
570 if key_dto == key_pat:
571 gmDispatcher.send(signal='statustext', msg=_('The only external patient is already active in GNUmed.'), beep=False)
572 return None
573
574 # one external person - look for internal match immediately ?
575 if (len(dtos) == 1) and search_immediately:
576 dto = dtos[0]['dto']
577
578 # several external persons
579 else:
580 if parent is None:
581 parent = wx.GetApp().GetTopWindow()
582 dlg = cSelectPersonDTOFromListDlg(parent=parent, id=-1)
583 dlg.set_dtos(dtos=dtos)
584 result = dlg.ShowModal()
585 if result == wx.ID_CANCEL:
586 return None
587 dto = dlg.get_selected_dto()['dto']
588 dlg.Destroy()
589
590 # search
591 idents = dto.get_candidate_identities(can_create=True)
592 if idents is None:
593 gmGuiHelpers.gm_show_info (_(
594 'Cannot create new patient:\n\n'
595 ' [%s %s (%s), %s]'
596 ) % (dto.firstnames, dto.lastnames, dto.gender, dto.dob.strftime('%x').decode(gmI18N.get_encoding())),
597 _('Activating external patient')
598 )
599 return None
600
601 if len(idents) == 1:
602 ident = idents[0]
603
604 if len(idents) > 1:
605 if parent is None:
606 parent = wx.GetApp().GetTopWindow()
607 dlg = cSelectPersonFromListDlg(parent=parent, id=-1)
608 dlg.set_persons(persons=idents)
609 result = dlg.ShowModal()
610 if result == wx.ID_CANCEL:
611 return None
612 ident = dlg.get_selected_person()
613 dlg.Destroy()
614
615 if activate_immediately:
616 if not set_active_patient(patient = ident):
617 gmGuiHelpers.gm_show_info (
618 _(
619 'Cannot activate patient:\n\n'
620 '%s %s (%s)\n'
621 '%s'
622 ) % (dto.firstnames, dto.lastnames, dto.gender, dto.dob.strftime('%x').decode(gmI18N.get_encoding())),
623 _('Activating external patient')
624 )
625 return None
626
627 dto.import_extra_data(identity = ident)
628 dto.delete_from_source()
629
630 return ident
631 #============================================================
633 """Widget for smart search for persons."""
634
636
637 try:
638 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_ENTER
639 except KeyError:
640 kwargs['style'] = wx.TE_PROCESS_ENTER
641
642 # need to explicitly process ENTER events to avoid
643 # them being handed over to the next control
644 wx.TextCtrl.__init__(self, *args, **kwargs)
645
646 self.person = None
647
648 self._tt_search_hints = _(
649 'To search for a person, type any of: \n'
650 '\n'
651 ' - fragment(s) of last and/or first name(s)\n'
652 " - GNUmed ID of person (can start with '#')\n"
653 ' - any external ID of person\n'
654 " - date of birth (can start with '$' or '*')\n"
655 '\n'
656 'and hit <ENTER>.\n'
657 '\n'
658 'Shortcuts:\n'
659 ' <F2>\n'
660 ' - scan external sources for persons\n'
661 ' <CURSOR-UP>\n'
662 ' - recall most recently used search term\n'
663 ' <CURSOR-DOWN>\n'
664 ' - list 10 most recently found persons\n'
665 )
666 self.SetToolTipString(self._tt_search_hints)
667
668 # FIXME: set query generator
669 self.__person_searcher = gmPersonSearch.cPatientSearcher_SQL()
670
671 self._prev_search_term = None
672 self.__prev_idents = []
673 self._lclick_count = 0
674
675 self.__register_events()
676 #--------------------------------------------------------
677 # properties
678 #--------------------------------------------------------
682
685
686 person = property(_get_person, _set_person)
687 #--------------------------------------------------------
688 # utility methods
689 #--------------------------------------------------------
691 name = u''
692
693 if self.person is not None:
694 name = self.person['description']
695
696 self.SetValue(name)
697 #--------------------------------------------------------
699
700 if not isinstance(ident, gmPerson.cIdentity):
701 return False
702
703 # only unique identities
704 for known_ident in self.__prev_idents:
705 if known_ident['pk_identity'] == ident['pk_identity']:
706 return True
707
708 self.__prev_idents.append(ident)
709
710 # and only 10 of them
711 if len(self.__prev_idents) > 10:
712 self.__prev_idents.pop(0)
713
714 return True
715 #--------------------------------------------------------
716 # event handling
717 #--------------------------------------------------------
719 wx.EVT_CHAR(self, self.__on_char)
720 wx.EVT_SET_FOCUS(self, self._on_get_focus)
721 wx.EVT_KILL_FOCUS (self, self._on_loose_focus)
722 wx.EVT_TEXT_ENTER (self, self.GetId(), self.__on_enter)
723 #--------------------------------------------------------
725 """upon tabbing in
726
727 - select all text in the field so that the next
728 character typed will delete it
729 """
730 wx.CallAfter(self.SetSelection, -1, -1)
731 evt.Skip()
732 #--------------------------------------------------------
734 # - redraw the currently active name upon losing focus
735
736 # if we use wx.EVT_KILL_FOCUS we will also receive this event
737 # when closing our application or loosing focus to another
738 # application which is NOT what we intend to achieve,
739 # however, this is the least ugly way of doing this due to
740 # certain vagaries of wxPython (see the Wiki)
741
742 # just for good measure
743 wx.CallAfter(self.SetSelection, 0, 0)
744
745 self._display_name()
746 self._remember_ident(self.person)
747
748 evt.Skip()
749 #--------------------------------------------------------
752
754 """True: patient was selected.
755 False: no patient was selected.
756 """
757 keycode = evt.GetKeyCode()
758
759 # list of previously active patients
760 if keycode == wx.WXK_DOWN:
761 evt.Skip()
762 if len(self.__prev_idents) == 0:
763 return False
764
765 dlg = cSelectPersonFromListDlg(parent = wx.GetTopLevelParent(self), id = -1)
766 dlg.set_persons(persons = self.__prev_idents)
767 result = dlg.ShowModal()
768 if result == wx.ID_OK:
769 wx.BeginBusyCursor()
770 self.person = dlg.get_selected_person()
771 dlg.Destroy()
772 wx.EndBusyCursor()
773 return True
774
775 dlg.Destroy()
776 return False
777
778 # recall previous search fragment
779 if keycode == wx.WXK_UP:
780 evt.Skip()
781 # FIXME: cycling through previous fragments
782 if self._prev_search_term is not None:
783 self.SetValue(self._prev_search_term)
784 return False
785
786 # invoke external patient sources
787 if keycode == wx.WXK_F2:
788 evt.Skip()
789 dbcfg = gmCfg.cCfgSQL()
790 search_immediately = bool(dbcfg.get2 (
791 option = 'patient_search.external_sources.immediately_search_if_single_source',
792 workplace = gmSurgery.gmCurrentPractice().active_workplace,
793 bias = 'user',
794 default = 0
795 ))
796 p = get_person_from_external_sources (
797 parent = wx.GetTopLevelParent(self),
798 search_immediately = search_immediately
799 )
800 if p is not None:
801 self.person = p
802 return True
803 return False
804
805 # FIXME: invoke add new person
806 # FIXME: add popup menu apart from system one
807
808 evt.Skip()
809 #--------------------------------------------------------
811 """This is called from the ENTER handler."""
812
813 # ENTER but no search term ?
814 curr_search_term = self.GetValue().strip()
815 if curr_search_term == '':
816 return None
817
818 # same person anywys ?
819 if self.person is not None:
820 if curr_search_term == self.person['description']:
821 return None
822
823 # remember search fragment
824 if self.IsModified():
825 self._prev_search_term = curr_search_term
826
827 self._on_enter(search_term = curr_search_term)
828 #--------------------------------------------------------
830 """This can be overridden in child classes."""
831
832 wx.BeginBusyCursor()
833
834 # get list of matching ids
835 idents = self.__person_searcher.get_identities(search_term)
836
837 if idents is None:
838 wx.EndBusyCursor()
839 gmGuiHelpers.gm_show_info (
840 _('Error searching for matching persons.\n\n'
841 'Search term: "%s"'
842 ) % search_term,
843 _('selecting person')
844 )
845 return None
846
847 _log.info("%s matching person(s) found", len(idents))
848
849 if len(idents) == 0:
850 wx.EndBusyCursor()
851
852 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
853 wx.GetTopLevelParent(self),
854 -1,
855 caption = _('Selecting patient'),
856 question = _(
857 'Cannot find any matching patients for the search term\n\n'
858 ' "%s"\n\n'
859 'You may want to try a shorter search term.\n'
860 ) % search_term,
861 button_defs = [
862 {'label': _('Go back'), 'tooltip': _('Go back and search again.'), 'default': True},
863 {'label': _('Create new'), 'tooltip': _('Create new patient.')}
864 ]
865 )
866 if dlg.ShowModal() != wx.ID_NO:
867 return
868
869 success = gmDemographicsWidgets.create_new_person(activate = True)
870 if success:
871 self.person = gmPerson.gmCurrentPatient()
872 else:
873 self.person = None
874 return None
875
876 # only one matching identity
877 if len(idents) == 1:
878 self.person = idents[0]
879 wx.EndBusyCursor()
880 return None
881
882 # more than one matching identity: let user select from pick list
883 dlg = cSelectPersonFromListDlg(parent=wx.GetTopLevelParent(self), id=-1)
884 dlg.set_persons(persons=idents)
885 wx.EndBusyCursor()
886 result = dlg.ShowModal()
887 if result == wx.ID_CANCEL:
888 dlg.Destroy()
889 return None
890
891 wx.BeginBusyCursor()
892 self.person = dlg.get_selected_person()
893 dlg.Destroy()
894 wx.EndBusyCursor()
895
896 return None
897 #============================================================
899
900 if patient is None:
901 return
902
903 if patient['dob'] is None:
904 gmGuiHelpers.gm_show_warning (
905 aTitle = _('Checking date of birth'),
906 aMessage = _(
907 '\n'
908 ' %s\n'
909 '\n'
910 'The date of birth for this patient is not known !\n'
911 '\n'
912 'You can proceed to work on the patient but\n'
913 'GNUmed will be unable to assist you with\n'
914 'age-related decisions.\n'
915 ) % patient['description_gender']
916 )
917
918 return
919 #------------------------------------------------------------
921
922 if patient is None:
923 return True
924
925 curr_prov = gmStaff.gmCurrentProvider()
926
927 # can view my own chart
928 if patient.ID == curr_prov['pk_identity']:
929 return True
930
931 if patient.ID not in [ s['pk_identity'] for s in gmStaff.get_staff_list() ]:
932 return True
933
934 proceed = gmGuiHelpers.gm_show_question (
935 aTitle = _('Privacy check'),
936 aMessage = _(
937 'You have selected the chart of a member of staff,\n'
938 'for whom privacy is especially important:\n'
939 '\n'
940 ' %s, %s\n'
941 '\n'
942 'This may be OK depending on circumstances.\n'
943 '\n'
944 'Please be aware that accessing patient charts is\n'
945 'logged and that %s%s will be\n'
946 'notified of the access if you choose to proceed.\n'
947 '\n'
948 'Are you sure you want to draw this chart ?'
949 ) % (
950 patient.get_description_gender(),
951 patient.get_formatted_dob(),
952 gmTools.coalesce(patient['title'], u'', u'%s '),
953 patient['lastnames']
954 )
955 )
956
957 if proceed:
958 prov = u'%s (%s%s %s)' % (
959 curr_prov['short_alias'],
960 gmTools.coalesce(curr_prov['title'], u'', u'%s '),
961 curr_prov['firstnames'],
962 curr_prov['lastnames']
963 )
964 pat = u'%s%s %s' % (
965 gmTools.coalesce(patient['title'], u'', u'%s '),
966 patient['firstnames'],
967 patient['lastnames']
968 )
969 # notify the staff member
970 gmProviderInbox.create_inbox_message (
971 staff = patient.staff_id,
972 message_type = _('Privacy notice'),
973 subject = _('Your chart has been accessed by %s.') % prov,
974 patient = patient.ID
975 )
976 # notify /me about the staff member notification
977 gmProviderInbox.create_inbox_message (
978 staff = curr_prov['pk_staff'],
979 message_type = _('Privacy notice'),
980 subject = _('Staff member %s has been notified of your chart access.') % pat
981 )
982
983 return proceed
984 #------------------------------------------------------------
986
987 if patient['dob'] is None:
988 return
989
990 dbcfg = gmCfg.cCfgSQL()
991 dob_distance = dbcfg.get2 (
992 option = u'patient_search.dob_warn_interval',
993 workplace = gmSurgery.gmCurrentPractice().active_workplace,
994 bias = u'user',
995 default = u'1 week'
996 )
997
998 if not patient.dob_in_range(dob_distance, dob_distance):
999 return
1000
1001 now = gmDateTime.pydt_now_here()
1002 enc = gmI18N.get_encoding()
1003 msg = _('%(pat)s turns %(age)s on %(month)s %(day)s ! (today is %(month_now)s %(day_now)s)') % {
1004 'pat': patient.get_description_gender(),
1005 'age': patient.get_medical_age().strip('y'),
1006 'month': patient.get_formatted_dob(format = '%B', encoding = enc),
1007 'day': patient.get_formatted_dob(format = '%d', encoding = enc),
1008 'month_now': gmDateTime.pydt_strftime(now, '%B', enc, gmDateTime.acc_months),
1009 'day_now': gmDateTime.pydt_strftime(now, '%d', enc, gmDateTime.acc_days)
1010 }
1011 gmDispatcher.send(signal = 'statustext', msg = msg)
1012 #------------------------------------------------------------
1014
1015 if isinstance(patient, gmPerson.cPatient):
1016 pass
1017 elif isinstance(patient, gmPerson.cIdentity):
1018 patient = gmPerson.cPatient(aPK_obj = patient['pk_identity'])
1019 # elif isinstance(patient, cStaff):
1020 # patient = cPatient(aPK_obj=patient['pk_identity'])
1021 elif isinstance(patient, gmPerson.gmCurrentPatient):
1022 patient = patient.patient
1023 elif patient == -1:
1024 pass
1025 else:
1026 # maybe integer ?
1027 success, pk = gmTools.input2int(initial = patient, minval = 1)
1028 if not success:
1029 raise ValueError('<patient> must be either -1, >0, or a cPatient, cIdentity or gmCurrentPatient instance, is: %s' % patient)
1030 # but also valid patient ID ?
1031 try:
1032 patient = gmPerson.cPatient(aPK_obj = pk)
1033 except:
1034 _log.exception('error changing active patient to [%s]' % patient)
1035 return False
1036
1037 _check_has_dob(patient = patient)
1038
1039 if not _check_for_provider_chart_access(patient = patient):
1040 return False
1041
1042 success = gmPerson.set_active_patient(patient = patient, forced_reload = forced_reload)
1043
1044 if not success:
1045 return False
1046
1047 _check_birthday(patient = patient)
1048
1049 return True
1050 #------------------------------------------------------------
1052
1054
1055 cPersonSearchCtrl.__init__(self, *args, **kwargs)
1056
1057 # get configuration
1058 cfg = gmCfg.cCfgSQL()
1059
1060 self.__always_dismiss_on_search = bool (
1061 cfg.get2 (
1062 option = 'patient_search.always_dismiss_previous_patient',
1063 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1064 bias = 'user',
1065 default = 0
1066 )
1067 )
1068
1069 self.__always_reload_after_search = bool (
1070 cfg.get2 (
1071 option = 'patient_search.always_reload_new_patient',
1072 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1073 bias = 'user',
1074 default = 0
1075 )
1076 )
1077
1078 self.__register_events()
1079 #--------------------------------------------------------
1080 # utility methods
1081 #--------------------------------------------------------
1083
1084 curr_pat = gmPerson.gmCurrentPatient()
1085 if curr_pat.connected:
1086 name = curr_pat['description']
1087 if curr_pat.locked:
1088 name = _('%(name)s (locked)') % {'name': name}
1089 else:
1090 if curr_pat.locked:
1091 name = _('<patient search locked>')
1092 else:
1093 name = _('<type here to search patient>')
1094
1095 self.SetValue(name)
1096
1097 # adjust tooltip
1098 if self.person is None:
1099 self.SetToolTipString(self._tt_search_hints)
1100 return
1101
1102 if (self.person['emergency_contact'] is None) and (self.person['comment'] is None):
1103 separator = u''
1104 else:
1105 separator = u'%s\n' % (gmTools.u_box_horiz_single * 40)
1106
1107 tt = u'%s%s%s%s' % (
1108 gmTools.coalesce(self.person['emergency_contact'], u'', u'%s\n %%s\n' % _('In case of emergency contact:')),
1109 gmTools.coalesce(self.person['comment'], u'', u'\n%s\n'),
1110 separator,
1111 self._tt_search_hints
1112 )
1113 self.SetToolTipString(tt)
1114 #--------------------------------------------------------
1116 if not set_active_patient(patient=pat, forced_reload = self.__always_reload_after_search):
1117 _log.error('cannot change active patient')
1118 return None
1119
1120 self._remember_ident(pat)
1121
1122 return True
1123 #--------------------------------------------------------
1124 # event handling
1125 #--------------------------------------------------------
1127 # client internal signals
1128 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1129 gmDispatcher.connect(signal = u'name_mod_db', receiver = self._on_name_identity_change)
1130 gmDispatcher.connect(signal = u'identity_mod_db', receiver = self._on_name_identity_change)
1131
1132 gmDispatcher.connect(signal = 'patient_locked', receiver = self._on_post_patient_selection)
1133 gmDispatcher.connect(signal = 'patient_unlocked', receiver = self._on_post_patient_selection)
1134 #----------------------------------------------
1137 #----------------------------------------------
1139 if gmPerson.gmCurrentPatient().connected:
1140 self.person = gmPerson.gmCurrentPatient().patient
1141 else:
1142 self.person = None
1143 #----------------------------------------------
1145
1146 if self.__always_dismiss_on_search:
1147 _log.warning("dismissing patient before patient search")
1148 self._set_person_as_active_patient(-1)
1149
1150 super(self.__class__, self)._on_enter(search_term=search_term)
1151
1152 if self.person is None:
1153 return
1154
1155 self._set_person_as_active_patient(self.person)
1156 #----------------------------------------------
1158
1159 success = super(self.__class__, self)._on_char(evt)
1160 if success:
1161 self._set_person_as_active_patient(self.person)
1162
1163 #============================================================
1164 # main
1165 #------------------------------------------------------------
1166 if __name__ == "__main__":
1167
1168 if len(sys.argv) > 1:
1169 if sys.argv[1] == 'test':
1170 gmI18N.activate_locale()
1171 gmI18N.install_domain()
1172
1173 app = wx.PyWidgetTester(size = (200, 40))
1174 # app.SetWidget(cSelectPersonFromListDlg, -1)
1175 app.SetWidget(cPersonSearchCtrl, -1)
1176 # app.SetWidget(cActivePatientSelector, -1)
1177 app.MainLoop()
1178
1179 #============================================================
1180 # docs
1181 #------------------------------------------------------------
1182 # functionality
1183 # -------------
1184 # - hitting ENTER on non-empty field (and more than threshold chars)
1185 # - start search
1186 # - display results in a list, prefixed with numbers
1187 # - last name
1188 # - first name
1189 # - gender
1190 # - age
1191 # - city + street (no ZIP, no number)
1192 # - last visit (highlighted if within a certain interval)
1193 # - arbitrary marker (e.g. office attendance this quartal, missing KVK, appointments, due dates)
1194 # - if none found -> go to entry of new patient
1195 # - scrolling in this list
1196 # - ENTER selects patient
1197 # - ESC cancels selection
1198 # - number selects patient
1199 #
1200 # - hitting cursor-up/-down
1201 # - cycle through history of last 10 search fragments
1202 #
1203 # - hitting alt-L = List, alt-P = previous
1204 # - show list of previous ten patients prefixed with numbers
1205 # - scrolling in list
1206 # - ENTER selects patient
1207 # - ESC cancels selection
1208 # - number selects patient
1209 #
1210 # - hitting ALT-N
1211 # - immediately goes to entry of new patient
1212 #
1213 # - hitting cursor-right in a patient selection list
1214 # - pops up more detail about the patient
1215 # - ESC/cursor-left goes back to list
1216 #
1217 # - hitting TAB
1218 # - makes sure the currently active patient is displayed
1219
1220 #------------------------------------------------------------
1221 # samples
1222 # -------
1223 # working:
1224 # Ian Haywood
1225 # Haywood Ian
1226 # Haywood
1227 # Amador Jimenez (yes, two last names but no hyphen: Spain, for example)
1228 # Ian Haywood 19/12/1977
1229 # 19/12/1977
1230 # 19-12-1977
1231 # 19.12.1977
1232 # 19771219
1233 # $dob
1234 # *dob
1235 # #ID
1236 # ID
1237 # HIlbert, karsten
1238 # karsten, hilbert
1239 # kars, hilb
1240 #
1241 # non-working:
1242 # Haywood, Ian <40
1243 # ?, Ian 1977
1244 # Ian Haywood, 19/12/77
1245 # PUPIC
1246 # "hilb; karsten, 23.10.74"
1247
1248 #------------------------------------------------------------
1249 # notes
1250 # -----
1251 # >> 3. There are countries in which people have more than one
1252 # >> (significant) lastname (spanish-speaking countries are one case :), some
1253 # >> asian countries might be another one).
1254 # -> we need per-country query generators ...
1255
1256 # search case sensitive by default, switch to insensitive if not found ?
1257
1258 # accent insensitive search:
1259 # select * from * where to_ascii(column, 'encoding') like '%test%';
1260 # may not work with Unicode
1261
1262 # phrase wheel is most likely too slow
1263
1264 # extend search fragment history
1265
1266 # ask user whether to send off level 3 queries - or thread them
1267
1268 # we don't expect patient IDs in complicated patterns, hence any digits signify a date
1269
1270 # FIXME: make list window fit list size ...
1271
1272 # clear search field upon get-focus ?
1273
1274 # F1 -> context help with hotkey listing
1275
1276 # th -> th|t
1277 # v/f/ph -> f|v|ph
1278 # maybe don't do umlaut translation in the first 2-3 letters
1279 # such that not to defeat index use for the first level query ?
1280
1281 # user defined function key to start search
1282
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Feb 1 03:57:12 2013 | http://epydoc.sourceforge.net |