his_emr_api_lab 1.1.22 → 1.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +71 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/lab/application_controller.rb +6 -0
  6. data/app/controllers/lab/labels_controller.rb +17 -0
  7. data/app/controllers/lab/orders_controller.rb +38 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +19 -0
  10. data/app/controllers/lab/specimen_types_controller.rb +15 -0
  11. data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
  12. data/app/controllers/lab/test_types_controller.rb +15 -0
  13. data/app/controllers/lab/tests_controller.rb +26 -0
  14. data/app/jobs/lab/application_job.rb +4 -0
  15. data/app/jobs/lab/push_order_job.rb +12 -0
  16. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  17. data/app/jobs/lab/void_order_job.rb +17 -0
  18. data/app/mailers/lab/application_mailer.rb +6 -0
  19. data/app/models/lab/application_record.rb +5 -0
  20. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  21. data/app/models/lab/lab_encounter.rb +7 -0
  22. data/app/models/lab/lab_order.rb +58 -0
  23. data/app/models/lab/lab_result.rb +31 -0
  24. data/app/models/lab/lab_test.rb +19 -0
  25. data/app/models/lab/lims_failed_import.rb +4 -0
  26. data/app/models/lab/lims_order_mapping.rb +10 -0
  27. data/app/serializers/lab/lab_order_serializer.rb +55 -0
  28. data/app/serializers/lab/result_serializer.rb +36 -0
  29. data/app/serializers/lab/test_serializer.rb +29 -0
  30. data/app/services/lab/accession_number_service.rb +77 -0
  31. data/app/services/lab/concepts_service.rb +82 -0
  32. data/app/services/lab/labelling_service/order_label.rb +106 -0
  33. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  34. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  35. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  36. data/app/services/lab/lims/api/rest_api.rb +416 -0
  37. data/app/services/lab/lims/api/ws_api.rb +121 -0
  38. data/app/services/lab/lims/api_factory.rb +19 -0
  39. data/app/services/lab/lims/config.rb +100 -0
  40. data/app/services/lab/lims/exceptions.rb +11 -0
  41. data/app/services/lab/lims/migrator.rb +216 -0
  42. data/app/services/lab/lims/order_dto.rb +105 -0
  43. data/app/services/lab/lims/order_serializer.rb +244 -0
  44. data/app/services/lab/lims/pull_worker.rb +289 -0
  45. data/app/services/lab/lims/push_worker.rb +149 -0
  46. data/app/services/lab/lims/utils.rb +91 -0
  47. data/app/services/lab/lims/worker.rb +86 -0
  48. data/app/services/lab/metadata.rb +24 -0
  49. data/app/services/lab/orders_search_service.rb +66 -0
  50. data/app/services/lab/orders_service.rb +212 -0
  51. data/app/services/lab/results_service.rb +149 -0
  52. data/app/services/lab/tests_service.rb +93 -0
  53. data/config/routes.rb +17 -0
  54. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  55. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  56. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  57. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  58. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  59. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  60. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  61. data/lib/auto12epl.rb +201 -0
  62. data/lib/couch_bum/couch_bum.rb +92 -0
  63. data/lib/generators/lab/install/USAGE +9 -0
  64. data/lib/generators/lab/install/install_generator.rb +19 -0
  65. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  66. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  67. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  68. data/lib/his_emr_api_lab.rb +5 -0
  69. data/lib/lab/engine.rb +15 -0
  70. data/lib/lab/version.rb +5 -0
  71. data/lib/logger_multiplexor.rb +38 -0
  72. data/lib/tasks/lab_tasks.rake +25 -0
  73. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  74. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  75. data/lib/tasks/loaders/data/tests.csv +161 -0
  76. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  77. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  78. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  79. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  80. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  81. metadata +81 -2
