mahis_emr_api_lab 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +71 -0
- data/Rakefile +32 -0
- data/app/controllers/lab/application_controller.rb +6 -0
- data/app/controllers/lab/labels_controller.rb +17 -0
- data/app/controllers/lab/orders_controller.rb +78 -0
- data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
- data/app/controllers/lab/results_controller.rb +20 -0
- data/app/controllers/lab/specimen_types_controller.rb +15 -0
- data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
- data/app/controllers/lab/test_types_controller.rb +15 -0
- data/app/controllers/lab/tests_controller.rb +25 -0
- data/app/controllers/lab/users_controller.rb +32 -0
- data/app/jobs/lab/application_job.rb +4 -0
- data/app/jobs/lab/push_order_job.rb +12 -0
- data/app/jobs/lab/update_patient_orders_job.rb +32 -0
- data/app/jobs/lab/void_order_job.rb +17 -0
- data/app/mailers/lab/application_mailer.rb +6 -0
- data/app/models/lab/application_record.rb +5 -0
- data/app/models/lab/lab_accession_number_counter.rb +13 -0
- data/app/models/lab/lab_acknowledgement.rb +6 -0
- data/app/models/lab/lab_encounter.rb +7 -0
- data/app/models/lab/lab_order.rb +58 -0
- data/app/models/lab/lab_result.rb +31 -0
- data/app/models/lab/lab_test.rb +19 -0
- data/app/models/lab/lims_failed_import.rb +4 -0
- data/app/models/lab/lims_order_mapping.rb +10 -0
- data/app/models/lab/order_extension.rb +14 -0
- data/app/serializers/lab/lab_order_serializer.rb +56 -0
- data/app/serializers/lab/result_serializer.rb +36 -0
- data/app/serializers/lab/test_serializer.rb +52 -0
- data/app/services/lab/accession_number_service.rb +77 -0
- data/app/services/lab/acknowledgement_service.rb +47 -0
- data/app/services/lab/concepts_service.rb +82 -0
- data/app/services/lab/json_web_token_service.rb +20 -0
- data/app/services/lab/labelling_service/order_label.rb +106 -0
- data/app/services/lab/lims/acknowledgement_serializer.rb +29 -0
- data/app/services/lab/lims/acknowledgement_worker.rb +37 -0
- data/app/services/lab/lims/api/blackhole_api.rb +21 -0
- data/app/services/lab/lims/api/couchdb_api.rb +53 -0
- data/app/services/lab/lims/api/mysql_api.rb +316 -0
- data/app/services/lab/lims/api/rest_api.rb +434 -0
- data/app/services/lab/lims/api/ws_api.rb +121 -0
- data/app/services/lab/lims/api_factory.rb +19 -0
- data/app/services/lab/lims/config.rb +105 -0
- data/app/services/lab/lims/exceptions.rb +11 -0
- data/app/services/lab/lims/migrator.rb +216 -0
- data/app/services/lab/lims/order_dto.rb +105 -0
- data/app/services/lab/lims/order_serializer.rb +251 -0
- data/app/services/lab/lims/pull_worker.rb +314 -0
- data/app/services/lab/lims/push_worker.rb +152 -0
- data/app/services/lab/lims/utils.rb +91 -0
- data/app/services/lab/lims/worker.rb +94 -0
- data/app/services/lab/metadata.rb +26 -0
- data/app/services/lab/notification_service.rb +72 -0
- data/app/services/lab/orders_search_service.rb +72 -0
- data/app/services/lab/orders_service.rb +330 -0
- data/app/services/lab/results_service.rb +166 -0
- data/app/services/lab/tests_service.rb +105 -0
- data/app/services/lab/user_service.rb +62 -0
- data/config/routes.rb +28 -0
- data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
- data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
- data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
- data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
- data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
- data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
- data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
- data/lib/auto12epl.rb +201 -0
- data/lib/couch_bum/couch_bum.rb +92 -0
- data/lib/generators/lab/install/USAGE +9 -0
- data/lib/generators/lab/install/install_generator.rb +19 -0
- data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
- data/lib/generators/lab/install/templates/start_worker.rb +32 -0
- data/lib/generators/lab/install/templates/swagger.yaml +714 -0
- data/lib/lab/engine.rb +13 -0
- data/lib/lab/version.rb +5 -0
- data/lib/logger_multiplexor.rb +38 -0
- data/lib/mahis_emr_api_lab.rb +6 -0
- data/lib/tasks/lab_tasks.rake +25 -0
- data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
- data/lib/tasks/loaders/data/test-measures.csv +225 -0
- data/lib/tasks/loaders/data/tests.csv +161 -0
- data/lib/tasks/loaders/loader_mixin.rb +53 -0
- data/lib/tasks/loaders/metadata_loader.rb +26 -0
- data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
- data/lib/tasks/loaders/specimens_loader.rb +65 -0
- data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
- 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,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,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
|
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,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,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
|