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