| 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 _check_has_dob(patient = patient)
1016
1017 if not _check_for_provider_chart_access(patient = patient):
1018 return False
1019
1020 success = gmPerson.set_active_patient(patient = patient, forced_reload = forced_reload)
1021
1022 if not success:
1023 return False
1024
1025 _check_birthday(patient = patient)
1026
1027 return True
1028 #------------------------------------------------------------
1030
1032
1033 cPersonSearchCtrl.__init__(self, *args, **kwargs)
1034
1035 # get configuration
1036 cfg = gmCfg.cCfgSQL()
1037
1038 self.__always_dismiss_on_search = bool (
1039 cfg.get2 (
1040 option = 'patient_search.always_dismiss_previous_patient',
1041 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1042 bias = 'user',
1043 default = 0
1044 )
1045 )
1046
1047 self.__always_reload_after_search = bool (
1048 cfg.get2 (
1049 option = 'patient_search.always_reload_new_patient',
1050 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1051 bias = 'user',
1052 default = 0
1053 )
1054 )
1055
1056 self.__register_events()
1057 #--------------------------------------------------------
1058 # utility methods
1059 #--------------------------------------------------------
1061
1062 curr_pat = gmPerson.gmCurrentPatient()
1063 if curr_pat.connected:
1064 name = curr_pat['description']
1065 if curr_pat.locked:
1066 name = _('%(name)s (locked)') % {'name': name}
1067 else:
1068 if curr_pat.locked:
1069 name = _('<patient search locked>')
1070 else:
1071 name = _('<type here to search patient>')
1072
1073 self.SetValue(name)
1074
1075 # adjust tooltip
1076 if self.person is None:
1077 self.SetToolTipString(self._tt_search_hints)
1078 return
1079
1080 if (self.person['emergency_contact'] is None) and (self.person['comment'] is None):
1081 separator = u''
1082 else:
1083 separator = u'%s\n' % (gmTools.u_box_horiz_single * 40)
1084
1085 tt = u'%s%s%s%s' % (
1086 gmTools.coalesce(self.person['emergency_contact'], u'', u'%s\n %%s\n' % _('In case of emergency contact:')),
1087 gmTools.coalesce(self.person['comment'], u'', u'\n%s\n'),
1088 separator,
1089 self._tt_search_hints
1090 )
1091 self.SetToolTipString(tt)
1092 #--------------------------------------------------------
1094 if not set_active_patient(patient=pat, forced_reload = self.__always_reload_after_search):
1095 _log.error('cannot change active patient')
1096 return None
1097
1098 self._remember_ident(pat)
1099
1100 return True
1101 #--------------------------------------------------------
1102 # event handling
1103 #--------------------------------------------------------
1105 # client internal signals
1106 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1107 gmDispatcher.connect(signal = u'name_mod_db', receiver = self._on_name_identity_change)
1108 gmDispatcher.connect(signal = u'identity_mod_db', receiver = self._on_name_identity_change)
1109
1110 gmDispatcher.connect(signal = 'patient_locked', receiver = self._on_post_patient_selection)
1111 gmDispatcher.connect(signal = 'patient_unlocked', receiver = self._on_post_patient_selection)
1112 #----------------------------------------------
1115 #----------------------------------------------
1117 if gmPerson.gmCurrentPatient().connected:
1118 self.person = gmPerson.gmCurrentPatient().patient
1119 else:
1120 self.person = None
1121 #----------------------------------------------
1123
1124 if self.__always_dismiss_on_search:
1125 _log.warning("dismissing patient before patient search")
1126 self._set_person_as_active_patient(-1)
1127
1128 super(self.__class__, self)._on_enter(search_term=search_term)
1129
1130 if self.person is None:
1131 return
1132
1133 self._set_person_as_active_patient(self.person)
1134 #----------------------------------------------
1136
1137 success = super(self.__class__, self)._on_char(evt)
1138 if success:
1139 self._set_person_as_active_patient(self.person)
1140
1141 #============================================================
1142 # main
1143 #------------------------------------------------------------
1144 if __name__ == "__main__":
1145
1146 if len(sys.argv) > 1:
1147 if sys.argv[1] == 'test':
1148 gmI18N.activate_locale()
1149 gmI18N.install_domain()
1150
1151 app = wx.PyWidgetTester(size = (200, 40))
1152 # app.SetWidget(cSelectPersonFromListDlg, -1)
1153 app.SetWidget(cPersonSearchCtrl, -1)
1154 # app.SetWidget(cActivePatientSelector, -1)
1155 app.MainLoop()
1156
1157 #============================================================
1158 # docs
1159 #------------------------------------------------------------
1160 # functionality
1161 # -------------
1162 # - hitting ENTER on non-empty field (and more than threshold chars)
1163 # - start search
1164 # - display results in a list, prefixed with numbers
1165 # - last name
1166 # - first name
1167 # - gender
1168 # - age
1169 # - city + street (no ZIP, no number)
1170 # - last visit (highlighted if within a certain interval)
1171 # - arbitrary marker (e.g. office attendance this quartal, missing KVK, appointments, due dates)
1172 # - if none found -> go to entry of new patient
1173 # - scrolling in this list
1174 # - ENTER selects patient
1175 # - ESC cancels selection
1176 # - number selects patient
1177 #
1178 # - hitting cursor-up/-down
1179 # - cycle through history of last 10 search fragments
1180 #
1181 # - hitting alt-L = List, alt-P = previous
1182 # - show list of previous ten patients prefixed with numbers
1183 # - scrolling in list
1184 # - ENTER selects patient
1185 # - ESC cancels selection
1186 # - number selects patient
1187 #
1188 # - hitting ALT-N
1189 # - immediately goes to entry of new patient
1190 #
1191 # - hitting cursor-right in a patient selection list
1192 # - pops up more detail about the patient
1193 # - ESC/cursor-left goes back to list
1194 #
1195 # - hitting TAB
1196 # - makes sure the currently active patient is displayed
1197
1198 #------------------------------------------------------------
1199 # samples
1200 # -------
1201 # working:
1202 # Ian Haywood
1203 # Haywood Ian
1204 # Haywood
1205 # Amador Jimenez (yes, two last names but no hyphen: Spain, for example)
1206 # Ian Haywood 19/12/1977
1207 # 19/12/1977
1208 # 19-12-1977
1209 # 19.12.1977
1210 # 19771219
1211 # $dob
1212 # *dob
1213 # #ID
1214 # ID
1215 # HIlbert, karsten
1216 # karsten, hilbert
1217 # kars, hilb
1218 #
1219 # non-working:
1220 # Haywood, Ian <40
1221 # ?, Ian 1977
1222 # Ian Haywood, 19/12/77
1223 # PUPIC
1224 # "hilb; karsten, 23.10.74"
1225
1226 #------------------------------------------------------------
1227 # notes
1228 # -----
1229 # >> 3. There are countries in which people have more than one
1230 # >> (significant) lastname (spanish-speaking countries are one case :), some
1231 # >> asian countries might be another one).
1232 # -> we need per-country query generators ...
1233
1234 # search case sensitive by default, switch to insensitive if not found ?
1235
1236 # accent insensitive search:
1237 # select * from * where to_ascii(column, 'encoding') like '%test%';
1238 # may not work with Unicode
1239
1240 # phrase wheel is most likely too slow
1241
1242 # extend search fragment history
1243
1244 # ask user whether to send off level 3 queries - or thread them
1245
1246 # we don't expect patient IDs in complicated patterns, hence any digits signify a date
1247
1248 # FIXME: make list window fit list size ...
1249
1250 # clear search field upon get-focus ?
1251
1252 # F1 -> context help with hotkey listing
1253
1254 # th -> th|t
1255 # v/f/ph -> f|v|ph
1256 # maybe don't do umlaut translation in the first 2-3 letters
1257 # such that not to defeat index use for the first level query ?
1258
1259 # user defined function key to start search
1260
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Mon Jun 25 03:58:36 2012 | http://epydoc.sourceforge.net |