data/lib/auto12epl.rb ADDED
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/ruby
2
+ # Jeremy Espino MD MS
3
+ # 28-JAN-2016
4
+
5
+
6
+ class Float
7
+ # function to round down a float to an integer value
8
+ def round_down n=0
9
+ n < 1 ? self.to_i.to_f : (self - 0.5 / 10**n).round(n)
10
+ end
11
+ end
12
+
13
+ # Generates EPL code that conforms to the Auto12-A standard for specimen labeling
14
+ class Auto12Epl
15
+
16
+ attr_accessor :element_font
17
+ attr_accessor :barcode_human_font
18
+
19
+ DPI = 203
20
+ LABEL_WIDTH_IN = 2.0
21
+ LABEL_HEIGHT_IN = 0.5
22
+
23
+ # font constants
24
+ FONT_X_DOTS = [8, 10, 12, 14, 32]
25
+ FONT_Y_DOTS = [12, 16, 20, 24, 24]
26
+ FONT_PAD_DOTS = 2
27
+
28
+ # element heights
29
+ HEIGHT_MARGIN = 0.031
30
+ HEIGHT_ELEMENT = 0.1
31
+ HEIGHT_ELEMENT_SPACE = 0.01
32
+ HEIGHT_PID = 0.1
33
+ HEIGHT_BARCODE = 0.200
34
+ HEIGHT_BARCODE_HUMAN = 0.050
35
+
36
+ # element widths
37
+ WIDTH_ELEMENT = 1.94
38
+ WIDTH_BARCODE = 1.395
39
+ WIDTH_BARCODE_HUMAN = 1.688
40
+
41
+ # margins
42
+ L_MARGIN = 0.031
43
+ L_MARGIN_BARCODE = 0.25
44
+
45
+ # stat locations
46
+ L_MARGIN_BARCODE_W_STAT = 0.200
47
+ L_MARGIN_W_STAT = 0.150
48
+ STAT_WIDTH_ELEMENT = 1.78
49
+ STAT_WIDTH_BARCODE = 1.150
50
+ STAT_WIDTH_BARCODE_HUMAN = 1.400
51
+
52
+ # constants for generated EPL code
53
+ BARCODE_TYPE = '1A'
54
+ BARCODE_NARROW_WIDTH = '2'
55
+ BARCODE_WIDE_WIDTH = '2'
56
+ BARCODE_ROTATION = '0'
57
+ BARCODE_IS_HUMAN_READABLE = 'N'
58
+ ASCII_HORZ_MULT = 1
59
+ ASCII_VERT_MULT = 1
60
+
61
+
62
+ def initialize(element_font = 1, barcode_human_font = 1)
63
+ @element_font = element_font
64
+ @barcode_human_font = barcode_human_font
65
+ end
66
+
67
+ # Calculate the number of characters that will fit in a given length
68
+ def max_characters(font, length)
69
+
70
+ dots_per_char = FONT_X_DOTS.at(font-1) + FONT_PAD_DOTS
71
+
72
+ num_char = ( (length * DPI) / dots_per_char).round_down
73
+
74
+ num_char.to_int
75
+ end
76
+
77
+ # Use basic truncation rule to truncate the name element i.e., if > maxCharacters cutoff and trail with +
78
+ def truncate_name(last_name, first_name, middle_initial, is_stat)
79
+ if is_stat
80
+ name_max_characters = max_characters(@element_font, STAT_WIDTH_ELEMENT)
81
+ else
82
+ name_max_characters = max_characters(@element_font, WIDTH_ELEMENT)
83
+ end
84
+
85
+ if concatName(last_name, first_name, middle_initial).length > name_max_characters
86
+ # truncate last?
87
+ if last_name.length > 12
88
+ last_name = last_name[0..11] + '+'
89
+ end
90
+
91
+ # truncate first?
92
+ if concatName(last_name, first_name, middle_initial).length > name_max_characters && first_name.length > 7
93
+ first_name = first_name[0..7] + '+'
94
+ end
95
+ end
96
+
97
+ concatName(last_name, first_name, middle_initial)
98
+
99
+ end
100
+
101
+ def concatName(last_name, first_name, middle_initial)
102
+ last_name + ', ' + first_name + (middle_initial == nil ? '' : ' ' + middle_initial)
103
+ end
104
+
105
+ # The main function to generate the EPL
106
+ def generate_epl(last_name, first_name, middle_initial, pid, dob, age, gender, col_date_time, col_name, tests, stat, acc_num, schema_track)
107
+
108
+ # format text and set margin
109
+ if stat == nil
110
+ name_text = truncate_name(last_name, first_name, middle_initial, false)
111
+ pid_dob_age_gender_text = full_justify(pid, dob + ' ' + age + ' ' + gender, @element_font, WIDTH_ELEMENT)
112
+ l_margin = L_MARGIN
113
+ l_margin_barcode = L_MARGIN_BARCODE
114
+ else
115
+ name_text = truncate_name(last_name, first_name, middle_initial, true)
116
+ pid_dob_age_gender_text = full_justify(pid, dob + ' ' + age + ' ' + gender, @element_font, STAT_WIDTH_ELEMENT)
117
+ stat_element_text = pad_stat_w_space(stat)
118
+ l_margin = L_MARGIN_W_STAT
119
+ l_margin_barcode = L_MARGIN_BARCODE_W_STAT
120
+ end
121
+ barcode_human_text = "#{acc_num} * #{schema_track.gsub(/\-/i, '')}"
122
+ collector_element_text = "Col: #{col_date_time} #{col_name}"
123
+ tests_element_text = tests
124
+
125
+ # generate EPL statements
126
+ name_element = generate_ascii_element(to_dots(l_margin), to_dots(HEIGHT_MARGIN), 0, @element_font, false, name_text)
127
+ pid_dob_age_gender_element = generate_ascii_element(to_dots(l_margin), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE), 0, @element_font, false, pid_dob_age_gender_text)
128
+ barcode_human_element = generate_ascii_element(to_dots(l_margin_barcode), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_BARCODE), 0, @barcode_human_font, false, barcode_human_text)
129
+ collector_element = generate_ascii_element(to_dots(l_margin), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_BARCODE + HEIGHT_BARCODE_HUMAN + HEIGHT_ELEMENT_SPACE), 0, @element_font, false, collector_element_text)
130
+ tests_element = generate_ascii_element(to_dots(l_margin), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_BARCODE + HEIGHT_BARCODE_HUMAN + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE), 0, @element_font, false, tests_element_text)
131
+ barcode_element = generate_barcode_element(to_dots(l_margin_barcode), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE), to_dots(HEIGHT_BARCODE)-4, schema_track)
132
+ stat_element = generate_ascii_element(to_dots(L_MARGIN)+FONT_Y_DOTS.at(@element_font - 1)+FONT_PAD_DOTS, to_dots(HEIGHT_MARGIN), 1, @element_font, true, stat_element_text)
133
+
134
+ # combine EPL statements
135
+ if stat == nil
136
+ "\nN\nR216,0\nZT\nS1\n#{name_element}\n#{pid_dob_age_gender_element}\n#{barcode_element}\n#{barcode_human_element}\n#{collector_element}\n#{tests_element}\nP3\n"
137
+ else
138
+ "\nN\nR216,0\nZT\nS1\n#{name_element}\n#{pid_dob_age_gender_element}\n#{barcode_element}\n#{barcode_human_element}\n#{collector_element}\n#{tests_element}\n#{stat_element}\nP3\n"
139
+ end
140
+
141
+ end
142
+
143
+ # Add spaces before and after the stat text so that black bars appear across the left edge of label
144
+ def pad_stat_w_space(stat)
145
+ num_char = max_characters(@element_font, LABEL_HEIGHT_IN)
146
+ spaces_needed = (num_char - stat.length) / 1
147
+ space = ''
148
+ spaces_needed.times do
149
+ space = space + ' '
150
+ end
151
+ space + stat + space
152
+ end
153
+
154
+ # Add spaces between the NPID and the dob/age/gender so that line is fully justified
155
+ def full_justify(pid, dag, font, length)
156
+ max_char = max_characters(font, length)
157
+ spaces_needed = max_char - pid.length - dag.length
158
+ space = ''
159
+ spaces_needed.times do
160
+ space = space + ' '
161
+ end
162
+ pid + space + dag
163
+ end
164
+
165
+ # convert inches to number of dots using DPI
166
+ def to_dots(inches)
167
+ (inches * DPI).round
168
+ end
169
+
170
+ # generate ascii EPL
171
+ def generate_ascii_element(x, y, rotation, font, is_reverse, text)
172
+ "A#{x.to_s},#{y.to_s},#{rotation.to_s},#{font.to_s},#{ASCII_HORZ_MULT},#{ASCII_VERT_MULT},#{is_reverse ? 'R' : 'N'},\"#{text}\""
173
+ end
174
+
175
+ # generate barcode EPL
176
+ def generate_barcode_element(x, y, height, schema_track)
177
+ schema_track = schema_track.gsub("-", "").strip
178
+ "B#{x.to_s},#{y.to_s},#{BARCODE_ROTATION},#{BARCODE_TYPE},#{BARCODE_NARROW_WIDTH},#{BARCODE_WIDE_WIDTH},#{height.to_s},#{BARCODE_IS_HUMAN_READABLE},\"#{schema_track}\""
179
+ end
180
+
181
+ end
182
+
183
+ if __FILE__ == $0
184
+
185
+ auto = Auto12Epl.new
186
+
187
+ puts auto.generate_epl("Banda", "Mary", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", nil, "KCH-16-00001234", "1600001234")
188
+ puts "\n"
189
+ puts auto.generate_epl("Banda", "Mary", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
190
+ puts "\n"
191
+ puts auto.generate_epl("Bandajustrightlas", "Mary", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
192
+ puts "\n"
193
+ puts auto.generate_epl("Bandasuperlonglastnamethatwonfit", "Marysuperlonglastnamethatwonfit", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
194
+ puts "\n"
195
+ puts auto.generate_epl("Bandasuperlonglastnamethatwonfit", "Mary", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
196
+ puts "\n"
197
+ puts auto.generate_epl("Banda", "Marysuperlonglastnamethatwonfit", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
198
+
199
+
200
+
201
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require 'couchrest'
5
+
6
+ ##
7
+ # A CouchRest wrapper for the changes API.
8
+ #
9
+ # See: https://github.com/couchrest/couchrest
10
+ class CouchBum
11
+ cattr_accessor :logger
12
+
13
+ def initialize(database:, protocol: 'http', host: 'localhost', port: 5984, username: nil, password: nil)
14
+ @connection_string = make_connection_string(protocol, username, password, host, port, database)
15
+
16
+ CouchBum.logger ||= Logger.new(STDOUT)
17
+ end
18
+
19
+ ##
20
+ # Attaches to the Changes API and streams the updates to passed block.
21
+ #
22
+ # This is a blocking call that only stops when there are no more
23
+ # changes to pull or is explicitly terminated by calling +choke+
24
+ # within the passed block.
25
+ def binge_changes(since: 0, limit: nil, include_docs: nil, &block)
26
+ catch(:choke) do
27
+ logger.debug("Binging #{limit} changes from '#{since}'")
28
+ params = stringify_params(limit: limit, include_docs: include_docs)
29
+ params = "since=#{since}&#{params}" unless since.blank?
30
+
31
+ changes = couch_rest(:get, "_changes?#{params}")
32
+ context = BingeContext.new(changes)
33
+ changes['results'].each do |change|
34
+ context.current_seq = change['seq']
35
+ context.instance_exec(change, &block)
36
+ end
37
+ end
38
+ end
39
+
40
+ def couch_rest(method, route, *args, **kwargs)
41
+ url = expand_route(route)
42
+ CouchRest.send(method, url, *args, **kwargs)
43
+ rescue CouchRest::Exception => e
44
+ logger.error("Failed to communicate with CouchDB: Status: #{e.http_code} - #{e.http_body}")
45
+ raise e
46
+ end
47
+
48
+ private
49
+
50
+ # Context under which the callback passed to binge_changes is executed.
51
+ class BingeContext
52
+ attr_accessor :current_seq
53
+
54
+ def initialize(changes)
55
+ @changes = changes
56
+ end
57
+
58
+ def choke
59
+ throw :choke
60
+ end
61
+
62
+ def last_seq
63
+ @changes['last_seq']
64
+ end
65
+
66
+ def pending
67
+ @changes['pending']
68
+ end
69
+ end
70
+
71
+ def make_connection_string(protocol, username, password, host, port, database)
72
+ auth = username ? "#{CGI.escape(username)}:#{CGI.escape(password)}@" : ''
73
+
74
+ "#{protocol}://#{auth}#{host}:#{port}/#{database}"
75
+ end
76
+
77
+ def expand_route(route)
78
+ route = route.gsub(%r{^/+}, '')
79
+
80
+ "#{@connection_string}/#{route}"
81
+ end
82
+
83
+ def stringify_params(params)
84
+ params.reduce('') do |str_params, entry|
85
+ name, value = entry
86
+ next params unless value
87
+
88
+ param = "#{name}=#{value}"
89
+ str_params.empty? ? param : "#{str_params}&#{param}"
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Installs the Lab engine.
3
+
4
+ Example:
5
+ rails generate lab:install
6
+
7
+ This will create:
8
+ config/initializers/rswag-ui-lab.rb
9
+ swagger/lab/v1/swagger.yaml
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def copy_openapi_docs
8
+ copy_file('swagger.yaml', 'swagger/lab/v1/swagger.yaml')
9
+ end
10
+
11
+ def copy_rswag_initializer
12
+ copy_file('rswag-ui-lab.rb', 'config/initializers/rswag-ui-lab.rb')
13
+ end
14
+
15
+ def copy_worker
16
+ copy_file('start_worker.rb', 'bin/lab/start_worker.rb')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ require 'rswag/ui'
2
+
3
+ Rswag::Ui.configure do |c|
4
+ c.swagger_endpoint '/api-docs/lab/v1/swagger.yaml', 'Lab API V1 Docs'
5
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger_multiplexor'
4
+
5
+ Rails.logger = LoggerMultiplexor.new(Rails.root.join('log/lims-push.log'), $stdout)
6
+ api = Lab::Lims::Api.new
7
+ worker = Lab::Lims::Worker.new(api)
8
+
9
+ def with_lock(lock_file)
10
+ File.open("log/#{lock_file}", File::RDWR | File::CREAT, 0o644) do |file|
11
+ unless file.flock(File::LOCK_EX | File::LOCK_NB)
12
+ Rails.logger.warn("Failed to start new process due to lock: #{lock_file}")
13
+ exit 2
14
+ end
15
+
16
+ file.rewind
17
+ file.puts("Process ##{Process.pid} started at #{Time.now}")
18
+
19
+ yield
20
+ end
21
+ end
22
+
23
+ case ARGV[0]&.downcase
24
+ when 'push'
25
+ with_lock('lims-push.lock') { worker.push_orders }
26
+ when 'pull'
27
+ with_lock('lims-pull.lock') { worker.pull_orders }
28
+ else
29
+ warn 'Error: No or invalid action specified: Valid actions are push and pull'
30
+ warn 'USAGE: rails runner start_worker.rb push'
31
+ exit 1
32
+ end