| 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 __version__ = "$Revision: 1.132 $"
13 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
14 __license__ = 'GPL (for details see http://www.gnu.org/)'
15
16 import sys, os.path, glob, datetime as pyDT, re as regex, logging, webbrowser
17
18
19 import wx
20
21
22 if __name__ == '__main__':
23 sys.path.insert(0, '../../')
24 from Gnumed.pycommon import gmLog2
25 from Gnumed.pycommon import gmDispatcher, gmPG2, gmI18N, gmCfg, gmTools, gmDateTime, gmMatchProvider, gmCfg2
26 from Gnumed.business import gmPerson, gmKVK, gmSurgery
27 from Gnumed.wxpython import gmGuiHelpers, gmDemographicsWidgets, gmAuthWidgets, gmRegetMixin, gmPhraseWheel, gmEditArea
28 from Gnumed.wxGladeWidgets import wxgSelectPersonFromListDlg, wxgSelectPersonDTOFromListDlg, wxgMergePatientsDlg
29
30
31 _log = logging.getLogger('gm.person')
32 _log.info(__version__)
33
34 _cfg = gmCfg2.gmCfgData()
35
36 ID_PatPickList = wx.NewId()
37 ID_BTN_AddNew = wx.NewId()
38
39 #============================================================
43 #============================================================
45
47 wxgMergePatientsDlg.wxgMergePatientsDlg.__init__(self, *args, **kwargs)
48
49 curr_pat = gmPerson.gmCurrentPatient()
50 if curr_pat.connected:
51 self._TCTRL_patient1.person = curr_pat
52 self._TCTRL_patient1._display_name()
53 self._RBTN_patient1.SetValue(True)
54 #--------------------------------------------------------
153 #============================================================
155
157 wxgSelectPersonFromListDlg.wxgSelectPersonFromListDlg.__init__(self, *args, **kwargs)
158
159 self.__cols = [
160 _('Title'),
161 _('Lastname'),
162 _('Firstname'),
163 _('Nickname'),
164 _('DOB'),
165 _('Gender'),
166 _('last visit'),
167 _('found via')
168 ]
169 self.__init_ui()
170 #--------------------------------------------------------
174 #--------------------------------------------------------
176 self._LCTRL_persons.DeleteAllItems()
177
178 pos = len(persons) + 1
179 if pos == 1:
180 return False
181
182 for person in persons:
183 row_num = self._LCTRL_persons.InsertStringItem(pos, label = gmTools.coalesce(person['title'], ''))
184 self._LCTRL_persons.SetStringItem(index = row_num, col = 1, label = person['lastnames'])
185 self._LCTRL_persons.SetStringItem(index = row_num, col = 2, label = person['firstnames'])
186 self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = gmTools.coalesce(person['preferred'], ''))
187 self._LCTRL_persons.SetStringItem(index = row_num, col = 4, label = person.get_formatted_dob(format = '%x', encoding = gmI18N.get_encoding()))
188 self._LCTRL_persons.SetStringItem(index = row_num, col = 5, label = gmTools.coalesce(person['l10n_gender'], '?'))
189 label = u''
190 if person.is_patient:
191 enc = person.get_last_encounter()
192 if enc is not None:
193 label = u'%s (%s)' % (enc['started'].strftime('%x').decode(gmI18N.get_encoding()), enc['l10n_type'])
194 self._LCTRL_persons.SetStringItem(index = row_num, col = 6, label = label)
195 try: self._LCTRL_persons.SetStringItem(index = row_num, col = 7, label = person['match_type'])
196 except:
197 _log.exception('cannot set match_type field')
198 self._LCTRL_persons.SetStringItem(index = row_num, col = 7, label = u'??')
199
200 for col in range(len(self.__cols)):
201 self._LCTRL_persons.SetColumnWidth(col=col, width=wx.LIST_AUTOSIZE)
202
203 self._BTN_select.Enable(False)
204 self._LCTRL_persons.SetFocus()
205 self._LCTRL_persons.Select(0)
206
207 self._LCTRL_persons.set_data(data=persons)
208 #--------------------------------------------------------
210 return self._LCTRL_persons.get_item_data(self._LCTRL_persons.GetFirstSelected())
211 #--------------------------------------------------------
212 # event handlers
213 #--------------------------------------------------------
217 #--------------------------------------------------------
224 #============================================================
225 -class cSelectPersonDTOFromListDlg(wxgSelectPersonDTOFromListDlg.wxgSelectPersonDTOFromListDlg):
226
228 wxgSelectPersonDTOFromListDlg.wxgSelectPersonDTOFromListDlg.__init__(self, *args, **kwargs)
229
230 self.__cols = [
231 _('Source'),
232 _('Lastname'),
233 _('Firstname'),
234 _('DOB'),
235 _('Gender')
236 ]
237 self.__init_ui()
238 #--------------------------------------------------------
242 #--------------------------------------------------------
244 self._LCTRL_persons.DeleteAllItems()
245
246 pos = len(dtos) + 1
247 if pos == 1:
248 return False
249
250 for rec in dtos:
251 row_num = self._LCTRL_persons.InsertStringItem(pos, label = rec['source'])
252 dto = rec['dto']
253 self._LCTRL_persons.SetStringItem(index = row_num, col = 1, label = dto.lastnames)
254 self._LCTRL_persons.SetStringItem(index = row_num, col = 2, label = dto.firstnames)
255 if dto.dob is None:
256 self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = u'')
257 else:
258 self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = dto.dob.strftime('%x').decode(gmI18N.get_encoding()))
259 self._LCTRL_persons.SetStringItem(index = row_num, col = 4, label = gmTools.coalesce(dto.gender, ''))
260
261 for col in range(len(self.__cols)):
262 self._LCTRL_persons.SetColumnWidth(col=col, width=wx.LIST_AUTOSIZE)
263
264 self._BTN_select.Enable(False)
265 self._LCTRL_persons.SetFocus()
266 self._LCTRL_persons.Select(0)
267
268 self._LCTRL_persons.set_data(data=dtos)
269 #--------------------------------------------------------
271 return self._LCTRL_persons.get_item_data(self._LCTRL_persons.GetFirstSelected())
272 #--------------------------------------------------------
273 # event handlers
274 #--------------------------------------------------------
278 #--------------------------------------------------------
285 #============================================================
287
288 bdt_files = []
289
290 # some can be auto-detected
291 # MCS/Isynet: $DRIVE:\Winacs\TEMP\BDTxx.tmp where xx is the workplace
292 candidates = []
293 drives = 'cdefghijklmnopqrstuvwxyz'
294 for drive in drives:
295 candidate = drive + ':\Winacs\TEMP\BDT*.tmp'
296 candidates.extend(glob.glob(candidate))
297 for candidate in candidates:
298 path, filename = os.path.split(candidate)
299 # FIXME: add encoding !
300 bdt_files.append({'file': candidate, 'source': 'MCS/Isynet %s' % filename[-6:-4]})
301
302 # some need to be configured
303 # aggregate sources
304 src_order = [
305 ('explicit', 'return'),
306 ('workbase', 'append'),
307 ('local', 'append'),
308 ('user', 'append'),
309 ('system', 'append')
310 ]
311 xdt_profiles = _cfg.get (
312 group = 'workplace',
313 option = 'XDT profiles',
314 source_order = src_order
315 )
316 if xdt_profiles is None:
317 return []
318
319 # first come first serve
320 src_order = [
321 ('explicit', 'return'),
322 ('workbase', 'return'),
323 ('local', 'return'),
324 ('user', 'return'),
325 ('system', 'return')
326 ]
327 for profile in xdt_profiles:
328 name = _cfg.get (
329 group = 'XDT profile %s' % profile,
330 option = 'filename',
331 source_order = src_order
332 )
333 if name is None:
334 _log.error('XDT profile [%s] does not define a <filename>' % profile)
335 continue
336 encoding = _cfg.get (
337 group = 'XDT profile %s' % profile,
338 option = 'encoding',
339 source_order = src_order
340 )
341 if encoding is None:
342 _log.warning('xDT source profile [%s] does not specify an <encoding> for BDT file [%s]' % (profile, name))
343 source = _cfg.get (
344 group = 'XDT profile %s' % profile,
345 option = 'source',
346 source_order = src_order
347 )
348 dob_format = _cfg.get (
349 group = 'XDT profile %s' % profile,
350 option = 'DOB format',
351 source_order = src_order
352 )
353 if dob_format is None:
354 _log.warning('XDT profile [%s] does not define a date of birth format in <DOB format>' % profile)
355 bdt_files.append({'file': name, 'source': source, 'encoding': encoding, 'dob_format': dob_format})
356
357 dtos = []
358 for bdt_file in bdt_files:
359 try:
360 # FIXME: potentially return several persons per file
361 dto = gmPerson.get_person_from_xdt (
362 filename = bdt_file['file'],
363 encoding = bdt_file['encoding'],
364 dob_format = bdt_file['dob_format']
365 )
366
367 except IOError:
368 gmGuiHelpers.gm_show_info (
369 _(
370 'Cannot access BDT file\n\n'
371 ' [%s]\n\n'
372 'to import patient.\n\n'
373 'Please check your configuration.'
374 ) % bdt_file,
375 _('Activating xDT patient')
376 )
377 _log.exception('cannot access xDT file [%s]' % bdt_file['file'])
378 continue
379 except:
380 gmGuiHelpers.gm_show_error (
381 _(
382 'Cannot load patient from BDT file\n\n'
383 ' [%s]'
384 ) % bdt_file,
385 _('Activating xDT patient')
386 )
387 _log.exception('cannot read patient from xDT file [%s]' % bdt_file['file'])
388 continue
389
390 dtos.append({'dto': dto, 'source': gmTools.coalesce(bdt_file['source'], dto.source)})
391
392 return dtos
393 #============================================================
395
396 pracsoft_files = []
397
398 # try detecting PATIENTS.IN files
399 candidates = []
400 drives = 'cdefghijklmnopqrstuvwxyz'
401 for drive in drives:
402 candidate = drive + ':\MDW2\PATIENTS.IN'
403 candidates.extend(glob.glob(candidate))
404 for candidate in candidates:
405 drive, filename = os.path.splitdrive(candidate)
406 pracsoft_files.append({'file': candidate, 'source': 'PracSoft (AU): drive %s' % drive})
407
408 # add configured one(s)
409 src_order = [
410 ('explicit', 'append'),
411 ('workbase', 'append'),
412 ('local', 'append'),
413 ('user', 'append'),
414 ('system', 'append')
415 ]
416 fnames = _cfg.get (
417 group = 'AU PracSoft PATIENTS.IN',
418 option = 'filename',
419 source_order = src_order
420 )
421
422 src_order = [
423 ('explicit', 'return'),
424 ('user', 'return'),
425 ('system', 'return'),
426 ('local', 'return'),
427 ('workbase', 'return')
428 ]
429 source = _cfg.get (
430 group = 'AU PracSoft PATIENTS.IN',
431 option = 'source',
432 source_order = src_order
433 )
434
435 if source is not None:
436 for fname in fnames:
437 fname = os.path.abspath(os.path.expanduser(fname))
438 if os.access(fname, os.R_OK):
439 pracsoft_files.append({'file': os.path.expanduser(fname), 'source': source})
440 else:
441 _log.error('cannot read [%s] in AU PracSoft profile' % fname)
442
443 # and parse them
444 dtos = []
445 for pracsoft_file in pracsoft_files:
446 try:
447 tmp = gmPerson.get_persons_from_pracsoft_file(filename = pracsoft_file['file'])
448 except:
449 _log.exception('cannot parse PracSoft file [%s]' % pracsoft_file['file'])
450 continue
451 for dto in tmp:
452 dtos.append({'dto': dto, 'source': pracsoft_file['source']})
453
454 return dtos
455 #============================================================
457
458 dbcfg = gmCfg.cCfgSQL()
459 kvk_dir = os.path.abspath(os.path.expanduser(dbcfg.get2 (
460 option = 'DE.KVK.spool_dir',
461 workplace = gmSurgery.gmCurrentPractice().active_workplace,
462 bias = 'workplace',
463 default = u'/var/spool/kvkd/'
464 )))
465 dtos = []
466 for dto in gmKVK.get_available_kvks_as_dtos(spool_dir = kvk_dir):
467 dtos.append({'dto': dto, 'source': 'KVK'})
468
469 return dtos
470 #============================================================
471 -def get_person_from_external_sources(parent=None, search_immediately=False, activate_immediately=False):
472 """Load patient from external source.
473
474 - scan external sources for candidates
475 - let user select source
476 - if > 1 available: always
477 - if only 1 available: depending on search_immediately
478 - search for patients matching info from external source
479 - if more than one match:
480 - let user select patient
481 - if no match:
482 - create patient
483 - activate patient
484 """
485 # get DTOs from interfaces
486 dtos = []
487 dtos.extend(load_persons_from_xdt())
488 dtos.extend(load_persons_from_pracsoft_au())
489 dtos.extend(load_persons_from_kvks())
490
491 # no external persons
492 if len(dtos) == 0:
493 gmDispatcher.send(signal='statustext', msg=_('No patients found in external sources.'))
494 return None
495
496 # one external patient with DOB - already active ?
497 if (len(dtos) == 1) and (dtos[0]['dto'].dob is not None):
498 dto = dtos[0]['dto']
499 # is it already the current patient ?
500 curr_pat = gmPerson.gmCurrentPatient()
501 if curr_pat.connected:
502 key_dto = dto.firstnames + dto.lastnames + dto.dob.strftime('%Y-%m-%d') + dto.gender
503 names = curr_pat.get_active_name()
504 key_pat = names['firstnames'] + names['lastnames'] + curr_pat.get_formatted_dob(format = '%Y-%m-%d') + curr_pat['gender']
505 _log.debug('current patient: %s' % key_pat)
506 _log.debug('dto patient : %s' % key_dto)
507 if key_dto == key_pat:
508 gmDispatcher.send(signal='statustext', msg=_('The only external patient is already active in GNUmed.'), beep=False)
509 return None
510
511 # one external person - look for internal match immediately ?
512 if (len(dtos) == 1) and search_immediately:
513 dto = dtos[0]['dto']
514
515 # several external persons
516 else:
517 if parent is None:
518 parent = wx.GetApp().GetTopWindow()
519 dlg = cSelectPersonDTOFromListDlg(parent=parent, id=-1)
520 dlg.set_dtos(dtos=dtos)
521 result = dlg.ShowModal()
522 if result == wx.ID_CANCEL:
523 return None
524 dto = dlg.get_selected_dto()['dto']
525 dlg.Destroy()
526
527 # search
528 idents = dto.get_candidate_identities(can_create=True)
529 if idents is None:
530 gmGuiHelpers.gm_show_info (_(
531 'Cannot create new patient:\n\n'
532 ' [%s %s (%s), %s]'
533 ) % (dto.firstnames, dto.lastnames, dto.gender, dto.dob.strftime('%x').decode(gmI18N.get_encoding())),
534 _('Activating external patient')
535 )
536 return None
537
538 if len(idents) == 1:
539 ident = idents[0]
540
541 if len(idents) > 1:
542 if parent is None:
543 parent = wx.GetApp().GetTopWindow()
544 dlg = cSelectPersonFromListDlg(parent=parent, id=-1)
545 dlg.set_persons(persons=idents)
546 result = dlg.ShowModal()
547 if result == wx.ID_CANCEL:
548 return None
549 ident = dlg.get_selected_person()
550 dlg.Destroy()
551
552 if activate_immediately:
553 if not set_active_patient(patient = ident):
554 gmGuiHelpers.gm_show_info (
555 _(
556 'Cannot activate patient:\n\n'
557 '%s %s (%s)\n'
558 '%s'
559 ) % (dto.firstnames, dto.lastnames, dto.gender, dto.dob.strftime('%x').decode(gmI18N.get_encoding())),
560 _('Activating external patient')
561 )
562 return None
563
564 dto.import_extra_data(identity = ident)
565 dto.delete_from_source()
566
567 return ident
568 #============================================================
570 """Widget for smart search for persons."""
571
573
574 try:
575 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_ENTER
576 except KeyError:
577 kwargs['style'] = wx.TE_PROCESS_ENTER
578
579 # need to explicitly process ENTER events to avoid
580 # them being handed over to the next control
581 wx.TextCtrl.__init__(self, *args, **kwargs)
582
583 self.person = None
584
585 self._tt_search_hints = _(
586 'To search for a person type any of: \n'
587 '\n'
588 ' - fragment of last or first name\n'
589 " - date of birth (can start with '$' or '*')\n"
590 " - GNUmed ID of person (can start with '#')\n"
591 ' - exterenal ID of person\n'
592 '\n'
593 'and hit <ENTER>.\n'
594 '\n'
595 'Shortcuts:\n'
596 ' <F2>\n'
597 ' - scan external sources for persons\n'
598 ' <CURSOR-UP>\n'
599 ' - recall most recently used search term\n'
600 ' <CURSOR-DOWN>\n'
601 ' - list 10 most recently found persons\n'
602 )
603 self.SetToolTipString(self._tt_search_hints)
604
605 # FIXME: set query generator
606 self.__person_searcher = gmPerson.cPatientSearcher_SQL()
607
608 self._prev_search_term = None
609 self.__prev_idents = []
610 self._lclick_count = 0
611
612 self.__register_events()
613 #--------------------------------------------------------
614 # properties
615 #--------------------------------------------------------
619
622
623 person = property(_get_person, _set_person)
624 #--------------------------------------------------------
625 # utility methods
626 #--------------------------------------------------------
628 name = u''
629
630 if self.person is not None:
631 name = self.person['description']
632
633 self.SetValue(name)
634 #--------------------------------------------------------
636
637 if not isinstance(ident, gmPerson.cIdentity):
638 return False
639
640 # only unique identities
641 for known_ident in self.__prev_idents:
642 if known_ident['pk_identity'] == ident['pk_identity']:
643 return True
644
645 self.__prev_idents.append(ident)
646
647 # and only 10 of them
648 if len(self.__prev_idents) > 10:
649 self.__prev_idents.pop(0)
650
651 return True
652 #--------------------------------------------------------
653 # event handling
654 #--------------------------------------------------------
656 wx.EVT_CHAR(self, self.__on_char)
657 wx.EVT_SET_FOCUS(self, self._on_get_focus)
658 wx.EVT_KILL_FOCUS (self, self._on_loose_focus)
659 wx.EVT_TEXT_ENTER (self, self.GetId(), self.__on_enter)
660 #--------------------------------------------------------
662 """upon tabbing in
663
664 - select all text in the field so that the next
665 character typed will delete it
666 """
667 wx.CallAfter(self.SetSelection, -1, -1)
668 evt.Skip()
669 #--------------------------------------------------------
671 # - redraw the currently active name upon losing focus
672
673 # if we use wx.EVT_KILL_FOCUS we will also receive this event
674 # when closing our application or loosing focus to another
675 # application which is NOT what we intend to achieve,
676 # however, this is the least ugly way of doing this due to
677 # certain vagaries of wxPython (see the Wiki)
678
679 # just for good measure
680 wx.CallAfter(self.SetSelection, 0, 0)
681
682 self._display_name()
683 self._remember_ident(self.person)
684
685 evt.Skip()
686 #--------------------------------------------------------
689
691 """True: patient was selected.
692 False: no patient was selected.
693 """
694 keycode = evt.GetKeyCode()
695
696 # list of previously active patients
697 if keycode == wx.WXK_DOWN:
698 evt.Skip()
699 if len(self.__prev_idents) == 0:
700 return False
701
702 dlg = cSelectPersonFromListDlg(parent = wx.GetTopLevelParent(self), id = -1)
703 dlg.set_persons(persons = self.__prev_idents)
704 result = dlg.ShowModal()
705 if result == wx.ID_OK:
706 wx.BeginBusyCursor()
707 self.person = dlg.get_selected_person()
708 dlg.Destroy()
709 wx.EndBusyCursor()
710 return True
711
712 dlg.Destroy()
713 return False
714
715 # recall previous search fragment
716 if keycode == wx.WXK_UP:
717 evt.Skip()
718 # FIXME: cycling through previous fragments
719 if self._prev_search_term is not None:
720 self.SetValue(self._prev_search_term)
721 return False
722
723 # invoke external patient sources
724 if keycode == wx.WXK_F2:
725 evt.Skip()
726 dbcfg = gmCfg.cCfgSQL()
727 search_immediately = bool(dbcfg.get2 (
728 option = 'patient_search.external_sources.immediately_search_if_single_source',
729 workplace = gmSurgery.gmCurrentPractice().active_workplace,
730 bias = 'user',
731 default = 0
732 ))
733 p = get_person_from_external_sources (
734 parent = wx.GetTopLevelParent(self),
735 search_immediately = search_immediately
736 )
737 if p is not None:
738 self.person = p
739 return True
740 return False
741
742 # FIXME: invoke add new person
743 # FIXME: add popup menu apart from system one
744
745 evt.Skip()
746 #--------------------------------------------------------
748 """This is called from the ENTER handler."""
749
750 # ENTER but no search term ?
751 curr_search_term = self.GetValue().strip()
752 if curr_search_term == '':
753 return None
754
755 # same person anywys ?
756 if self.person is not None:
757 if curr_search_term == self.person['description']:
758 return None
759
760 # remember search fragment
761 if self.IsModified():
762 self._prev_search_term = curr_search_term
763
764 self._on_enter(search_term = curr_search_term)
765 #--------------------------------------------------------
767 """This can be overridden in child classes."""
768
769 wx.BeginBusyCursor()
770
771 # get list of matching ids
772 idents = self.__person_searcher.get_identities(search_term)
773
774 if idents is None:
775 wx.EndBusyCursor()
776 gmGuiHelpers.gm_show_info (
777 _('Error searching for matching persons.\n\n'
778 'Search term: "%s"'
779 ) % search_term,
780 _('selecting person')
781 )
782 return None
783
784 _log.info("%s matching person(s) found", len(idents))
785
786 if len(idents) == 0:
787 wx.EndBusyCursor()
788
789 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
790 wx.GetTopLevelParent(self),
791 -1,
792 caption = _('Selecting patient'),
793 question = _(
794 'Cannot find any matching patients for the search term\n\n'
795 ' "%s"\n\n'
796 'You may want to try a shorter search term.\n'
797 ) % search_term,
798 button_defs = [
799 {'label': _('Go back'), 'tooltip': _('Go back and search again.'), 'default': True},
800 {'label': _('Create new'), 'tooltip': _('Create new patient.')}
801 ]
802 )
803 if dlg.ShowModal() != wx.ID_NO:
804 return
805
806 success = gmDemographicsWidgets.create_new_person(activate = True)
807 if success:
808 self.person = gmPerson.gmCurrentPatient()
809 else:
810 self.person = None
811 return None
812
813 # only one matching identity
814 if len(idents) == 1:
815 self.person = idents[0]
816 wx.EndBusyCursor()
817 return None
818
819 # more than one matching identity: let user select from pick list
820 dlg = cSelectPersonFromListDlg(parent=wx.GetTopLevelParent(self), id=-1)
821 dlg.set_persons(persons=idents)
822 wx.EndBusyCursor()
823 result = dlg.ShowModal()
824 if result == wx.ID_CANCEL:
825 dlg.Destroy()
826 return None
827
828 wx.BeginBusyCursor()
829 self.person = dlg.get_selected_person()
830 dlg.Destroy()
831 wx.EndBusyCursor()
832
833 return None
834 #============================================================
836
837 # warn if DOB is missing
838 try:
839 patient['dob']
840 check_dob = True
841 except TypeError:
842 check_dob = False
843
844 if check_dob:
845 if patient['dob'] is None:
846 gmGuiHelpers.gm_show_warning (
847 aTitle = _('Checking date of birth'),
848 aMessage = _(
849 '\n'
850 ' %s\n'
851 '\n'
852 'The date of birth for this patient is not known !\n'
853 '\n'
854 'You can proceed to work on the patient but\n'
855 'GNUmed will be unable to assist you with\n'
856 'age-related decisions.\n'
857 ) % patient['description_gender']
858 )
859
860 success = gmPerson.set_active_patient(patient = patient, forced_reload = forced_reload)
861
862 if success:
863 if patient['dob'] is not None:
864 dbcfg = gmCfg.cCfgSQL()
865 dob_distance = dbcfg.get2 (
866 option = u'patient_search.dob_warn_interval',
867 workplace = gmSurgery.gmCurrentPractice().active_workplace,
868 bias = u'user',
869 default = u'1 week'
870 )
871
872 if patient.dob_in_range(dob_distance, dob_distance):
873 now = pyDT.datetime.now(tz = gmDateTime.gmCurrentLocalTimezone)
874 enc = gmI18N.get_encoding()
875 gmDispatcher.send(signal = 'statustext', msg = _(
876 '%(pat)s turns %(age)s on %(month)s %(day)s ! (today is %(month_now)s %(day_now)s)') % {
877 'pat': patient.get_description_gender(),
878 'age': patient.get_medical_age().strip('y'),
879 'month': patient.get_formatted_dob(format = '%B', encoding = enc),
880 'day': patient.get_formatted_dob(format = '%d', encoding = enc),
881 'month_now': now.strftime('%B').decode(enc),
882 'day_now': now.strftime('%d')
883 }
884 )
885
886 return success
887 #------------------------------------------------------------
889
891
892 cPersonSearchCtrl.__init__(self, *args, **kwargs)
893
894 # selector_tooltip = _(
895 # 'Patient search field. \n'
896 # '\n'
897 # 'To search, type any of:\n'
898 # ' - fragment of last or first name\n'
899 # " - date of birth (can start with '$' or '*')\n"
900 # " - patient ID (can start with '#')\n"
901 # 'and hit <ENTER>.\n'
902 # '\n'
903 # '<CURSOR-UP>\n'
904 # ' - recall most recently used search term\n'
905 # '<CURSOR-DOWN>\n'
906 # ' - list 10 most recently activated patients\n'
907 # '<F2>\n'
908 # ' - scan external sources for patients to import and activate\n'
909 # )
910 # self.SetToolTip(wx.ToolTip(selector_tooltip))
911
912 # get configuration
913 cfg = gmCfg.cCfgSQL()
914
915 self.__always_dismiss_on_search = bool (
916 cfg.get2 (
917 option = 'patient_search.always_dismiss_previous_patient',
918 workplace = gmSurgery.gmCurrentPractice().active_workplace,
919 bias = 'user',
920 default = 0
921 )
922 )
923
924 self.__always_reload_after_search = bool (
925 cfg.get2 (
926 option = 'patient_search.always_reload_new_patient',
927 workplace = gmSurgery.gmCurrentPractice().active_workplace,
928 bias = 'user',
929 default = 0
930 )
931 )
932
933 self.__register_events()
934 #--------------------------------------------------------
935 # utility methods
936 #--------------------------------------------------------
938 name = _('<type here to search patient>')
939
940 curr_pat = gmPerson.gmCurrentPatient()
941 if curr_pat.connected:
942 name = curr_pat['description']
943 if curr_pat.locked:
944 name = _('%(name)s (locked)') % {'name': name}
945
946 self.SetValue(name)
947
948 if self.person is None:
949 self.SetToolTipString(self._tt_search_hints)
950 return
951
952 tt = u'%s%s-----------------------------------\n%s' % (
953 gmTools.coalesce(self.person['emergency_contact'], u'', _('In case of emergency contact:') + u'\n %s\n'),
954 gmTools.coalesce(self.person['comment'], u'', u'\n%s\n'),
955 self._tt_search_hints
956 )
957 self.SetToolTipString(tt)
958 #--------------------------------------------------------
960 if not set_active_patient(patient=pat, forced_reload = self.__always_reload_after_search):
961 _log.error('cannot change active patient')
962 return None
963
964 self._remember_ident(pat)
965
966 return True
967 #--------------------------------------------------------
968 # event handling
969 #--------------------------------------------------------
971 # client internal signals
972 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
973 gmDispatcher.connect(signal = u'name_mod_db', receiver = self._on_name_identity_change)
974 gmDispatcher.connect(signal = u'identity_mod_db', receiver = self._on_name_identity_change)
975
976 gmDispatcher.connect(signal = 'patient_locked', receiver = self._on_post_patient_selection)
977 gmDispatcher.connect(signal = 'patient_unlocked', receiver = self._on_post_patient_selection)
978 #----------------------------------------------
981 #----------------------------------------------
983 if gmPerson.gmCurrentPatient().connected:
984 self.person = gmPerson.gmCurrentPatient().patient
985 else:
986 self.person = None
987 #----------------------------------------------
989
990 if self.__always_dismiss_on_search:
991 _log.warning("dismissing patient before patient search")
992 self._set_person_as_active_patient(-1)
993
994 super(self.__class__, self)._on_enter(search_term=search_term)
995
996 if self.person is None:
997 return
998
999 self._set_person_as_active_patient(self.person)
1000 #----------------------------------------------
1002
1003 success = super(self.__class__, self)._on_char(evt)
1004 if success:
1005 self._set_person_as_active_patient(self.person)
1006 #============================================================
1007 # waiting list widgets
1008 #============================================================
1010
1012
1013 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1014
1015 mp = gmMatchProvider.cMatchProvider_FixedList(aSeq = [])
1016 mp.setThresholds(1, 2, 2)
1017 self.matcher = mp
1018 self.selection_only = False
1019
1020 #--------------------------------------------------------
1022 self.matcher.set_items([ {'data': i, 'label': i, 'weight': 1} for i in items ])
1023
1024 #============================================================
1025 from Gnumed.wxGladeWidgets import wxgWaitingListEntryEditAreaPnl
1026
1027 -class cWaitingListEntryEditAreaPnl(wxgWaitingListEntryEditAreaPnl.wxgWaitingListEntryEditAreaPnl, gmEditArea.cGenericEditAreaMixin):
1028
1030
1031 try:
1032 self.patient = kwargs['patient']
1033 del kwargs['patient']
1034 except KeyError:
1035 self.patient = None
1036
1037 try:
1038 data = kwargs['entry']
1039 del kwargs['entry']
1040 except KeyError:
1041 data = None
1042
1043 wxgWaitingListEntryEditAreaPnl.wxgWaitingListEntryEditAreaPnl.__init__(self, *args, **kwargs)
1044 gmEditArea.cGenericEditAreaMixin.__init__(self)
1045
1046 if data is None:
1047 self.mode = 'new'
1048 else:
1049 self.data = data
1050 self.mode = 'edit'
1051
1052 praxis = gmSurgery.gmCurrentPractice()
1053 pats = praxis.waiting_list_patients
1054 zones = {}
1055 zones.update([ [p['waiting_zone'], None] for p in pats if p['waiting_zone'] is not None ])
1056 self._PRW_zone.update_matcher(items = zones.keys())
1057 #--------------------------------------------------------
1058 # edit area mixin API
1059 #--------------------------------------------------------
1061 if self.patient is None:
1062 self._PRW_patient.person = None
1063 self._PRW_patient.Enable(True)
1064 self._PRW_patient.SetFocus()
1065 else:
1066 self._PRW_patient.person = self.patient
1067 self._PRW_patient.Enable(False)
1068 self._PRW_comment.SetFocus()
1069 self._PRW_patient._display_name()
1070
1071 self._PRW_comment.SetValue(u'')
1072 self._PRW_zone.SetValue(u'')
1073 self._SPCTRL_urgency.SetValue(0)
1074 #--------------------------------------------------------
1076 self._PRW_patient.person = gmPerson.cIdentity(aPK_obj = self.data['pk_identity'])
1077 self._PRW_patient.Enable(False)
1078 self._PRW_patient._display_name()
1079
1080 self._PRW_comment.SetValue(gmTools.coalesce(self.data['comment'], u''))
1081 self._PRW_zone.SetValue(gmTools.coalesce(self.data['waiting_zone'], u''))
1082 self._SPCTRL_urgency.SetValue(self.data['urgency'])
1083
1084 self._PRW_comment.SetFocus()
1085 #--------------------------------------------------------
1087 validity = True
1088
1089 self.display_tctrl_as_valid(tctrl = self._PRW_patient, valid = (self._PRW_patient.person is not None))
1090 validity = (self._PRW_patient.person is not None)
1091
1092 if validity is False:
1093 gmDispatcher.send(signal = 'statustext', msg = _('Cannot add to waiting list. Missing essential input.'))
1094
1095 return validity
1096 #----------------------------------------------------------------
1098 # FIXME: filter out dupes
1099 self._PRW_patient.person.put_on_waiting_list (
1100 urgency = self._SPCTRL_urgency.GetValue(),
1101 comment = gmTools.none_if(self._PRW_comment.GetValue().strip(), u''),
1102 zone = gmTools.none_if(self._PRW_zone.GetValue().strip(), u'')
1103 )
1104 # dummy:
1105 self.data = {'pk_identity': None, 'comment': None, 'waiting_zone': None, 'urgency': 0}
1106 return True
1107 #----------------------------------------------------------------
1109 gmSurgery.gmCurrentPractice().update_in_waiting_list (
1110 pk = self.data['pk_waiting_list'],
1111 urgency = self._SPCTRL_urgency.GetValue(),
1112 comment = self._PRW_comment.GetValue().strip(),
1113 zone = self._PRW_zone.GetValue().strip()
1114 )
1115 return True
1116 #============================================================
1117 from Gnumed.wxGladeWidgets import wxgWaitingListPnl
1118
1120
1122
1123 wxgWaitingListPnl.wxgWaitingListPnl.__init__(self, *args, **kwargs)
1124 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1125
1126 self.__current_zone = None
1127
1128 self.__init_ui()
1129 self.__register_events()
1130 #--------------------------------------------------------
1131 # interal helpers
1132 #--------------------------------------------------------
1134 self._LCTRL_patients.set_columns ([
1135 _('Zone'),
1136 _('Urgency'),
1137 #' ! ',
1138 _('Waiting time'),
1139 _('Patient'),
1140 _('Born'),
1141 _('Comment')
1142 ])
1143 self._LCTRL_patients.set_column_widths(widths = [wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE_USEHEADER, wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE])
1144 self._PRW_zone.add_callback_on_selection(callback = self._on_zone_selected)
1145 self._PRW_zone.add_callback_on_lose_focus(callback = self._on_zone_selected)
1146 #--------------------------------------------------------
1148 gmDispatcher.connect(signal = u'waiting_list_generic_mod_db', receiver = self._on_waiting_list_modified)
1149 #--------------------------------------------------------
1151
1152 praxis = gmSurgery.gmCurrentPractice()
1153 pats = praxis.waiting_list_patients
1154
1155 # set matcher to all zones currently in use
1156 zones = {}
1157 zones.update([ [p['waiting_zone'], None] for p in pats if p['waiting_zone'] is not None ])
1158 self._PRW_zone.update_matcher(items = zones.keys())
1159 del zones
1160
1161 # filter patient list by zone and set waiting list
1162 self.__current_zone = self._PRW_zone.GetValue().strip()
1163 if self.__current_zone == u'':
1164 pats = [ p for p in pats ]
1165 else:
1166 pats = [ p for p in pats if p['waiting_zone'] == self.__current_zone ]
1167
1168 self._LCTRL_patients.set_string_items (
1169 [ [
1170 gmTools.coalesce(p['waiting_zone'], u''),
1171 p['urgency'],
1172 p['waiting_time_formatted'].replace(u'00 ', u'', 1).replace('00:', u'').lstrip('0'),
1173 u'%s, %s (%s)' % (p['lastnames'], p['firstnames'], p['l10n_gender']),
1174 p['dob'],
1175 gmTools.coalesce(p['comment'], u'')
1176 ] for p in pats
1177 ]
1178 )
1179 self._LCTRL_patients.set_column_widths()
1180 self._LCTRL_patients.set_data(pats)
1181 self._LCTRL_patients.Refresh()
1182 self._LCTRL_patients.SetToolTipString ( _(
1183 '%s patients are waiting.\n'
1184 '\n'
1185 'Doubleclick to activate (entry will stay in list).'
1186 ) % len(pats))
1187
1188 self._LBL_no_of_patients.SetLabel(_('(%s patients)') % len(pats))
1189
1190 if len(pats) == 0:
1191 self._BTN_activate.Enable(False)
1192 self._BTN_activateplus.Enable(False)
1193 self._BTN_remove.Enable(False)
1194 self._BTN_edit.Enable(False)
1195 self._BTN_up.Enable(False)
1196 self._BTN_down.Enable(False)
1197 else:
1198 self._BTN_activate.Enable(True)
1199 self._BTN_activateplus.Enable(True)
1200 self._BTN_remove.Enable(True)
1201 self._BTN_edit.Enable(True)
1202 if len(pats) > 1:
1203 self._BTN_up.Enable(True)
1204 self._BTN_down.Enable(True)
1205 #--------------------------------------------------------
1206 # event handlers
1207 #--------------------------------------------------------
1209 if self.__current_zone == self._PRW_zone.GetValue().strip():
1210 return True
1211 wx.CallAfter(self.__refresh_waiting_list)
1212 return True
1213 #--------------------------------------------------------
1216 #--------------------------------------------------------
1218 item = self._LCTRL_patients.get_selected_item_data(only_one=True)
1219 if item is None:
1220 return
1221 pat = gmPerson.cIdentity(aPK_obj = item['pk_identity'])
1222 wx.CallAfter(set_active_patient, patient = pat)
1223 #--------------------------------------------------------
1230 #--------------------------------------------------------
1238 #--------------------------------------------------------
1250 #--------------------------------------------------------
1259 #--------------------------------------------------------
1265 #--------------------------------------------------------
1271 #--------------------------------------------------------
1277 #--------------------------------------------------------
1278 # edit
1279 #--------------------------------------------------------
1280 # reget-on-paint API
1281 #--------------------------------------------------------
1285 #============================================================
1286 # main
1287 #------------------------------------------------------------
1288 if __name__ == "__main__":
1289
1290 if len(sys.argv) > 1:
1291 if sys.argv[1] == 'test':
1292 gmI18N.activate_locale()
1293 gmI18N.install_domain()
1294
1295 app = wx.PyWidgetTester(size = (200, 40))
1296 # app.SetWidget(cSelectPersonFromListDlg, -1)
1297 # app.SetWidget(cPersonSearchCtrl, -1)
1298 # app.SetWidget(cActivePatientSelector, -1)
1299 app.SetWidget(cWaitingListPnl, -1)
1300 app.MainLoop()
1301
1302 #============================================================
1303 # docs
1304 #------------------------------------------------------------
1305 # functionality
1306 # -------------
1307 # - hitting ENTER on non-empty field (and more than threshold chars)
1308 # - start search
1309 # - display results in a list, prefixed with numbers
1310 # - last name
1311 # - first name
1312 # - gender
1313 # - age
1314 # - city + street (no ZIP, no number)
1315 # - last visit (highlighted if within a certain interval)
1316 # - arbitrary marker (e.g. office attendance this quartal, missing KVK, appointments, due dates)
1317 # - if none found -> go to entry of new patient
1318 # - scrolling in this list
1319 # - ENTER selects patient
1320 # - ESC cancels selection
1321 # - number selects patient
1322 #
1323 # - hitting cursor-up/-down
1324 # - cycle through history of last 10 search fragments
1325 #
1326 # - hitting alt-L = List, alt-P = previous
1327 # - show list of previous ten patients prefixed with numbers
1328 # - scrolling in list
1329 # - ENTER selects patient
1330 # - ESC cancels selection
1331 # - number selects patient
1332 #
1333 # - hitting ALT-N
1334 # - immediately goes to entry of new patient
1335 #
1336 # - hitting cursor-right in a patient selection list
1337 # - pops up more detail about the patient
1338 # - ESC/cursor-left goes back to list
1339 #
1340 # - hitting TAB
1341 # - makes sure the currently active patient is displayed
1342
1343 #------------------------------------------------------------
1344 # samples
1345 # -------
1346 # working:
1347 # Ian Haywood
1348 # Haywood Ian
1349 # Haywood
1350 # Amador Jimenez (yes, two last names but no hyphen: Spain, for example)
1351 # Ian Haywood 19/12/1977
1352 # 19/12/1977
1353 # 19-12-1977
1354 # 19.12.1977
1355 # 19771219
1356 # $dob
1357 # *dob
1358 # #ID
1359 # ID
1360 # HIlbert, karsten
1361 # karsten, hilbert
1362 # kars, hilb
1363 #
1364 # non-working:
1365 # Haywood, Ian <40
1366 # ?, Ian 1977
1367 # Ian Haywood, 19/12/77
1368 # PUPIC
1369 # "hilb; karsten, 23.10.74"
1370
1371 #------------------------------------------------------------
1372 # notes
1373 # -----
1374 # >> 3. There are countries in which people have more than one
1375 # >> (significant) lastname (spanish-speaking countries are one case :), some
1376 # >> asian countries might be another one).
1377 # -> we need per-country query generators ...
1378
1379 # search case sensitive by default, switch to insensitive if not found ?
1380
1381 # accent insensitive search:
1382 # select * from * where to_ascii(column, 'encoding') like '%test%';
1383 # may not work with Unicode
1384
1385 # phrase wheel is most likely too slow
1386
1387 # extend search fragment history
1388
1389 # ask user whether to send off level 3 queries - or thread them
1390
1391 # we don't expect patient IDs in complicated patterns, hence any digits signify a date
1392
1393 # FIXME: make list window fit list size ...
1394
1395 # clear search field upon get-focus ?
1396
1397 # F1 -> context help with hotkey listing
1398
1399 # th -> th|t
1400 # v/f/ph -> f|v|ph
1401 # maybe don't do umlaut translation in the first 2-3 letters
1402 # such that not to defeat index use for the first level query ?
1403
1404 # user defined function key to start search
1405
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Mon Jun 28 04:11:42 2010 | http://epydoc.sourceforge.net |