1 """GNUmed measurement widgets.
2 """
3
4
5
6 __version__ = "$Revision: 1.64 $"
7 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
8 __license__ = "GPL"
9
10
11 import sys, logging, datetime as pyDT, decimal, os
12
13
14 import wx, wx.grid, wx.lib.hyperlink
15
16
17 if __name__ == '__main__':
18 sys.path.insert(0, '../../')
19 from Gnumed.business import gmPerson, gmPathLab, gmSurgery, gmLOINC
20 from Gnumed.pycommon import gmTools, gmDispatcher, gmMatchProvider, gmDateTime, gmI18N, gmCfg, gmShellAPI
21 from Gnumed.wxpython import gmRegetMixin, gmPhraseWheel, gmEditArea, gmGuiHelpers, gmListWidgets, gmAuthWidgets, gmPatSearchWidgets
22 from Gnumed.wxGladeWidgets import wxgMeasurementsPnl, wxgMeasurementsReviewDlg
23 from Gnumed.wxGladeWidgets import wxgMeasurementEditAreaPnl
24
25
26 _log = logging.getLogger('gm.ui')
27 _log.info(__version__)
28
29
30
32
33 wx.BeginBusyCursor()
34
35 gmDispatcher.send(signal = 'statustext', msg = _('Updating LOINC data can take quite a while...'), beep = True)
36
37
38 downloaded = gmShellAPI.run_command_in_shell(command = 'gm-download_loinc', blocking = True)
39 if not downloaded:
40 wx.EndBusyCursor()
41 gmGuiHelpers.gm_show_warning (
42 aTitle = _('Downloading LOINC'),
43 aMessage = _(
44 'Running <gm-download_loinc> to retrieve\n'
45 'the latest LOINC data failed.\n'
46 )
47 )
48 return False
49
50
51 data_fname, license_fname = gmLOINC.split_LOINCDBTXT(input_fname = '/tmp/LOINCDB.TXT')
52
53 wx.EndBusyCursor()
54
55 conn = gmAuthWidgets.get_dbowner_connection(procedure = _('importing LOINC reference data'))
56 if conn is None:
57 return False
58
59 wx.BeginBusyCursor()
60
61 if gmLOINC.loinc_import(data_fname = data_fname, license_fname = license_fname, conn = conn):
62 gmDispatcher.send(signal = 'statustext', msg = _('Successfully imported LOINC reference data.'))
63 try:
64 os.remove(data_fname)
65 os.remove(license_fname)
66 except OSError:
67 _log.error('unable to remove [%s] or [%s]', data_fname, license_fname)
68 else:
69 gmDispatcher.send(signal = 'statustext', msg = _('Importing LOINC reference data failed.'), beep = True)
70
71 wx.EndBusyCursor()
72 return True
73
74
75
87
88
89
90
91
92
93
94
95
96
97
99 """A grid class for displaying measurment results.
100
101 - does NOT listen to the currently active patient
102 - thereby it can display any patient at any time
103 """
104
105
106
107
108
109
111
112 wx.grid.Grid.__init__(self, *args, **kwargs)
113
114 self.__patient = None
115 self.__cell_data = {}
116 self.__row_label_data = []
117
118 self.__prev_row = None
119 self.__prev_col = None
120 self.__prev_label_row = None
121 self.__date_format = str((_('lab_grid_date_format::%Y\n%b %d')).lstrip('lab_grid_date_format::'))
122
123 self.__init_ui()
124 self.__register_events()
125
126
127
129 if not self.IsSelection():
130 gmDispatcher.send(signal = u'statustext', msg = _('No results selected for deletion.'))
131 return True
132
133 selected_cells = self.get_selected_cells()
134 if len(selected_cells) > 20:
135 results = None
136 msg = _(
137 'There are %s results marked for deletion.\n'
138 '\n'
139 'Are you sure you want to delete these results ?'
140 ) % len(selected_cells)
141 else:
142 results = self.__cells_to_data(cells = selected_cells, exclude_multi_cells = False)
143 txt = u'\n'.join([ u'%s %s (%s): %s %s%s' % (
144 r['clin_when'].strftime('%x %H:%M').decode(gmI18N.get_encoding()),
145 r['unified_abbrev'],
146 r['unified_name'],
147 r['unified_val'],
148 r['val_unit'],
149 gmTools.coalesce(r['abnormality_indicator'], u'', u' (%s)')
150 ) for r in results
151 ])
152 msg = _(
153 'The following results are marked for deletion:\n'
154 '\n'
155 '%s\n'
156 '\n'
157 'Are you sure you want to delete these results ?'
158 ) % txt
159
160 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
161 self,
162 -1,
163 caption = _('Deleting test results'),
164 question = msg,
165 button_defs = [
166 {'label': _('Delete'), 'tooltip': _('Yes, delete all the results.'), 'default': False},
167 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete any results.'), 'default': True}
168 ]
169 )
170 decision = dlg.ShowModal()
171
172 if decision == wx.ID_YES:
173 if results is None:
174 results = self.__cells_to_data(cells = selected_cells, exclude_multi_cells = False)
175 for result in results:
176 gmPathLab.delete_test_result(result)
177
179 if not self.IsSelection():
180 gmDispatcher.send(signal = u'statustext', msg = _('Cannot sign results. No results selected.'))
181 return True
182
183 selected_cells = self.get_selected_cells()
184 if len(selected_cells) > 10:
185 test_count = len(selected_cells)
186 tests = None
187 else:
188 test_count = None
189 tests = self.__cells_to_data(cells = selected_cells, exclude_multi_cells = False)
190
191 dlg = cMeasurementsReviewDlg (
192 self,
193 -1,
194 tests = tests,
195 test_count = test_count
196 )
197 decision = dlg.ShowModal()
198
199 if decision == wx.ID_APPLY:
200 wx.BeginBusyCursor()
201
202 if dlg._RBTN_confirm_abnormal.GetValue():
203 abnormal = None
204 elif dlg._RBTN_results_normal.GetValue():
205 abnormal = False
206 else:
207 abnormal = True
208
209 if dlg._RBTN_confirm_relevance.GetValue():
210 relevant = None
211 elif dlg._RBTN_results_not_relevant.GetValue():
212 relevant = False
213 else:
214 relevant = True
215
216 if tests is None:
217 tests = self.__cells_to_data(cells = selected_cells, exclude_multi_cells = False)
218
219 comment = None
220 if len(tests) == 1:
221 comment = dlg._TCTRL_comment.GetValue()
222
223 for test in tests:
224 test.set_review (
225 technically_abnormal = abnormal,
226 clinically_relevant = relevant,
227 comment = comment,
228 make_me_responsible = dlg._CHBOX_responsible.IsChecked()
229 )
230
231 wx.EndBusyCursor()
232
233 dlg.Destroy()
234
236
237 sel_block_top_left = self.GetSelectionBlockTopLeft()
238 sel_block_bottom_right = self.GetSelectionBlockBottomRight()
239 sel_cols = self.GetSelectedCols()
240 sel_rows = self.GetSelectedRows()
241
242 selected_cells = []
243
244
245 selected_cells += self.GetSelectedCells()
246
247
248 selected_cells += list (
249 (row, col)
250 for row in sel_rows
251 for col in xrange(self.GetNumberCols())
252 )
253
254
255 selected_cells += list (
256 (row, col)
257 for row in xrange(self.GetNumberRows())
258 for col in sel_cols
259 )
260
261
262 for top_left, bottom_right in zip(self.GetSelectionBlockTopLeft(), self.GetSelectionBlockBottomRight()):
263 selected_cells += [
264 (row, col)
265 for row in xrange(top_left[0], bottom_right[0] + 1)
266 for col in xrange(top_left[1], bottom_right[1] + 1)
267 ]
268
269 return set(selected_cells)
270
271 - def select_cells(self, unsigned_only=False, accountables_only=False, keep_preselections=False):
272 """Select a range of cells according to criteria.
273
274 unsigned_only: include only those which are not signed at all yet
275 accountable_only: include only those for which the current user is responsible
276 keep_preselections: broaden (rather than replace) the range of selected cells
277
278 Combinations are powerful !
279 """
280 wx.BeginBusyCursor()
281 self.BeginBatch()
282
283 if not keep_preselections:
284 self.ClearSelection()
285
286 for col_idx in self.__cell_data.keys():
287 for row_idx in self.__cell_data[col_idx].keys():
288
289
290 do_not_include = False
291 for result in self.__cell_data[col_idx][row_idx]:
292 if unsigned_only:
293 if result['reviewed']:
294 do_not_include = True
295 break
296 if accountables_only:
297 if not result['you_are_responsible']:
298 do_not_include = True
299 break
300 if do_not_include:
301 continue
302
303 self.SelectBlock(row_idx, col_idx, row_idx, col_idx, addToSelected = True)
304
305 self.EndBatch()
306 wx.EndBusyCursor()
307
309
310 self.empty_grid()
311 if self.__patient is None:
312 return
313
314 emr = self.__patient.get_emr()
315
316 self.__row_label_data = emr.get_test_types_for_results()
317 test_type_labels = [ u'%s (%s)' % (test['unified_abbrev'], test['unified_name']) for test in self.__row_label_data ]
318 if len(test_type_labels) == 0:
319 return
320
321 test_date_labels = [ date[0].strftime(self.__date_format) for date in emr.get_dates_for_results() ]
322 results = emr.get_test_results_by_date()
323
324 self.BeginBatch()
325
326
327 self.AppendRows(numRows = len(test_type_labels))
328 for row_idx in range(len(test_type_labels)):
329 self.SetRowLabelValue(row_idx, test_type_labels[row_idx])
330
331
332 self.AppendCols(numCols = len(test_date_labels))
333 for date_idx in range(len(test_date_labels)):
334 self.SetColLabelValue(date_idx, test_date_labels[date_idx])
335
336
337 for result in results:
338 row = test_type_labels.index(u'%s (%s)' % (result['unified_abbrev'], result['unified_name']))
339 col = test_date_labels.index(result['clin_when'].strftime(self.__date_format))
340
341 try:
342 self.__cell_data[col]
343 except KeyError:
344 self.__cell_data[col] = {}
345
346
347 if self.__cell_data[col].has_key(row):
348 self.__cell_data[col][row].append(result)
349 self.__cell_data[col][row].sort(key = lambda x: x['clin_when'], reverse = True)
350 else:
351 self.__cell_data[col][row] = [result]
352
353
354 vals2display = []
355 for sub_result in self.__cell_data[col][row]:
356
357
358 ind = gmTools.coalesce(sub_result['abnormality_indicator'], u'').strip()
359 if ind != u'':
360 lab_abnormality_indicator = u' (%s)' % ind[:3]
361 else:
362 lab_abnormality_indicator = u''
363
364 if sub_result['is_technically_abnormal'] is None:
365 abnormality_indicator = lab_abnormality_indicator
366
367 elif sub_result['is_technically_abnormal'] is False:
368 abnormality_indicator = u''
369
370 else:
371
372 if lab_abnormality_indicator == u'':
373
374 abnormality_indicator = u' (%s)' % gmTools.u_plus_minus
375
376 else:
377 abnormality_indicator = lab_abnormality_indicator
378
379
380
381 sub_result_relevant = sub_result['is_clinically_relevant']
382 if sub_result_relevant is None:
383
384 sub_result_relevant = False
385
386 missing_review = False
387
388
389 if not sub_result['reviewed']:
390 missing_review = True
391
392 else:
393
394 if sub_result['you_are_responsible'] and not sub_result['review_by_you']:
395 missing_review = True
396
397
398 if len(sub_result['unified_val']) > 8:
399 tmp = u'%.7s%s' % (sub_result['unified_val'][:7], gmTools.u_ellipsis)
400 else:
401 tmp = u'%.8s' % sub_result['unified_val'][:8]
402
403
404 tmp = u'%s%.6s' % (tmp, abnormality_indicator)
405
406
407 has_sub_result_comment = gmTools.coalesce (
408 gmTools.coalesce(sub_result['note_test_org'], sub_result['comment']),
409 u''
410 ).strip() != u''
411 if has_sub_result_comment:
412 tmp = u'%s %s' % (tmp, gmTools.u_ellipsis)
413
414
415 if missing_review:
416 tmp = u'%s %s' % (tmp, gmTools.u_writing_hand)
417
418
419 if len(self.__cell_data[col][row]) > 1:
420 tmp = u'%s %s' % (sub_result['clin_when'].strftime('%H:%M'), tmp)
421
422 vals2display.append(tmp)
423
424 self.SetCellValue(row, col, u'\n'.join(vals2display))
425 self.SetCellAlignment(row, col, horiz = wx.ALIGN_RIGHT, vert = wx.ALIGN_CENTRE)
426
427
428
429
430 if sub_result_relevant:
431 font = self.GetCellFont(row, col)
432 self.SetCellTextColour(row, col, 'firebrick')
433 font.SetWeight(wx.FONTWEIGHT_BOLD)
434 self.SetCellFont(row, col, font)
435
436
437 self.AutoSize()
438 self.EndBatch()
439 return
440
442 self.BeginBatch()
443 self.ClearGrid()
444
445
446 if self.GetNumberRows() > 0:
447 self.DeleteRows(pos = 0, numRows = self.GetNumberRows())
448 if self.GetNumberCols() > 0:
449 self.DeleteCols(pos = 0, numCols = self.GetNumberCols())
450 self.EndBatch()
451 self.__cell_data = {}
452 self.__row_label_data = []
453
486
718
719
720
722 self.CreateGrid(0, 1)
723 self.EnableEditing(0)
724 self.EnableDragGridSize(1)
725
726
727
728
729
730 self.SetRowLabelSize(150)
731 self.SetRowLabelAlignment(horiz = wx.ALIGN_LEFT, vert = wx.ALIGN_CENTRE)
732
733
734 dbcfg = gmCfg.cCfgSQL()
735 url = dbcfg.get2 (
736 option = u'external.urls.measurements_encyclopedia',
737 workplace = gmSurgery.gmCurrentPractice().active_workplace,
738 bias = 'user',
739 default = u'http://www.laborlexikon.de'
740 )
741
742 self.__WIN_corner = self.GetGridCornerLabelWindow()
743
744 LNK_lab = wx.lib.hyperlink.HyperLinkCtrl (
745 self.__WIN_corner,
746 -1,
747 label = _('Reference'),
748 style = wx.HL_DEFAULT_STYLE
749 )
750 LNK_lab.SetURL(url)
751 LNK_lab.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BACKGROUND))
752 LNK_lab.SetToolTipString(_(
753 'Navigate to an encyclopedia of measurements\n'
754 'and test methods on the web.\n'
755 '\n'
756 ' <%s>'
757 ) % url)
758
759 SZR_inner = wx.BoxSizer(wx.HORIZONTAL)
760 SZR_inner.Add((20, 20), 1, wx.EXPAND, 0)
761 SZR_inner.Add(LNK_lab, 0, wx.ALIGN_CENTER_VERTICAL, 0)
762 SZR_inner.Add((20, 20), 1, wx.EXPAND, 0)
763
764 SZR_corner = wx.BoxSizer(wx.VERTICAL)
765 SZR_corner.Add((20, 20), 1, wx.EXPAND, 0)
766 SZR_corner.AddWindow(SZR_inner, 0, wx.EXPAND)
767 SZR_corner.Add((20, 20), 1, wx.EXPAND, 0)
768
769 self.__WIN_corner.SetSizer(SZR_corner)
770 SZR_corner.Fit(self.__WIN_corner)
771
773 self.__WIN_corner.Layout()
774
776 """List of <cells> must be in row / col order."""
777 data = []
778 for row, col in cells:
779 try:
780
781 data_list = self.__cell_data[col][row]
782 except KeyError:
783 continue
784
785 if len(data_list) == 1:
786 data.append(data_list[0])
787 continue
788
789 if exclude_multi_cells:
790 gmDispatcher.send(signal = u'statustext', msg = _('Excluding multi-result field from further processing.'))
791 continue
792
793 data_to_include = self.__get_choices_from_multi_cell(cell_data = data_list)
794
795 if data_to_include is None:
796 continue
797
798 data.extend(data_to_include)
799
800 return data
801
803 data = gmListWidgets.get_choices_from_list (
804 parent = self,
805 msg = _(
806 'Your selection includes a field with multiple results.\n'
807 '\n'
808 'Please select the individual results you want to work on:'
809 ),
810 caption = _('Selecting test results'),
811 choices = [ [d['clin_when'], d['unified_abbrev'], d['unified_name'], d['unified_val']] for d in cell_data ],
812 columns = [_('Date / Time'), _('Code'), _('Test'), _('Result')],
813 data = cell_data,
814 single_selection = single_selection
815 )
816 return data
817
818
819
821
822 self.GetGridWindow().Bind(wx.EVT_MOTION, self.__on_mouse_over_cells)
823 self.GetGridRowLabelWindow().Bind(wx.EVT_MOTION, self.__on_mouse_over_row_labels)
824
825
826
827 self.Bind(wx.EVT_SIZE, self.__resize_corner_window)
828
829
830 self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.__on_cell_left_dclicked)
831
833 col = evt.GetCol()
834 row = evt.GetRow()
835
836
837 try:
838 self.__cell_data[col][row]
839 except KeyError:
840
841
842 return
843
844 if len(self.__cell_data[col][row]) > 1:
845 data = self.__get_choices_from_multi_cell(cell_data = self.__cell_data[col][row], single_selection = True)
846 else:
847 data = self.__cell_data[col][row][0]
848
849 if data is None:
850 return
851
852 edit_measurement(parent = self, measurement = data)
853
854
855
856
857
858
859
861
862
863
864 x, y = self.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
865
866 row = self.YToRow(y)
867
868 if self.__prev_label_row == row:
869 return
870
871 self.__prev_label_row == row
872
873 evt.GetEventObject().SetToolTipString(self.get_row_tooltip(row = row))
874
875
876
877
878
879
880
881
883 """Calculate where the mouse is and set the tooltip dynamically."""
884
885
886
887 x, y = self.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
888
889
890
891
892
893
894
895
896
897
898
899
900
901 row, col = self.XYToCell(x, y)
902
903 if (row == self.__prev_row) and (col == self.__prev_col):
904 return
905
906 self.__prev_row = row
907 self.__prev_col = col
908
909 evt.GetEventObject().SetToolTipString(self.get_cell_tooltip(col=col, row=row))
910
911
912
916
917 patient = property(lambda x:x, _set_patient)
918
919 -class cMeasurementsPnl(wxgMeasurementsPnl.wxgMeasurementsPnl, gmRegetMixin.cRegetOnPaintMixin):
920
921 """Panel holding a grid with lab data. Used as notebook page."""
922
929
930
931
933 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
934 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
935 gmDispatcher.connect(signal = u'test_result_mod_db', receiver = self._schedule_data_reget)
936 gmDispatcher.connect(signal = u'reviewed_test_results_mod_db', receiver = self._schedule_data_reget)
937
939 wx.CallAfter(self.__on_post_patient_selection)
940
942 self._schedule_data_reget()
943
945 wx.CallAfter(self.__on_pre_patient_selection)
946
949
952
958
961
964
965
966
968 self.__action_button_popup = wx.Menu(title = _('Act on selected results'))
969
970 menu_id = wx.NewId()
971 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('Review and &sign')))
972 wx.EVT_MENU(self.__action_button_popup, menu_id, self.__on_sign_current_selection)
973
974 menu_id = wx.NewId()
975 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('Export to &file')))
976
977 self.__action_button_popup.Enable(id = menu_id, enable = False)
978
979 menu_id = wx.NewId()
980 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('Export to &clipboard')))
981
982 self.__action_button_popup.Enable(id = menu_id, enable = False)
983
984 menu_id = wx.NewId()
985 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('&Delete')))
986 wx.EVT_MENU(self.__action_button_popup, menu_id, self.__on_delete_current_selection)
987
988
989
998
999
1000
1002
1004
1005 try:
1006 tests = kwargs['tests']
1007 del kwargs['tests']
1008 test_count = len(tests)
1009 try: del kwargs['test_count']
1010 except KeyError: pass
1011 except KeyError:
1012 tests = None
1013 test_count = kwargs['test_count']
1014 del kwargs['test_count']
1015
1016 wxgMeasurementsReviewDlg.wxgMeasurementsReviewDlg.__init__(self, *args, **kwargs)
1017
1018 if tests is None:
1019 msg = _('%s results selected. Too many to list individually.') % test_count
1020 else:
1021 msg = ' // '.join (
1022 [ u'%s: %s %s (%s)' % (
1023 t['unified_abbrev'],
1024 t['unified_val'],
1025 t['val_unit'],
1026 t['clin_when'].strftime('%x').decode(gmI18N.get_encoding())
1027 ) for t in tests
1028 ]
1029 )
1030
1031 self._LBL_tests.SetLabel(msg)
1032
1033 if test_count == 1:
1034 self._TCTRL_comment.Enable(True)
1035 self._TCTRL_comment.SetValue(gmTools.coalesce(tests[0]['review_comment'], u''))
1036 if tests[0]['you_are_responsible']:
1037 self._CHBOX_responsible.Enable(False)
1038
1039 self.Fit()
1040
1041
1042
1048
1049 -class cMeasurementEditAreaPnl(wxgMeasurementEditAreaPnl.wxgMeasurementEditAreaPnl, gmEditArea.cGenericEditAreaMixin):
1050 """This edit area saves *new* measurements into the active patient only."""
1051
1053
1054 try:
1055 self.__default_date = kwargs['date']
1056 del kwargs['date']
1057 except KeyError:
1058 self.__default_date = None
1059
1060 wxgMeasurementEditAreaPnl.wxgMeasurementEditAreaPnl.__init__(self, *args, **kwargs)
1061 gmEditArea.cGenericEditAreaMixin.__init__(self)
1062
1063 self.__register_interests()
1064
1065 self.successful_save_msg = _('Successfully saved measurement.')
1066
1067
1068
1070 self._PRW_test.SetText(u'', None, True)
1071 self._TCTRL_result.SetValue(u'')
1072 self._PRW_units.SetText(u'', None, True)
1073 self._PRW_abnormality_indicator.SetText(u'', None, True)
1074 if self.__default_date is None:
1075 self._DPRW_evaluated.SetData(data = pyDT.datetime.now(tz = gmDateTime.gmCurrentLocalTimezone))
1076 else:
1077 self._DPRW_evaluated.SetData(data = None)
1078 self._TCTRL_note_test_org.SetValue(u'')
1079 self._PRW_intended_reviewer.SetData()
1080 self._PRW_problem.SetData()
1081 self._TCTRL_narrative.SetValue(u'')
1082 self._CHBOX_review.SetValue(False)
1083 self._CHBOX_abnormal.SetValue(False)
1084 self._CHBOX_relevant.SetValue(False)
1085 self._CHBOX_abnormal.Enable(False)
1086 self._CHBOX_relevant.Enable(False)
1087 self._TCTRL_review_comment.SetValue(u'')
1088 self._TCTRL_normal_min.SetValue(u'')
1089 self._TCTRL_normal_max.SetValue(u'')
1090 self._TCTRL_normal_range.SetValue(u'')
1091 self._TCTRL_target_min.SetValue(u'')
1092 self._TCTRL_target_max.SetValue(u'')
1093 self._TCTRL_target_range.SetValue(u'')
1094 self._TCTRL_norm_ref_group.SetValue(u'')
1095
1096 self._PRW_test.SetFocus()
1097
1099 self._PRW_test.SetData(data = self.data['pk_test_type'])
1100 self._TCTRL_result.SetValue(self.data['unified_val'])
1101 self._PRW_units.SetText(self.data['val_unit'], self.data['val_unit'], True)
1102 self._PRW_abnormality_indicator.SetText (
1103 gmTools.coalesce(self.data['abnormality_indicator'], u''),
1104 gmTools.coalesce(self.data['abnormality_indicator'], u''),
1105 True
1106 )
1107 self._DPRW_evaluated.SetData(data = self.data['clin_when'])
1108 self._TCTRL_note_test_org.SetValue(gmTools.coalesce(self.data['note_test_org'], u''))
1109 self._PRW_intended_reviewer.SetData(self.data['pk_intended_reviewer'])
1110 self._PRW_problem.SetData(self.data['pk_episode'])
1111 self._TCTRL_narrative.SetValue(gmTools.coalesce(self.data['comment'], u''))
1112 self._CHBOX_review.SetValue(False)
1113 self._CHBOX_abnormal.SetValue(gmTools.coalesce(self.data['is_technically_abnormal'], False))
1114 self._CHBOX_relevant.SetValue(gmTools.coalesce(self.data['is_clinically_relevant'], False))
1115 self._CHBOX_abnormal.Enable(False)
1116 self._CHBOX_relevant.Enable(False)
1117 self._TCTRL_review_comment.SetValue(gmTools.coalesce(self.data['review_comment'], u''))
1118 self._TCTRL_normal_min.SetValue(unicode(gmTools.coalesce(self.data['val_normal_min'], u'')))
1119 self._TCTRL_normal_max.SetValue(unicode(gmTools.coalesce(self.data['val_normal_max'], u'')))
1120 self._TCTRL_normal_range.SetValue(gmTools.coalesce(self.data['val_normal_range'], u''))
1121 self._TCTRL_target_min.SetValue(unicode(gmTools.coalesce(self.data['val_target_min'], u'')))
1122 self._TCTRL_target_max.SetValue(unicode(gmTools.coalesce(self.data['val_target_max'], u'')))
1123 self._TCTRL_target_range.SetValue(gmTools.coalesce(self.data['val_target_range'], u''))
1124 self._TCTRL_norm_ref_group.SetValue(gmTools.coalesce(self.data['norm_ref_group'], u''))
1125
1126 self._TCTRL_result.SetFocus()
1127
1129 self._refresh_from_existing()
1130
1131 self._PRW_test.SetText(u'', None, True)
1132 self._TCTRL_result.SetValue(u'')
1133 self._PRW_units.SetText(u'', None, True)
1134 self._PRW_abnormality_indicator.SetText(u'', None, True)
1135
1136 self._TCTRL_note_test_org.SetValue(u'')
1137 self._TCTRL_narrative.SetValue(u'')
1138 self._CHBOX_review.SetValue(False)
1139 self._CHBOX_abnormal.SetValue(False)
1140 self._CHBOX_relevant.SetValue(False)
1141 self._CHBOX_abnormal.Enable(False)
1142 self._CHBOX_relevant.Enable(False)
1143 self._TCTRL_review_comment.SetValue(u'')
1144 self._TCTRL_normal_min.SetValue(u'')
1145 self._TCTRL_normal_max.SetValue(u'')
1146 self._TCTRL_normal_range.SetValue(u'')
1147 self._TCTRL_target_min.SetValue(u'')
1148 self._TCTRL_target_max.SetValue(u'')
1149 self._TCTRL_target_range.SetValue(u'')
1150 self._TCTRL_norm_ref_group.SetValue(u'')
1151
1152 self._PRW_test.SetFocus()
1153
1155
1156 validity = True
1157
1158 if not self._DPRW_evaluated.is_valid_timestamp():
1159 self._DPRW_evaluated.display_as_valid(False)
1160 validity = False
1161 else:
1162 self._DPRW_evaluated.display_as_valid(True)
1163
1164 if self._TCTRL_result.GetValue().strip() == u'':
1165 self._TCTRL_result.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1166 validity = False
1167 else:
1168 self._TCTRL_result.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1169
1170 if self._PRW_problem.GetValue().strip() == u'':
1171 self._PRW_problem.display_as_valid(False)
1172 validity = False
1173 else:
1174 self._PRW_problem.display_as_valid(True)
1175
1176 if self._PRW_test.GetValue().strip() == u'':
1177 self._PRW_test.display_as_valid(False)
1178 validity = False
1179 else:
1180 self._PRW_test.display_as_valid(True)
1181
1182 if self._PRW_intended_reviewer.GetData() is None:
1183 self._PRW_intended_reviewer.display_as_valid(False)
1184 validity = False
1185 else:
1186 self._PRW_intended_reviewer.display_as_valid(True)
1187
1188 if self._PRW_units.GetValue().strip() == u'':
1189 self._PRW_units.display_as_valid(False)
1190 validity = False
1191 else:
1192 self._PRW_units.display_as_valid(True)
1193
1194 ctrls = [self._TCTRL_normal_min, self._TCTRL_normal_max, self._TCTRL_target_min, self._TCTRL_target_max]
1195 for widget in ctrls:
1196 val = widget.GetValue().strip()
1197 if val == u'':
1198 continue
1199 try:
1200 decimal.Decimal(val.replace(',', u'.', 1))
1201 widget.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1202 except:
1203 widget.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1204 validity = False
1205
1206 if validity is False:
1207 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save result. Invalid or missing essential input.'))
1208
1209 return validity
1210
1212
1213 emr = gmPerson.gmCurrentPatient().get_emr()
1214
1215 try:
1216 v_num = decimal.Decimal(self._TCTRL_result.GetValue().strip().replace(',', '.', 1))
1217 v_al = None
1218 except:
1219 v_num = None
1220 v_al = self._TCTRL_result.GetValue().strip()
1221
1222 pk_type = self._PRW_test.GetData()
1223 if pk_type is None:
1224 tt = gmPathLab.create_measurement_type (
1225 lab = None,
1226 abbrev = self._PRW_test.GetValue().strip(),
1227 name = self._PRW_test.GetValue().strip(),
1228 unit = gmTools.none_if(self._PRW_units.GetValue().strip(), u'')
1229 )
1230 pk_type = tt['pk_test_type']
1231
1232 tr = emr.add_test_result (
1233 episode = self._PRW_problem.GetData(can_create=True, is_open=False),
1234 type = pk_type,
1235 intended_reviewer = self._PRW_intended_reviewer.GetData(),
1236 val_num = v_num,
1237 val_alpha = v_al,
1238 unit = self._PRW_units.GetValue()
1239 )
1240
1241 tr['clin_when'] = self._DPRW_evaluated.GetData().get_pydt()
1242
1243 ctrls = [
1244 ('abnormality_indicator', self._PRW_abnormality_indicator),
1245 ('note_test_org', self._TCTRL_note_test_org),
1246 ('comment', self._TCTRL_narrative),
1247 ('val_normal_min', self._TCTRL_normal_min),
1248 ('val_normal_max', self._TCTRL_normal_max),
1249 ('val_normal_range', self._TCTRL_normal_range),
1250 ('val_target_min', self._TCTRL_target_min),
1251 ('val_target_max', self._TCTRL_target_max),
1252 ('val_target_range', self._TCTRL_target_range),
1253 ('norm_ref_group', self._TCTRL_norm_ref_group)
1254 ]
1255 for field, widget in ctrls:
1256 val = widget.GetValue().strip()
1257 if val != u'':
1258 tr[field] = val
1259
1260 tr.save_payload()
1261
1262 if self._CHBOX_review.GetValue() is True:
1263 tr.set_review (
1264 technically_abnormal = self._CHBOX_abnormal.GetValue(),
1265 clinically_relevant = self._CHBOX_relevant.GetValue(),
1266 comment = gmTools.none_if(self._TCTRL_review_comment.GetValue().strip(), u''),
1267 make_me_responsible = False
1268 )
1269
1270 self.data = tr
1271
1272 return True
1273
1275
1276 success, result = gmTools.input2decimal(self._TCTRL_result.GetValue())
1277 if success:
1278 v_num = result
1279 v_al = None
1280 else:
1281 v_num = None
1282 v_al = self._TCTRL_result.GetValue().strip()
1283
1284 pk_type = self._PRW_test.GetData()
1285 if pk_type is None:
1286 tt = gmPathLab.create_measurement_type (
1287 lab = None,
1288 abbrev = self._PRW_test.GetValue().strip(),
1289 name = self._PRW_test.GetValue().strip(),
1290 unit = gmTools.none_if(self._PRW_units.GetValue().strip(), u'')
1291 )
1292 pk_type = tt['pk_test_type']
1293
1294 tr = self.data
1295
1296 tr['pk_episode'] = self._PRW_problem.GetData(can_create=True, is_open=False)
1297 tr['pk_test_type'] = pk_type
1298 tr['pk_intended_reviewer'] = self._PRW_intended_reviewer.GetData()
1299 tr['val_num'] = v_num
1300 tr['val_alpha'] = v_al
1301 tr['val_unit'] = self._PRW_units.GetValue().strip()
1302 tr['clin_when'] = self._DPRW_evaluated.GetData().get_pydt()
1303 tr['abnormality_indicator'] = self._PRW_abnormality_indicator.strip()
1304
1305 ctrls = [
1306 ('note_test_org', self._TCTRL_note_test_org),
1307 ('comment', self._TCTRL_narrative),
1308 ('val_normal_min', self._TCTRL_normal_min),
1309 ('val_normal_max', self._TCTRL_normal_max),
1310 ('val_normal_range', self._TCTRL_normal_range),
1311 ('val_target_min', self._TCTRL_target_min),
1312 ('val_target_max', self._TCTRL_target_max),
1313 ('val_target_range', self._TCTRL_target_range),
1314 ('norm_ref_group', self._TCTRL_norm_ref_group)
1315 ]
1316 for field, widget in ctrls:
1317 val = widget.GetValue().strip()
1318 if val != u'':
1319 tr[field] = val
1320
1321 tr.save_payload()
1322
1323 if self._CHBOX_review.GetValue() is True:
1324 tr.set_review (
1325 technically_abnormal = self._CHBOX_abnormal.GetValue(),
1326 clinically_relevant = self._CHBOX_relevant.GetValue(),
1327 comment = gmTools.none_if(self._TCTRL_review_comment.GetValue().strip(), u''),
1328 make_me_responsible = False
1329 )
1330
1331 return True
1332
1333
1334
1338
1340 pk_type = self._PRW_test.GetData()
1341
1342 if pk_type is None:
1343 self._PRW_units.unset_context(context = u'pk_type')
1344 else:
1345 self._PRW_units.set_context(context = u'pk_type', val = pk_type)
1346
1348
1349 if not self._CHBOX_review.GetValue():
1350 self._CHBOX_abnormal.SetValue(self._PRW_abnormality_indicator.GetValue().strip() != u'')
1351
1353 self._CHBOX_abnormal.Enable(self._CHBOX_review.GetValue())
1354 self._CHBOX_relevant.Enable(self._CHBOX_review.GetValue())
1355 self._TCTRL_review_comment.Enable(self._CHBOX_review.GetValue())
1356
1357
1358
1360
1361 if parent is None:
1362 parent = wx.GetApp().GetTopWindow()
1363
1364
1365 def edit(test_type=None):
1366 ea = cMeasurementTypeEAPnl(parent = parent, id = -1, type = test_type)
1367 dlg = gmEditArea.cGenericEditAreaDlg2 (
1368 parent = parent,
1369 id = -1,
1370 edit_area = ea,
1371 single_entry = gmTools.bool2subst((test_type is None), False, True)
1372 )
1373 dlg.SetTitle(gmTools.coalesce(test_type, _('Adding measurement type'), _('Editing measurement type')))
1374
1375 if dlg.ShowModal() == wx.ID_OK:
1376 dlg.Destroy()
1377 return True
1378
1379 dlg.Destroy()
1380 return False
1381
1382 def refresh(lctrl):
1383 mtypes = gmPathLab.get_measurement_types(order_by = 'name, abbrev')
1384 items = [ [
1385 m['abbrev'],
1386 m['name'],
1387 gmTools.coalesce(m['loinc'], u''),
1388 gmTools.coalesce(m['conversion_unit'], u''),
1389 gmTools.coalesce(m['comment_type'], u''),
1390 gmTools.coalesce(m['internal_name_org'], _('in-house')),
1391 gmTools.coalesce(m['comment_org'], u''),
1392 m['pk_test_type']
1393 ] for m in mtypes ]
1394 lctrl.set_string_items(items)
1395 lctrl.set_data(mtypes)
1396
1397 def delete(measurement_type):
1398 if measurement_type.in_use:
1399 gmDispatcher.send (
1400 signal = 'statustext',
1401 beep = True,
1402 msg = _('Cannot delete measurement type [%s (%s)] because it is in use.') % (measurement_type['name'], measurement_type['abbrev'])
1403 )
1404 return False
1405 gmPathLab.delete_measurement_type(measurement_type = measurement_type['pk_test_type'])
1406 return True
1407
1408 msg = _(
1409 '\n'
1410 'These are the measurement types currently defined in GNUmed.\n'
1411 '\n'
1412 )
1413
1414 gmListWidgets.get_choices_from_list (
1415 parent = parent,
1416 msg = msg,
1417 caption = _('Showing measurement types.'),
1418 columns = [_('Abbrev'), _('Name'), _('LOINC'), _('Base unit'), _('Comment'), _('Org'), _('Comment'), u'#'],
1419 single_selection = True,
1420 refresh_callback = refresh,
1421 edit_callback = edit,
1422 new_callback = edit,
1423 delete_callback = delete
1424 )
1425
1427
1429
1430 query = u"""
1431 (
1432 select
1433 pk_test_type,
1434 name_tt
1435 || ' ('
1436 || coalesce (
1437 (select internal_name from clin.test_org cto where cto.pk = vcutt.pk_test_org),
1438 '%(in_house)s'
1439 )
1440 || ')'
1441 as name
1442 from clin.v_unified_test_types vcutt
1443 where
1444 name_meta %%(fragment_condition)s
1445
1446 ) union (
1447
1448 select
1449 pk_test_type,
1450 name_tt
1451 || ' ('
1452 || coalesce (
1453 (select internal_name from clin.test_org cto where cto.pk = vcutt.pk_test_org),
1454 '%(in_house)s'
1455 )
1456 || ')'
1457 as name
1458 from clin.v_unified_test_types vcutt
1459 where
1460 name_tt %%(fragment_condition)s
1461
1462 ) union (
1463
1464 select
1465 pk_test_type,
1466 name_tt
1467 || ' ('
1468 || coalesce (
1469 (select internal_name from clin.test_org cto where cto.pk = vcutt.pk_test_org),
1470 '%(in_house)s'
1471 )
1472 || ')'
1473 as name
1474 from clin.v_unified_test_types vcutt
1475 where
1476 abbrev_meta %%(fragment_condition)s
1477
1478 ) union (
1479
1480 select
1481 pk_test_type,
1482 name_tt
1483 || ' ('
1484 || coalesce (
1485 (select internal_name from clin.test_org cto where cto.pk = vcutt.pk_test_org),
1486 '%(in_house)s'
1487 )
1488 || ')'
1489 as name
1490 from clin.v_unified_test_types vcutt
1491 where
1492 code_tt %%(fragment_condition)s
1493 )
1494
1495 order by name
1496 limit 50""" % {'in_house': _('in house lab')}
1497
1498 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1499 mp.setThresholds(1, 2, 4)
1500 mp.word_separators = '[ \t:@]+'
1501 gmPhraseWheel.cPhraseWheel.__init__ (
1502 self,
1503 *args,
1504 **kwargs
1505 )
1506 self.matcher = mp
1507 self.SetToolTipString(_('Select the type of measurement.'))
1508 self.selection_only = False
1509
1510 from Gnumed.wxGladeWidgets import wxgMeasurementTypeEAPnl
1511
1512 -class cMeasurementTypeEAPnl(wxgMeasurementTypeEAPnl.wxgMeasurementTypeEAPnl, gmEditArea.cGenericEditAreaMixin):
1513
1530
1531
1533
1534
1535 query = u"""
1536 select distinct on (name)
1537 pk,
1538 name
1539 from clin.test_type
1540 where
1541 name %(fragment_condition)s
1542 order by name
1543 limit 50"""
1544 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1545 mp.setThresholds(1, 2, 4)
1546 self._PRW_name.matcher = mp
1547 self._PRW_name.selection_only = False
1548
1549
1550 query = u"""
1551 select distinct on (abbrev)
1552 pk,
1553 abbrev
1554 from clin.test_type
1555 where
1556 abbrev %(fragment_condition)s
1557 order by abbrev
1558 limit 50"""
1559 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1560 mp.setThresholds(1, 2, 3)
1561 self._PRW_abbrev.matcher = mp
1562 self._PRW_abbrev.selection_only = False
1563
1564
1565
1566 query = u"""
1567 select distinct on (conversion_unit)
1568 conversion_unit,
1569 conversion_unit
1570 from clin.test_type
1571 where
1572 conversion_unit %(fragment_condition)s
1573 order by conversion_unit
1574 limit 50"""
1575 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1576 mp.setThresholds(1, 2, 3)
1577 self._PRW_conversion_unit.matcher = mp
1578 self._PRW_conversion_unit.selection_only = False
1579
1580
1581 query = u"""
1582 select distinct on (term)
1583 loinc,
1584 term
1585 from ((
1586 select
1587 loinc,
1588 (loinc || ': ' || abbrev || ' (' || name || ')') as term
1589 from clin.test_type
1590 where loinc %(fragment_condition)s
1591 limit 50
1592 ) union all (
1593 select
1594 code as loinc,
1595 (code || ': ' || term) as term
1596 from ref.v_coded_terms
1597 where
1598 coding_system = 'LOINC'
1599 and
1600 lang = i18n.get_curr_lang()
1601 and
1602 (code %(fragment_condition)s
1603 or
1604 term %(fragment_condition)s)
1605 limit 50
1606 ) union all (
1607 select
1608 code as loinc,
1609 (code || ': ' || term) as term
1610 from ref.v_coded_terms
1611 where
1612 coding_system = 'LOINC'
1613 and
1614 lang = 'en_EN'
1615 and
1616 (code %(fragment_condition)s
1617 or
1618 term %(fragment_condition)s)
1619 limit 50
1620 ) union all (
1621 select
1622 code as loinc,
1623 (code || ': ' || term) as term
1624 from ref.v_coded_terms
1625 where
1626 coding_system = 'LOINC'
1627 and
1628 (code %(fragment_condition)s
1629 or
1630 term %(fragment_condition)s)
1631 limit 50
1632 )
1633 ) as all_known_loinc
1634 order by term
1635 limit 50"""
1636 mp = gmMatchProvider.cMatchProvider_SQL2(queries = query)
1637 mp.setThresholds(1, 2, 4)
1638 self._PRW_loinc.matcher = mp
1639 self._PRW_loinc.selection_only = False
1640 self._PRW_loinc.add_callback_on_lose_focus(callback = self._on_loinc_lost_focus)
1641
1642
1643 query = u"""
1644 select distinct on (internal_name)
1645 pk,
1646 internal_name
1647 from clin.test_org
1648 where
1649 internal_name %(fragment_condition)s
1650 order by internal_name
1651 limit 50"""
1652 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1653 mp.setThresholds(1, 2, 4)
1654 self._PRW_test_org.matcher = mp
1655 self._PRW_test_org.selection_only = False
1656
1658 loinc = self._PRW_loinc.GetData()
1659
1660 if loinc is None:
1661 self._TCTRL_loinc_info.SetValue(u'')
1662 return
1663
1664 info = gmLOINC.loinc2info(loinc = loinc)
1665 if len(info) == 0:
1666 self._TCTRL_loinc_info.SetValue(u'')
1667 return
1668
1669 self._TCTRL_loinc_info.SetValue(info[0])
1670
1671
1672
1674
1675 has_errors = False
1676 for field in [self._PRW_name, self._PRW_abbrev, self._PRW_conversion_unit]:
1677 if field.GetValue().strip() in [u'', None]:
1678 has_errors = True
1679 field.display_as_valid(valid = False)
1680 else:
1681 field.display_as_valid(valid = True)
1682 field.Refresh()
1683
1684 return (not has_errors)
1685
1708
1727
1729 self._PRW_name.SetText(u'', None, True)
1730 self._PRW_abbrev.SetText(u'', None, True)
1731 self._PRW_conversion_unit.SetText(u'', None, True)
1732 self._PRW_loinc.SetText(u'', None, True)
1733 self._TCTRL_loinc_info.SetValue(u'')
1734 self._TCTRL_comment_type.SetValue(u'')
1735 self._PRW_test_org.SetText(u'', None, True)
1736 self._TCTRL_comment_org.SetValue(u'')
1737
1739 self._PRW_name.SetText(self.data['name'], self.data['name'], True)
1740 self._PRW_abbrev.SetText(self.data['abbrev'], self.data['abbrev'], True)
1741 self._PRW_conversion_unit.SetText (
1742 gmTools.coalesce(self.data['conversion_unit'], u''),
1743 self.data['conversion_unit'],
1744 True
1745 )
1746 self._PRW_loinc.SetText (
1747 gmTools.coalesce(self.data['loinc'], u''),
1748 self.data['loinc'],
1749 True
1750 )
1751 self._TCTRL_loinc_info.SetValue(u'')
1752 self._TCTRL_comment_type.SetValue(gmTools.coalesce(self.data['comment_type'], u''))
1753 self._PRW_test_org.SetText (
1754 gmTools.coalesce(self.data['pk_test_org'], u'', self.data['internal_name_org']),
1755 self.data['pk_test_org'],
1756 True
1757 )
1758 self._TCTRL_comment_org.SetValue(gmTools.coalesce(self.data['comment_org'], u''))
1759
1768
1770
1772
1773 query = u"""
1774 select distinct val_unit,
1775 val_unit, val_unit
1776 from clin.v_test_results
1777 where
1778 (
1779 val_unit %(fragment_condition)s
1780 or
1781 conversion_unit %(fragment_condition)s
1782 )
1783 %(ctxt_test_name)s
1784 %(ctxt_test_pk)s
1785 order by val_unit
1786 limit 25"""
1787
1788 ctxt = {
1789 'ctxt_test_name': {
1790 'where_part': u'and %(test)s in (name_tt, name_meta, code_tt, abbrev_meta)',
1791 'placeholder': u'test'
1792 },
1793 'ctxt_test_pk': {
1794 'where_part': u'and pk_test_type = %(pk_type)s',
1795 'placeholder': u'pk_type'
1796 }
1797 }
1798
1799 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=ctxt)
1800 mp.setThresholds(1, 2, 4)
1801 gmPhraseWheel.cPhraseWheel.__init__ (
1802 self,
1803 *args,
1804 **kwargs
1805 )
1806 self.matcher = mp
1807 self.SetToolTipString(_('Select the unit of the test result.'))
1808 self.selection_only = False
1809
1810
1811
1812
1814
1816
1817 query = u"""
1818 select distinct abnormality_indicator,
1819 abnormality_indicator, abnormality_indicator
1820 from clin.v_test_results
1821 where
1822 abnormality_indicator %(fragment_condition)s
1823 order by abnormality_indicator
1824 limit 25"""
1825
1826 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1827 mp.setThresholds(1, 1, 2)
1828 mp.ignored_chars = "[.'\\\[\]#$%_]+" + '"'
1829 mp.word_separators = '[ \t&:]+'
1830 gmPhraseWheel.cPhraseWheel.__init__ (
1831 self,
1832 *args,
1833 **kwargs
1834 )
1835 self.matcher = mp
1836 self.SetToolTipString(_('Select an indicator for the level of abnormality.'))
1837 self.selection_only = False
1838
1877
1878
1879
1880 if __name__ == '__main__':
1881
1882 from Gnumed.pycommon import gmLog2
1883
1884 gmI18N.activate_locale()
1885 gmI18N.install_domain()
1886 gmDateTime.init()
1887
1888
1896
1904
1905
1906
1907
1908
1909
1910
1911 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'):
1912
1913 test_test_ea_pnl()
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166