1 """Widgets dealing with patient demographics."""
2
3 __author__ = "R.Terry, SJ Tan, I Haywood, Carlos Moro <cfmoro1976@yahoo.es>"
4 __license__ = 'GPL v2 or later (details at http://www.gnu.org)'
5
6
7 import sys
8 import sys
9 import codecs
10 import re as regex
11 import logging
12 import os
13
14
15 import wx
16 import wx.wizard
17 import wx.lib.imagebrowser as wx_imagebrowser
18 import wx.lib.statbmp as wx_genstatbmp
19
20
21
22 if __name__ == '__main__':
23 sys.path.insert(0, '../../')
24 from Gnumed.pycommon import gmDispatcher
25 from Gnumed.pycommon import gmI18N
26 from Gnumed.pycommon import gmMatchProvider
27 from Gnumed.pycommon import gmPG2
28 from Gnumed.pycommon import gmTools
29 from Gnumed.pycommon import gmCfg
30 from Gnumed.pycommon import gmDateTime
31 from Gnumed.pycommon import gmShellAPI
32 from Gnumed.pycommon import gmNetworkTools
33
34 from Gnumed.business import gmDemographicRecord
35 from Gnumed.business import gmPersonSearch
36 from Gnumed.business import gmSurgery
37 from Gnumed.business import gmPerson
38
39 from Gnumed.wxpython import gmPhraseWheel
40 from Gnumed.wxpython import gmRegetMixin
41 from Gnumed.wxpython import gmAuthWidgets
42 from Gnumed.wxpython import gmPersonContactWidgets
43 from Gnumed.wxpython import gmEditArea
44 from Gnumed.wxpython import gmListWidgets
45 from Gnumed.wxpython import gmDateTimeInput
46 from Gnumed.wxpython import gmDataMiningWidgets
47 from Gnumed.wxpython import gmGuiHelpers
48
49
50
51 _log = logging.getLogger('gm.ui')
52
53
54 try:
55 _('dummy-no-need-to-translate-but-make-epydoc-happy')
56 except NameError:
57 _ = lambda x:x
58
59
60
61
63 if tag_image is not None:
64 if tag_image['is_in_use']:
65 gmGuiHelpers.gm_show_info (
66 aTitle = _('Editing tag'),
67 aMessage = _(
68 'Cannot edit the image tag\n'
69 '\n'
70 ' "%s"\n'
71 '\n'
72 'because it is currently in use.\n'
73 ) % tag_image['l10n_description']
74 )
75 return False
76
77 ea = cTagImageEAPnl(parent = parent, id = -1)
78 ea.data = tag_image
79 ea.mode = gmTools.coalesce(tag_image, 'new', 'edit')
80 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = single_entry)
81 dlg.SetTitle(gmTools.coalesce(tag_image, _('Adding new tag'), _('Editing tag')))
82 if dlg.ShowModal() == wx.ID_OK:
83 dlg.Destroy()
84 return True
85 dlg.Destroy()
86 return False
87
97
98 def edit(tag_image=None):
99 return edit_tag_image(parent = parent, tag_image = tag_image, single_entry = (tag_image is not None))
100
101 def delete(tag):
102 if tag['is_in_use']:
103 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this tag. It is in use.'), beep = True)
104 return False
105
106 return gmDemographicRecord.delete_tag_image(tag_image = tag['pk_tag_image'])
107
108 def refresh(lctrl):
109 tags = gmDemographicRecord.get_tag_images(order_by = u'l10n_description')
110 items = [ [
111 t['l10n_description'],
112 gmTools.bool2subst(t['is_in_use'], u'X', u''),
113 u'%s' % t['size'],
114 t['pk_tag_image']
115 ] for t in tags ]
116 lctrl.set_string_items(items)
117 lctrl.set_column_widths(widths = [wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE_USEHEADER, wx.LIST_AUTOSIZE_USEHEADER, wx.LIST_AUTOSIZE])
118 lctrl.set_data(tags)
119
120 msg = _('\nTags with images registered with GNUmed.\n')
121
122 tag = gmListWidgets.get_choices_from_list (
123 parent = parent,
124 msg = msg,
125 caption = _('Showing tags with images.'),
126 columns = [_('Tag name'), _('In use'), _('Image size'), u'#'],
127 single_selection = True,
128 new_callback = edit,
129 edit_callback = edit,
130 delete_callback = delete,
131 refresh_callback = refresh,
132 left_extra_button = (_('WWW'), _('Go to www.openclipart.org for images.'), go_to_openclipart_org)
133 )
134
135 return tag
136
137 from Gnumed.wxGladeWidgets import wxgTagImageEAPnl
138
139 -class cTagImageEAPnl(wxgTagImageEAPnl.wxgTagImageEAPnl, gmEditArea.cGenericEditAreaMixin):
140
158
159
160
162
163 valid = True
164
165 if self.mode == u'new':
166 if self.__selected_image_file is None:
167 valid = False
168 gmDispatcher.send(signal = 'statustext', msg = _('Must pick an image file for a new tag.'), beep = True)
169 self._BTN_pick_image.SetFocus()
170
171 if self.__selected_image_file is not None:
172 try:
173 open(self.__selected_image_file).close()
174 except StandardError:
175 valid = False
176 self.__selected_image_file = None
177 gmDispatcher.send(signal = 'statustext', msg = _('Cannot open the image file [%s].') % self.__selected_image_file, beep = True)
178 self._BTN_pick_image.SetFocus()
179
180 if self._TCTRL_description.GetValue().strip() == u'':
181 valid = False
182 self.display_tctrl_as_valid(self._TCTRL_description, False)
183 self._TCTRL_description.SetFocus()
184 else:
185 self.display_tctrl_as_valid(self._TCTRL_description, True)
186
187 return (valid is True)
188
207
227
229 self._TCTRL_description.SetValue(u'')
230 self._TCTRL_filename.SetValue(u'')
231 self._BMP_image.SetBitmap(bitmap = wx.EmptyBitmap(100, 100))
232
233 self.__selected_image_file = None
234
235 self._TCTRL_description.SetFocus()
236
238 self._refresh_as_new()
239
252
253
254
266
267
268 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
269
271
273 wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl.__init__(self, *args, **kwargs)
274 self._SZR_bitmaps = self.GetSizer()
275 self.__bitmaps = []
276
277 self.__context_popup = wx.Menu()
278
279 item = self.__context_popup.Append(-1, _('&Edit comment'))
280 self.Bind(wx.EVT_MENU, self.__edit_tag, item)
281
282 item = self.__context_popup.Append(-1, _('&Remove tag'))
283 self.Bind(wx.EVT_MENU, self.__remove_tag, item)
284
285
286
288
289 self.clear()
290
291 for tag in patient.get_tags(order_by = u'l10n_description'):
292 fname = tag.export_image2file()
293 if fname is None:
294 _log.warning('cannot export image data of tag [%s]', tag['l10n_description'])
295 continue
296 img = gmGuiHelpers.file2scaled_image(filename = fname, height = 20)
297 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
298 bmp.SetToolTipString(u'%s%s' % (
299 tag['l10n_description'],
300 gmTools.coalesce(tag['comment'], u'', u'\n\n%s')
301 ))
302 bmp.tag = tag
303 bmp.Bind(wx.EVT_RIGHT_UP, self._on_bitmap_rightclicked)
304
305 self._SZR_bitmaps.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, 1)
306 self.__bitmaps.append(bmp)
307
308 self.GetParent().Layout()
309
311 while len(self._SZR_bitmaps.GetChildren()) > 0:
312 self._SZR_bitmaps.Detach(0)
313
314
315 for bmp in self.__bitmaps:
316 bmp.Destroy()
317 self.__bitmaps = []
318
319
320
328
330 if self.__current_tag is None:
331 return
332
333 msg = _('Edit the comment on tag [%s]') % self.__current_tag['l10n_description']
334 comment = wx.GetTextFromUser (
335 message = msg,
336 caption = _('Editing tag comment'),
337 default_value = gmTools.coalesce(self.__current_tag['comment'], u''),
338 parent = self
339 )
340
341 if comment == u'':
342 return
343
344 if comment.strip() == self.__current_tag['comment']:
345 return
346
347 if comment == u' ':
348 self.__current_tag['comment'] = None
349 else:
350 self.__current_tag['comment'] = comment.strip()
351
352 self.__current_tag.save()
353
354
355
357 self.__current_tag = evt.GetEventObject().tag
358 self.PopupMenu(self.__context_popup, pos = wx.DefaultPosition)
359 self.__current_tag = None
360
361
363
365
366 kwargs['message'] = _("Today's KOrganizer appointments ...")
367 kwargs['button_defs'] = [
368 {'label': _('Reload'), 'tooltip': _('Reload appointments from KOrganizer')},
369 {'label': u''},
370 {'label': u''},
371 {'label': u''},
372 {'label': u'KOrganizer', 'tooltip': _('Launch KOrganizer')}
373 ]
374 gmDataMiningWidgets.cPatientListingPnl.__init__(self, *args, **kwargs)
375
376 self.fname = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp', 'korganizer2gnumed.csv'))
377 self.reload_cmd = 'konsolekalendar --view --export-type csv --export-file %s' % self.fname
378
379
383
393
395 try: os.remove(self.fname)
396 except OSError: pass
397 gmShellAPI.run_command_in_shell(command=self.reload_cmd, blocking=True)
398 try:
399 csv_file = codecs.open(self.fname , mode = 'rU', encoding = 'utf8', errors = 'replace')
400 except IOError:
401 gmDispatcher.send(signal = u'statustext', msg = _('Cannot access KOrganizer transfer file [%s]') % self.fname, beep = True)
402 return
403
404 csv_lines = gmTools.unicode_csv_reader (
405 csv_file,
406 delimiter = ','
407 )
408
409 self._LCTRL_items.set_columns ([
410 _('Place'),
411 _('Start'),
412 u'',
413 u'',
414 _('Patient'),
415 _('Comment')
416 ])
417 items = []
418 data = []
419 for line in csv_lines:
420 items.append([line[5], line[0], line[1], line[3], line[4], line[6]])
421 data.append([line[4], line[7]])
422
423 self._LCTRL_items.set_string_items(items = items)
424 self._LCTRL_items.set_column_widths()
425 self._LCTRL_items.set_data(data = data)
426 self._LCTRL_items.patient_key = 0
427
428
429
432
433
434
436
437 pat = gmPerson.gmCurrentPatient()
438 curr_jobs = pat.get_occupations()
439 if len(curr_jobs) > 0:
440 old_job = curr_jobs[0]['l10n_occupation']
441 update = curr_jobs[0]['modified_when'].strftime('%m/%Y')
442 else:
443 old_job = u''
444 update = u''
445
446 msg = _(
447 'Please enter the primary occupation of the patient.\n'
448 '\n'
449 'Currently recorded:\n'
450 '\n'
451 ' %s (last updated %s)'
452 ) % (old_job, update)
453
454 new_job = wx.GetTextFromUser (
455 message = msg,
456 caption = _('Editing primary occupation'),
457 default_value = old_job,
458 parent = None
459 )
460 if new_job.strip() == u'':
461 return
462
463 for job in curr_jobs:
464
465 if job['l10n_occupation'] != new_job:
466 pat.unlink_occupation(occupation = job['l10n_occupation'])
467
468 pat.link_occupation(occupation = new_job)
469
470
485
486
487
488
490
491 go_ahead = gmGuiHelpers.gm_show_question (
492 _('Are you sure you really, positively want\n'
493 'to disable the following person ?\n'
494 '\n'
495 ' %s %s %s\n'
496 ' born %s\n'
497 '\n'
498 '%s\n'
499 ) % (
500 identity['firstnames'],
501 identity['lastnames'],
502 identity['gender'],
503 identity['dob'],
504 gmTools.bool2subst (
505 identity.is_patient,
506 _('This patient DID receive care.'),
507 _('This person did NOT receive care.')
508 )
509 ),
510 _('Disabling person')
511 )
512 if not go_ahead:
513 return True
514
515
516 conn = gmAuthWidgets.get_dbowner_connection (
517 procedure = _('Disabling patient')
518 )
519
520 if conn is False:
521 return True
522
523 if conn is None:
524 return False
525
526
527 gmPG2.run_rw_queries(queries = [{'cmd': u"update dem.identity set deleted=True where pk=%s", 'args': [identity['pk_identity']]}])
528
529 return True
530
531
532
533
548
550
552 query = u"""
553 (SELECT distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
554 union
555 (SELECT distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
556 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
557 mp.setThresholds(3, 5, 9)
558 gmPhraseWheel.cPhraseWheel.__init__ (
559 self,
560 *args,
561 **kwargs
562 )
563 self.SetToolTipString(_("Type or select a first name (forename/Christian name/given name)."))
564 self.capitalisation_mode = gmTools.CAPS_NAMES
565 self.matcher = mp
566
568
570 query = u"""
571 (SELECT distinct preferred, preferred from dem.names where preferred %(fragment_condition)s order by preferred limit 20)
572 union
573 (SELECT distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
574 union
575 (SELECT distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
576 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
577 mp.setThresholds(3, 5, 9)
578 gmPhraseWheel.cPhraseWheel.__init__ (
579 self,
580 *args,
581 **kwargs
582 )
583 self.SetToolTipString(_("Type or select an alias (nick name, preferred name, call name, warrior name, artist name)."))
584
585
586 self.matcher = mp
587
589
591 query = u"SELECT distinct title, title from dem.identity where title %(fragment_condition)s"
592 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
593 mp.setThresholds(1, 3, 9)
594 gmPhraseWheel.cPhraseWheel.__init__ (
595 self,
596 *args,
597 **kwargs
598 )
599 self.SetToolTipString(_("Type or select a title. Note that the title applies to the person, not to a particular name !"))
600 self.matcher = mp
601
603 """Let user select a gender."""
604
605 _gender_map = None
606
608
609 if cGenderSelectionPhraseWheel._gender_map is None:
610 cmd = u"""
611 SELECT tag, l10n_label, sort_weight
612 from dem.v_gender_labels
613 order by sort_weight desc"""
614 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx=True)
615 cGenderSelectionPhraseWheel._gender_map = {}
616 for gender in rows:
617 cGenderSelectionPhraseWheel._gender_map[gender[idx['tag']]] = {
618 'data': gender[idx['tag']],
619 'field_label': gender[idx['l10n_label']],
620 'list_label': gender[idx['l10n_label']],
621 'weight': gender[idx['sort_weight']]
622 }
623
624 mp = gmMatchProvider.cMatchProvider_FixedList(aSeq = cGenderSelectionPhraseWheel._gender_map.values())
625 mp.setThresholds(1, 1, 3)
626
627 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
628 self.selection_only = True
629 self.matcher = mp
630 self.picklist_delay = 50
631
633
635 query = u"""
636 SELECT DISTINCT ON (list_label)
637 pk AS data,
638 name AS field_label,
639 name || coalesce(' (' || issuer || ')', '') as list_label
640 FROM dem.enum_ext_id_types
641 WHERE name %(fragment_condition)s
642 ORDER BY list_label
643 LIMIT 25
644 """
645 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
646 mp.setThresholds(1, 3, 5)
647 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
648 self.SetToolTipString(_("Enter or select a type for the external ID."))
649 self.matcher = mp
650
655
670
671
672
673 from Gnumed.wxGladeWidgets import wxgExternalIDEditAreaPnl
674
675 -class cExternalIDEditAreaPnl(wxgExternalIDEditAreaPnl.wxgExternalIDEditAreaPnl, gmEditArea.cGenericEditAreaMixin):
676 """An edit area for editing/creating external IDs.
677
678 Does NOT act on/listen to the current patient.
679 """
699
702
703
704
725
743
759
765
767 self._refresh_as_new()
768 self._PRW_issuer.SetText(self.data['issuer'])
769
775
776
777
779 """Set the issuer according to the selected type.
780
781 Matches are fetched from existing records in backend.
782 """
783 pk_curr_type = self._PRW_type.GetData()
784 if pk_curr_type is None:
785 return True
786 rows, idx = gmPG2.run_ro_queries(queries = [{
787 'cmd': u"SELECT issuer from dem.enum_ext_id_types where pk = %s",
788 'args': [pk_curr_type]
789 }])
790 if len(rows) == 0:
791 return True
792 wx.CallAfter(self._PRW_issuer.SetText, rows[0][0])
793 return True
794
795
796
797
799 allow_empty_dob = gmGuiHelpers.gm_show_question (
800 _(
801 'Are you sure you want to leave this person\n'
802 'without a valid date of birth ?\n'
803 '\n'
804 'This can be useful for temporary staff members\n'
805 'but will provoke nag screens if this person\n'
806 'becomes a patient.\n'
807 ),
808 _('Validating date of birth')
809 )
810 return allow_empty_dob
811
813
814
815 if dob_prw.is_valid_timestamp(allow_empty = False):
816 dob = dob_prw.date
817
818 if (dob.year > 1899) and (dob < gmDateTime.pydt_now_here()):
819 return True
820
821 if dob.year < 1900:
822 msg = _(
823 'DOB: %s\n'
824 '\n'
825 'While this is a valid point in time Python does\n'
826 'not know how to deal with it.\n'
827 '\n'
828 'We suggest using January 1st 1901 instead and adding\n'
829 'the true date of birth to the patient comment.\n'
830 '\n'
831 'Sorry for the inconvenience %s'
832 ) % (dob, gmTools.u_frowning_face)
833 else:
834 msg = _(
835 'DOB: %s\n'
836 '\n'
837 'Date of birth in the future !'
838 ) % dob
839 gmGuiHelpers.gm_show_error (
840 msg,
841 _('Validating date of birth')
842 )
843 dob_prw.display_as_valid(False)
844 dob_prw.SetFocus()
845 return False
846
847
848 if dob_prw.GetValue().strip() != u'':
849 dob_prw.display_as_valid(False)
850 gmDispatcher.send(signal = u'statustext', msg = _('Invalid date of birth.'))
851 dob_prw.SetFocus()
852 return False
853
854
855 dob_prw.display_as_valid(False)
856 return True
857
858 from Gnumed.wxGladeWidgets import wxgIdentityEAPnl
859
860 -class cIdentityEAPnl(wxgIdentityEAPnl.wxgIdentityEAPnl, gmEditArea.cGenericEditAreaMixin):
861 """An edit area for editing/creating title/gender/dob/dod etc."""
862
878
879
880
881
882
883
884
885
887
888 has_error = False
889
890 if self._PRW_gender.GetData() is None:
891 self._PRW_gender.SetFocus()
892 has_error = True
893
894 if self.data is not None:
895 if not _validate_dob_field(self._PRW_dob):
896 has_error = True
897
898 if not self._PRW_dod.is_valid_timestamp(allow_empty = True):
899 gmDispatcher.send(signal = u'statustext', msg = _('Invalid date of death.'))
900 self._PRW_dod.SetFocus()
901 has_error = True
902
903 return (has_error is False)
904
908
925
928
957
960
961 from Gnumed.wxGladeWidgets import wxgPersonNameEAPnl
962
963 -class cPersonNameEAPnl(wxgPersonNameEAPnl.wxgPersonNameEAPnl, gmEditArea.cGenericEditAreaMixin):
964 """An edit area for editing/creating names of people.
965
966 Does NOT act on/listen to the current patient.
967 """
988
989
990
991
992
993
994
995
997 validity = True
998
999 if self._PRW_lastname.GetValue().strip() == u'':
1000 validity = False
1001 self._PRW_lastname.display_as_valid(False)
1002 self._PRW_lastname.SetFocus()
1003 else:
1004 self._PRW_lastname.display_as_valid(True)
1005
1006 if self._PRW_firstname.GetValue().strip() == u'':
1007 validity = False
1008 self._PRW_firstname.display_as_valid(False)
1009 self._PRW_firstname.SetFocus()
1010 else:
1011 self._PRW_firstname.display_as_valid(True)
1012
1013 return validity
1014
1016
1017 first = self._PRW_firstname.GetValue().strip()
1018 last = self._PRW_lastname.GetValue().strip()
1019 active = self._CHBOX_active.GetValue()
1020
1021 data = self.__identity.add_name(first, last, active)
1022
1023 old_nick = self.__identity['active_name']['preferred']
1024 new_nick = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1025 if active:
1026 data['preferred'] = gmTools.coalesce(new_nick, old_nick)
1027 else:
1028 data['preferred'] = new_nick
1029 data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1030 data.save()
1031
1032 self.data = data
1033 return True
1034
1036 """The knack here is that we can only update a few fields.
1037
1038 Otherwise we need to clone the name and update that.
1039 """
1040 first = self._PRW_firstname.GetValue().strip()
1041 last = self._PRW_lastname.GetValue().strip()
1042 active = self._CHBOX_active.GetValue()
1043
1044 current_name = self.data['firstnames'].strip() + self.data['lastnames'].strip()
1045 new_name = first + last
1046
1047
1048 if new_name == current_name:
1049 self.data['active_name'] = self._CHBOX_active.GetValue()
1050 self.data['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1051 self.data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1052 self.data.save()
1053
1054 else:
1055 name = self.__identity.add_name(first, last, active)
1056 name['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1057 name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1058 name.save()
1059 self.data = name
1060
1061 return True
1062
1071
1078
1087
1088
1089
1091 """A list for managing a person's names.
1092
1093 Does NOT act on/listen to the current patient.
1094 """
1112
1113
1114
1115 - def refresh(self, *args, **kwargs):
1132
1133
1134
1136 self._LCTRL_items.set_columns(columns = [
1137 _('Active'),
1138 _('Lastname'),
1139 _('Firstname(s)'),
1140 _('Preferred Name'),
1141 _('Comment')
1142 ])
1143 self._BTN_edit.SetLabel(_('Clone and &edit'))
1144
1155
1165
1167
1168 if len(self.__identity.get_names()) == 1:
1169 gmDispatcher.send(signal = u'statustext', msg = _('Cannot delete the only name of a person.'), beep = True)
1170 return False
1171
1172 if name['active_name']:
1173 gmDispatcher.send(signal = u'statustext', msg = _('Cannot delete the active name of a person.'), beep = True)
1174 return False
1175
1176 go_ahead = gmGuiHelpers.gm_show_question (
1177 _( 'It is often advisable to keep old names around and\n'
1178 'just create a new "currently active" name.\n'
1179 '\n'
1180 'This allows finding the patient by both the old\n'
1181 'and the new name (think before/after marriage).\n'
1182 '\n'
1183 'Do you still want to really delete\n'
1184 "this name from the patient ?"
1185 ),
1186 _('Deleting name')
1187 )
1188 if not go_ahead:
1189 return False
1190
1191 self.__identity.delete_name(name = name)
1192 return True
1193
1194
1195
1197 return self.__identity
1198
1202
1203 identity = property(_get_identity, _set_identity)
1204
1206 """A list for managing a person's external IDs.
1207
1208 Does NOT act on/listen to the current patient.
1209 """
1227
1228
1229
1230 - def refresh(self, *args, **kwargs):
1247
1248
1249
1251 self._LCTRL_items.set_columns(columns = [
1252 _('ID type'),
1253 _('Value'),
1254 _('Issuer'),
1255 _('Comment')
1256 ])
1257
1268
1279
1281 go_ahead = gmGuiHelpers.gm_show_question (
1282 _( 'Do you really want to delete this\n'
1283 'external ID from the patient ?'),
1284 _('Deleting external ID')
1285 )
1286 if not go_ahead:
1287 return False
1288 self.__identity.delete_external_id(pk_ext_id = ext_id['pk_id'])
1289 return True
1290
1291
1292
1294 return self.__identity
1295
1299
1300 identity = property(_get_identity, _set_identity)
1301
1302
1303
1304 from Gnumed.wxGladeWidgets import wxgPersonIdentityManagerPnl
1305
1307 """A panel for editing identity data for a person.
1308
1309 - provides access to:
1310 - identity EA
1311 - name list manager
1312 - external IDs list manager
1313
1314 Does NOT act on/listen to the current patient.
1315 """
1322
1323
1324
1326 self._PNL_names.identity = self.__identity
1327 self._PNL_ids.identity = self.__identity
1328
1329 self._PNL_identity.mode = 'new'
1330 self._PNL_identity.data = self.__identity
1331 if self.__identity is not None:
1332 self._PNL_identity.mode = 'edit'
1333 self._PNL_identity._refresh_from_existing()
1334
1335
1336
1338 return self.__identity
1339
1343
1344 identity = property(_get_identity, _set_identity)
1345
1346
1347
1351
1352
1355
1356
1357 from Gnumed.wxGladeWidgets import wxgPersonSocialNetworkManagerPnl
1358
1367
1368
1369
1371
1372 tt = _('Link another person in this database as the emergency contact:\n\nEnter person name part or identifier and hit <enter>.')
1373
1374 if self.__identity is None:
1375 self._TCTRL_er_contact.SetValue(u'')
1376 self._TCTRL_person.person = None
1377 self._TCTRL_person.SetToolTipString(tt)
1378
1379 self._PRW_provider.SetText(value = u'', data = None)
1380 return
1381
1382 self._TCTRL_er_contact.SetValue(gmTools.coalesce(self.__identity['emergency_contact'], u''))
1383 if self.__identity['pk_emergency_contact'] is not None:
1384 ident = gmPerson.cIdentity(aPK_obj = self.__identity['pk_emergency_contact'])
1385 self._TCTRL_person.person = ident
1386 tt = u'%s\n\n%s\n\n%s' % (
1387 tt,
1388 ident['description_gender'],
1389 u'\n'.join([
1390 u'%s: %s%s' % (
1391 c['l10n_comm_type'],
1392 c['url'],
1393 gmTools.bool2subst(c['is_confidential'], _(' (confidential !)'), u'', u'')
1394 )
1395 for c in ident.get_comm_channels()
1396 ])
1397 )
1398 else:
1399 self._TCTRL_person.person = None
1400
1401 self._TCTRL_person.SetToolTipString(tt)
1402
1403 if self.__identity['pk_primary_provider'] is None:
1404 self._PRW_provider.SetText(value = u'', data = None)
1405 else:
1406 self._PRW_provider.SetData(data = self.__identity['pk_primary_provider'])
1407
1408
1409
1411 return self.__identity
1412
1416
1417 identity = property(_get_identity, _set_identity)
1418
1419
1420
1435
1438
1449
1457
1458
1459
1461
1462 dbcfg = gmCfg.cCfgSQL()
1463
1464 def_region = dbcfg.get2 (
1465 option = u'person.create.default_region',
1466 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1467 bias = u'user'
1468 )
1469 def_country = None
1470
1471 if def_region is None:
1472 def_country = dbcfg.get2 (
1473 option = u'person.create.default_country',
1474 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1475 bias = u'user'
1476 )
1477 else:
1478 countries = gmDemographicRecord.get_country_for_region(region = def_region)
1479 if len(countries) == 1:
1480 def_country = countries[0]['code_country']
1481
1482 if parent is None:
1483 parent = wx.GetApp().GetTopWindow()
1484
1485 ea = cNewPatientEAPnl(parent = parent, id = -1, country = def_country, region = def_region)
1486 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = True)
1487 dlg.SetTitle(_('Adding new person'))
1488 ea._PRW_lastname.SetFocus()
1489 result = dlg.ShowModal()
1490 pat = ea.data
1491 dlg.Destroy()
1492
1493 if result != wx.ID_OK:
1494 return False
1495
1496 _log.debug('created new person [%s]', pat.ID)
1497
1498 if activate:
1499 from Gnumed.wxpython import gmPatSearchWidgets
1500 gmPatSearchWidgets.set_active_patient(patient = pat)
1501
1502 gmDispatcher.send(signal = 'display_widget', name = 'gmNotebookedPatientEditionPlugin')
1503
1504 return True
1505
1506 from Gnumed.wxGladeWidgets import wxgNewPatientEAPnl
1507
1508 -class cNewPatientEAPnl(wxgNewPatientEAPnl.wxgNewPatientEAPnl, gmEditArea.cGenericEditAreaMixin):
1509
1511
1512 try:
1513 self.default_region = kwargs['region']
1514 del kwargs['region']
1515 except KeyError:
1516 self.default_region = None
1517
1518 try:
1519 self.default_country = kwargs['country']
1520 del kwargs['country']
1521 except KeyError:
1522 self.default_country = None
1523
1524 wxgNewPatientEAPnl.wxgNewPatientEAPnl.__init__(self, *args, **kwargs)
1525 gmEditArea.cGenericEditAreaMixin.__init__(self)
1526
1527 self.mode = 'new'
1528 self.data = None
1529 self._address = None
1530
1531 self.__init_ui()
1532 self.__register_interests()
1533
1534
1535
1537 self._PRW_lastname.final_regex = '.+'
1538 self._PRW_firstnames.final_regex = '.+'
1539 self._PRW_address_searcher.selection_only = False
1540
1541
1542
1543
1544 if self.default_country is not None:
1545 match = self._PRW_country._data2match(data = self.default_country)
1546 if match is not None:
1547 self._PRW_country.SetText(value = match['field_label'], data = match['data'])
1548
1549 if self.default_region is not None:
1550 self._PRW_region.SetText(value = self.default_region)
1551
1553
1554 adr = self._PRW_address_searcher.address
1555 if adr is None:
1556 return True
1557
1558 if ctrl.GetValue().strip() != adr[field]:
1559 wx.CallAfter(self._PRW_address_searcher.SetText, value = u'', data = None)
1560 return True
1561
1562 return False
1563
1565 adr = self._PRW_address_searcher.address
1566 if adr is None:
1567 return True
1568
1569 self._PRW_zip.SetText(value = adr['postcode'], data = adr['postcode'])
1570
1571 self._PRW_street.SetText(value = adr['street'], data = adr['street'])
1572 self._PRW_street.set_context(context = u'zip', val = adr['postcode'])
1573
1574 self._PRW_urb.SetText(value = adr['urb'], data = adr['urb'])
1575 self._PRW_urb.set_context(context = u'zip', val = adr['postcode'])
1576
1577 self._PRW_region.SetText(value = adr['l10n_state'], data = adr['code_state'])
1578 self._PRW_region.set_context(context = u'zip', val = adr['postcode'])
1579
1580 self._PRW_country.SetText(value = adr['l10n_country'], data = adr['code_country'])
1581 self._PRW_country.set_context(context = u'zip', val = adr['postcode'])
1582
1584 error = False
1585
1586
1587 if self._PRW_lastname.GetValue().strip() == u'':
1588 error = True
1589 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
1590 self._PRW_lastname.display_as_valid(False)
1591 else:
1592 self._PRW_lastname.display_as_valid(True)
1593
1594 if self._PRW_firstnames.GetValue().strip() == '':
1595 error = True
1596 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
1597 self._PRW_firstnames.display_as_valid(False)
1598 else:
1599 self._PRW_firstnames.display_as_valid(True)
1600
1601
1602 if self._PRW_gender.GetData() is None:
1603 error = True
1604 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
1605 self._PRW_gender.display_as_valid(False)
1606 else:
1607 self._PRW_gender.display_as_valid(True)
1608
1609
1610 if not _validate_dob_field(self._PRW_dob):
1611 error = True
1612
1613
1614
1615
1616 return (not error)
1617
1619
1620
1621 if self._PRW_address_searcher.GetData() is not None:
1622 wx.CallAfter(self.__set_fields_from_address_searcher)
1623 return True
1624
1625
1626 fields_to_fill = (
1627 self._TCTRL_number,
1628 self._PRW_zip,
1629 self._PRW_street,
1630 self._PRW_urb,
1631 self._PRW_type
1632 )
1633 no_of_filled_fields = 0
1634
1635 for field in fields_to_fill:
1636 if field.GetValue().strip() != u'':
1637 no_of_filled_fields += 1
1638 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1639 field.Refresh()
1640
1641
1642 if no_of_filled_fields == 0:
1643 if empty_address_is_valid:
1644 return True
1645 else:
1646 return None
1647
1648
1649 if no_of_filled_fields != len(fields_to_fill):
1650 for field in fields_to_fill:
1651 if field.GetValue().strip() == u'':
1652 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1653 field.SetFocus()
1654 field.Refresh()
1655 msg = _('To properly create an address, all the related fields must be filled in.')
1656 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1657 return False
1658
1659
1660
1661
1662 strict_fields = (
1663 self._PRW_type,
1664 self._PRW_region,
1665 self._PRW_country
1666 )
1667 error = False
1668 for field in strict_fields:
1669 if field.GetData() is None:
1670 error = True
1671 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1672 field.SetFocus()
1673 else:
1674 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1675 field.Refresh()
1676
1677 if error:
1678 msg = _('This field must contain an item selected from the dropdown list.')
1679 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1680 return False
1681
1682 return True
1683
1701
1702
1703
1705 """Set the gender according to entered firstname.
1706
1707 Matches are fetched from existing records in backend.
1708 """
1709
1710
1711 if self._PRW_gender.GetData() is not None:
1712 return True
1713
1714 firstname = self._PRW_firstnames.GetValue().strip()
1715 if firstname == u'':
1716 return True
1717
1718 gender = gmPerson.map_firstnames2gender(firstnames = firstname)
1719 if gender is None:
1720 return True
1721
1722 wx.CallAfter(self._PRW_gender.SetData, gender)
1723 return True
1724
1726 self.__perhaps_invalidate_address_searcher(self._PRW_zip, 'postcode')
1727
1728 zip_code = gmTools.none_if(self._PRW_zip.GetValue().strip(), u'')
1729 self._PRW_street.set_context(context = u'zip', val = zip_code)
1730 self._PRW_urb.set_context(context = u'zip', val = zip_code)
1731 self._PRW_region.set_context(context = u'zip', val = zip_code)
1732 self._PRW_country.set_context(context = u'zip', val = zip_code)
1733
1734 return True
1735
1737 self.__perhaps_invalidate_address_searcher(self._PRW_country, 'l10n_country')
1738
1739 country = gmTools.none_if(self._PRW_country.GetValue().strip(), u'')
1740 self._PRW_region.set_context(context = u'country', val = country)
1741
1742 return True
1743
1745 if self._TCTRL_number.GetValue().strip() == u'':
1746 adr = self._PRW_address_searcher.address
1747 if adr is None:
1748 return True
1749 self._TCTRL_number.SetValue(adr['number'])
1750 return True
1751
1752 self.__perhaps_invalidate_address_searcher(self._TCTRL_number, 'number')
1753 return True
1754
1756 if self._TCTRL_unit.GetValue().strip() == u'':
1757 adr = self._PRW_address_searcher.address
1758 if adr is None:
1759 return True
1760 self._TCTRL_unit.SetValue(gmTools.coalesce(adr['subunit'], u''))
1761 return True
1762
1763 self.__perhaps_invalidate_address_searcher(self._TCTRL_unit, 'subunit')
1764 return True
1765
1767 mapping = [
1768 (self._PRW_street, 'street'),
1769 (self._PRW_urb, 'urb'),
1770 (self._PRW_region, 'l10n_state')
1771 ]
1772
1773 for ctrl, field in mapping:
1774 if self.__perhaps_invalidate_address_searcher(ctrl, field):
1775 return True
1776
1777 return True
1778
1780 if self._PRW_address_searcher.address is None:
1781 return True
1782
1783 wx.CallAfter(self.__set_fields_from_address_searcher)
1784 return True
1785
1786
1787
1789 if self._PRW_primary_provider.GetValue().strip() == u'':
1790 self._PRW_primary_provider.display_as_valid(True)
1791 else:
1792 if self._PRW_primary_provider.GetData() is None:
1793 self._PRW_primary_provider.display_as_valid(False)
1794 else:
1795 self._PRW_primary_provider.display_as_valid(True)
1796 return (self.__identity_valid_for_save() and self.__address_valid_for_save(empty_address_is_valid = True))
1797
1799
1800 if self._PRW_dob.GetValue().strip() == u'':
1801 if not _empty_dob_allowed():
1802 self._PRW_dob.display_as_valid(False)
1803 self._PRW_dob.SetFocus()
1804 return False
1805
1806
1807 new_identity = gmPerson.create_identity (
1808 gender = self._PRW_gender.GetData(),
1809 dob = self._PRW_dob.GetData(),
1810 lastnames = self._PRW_lastname.GetValue().strip(),
1811 firstnames = self._PRW_firstnames.GetValue().strip()
1812 )
1813 _log.debug('identity created: %s' % new_identity)
1814
1815 new_identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip())
1816 new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
1817
1818 prov = self._PRW_primary_provider.GetData()
1819 if prov is not None:
1820 new_identity['pk_primary_provider'] = prov
1821 new_identity['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1822 new_identity.save()
1823
1824
1825
1826 is_valid = self.__address_valid_for_save(empty_address_is_valid = False)
1827 if is_valid is True:
1828
1829
1830 try:
1831 new_identity.link_address (
1832 number = self._TCTRL_number.GetValue().strip(),
1833 street = self._PRW_street.GetValue().strip(),
1834 postcode = self._PRW_zip.GetValue().strip(),
1835 urb = self._PRW_urb.GetValue().strip(),
1836 state = self._PRW_region.GetData(),
1837 country = self._PRW_country.GetData(),
1838 subunit = gmTools.none_if(self._TCTRL_unit.GetValue().strip(), u''),
1839 id_type = self._PRW_type.GetData()
1840 )
1841 except gmPG2.dbapi.InternalError:
1842 _log.debug('number: >>%s<<', self._TCTRL_number.GetValue().strip())
1843 _log.debug('(sub)unit: >>%s<<', self._TCTRL_unit.GetValue().strip())
1844 _log.debug('street: >>%s<<', self._PRW_street.GetValue().strip())
1845 _log.debug('postcode: >>%s<<', self._PRW_zip.GetValue().strip())
1846 _log.debug('urb: >>%s<<', self._PRW_urb.GetValue().strip())
1847 _log.debug('state: >>%s<<', self._PRW_region.GetData().strip())
1848 _log.debug('country: >>%s<<', self._PRW_country.GetData().strip())
1849 _log.exception('cannot link address')
1850 gmGuiHelpers.gm_show_error (
1851 aTitle = _('Saving address'),
1852 aMessage = _(
1853 'Cannot save this address.\n'
1854 '\n'
1855 'You will have to add it via the Demographics plugin.\n'
1856 )
1857 )
1858 elif is_valid is False:
1859 gmGuiHelpers.gm_show_error (
1860 aTitle = _('Saving address'),
1861 aMessage = _(
1862 'Address not saved.\n'
1863 '\n'
1864 'You will have to add it via the Demographics plugin.\n'
1865 )
1866 )
1867
1868
1869
1870 channel_name = self._PRW_channel_type.GetValue().strip()
1871 pk_channel_type = self._PRW_channel_type.GetData()
1872 if pk_channel_type is None:
1873 if channel_name == u'':
1874 channel_name = u'homephone'
1875 new_identity.link_comm_channel (
1876 comm_medium = channel_name,
1877 pk_channel_type = pk_channel_type,
1878 url = gmTools.none_if(self._TCTRL_phone.GetValue().strip(), u''),
1879 is_confidential = False
1880 )
1881
1882
1883 pk_type = self._PRW_external_id_type.GetData()
1884 id_value = self._TCTRL_external_id_value.GetValue().strip()
1885 if (pk_type is not None) and (id_value != u''):
1886 new_identity.add_external_id(value = id_value, pk_type = pk_type)
1887
1888
1889 new_identity.link_occupation (
1890 occupation = gmTools.none_if(self._PRW_occupation.GetValue().strip(), u'')
1891 )
1892
1893 self.data = new_identity
1894 return True
1895
1897 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
1898
1902
1905
1907 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
1908
1909
1910
1911
1913 """Notebook displaying demographics editing pages:
1914
1915 - Identity (as per Jim/Rogerio 12/2011)
1916 - Contacts (addresses, phone numbers, etc)
1917 - Social network (significant others, GP, etc)
1918
1919 Does NOT act on/listen to the current patient.
1920 """
1921
1923
1924 wx.Notebook.__init__ (
1925 self,
1926 parent = parent,
1927 id = id,
1928 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
1929 name = self.__class__.__name__
1930 )
1931
1932 self.__identity = None
1933 self.__do_layout()
1934 self.SetSelection(0)
1935
1936
1937
1939 """Populate fields in pages with data from model."""
1940 for page_idx in range(self.GetPageCount()):
1941 page = self.GetPage(page_idx)
1942 page.identity = self.__identity
1943
1944 return True
1945
1946
1947
1949 """Build patient edition notebook pages."""
1950
1951
1952 new_page = cPersonIdentityManagerPnl(self, -1)
1953 new_page.identity = self.__identity
1954 self.AddPage (
1955 page = new_page,
1956 text = _('Identity'),
1957 select = False
1958 )
1959
1960
1961 new_page = gmPersonContactWidgets.cPersonContactsManagerPnl(self, -1)
1962 new_page.identity = self.__identity
1963 self.AddPage (
1964 page = new_page,
1965 text = _('Contacts'),
1966 select = True
1967 )
1968
1969
1970 new_page = cPersonSocialNetworkManagerPnl(self, -1)
1971 new_page.identity = self.__identity
1972 self.AddPage (
1973 page = new_page,
1974 text = _('Social network'),
1975 select = False
1976 )
1977
1978
1979
1981 return self.__identity
1982
1985
1986 identity = property(_get_identity, _set_identity)
1987
1988
1989
1990
1991
1992
1994 """Page containing patient occupations edition fields.
1995 """
1996 - def __init__(self, parent, id, ident=None):
1997 """
1998 Creates a new instance of BasicPatDetailsPage
1999 @param parent - The parent widget
2000 @type parent - A wx.Window instance
2001 @param id - The widget id
2002 @type id - An integer
2003 """
2004 wx.Panel.__init__(self, parent, id)
2005 self.__ident = ident
2006 self.__do_layout()
2007
2009 PNL_form = wx.Panel(self, -1)
2010
2011 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
2012 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
2013 self.PRW_occupation.SetToolTipString(_("primary occupation of the patient"))
2014
2015 STT_occupation_updated = wx.StaticText(PNL_form, -1, _('Last updated'))
2016 self.TTC_occupation_updated = wx.TextCtrl(PNL_form, -1, style = wx.TE_READONLY)
2017
2018
2019 SZR_input = wx.FlexGridSizer(cols = 2, rows = 5, vgap = 4, hgap = 4)
2020 SZR_input.AddGrowableCol(1)
2021 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
2022 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
2023 SZR_input.Add(STT_occupation_updated, 0, wx.SHAPED)
2024 SZR_input.Add(self.TTC_occupation_updated, 1, wx.EXPAND)
2025 PNL_form.SetSizerAndFit(SZR_input)
2026
2027
2028 SZR_main = wx.BoxSizer(wx.VERTICAL)
2029 SZR_main.Add(PNL_form, 1, wx.EXPAND)
2030 self.SetSizer(SZR_main)
2031
2034
2035 - def refresh(self, identity=None):
2036 if identity is not None:
2037 self.__ident = identity
2038 jobs = self.__ident.get_occupations()
2039 if len(jobs) > 0:
2040 self.PRW_occupation.SetText(jobs[0]['l10n_occupation'])
2041 self.TTC_occupation_updated.SetValue(jobs[0]['modified_when'].strftime('%m/%Y'))
2042 return True
2043
2045 if self.PRW_occupation.IsModified():
2046 new_job = self.PRW_occupation.GetValue().strip()
2047 jobs = self.__ident.get_occupations()
2048 for job in jobs:
2049 if job['l10n_occupation'] == new_job:
2050 continue
2051 self.__ident.unlink_occupation(occupation = job['l10n_occupation'])
2052 self.__ident.link_occupation(occupation = new_job)
2053 return True
2054
2056 """Patient demographics plugin for main notebook.
2057
2058 Hosts another notebook with pages for Identity, Contacts, etc.
2059
2060 Acts on/listens to the currently active patient.
2061 """
2062
2068
2069
2070
2071
2072
2073
2075 """Arrange widgets."""
2076 self.__patient_notebook = cPersonDemographicsEditorNb(self, -1)
2077
2078 szr_main = wx.BoxSizer(wx.VERTICAL)
2079 szr_main.Add(self.__patient_notebook, 1, wx.EXPAND)
2080 self.SetSizerAndFit(szr_main)
2081
2082
2083
2085 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2086 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2087
2089 self._schedule_data_reget()
2090
2092 self._schedule_data_reget()
2093
2094
2104
2105
2106 if __name__ == "__main__":
2107
2108
2110 app = wx.PyWidgetTester(size = (600, 400))
2111 app.SetWidget(cKOrganizerSchedulePnl)
2112 app.MainLoop()
2113
2115 app = wx.PyWidgetTester(size = (600, 400))
2116 widget = cPersonNamesManagerPnl(app.frame, -1)
2117 widget.identity = activate_patient()
2118 app.frame.Show(True)
2119 app.MainLoop()
2120
2122 app = wx.PyWidgetTester(size = (600, 400))
2123 widget = cPersonIDsManagerPnl(app.frame, -1)
2124 widget.identity = activate_patient()
2125 app.frame.Show(True)
2126 app.MainLoop()
2127
2129 app = wx.PyWidgetTester(size = (600, 400))
2130 widget = cPersonIdentityManagerPnl(app.frame, -1)
2131 widget.identity = activate_patient()
2132 app.frame.Show(True)
2133 app.MainLoop()
2134
2139
2141 app = wx.PyWidgetTester(size = (600, 400))
2142 widget = cPersonDemographicsEditorNb(app.frame, -1)
2143 widget.identity = activate_patient()
2144 widget.refresh()
2145 app.frame.Show(True)
2146 app.MainLoop()
2147
2156
2157 if len(sys.argv) > 1 and sys.argv[1] == 'test':
2158
2159 gmI18N.activate_locale()
2160 gmI18N.install_domain(domain='gnumed')
2161 gmPG2.get_connection()
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173 test_person_ids_pnl()
2174
2175
2176
2177
2178
2179
2180