mahis_emr_api_lab 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  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 +78 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +20 -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 +25 -0
  14. data/app/controllers/lab/users_controller.rb +32 -0
  15. data/app/jobs/lab/application_job.rb +4 -0
  16. data/app/jobs/lab/push_order_job.rb +12 -0
  17. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  18. data/app/jobs/lab/void_order_job.rb +17 -0
  19. data/app/mailers/lab/application_mailer.rb +6 -0
  20. data/app/models/lab/application_record.rb +5 -0
  21. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  22. data/app/models/lab/lab_acknowledgement.rb +6 -0
  23. data/app/models/lab/lab_encounter.rb +7 -0
  24. data/app/models/lab/lab_order.rb +58 -0
  25. data/app/models/lab/lab_result.rb +31 -0
  26. data/app/models/lab/lab_test.rb +19 -0
  27. data/app/models/lab/lims_failed_import.rb +4 -0
  28. data/app/models/lab/lims_order_mapping.rb +10 -0
  29. data/app/models/lab/order_extension.rb +14 -0
  30. data/app/serializers/lab/lab_order_serializer.rb +56 -0
  31. data/app/serializers/lab/result_serializer.rb +36 -0
  32. data/app/serializers/lab/test_serializer.rb +52 -0
  33. data/app/services/lab/accession_number_service.rb +77 -0
  34. data/app/services/lab/acknowledgement_service.rb +47 -0
  35. data/app/services/lab/concepts_service.rb +82 -0
  36. data/app/services/lab/json_web_token_service.rb +20 -0
  37. data/app/services/lab/labelling_service/order_label.rb +106 -0
  38. data/app/services/lab/lims/acknowledgement_serializer.rb +29 -0
  39. data/app/services/lab/lims/acknowledgement_worker.rb +37 -0
  40. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  41. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  42. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  43. data/app/services/lab/lims/api/rest_api.rb +434 -0
  44. data/app/services/lab/lims/api/ws_api.rb +121 -0
  45. data/app/services/lab/lims/api_factory.rb +19 -0
  46. data/app/services/lab/lims/config.rb +105 -0
  47. data/app/services/lab/lims/exceptions.rb +11 -0
  48. data/app/services/lab/lims/migrator.rb +216 -0
  49. data/app/services/lab/lims/order_dto.rb +105 -0
  50. data/app/services/lab/lims/order_serializer.rb +251 -0
  51. data/app/services/lab/lims/pull_worker.rb +314 -0
  52. data/app/services/lab/lims/push_worker.rb +152 -0
  53. data/app/services/lab/lims/utils.rb +91 -0
  54. data/app/services/lab/lims/worker.rb +94 -0
  55. data/app/services/lab/metadata.rb +26 -0
  56. data/app/services/lab/notification_service.rb +72 -0
  57. data/app/services/lab/orders_search_service.rb +72 -0
  58. data/app/services/lab/orders_service.rb +330 -0
  59. data/app/services/lab/results_service.rb +166 -0
  60. data/app/services/lab/tests_service.rb +105 -0
  61. data/app/services/lab/user_service.rb +62 -0
  62. data/config/routes.rb +28 -0
  63. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  64. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  65. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  66. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  67. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  68. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  69. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  70. data/lib/auto12epl.rb +201 -0
  71. data/lib/couch_bum/couch_bum.rb +92 -0
  72. data/lib/generators/lab/install/USAGE +9 -0
  73. data/lib/generators/lab/install/install_generator.rb +19 -0
  74. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  75. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  76. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  77. data/lib/lab/engine.rb +13 -0
  78. data/lib/lab/version.rb +5 -0
  79. data/lib/logger_multiplexor.rb +38 -0
  80. data/lib/mahis_emr_api_lab.rb +6 -0
  81. data/lib/tasks/lab_tasks.rake +25 -0
  82. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  83. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  84. data/lib/tasks/loaders/data/tests.csv +161 -0
  85. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  86. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  87. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  88. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  89. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  90. metadata +331 -0
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ ##
5
+ # Manage tests that have been ordered through the ordering service.
6
+ module TestsService
7
+ class << self
8
+ def find_tests(filters)
9
+ tests = Lab::LabTest.all
10
+ patient_id = filters.delete(:patient_id)
11
+ patient_id ||= ::Patient.find(filters.delete(:patient))&.id if filters[:patient]
12
+
13
+ Lab::UpdatePatientOrdersJob.perform_later(patient_id) if patient_id
14
+
15
+ tests = filter_tests(tests, test_type_id: filters.delete(:test_type_id),
16
+ patient_id: filters.delete(:patient_id),
17
+ patient: filters.delete(:patient)
18
+ )
19
+
20
+ tests = filter_tests_by_results(tests) if %w[1 true].include?(filters[:pending_results]&.downcase)
21
+
22
+ tests = filter_tests_by_order(tests, accession_number: filters.delete(:accession_number),
23
+ order_date: filters.delete(:order_date),
24
+ specimen_type_id: filters.delete(:specimen_type_id))
25
+
26
+ tests.map { |test| Lab::TestSerializer.serialize(test) }
27
+ end
28
+
29
+ def create_tests(order, date, tests_params)
30
+ raise InvalidParameterError, 'tests are required' if tests_params.nil? || tests_params.empty?
31
+
32
+
33
+ Lab::LabTest.transaction do
34
+ tests_params.map do |params|
35
+ concept_id = params[:concept_id]
36
+ concept_id = Concept.find_concept_by_uuid(params[:concept]).id if concept_id.nil?
37
+
38
+ test = Lab::LabTest.create!(
39
+ concept_id: ConceptName.find_by_name!(Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
40
+ .concept_id,
41
+ encounter_id: order.encounter_id,
42
+ order_id: order.order_id,
43
+ person_id: order.patient_id,
44
+ obs_datetime: date&.to_time || Time.now,
45
+ value_coded: concept_id
46
+ )
47
+
48
+ Lab::TestSerializer.serialize(test, order: order)
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ ##
56
+ # Filter a LabTests Relation.
57
+ def filter_tests(tests, test_type_id: nil, patient_id: nil, patient: nil)
58
+ tests = tests.where(value_coded: test_type_id) if test_type_id
59
+ tests = tests.where(person_id: patient_id) if patient_id
60
+ person = ::Person.find_by_uuid(patient) if patient
61
+ tests = tests.where(person_id: person&.patient&.id) if patient
62
+
63
+ tests
64
+ end
65
+
66
+ ##
67
+ # Filter out any tests having results
68
+ def filter_tests_by_results(tests)
69
+ tests.where.not(obs_id: Lab::LabResult.all.select(:obs_group_id))
70
+ end
71
+
72
+ ##
73
+ # Filter LabTests Relation using their parent orders parameters.
74
+ def filter_tests_by_order(tests, accession_number: nil, order_date: nil, specimen_type_id: nil)
75
+ return tests unless accession_number || order_date || specimen_type_id
76
+
77
+ lab_orders = filter_orders(Lab::LabOrder.all, accession_number: accession_number,
78
+ order_date: order_date,
79
+ specimen_type_id: specimen_type_id)
80
+ tests.joins(:order).merge(lab_orders)
81
+ end
82
+
83
+ def filter_orders(orders, accession_number: nil, order_date: nil, specimen_type_id: nil)
84
+ if order_date
85
+ order_date = order_date.to_date
86
+ orders = orders.where('start_date >= ? AND start_date < ?', order_date, order_date + 1.day)
87
+ end
88
+
89
+ orders = orders.where(accession_number: accession_number) if accession_number
90
+ orders = orders.where(concept_id: specimen_type_id) if specimen_type_id
91
+
92
+ orders
93
+ end
94
+
95
+ def create_test(order, date, test_type_id)
96
+ create_order_observation(
97
+ order,
98
+ Lab::Metadata::TEST_TYPE_CONCEPT_NAME,
99
+ date,
100
+ value_coded: test_type_id
101
+ )
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ # Service for managing LIMS users
5
+ module UserService
6
+ class << self
7
+ include BCrypt
8
+
9
+ def create_lims_user(username:, password:)
10
+ validate username: username
11
+ ActiveRecord::Base.transaction do
12
+ person = create_lims_person
13
+ create_user username: username, password: password, person: person
14
+ end
15
+ end
16
+
17
+ def authenticate_user(username:, password:, user_agent:, request_ip:)
18
+ user = User.find_by_username username
19
+ encrypted_pass = Password.new(user.password)
20
+ if encrypted_pass == password
21
+ generate_token(user, user_agent, request_ip)
22
+ else
23
+ # throw authentication error
24
+ nil
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ ##
31
+ # Validate that the username doesn't already exists
32
+ def validate(username:)
33
+ raise UnprocessableEntityError, 'Username already exists' if User.find_by_username username
34
+ end
35
+
36
+ def create_lims_person
37
+ god_user = User.first
38
+ person = Person.create!(creator: god_user.id)
39
+ PersonName.create!(given_name: 'Lims', family_name: 'User', creator: god_user.id, person: person)
40
+ person
41
+ end
42
+
43
+ def create_user(username:, password:, person:)
44
+ salt = SecureRandom.base64
45
+ user = User.create!(
46
+ username: username,
47
+ password: Password.create(password),
48
+ salt: salt,
49
+ person: person,
50
+ creator: User.first.id
51
+ )
52
+ end
53
+
54
+ def generate_token(user, user_agent, request_ip)
55
+ browser = Browser.new(user_agent)
56
+ key_supplement = request_ip + browser.name + browser.version
57
+ token = Lab::JsonWebTokenService.encode({ user_id: user.id }, key_supplement)
58
+ { auth_token: token }
59
+ end
60
+ end
61
+ end
62
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ Lab::Engine.routes.draw do
4
+ resources :orders, path: 'api/v1/lab/orders' do
5
+ collection do
6
+ post :order_status
7
+ post :order_result
8
+ end
9
+ end
10
+ resources :tests, path: 'api/v1/lab/tests', except: %i[update] do # ?pending=true to select tests without results?
11
+ resources :results, only: %i[index create destroy]
12
+ end
13
+
14
+ get 'api/v1/lab/labels/order', to: 'labels#print_order_label'
15
+ get 'api/v1/lab/accession_number', to: 'orders#verify_tracking_number'
16
+
17
+ # Metadata
18
+ # TODO: Move the following to namespace /concepts
19
+ resources :specimen_types, only: %i[index], path: 'api/v1/lab/specimen_types'
20
+ resources :test_result_indicators, only: %i[index], path: 'api/v1/lab/test_result_indicators'
21
+ resources :test_types, only: %i[index], path: 'api/v1/lab/test_types'
22
+ resources :reasons_for_test, only: %i[index], path: 'api/v1/lab/reasons_for_test'
23
+ resources :users, only: %i[create], path: 'api/v1/lab/users' do
24
+ collection do
25
+ post :login
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ class CreateLabLabAccessionNumberCounters < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :lab_accession_number_counters do |t|
4
+ t.date :date
5
+ t.bigint :value
6
+
7
+ t.timestamps
8
+
9
+ t.index %i[date], unique: true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ class CreateLabLimsOrderMappings < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :lab_lims_order_mappings do |t|
4
+ t.integer :lims_id, null: false, unique: true
5
+ t.integer :order_id, null: false, unique: true
6
+ t.datetime :pushed_at
7
+ t.datetime :pulled_at
8
+
9
+ t.timestamps
10
+
11
+ t.foreign_key :orders, primary_key: :order_id, column: :order_id
12
+ t.index :lims_id
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChangeLimsIdToStringInLimsOrderMapping < ActiveRecord::Migration[5.2]
4
+ def change
5
+ reversible do |direction|
6
+ direction.up do
7
+ change_column :lab_lims_order_mappings, :lims_id, :string, null: false
8
+ end
9
+
10
+ direction.down do
11
+ change_column :lab_lims_order_mappings, :lims_id, :integer, null: false
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class AddOrderRevisionToLimsOrderMapping < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :lab_lims_order_mappings, :revision, :string
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateLabLimsFailedImports < ActiveRecord::Migration[5.2]
4
+ def change
5
+ create_table :lab_lims_failed_imports do |t|
6
+ t.string :lims_id, null: false
7
+ t.string :tracking_number, null: false
8
+ t.string :patient_nhid, null: false
9
+ t.string :reason, null: false
10
+ t.string :diff, limit: 2048
11
+
12
+ t.timestamps
13
+
14
+ t.index :lims_id
15
+ t.index :patient_nhid
16
+ t.index :tracking_number
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ class FixNumericResultsValueType < ActiveRecord::Migration[5.2]
2
+ def up
3
+ results = Lab::LabResult.all.includes(:children)
4
+
5
+ ActiveRecord::Base.connection.transaction do
6
+ results.each do |result|
7
+ result.children.each do |measure|
8
+ next unless measure.value_text&.match?(/^[+-]?((\d+(\.\d+)?)|\.\d+)$/)
9
+
10
+ puts "Updating result value type for result measure ##{measure.obs_id}"
11
+ measure.value_numeric = measure.value_text
12
+ measure.value_text = nil
13
+ measure.save!
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def down; end
20
+ end
@@ -0,0 +1,7 @@
1
+ class AddDefaultToLimsOrderMapping < ActiveRecord::Migration[5.2]
2
+ def up
3
+ change_column :lab_lims_order_mappings, :revision, :string, limit: 256, default: nil, null: true
4
+ end
5
+
6
+ def down; end
7
+ end
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