mahis_emr_api_lab 1.2.0

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 (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