his_emr_api_lab 0.0.3 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 778184a052cac6932ec700f03b99615024440af3944c371d0cfa997f357c525c
4
- data.tar.gz: 045fb73daab53abc457c08152c5148251a6dccb239f0c76e09f3172007e44b6e
3
+ metadata.gz: 7b410e215fad55950c150d2ec7a82bda3f434cca4cc287e0d4fca2a9116e9182
4
+ data.tar.gz: 6b3a0c75c303c2139c1f6f963875667c4606bce42d23f6a22c277412e24db960
5
5
  SHA512:
6
- metadata.gz: 95f56b7a5d1b36565074904d49e5ff69b34fb57bed690a7bec5d269749c6813e094b4e02b397517523edd90e0714710376f489338788838fad775f3c724659db
7
- data.tar.gz: 380096a9132eb857a4a321260593640eec0d6a5b08f661e97a1ba2396619eb5984c1aca488e364930dff737ff397b07865597b9ed321c7a93101c151a8d2566f
6
+ metadata.gz: 4f6068aecbc02d5c3df75ac422c4d4f9baeac86c6559a1e75078ef68088b4e6702239a366fb5e5879a2925592e3440a2880668bcc50f4730c9d2051f522ac94a
7
+ data.tar.gz: 5e3b4d61f507f2de7847951dcdd738a50dd29ba4ae5f981ce8284f3ba11c35d6d1c337787c1b78154b96eb318fc5343d404a3827fd02c89ffc3da60eb2d617c3
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Lab
4
4
  class LabelsController < ApplicationController
5
+ skip_before_action :authenticate
6
+
5
7
  def print_order_label
6
8
  order_id = params.require(:order_id)
7
9
 
@@ -27,7 +27,7 @@ module Lab
27
27
  '',
28
28
  drawer,
29
29
  '',
30
- specimen,
30
+ tests,
31
31
  reason_for_test,
32
32
  order.accession_number,
33
33
  order.accession_number)
@@ -36,7 +36,7 @@ module Lab
36
36
  def reason_for_test
37
37
  return 'Unknown' unless order.reason_for_test
38
38
 
39
- ConceptName.find_by_concept_id(order.reason_for_test.value_coded)&.name || 'Unknown'
39
+ short_concept_name(order.reason_for_test.value_coded) || 'Unknown'
40
40
  end
41
41
 
42
42
  def patient
@@ -73,6 +73,24 @@ module Lab
73
73
  ConceptName.find_by_concept_id(order.concept_id)&.name || 'Unknown'
74
74
  end
75
75
 
76
+ def tests
77
+ tests = order.tests.map do |test|
78
+ name = short_concept_name(test.value_coded) || 'Unknown'
79
+
80
+ next 'VL' if name.match?(/Viral load/i)
81
+
82
+ name.size > 7 ? name[0..6] : name
83
+ end
84
+
85
+ tests.join(', ')
86
+ end
87
+
88
+ def short_concept_name(concept_id)
89
+ ConceptName.where(concept_id: concept_id)
90
+ .min_by { |concept| concept.name.size }
91
+ &.name
92
+ end
93
+
76
94
  def unknown_concept
77
95
  ConceptName.find_by_name('Unknown')
78
96
  end
@@ -29,16 +29,11 @@ module Lab
29
29
  # given block until the queue is empty or connection is terminated
30
30
  # by calling method +choke+.
31
31
  def consume_orders(from: 0, limit: 30)
32
- last_seq = { value: 0 }
33
-
34
32
  bum.binge_changes(since: from, limit: limit, include_docs: true) do |change|
35
33
  next unless change['doc']['type']&.casecmp?('Order')
36
34
 
37
35
  yield OrderDTO.new(change['doc']), self
38
- last_seq[:value] = self.last_seq
39
36
  end
40
-
41
- last_seq[:value]
42
37
  end
43
38
 
44
39
  def create_order(order)
@@ -29,6 +29,7 @@ require 'lab/lab_test'
29
29
  require 'lab/lims_order_mapping'
30
30
  require 'lab/lims_failed_import'
31
31
 
32
+ require_relative './worker'
32
33
  require_relative '../orders_service'
33
34
  require_relative '../results_service'
34
35
  require_relative '../tests_service'
@@ -47,14 +48,16 @@ module Lab
47
48
 
48
49
  attr_reader :rejections
49
50
 
50
- def consume_orders(from: nil, limit: 50_000)
51
+ def consume_orders(from: nil, **_kwargs)
52
+ limit = 50_000
53
+
51
54
  Parallel.each(read_orders(from, limit),
52
55
  in_processes: MAX_THREADS,
53
56
  finish: order_pmap_post_processor(from)) do |row|
54
57
  next unless row['doc']['type']&.casecmp?('Order')
55
58
 
56
- User.current = Migrator.lab_user
57
- yield OrderDTO.new(row['doc']), OpenStruct.new(last_seq: from)
59
+ User.current = Utils.lab_user
60
+ yield OrderDTO.new(row['doc']), OpenStruct.new(last_seq: (from || 0) + limit, current_seq: from)
58
61
  end
59
62
  end
60
63
 
@@ -70,7 +73,7 @@ module Lab
70
73
  private
71
74
 
72
75
  def last_seq_path
73
- Rails.root.join('log/lims/migration-last-id.dat')
76
+ LIMS_LOG_PATH.join('migration-last-id.dat')
74
77
  end
75
78
 
76
79
  def order_pmap_post_processor(last_seq)
@@ -129,18 +132,6 @@ module Lab
129
132
  def update_last_seq(_last_seq); end
130
133
  end
131
134
 
132
- def self.lab_user
133
- user = User.find_by_username('lab_daemon')
134
- return user if user
135
-
136
- god_user = User.first
137
-
138
- person = Person.create!(creator: god_user.user_id)
139
- PersonName.create!(person: person, given_name: 'Lab', family_name: 'Daemon', creator: god_user.user_id)
140
-
141
- User.create!(username: 'lab_daemon', person: person, creator: god_user.user_id)
142
- end
143
-
144
135
  def self.save_csv(filename, rows:, headers: nil)
145
136
  CSV.open(filename, File::WRONLY | File::CREAT) do |csv|
146
137
  csv << headers if headers
@@ -148,12 +139,13 @@ module Lab
148
139
  end
149
140
  end
150
141
 
151
- MIGRATION_REJECTIONS_CSV_PATH = Rails.root.join('log/lims/migration-rejections.csv')
142
+ MIGRATION_REJECTIONS_CSV_PATH = LIMS_LOG_PATH.join('migration-rejections.csv')
152
143
 
153
144
  def self.export_rejections(rejections)
154
- headers = ['Accession number', 'NHID', 'First name', 'Last name', 'Reason']
145
+ headers = ['doc_id', 'Accession number', 'NHID', 'First name', 'Last name', 'Reason']
155
146
  rows = (rejections || []).map do |rejection|
156
147
  [
148
+ rejection.order[:_id],
157
149
  rejection.order[:tracking_number],
158
150
  rejection.order[:patient][:id],
159
151
  rejection.order[:patient][:first_name],
@@ -165,12 +157,13 @@ module Lab
165
157
  save_csv(MIGRATION_REJECTIONS_CSV_PATH, headers: headers, rows: rows)
166
158
  end
167
159
 
168
- MIGRATION_FAILURES_CSV_PATH = Rails.root.join('log/lims/migration-failures.csv')
160
+ MIGRATION_FAILURES_CSV_PATH = LIMS_LOG_PATH.join('migration-failures.csv')
169
161
 
170
162
  def self.export_failures
171
- headers = ['Accession number', 'NHID', 'Reason', 'Difference']
163
+ headers = ['doc_id', 'Accession number', 'NHID', 'Reason', 'Difference']
172
164
  rows = Lab::LimsFailedImport.all.map do |failure|
173
165
  [
166
+ failure.lims_id,
174
167
  failure.tracking_number,
175
168
  failure.patient_nhid,
176
169
  failure.reason,
@@ -181,7 +174,7 @@ module Lab
181
174
  save_csv(MIGRATION_FAILURES_CSV_PATH, headers: headers, rows: rows)
182
175
  end
183
176
 
184
- MIGRATION_LOG_PATH = Rails.root.join('log/lims/migration.log')
177
+ MIGRATION_LOG_PATH = LIMS_LOG_PATH.join('migration.log')
185
178
 
186
179
  def self.start_migration
187
180
  log_dir = Rails.root.join('log/lims')
@@ -30,9 +30,9 @@ module Lab
30
30
 
31
31
  # Translates a LIMS specimen name to an OpenMRS concept_id
32
32
  def specimen_type_id
33
- lims_specimen_name = self['sample_type']
33
+ lims_specimen_name = self['sample_type']&.strip&.downcase
34
34
 
35
- if %w[specimen_not_collected not_assigned].include?(lims_specimen_name)
35
+ if %w[specimen_not_collected not_assigned not_specified].include?(lims_specimen_name)
36
36
  return ConceptName.select(:concept_id).find_by_name!('Unknown').concept_id
37
37
  end
38
38
 
@@ -53,6 +53,8 @@ module Lab
53
53
 
54
54
  # Extract requesting clinician name from LIMS
55
55
  def requesting_clinician
56
+ return 'Unknown' unless self['who_order_test']
57
+
56
58
  # TODO: Extend requesting clinician to an obs tree having extra parameters
57
59
  # like phone number and ID to closely match the lims user.
58
60
  first_name = self['who_order_test']['first_name'] || ''
@@ -97,7 +97,9 @@ module Lab
97
97
  end
98
98
 
99
99
  def format_test_results(order)
100
- order.tests.each_with_object({}) do |test, results|
100
+ order.tests&.each_with_object({}) do |test, results|
101
+ next unless test.result
102
+
101
103
  results[test.name] = {
102
104
  results: test.result.each_with_object({}) do |measure, measures|
103
105
  measures[measure.indicator.name] = { result_value: "#{measure.value_modifier}#{measure.value}" }
@@ -39,6 +39,18 @@ module Lab
39
39
  end
40
40
  end
41
41
 
42
+ def self.lab_user
43
+ user = User.find_by_username('lab_daemon')
44
+ return user if user
45
+
46
+ god_user = User.first
47
+
48
+ person = Person.create!(creator: god_user.user_id)
49
+ PersonName.create!(person: person, given_name: 'Lab', family_name: 'Daemon', creator: god_user.user_id)
50
+
51
+ User.create!(username: 'lab_daemon', person: person, creator: god_user.user_id)
52
+ end
53
+
42
54
  def self.parse_date(str_date, fallback_date = nil)
43
55
  if str_date.blank? && fallback_date.blank?
44
56
  raise "Can't parse blank date"
@@ -2,12 +2,16 @@
2
2
 
3
3
  require 'cgi/util'
4
4
 
5
+ require_relative './api'
5
6
  require_relative './exceptions'
6
7
  require_relative './order_serializer'
7
8
  require_relative './utils'
8
9
 
9
10
  module Lab
10
11
  module Lims
12
+ LIMS_LOG_PATH = Rails.root.join('log/lims')
13
+ Dir.mkdir(LIMS_LOG_PATH) unless File.exist?(LIMS_LOG_PATH)
14
+
11
15
  ##
12
16
  # Pull/Push orders from/to the LIMS queue (Oops meant CouchDB).
13
17
  class Worker
@@ -15,6 +19,21 @@ module Lab
15
19
 
16
20
  attr_reader :lims_api
17
21
 
22
+ def self.start
23
+ File.open(LIMS_LOG_PATH.join('worker.lock'), File::WRONLY | File::CREAT, 0o644) do |fout|
24
+ fout.flock(File::LOCK_EX)
25
+
26
+ User.current = Utils.lab_user
27
+
28
+ fout.write("Worker ##{Process.pid} started at #{Time.now}")
29
+ worker = new(Api.new)
30
+ worker.pull_orders
31
+ # TODO: Verify that names being pushed to LIMS are of the correct format (ie matching
32
+ # LIMS naming conventions). Enable pushing when that is done
33
+ # worker.push_orders
34
+ end
35
+ end
36
+
18
37
  def initialize(lims_api)
19
38
  @lims_api = lims_api
20
39
  end
@@ -52,8 +71,8 @@ module Lab
52
71
  lims_api.update_order(mapping.lims_id, order_dto)
53
72
  mapping.update(pushed_at: Time.now)
54
73
  else
55
- order_dto = lims_api.create_order(order_dto)
56
- LimsOrderMapping.create!(order: order, lims_id: order_dto['_id'], pushed_at: Time.now)
74
+ update = lims_api.create_order(order_dto)
75
+ LimsOrderMapping.create!(order: order, lims_id: update['id'], revision: update['rev'], pushed_at: Time.now)
57
76
  end
58
77
  end
59
78
 
@@ -64,7 +83,8 @@ module Lab
64
83
  # Pulls orders from the LIMS queue and writes them to the local database
65
84
  def pull_orders
66
85
  logger.info("Retrieving LIMS orders starting from #{last_seq}")
67
- lims_api.consume_orders(from: last_seq) do |order_dto, context|
86
+
87
+ lims_api.consume_orders(from: last_seq, limit: 100) do |order_dto, context|
68
88
  logger.debug("Retrieved order ##{order_dto[:tracking_number]}: #{order_dto}")
69
89
 
70
90
  patient = find_patient_by_nhid(order_dto[:patient][:id])
@@ -80,6 +100,8 @@ module Lab
80
100
  save_failed_import(order_dto, 'Demographics not matching', diff)
81
101
  end
82
102
 
103
+ update_last_seq(context.current_seq)
104
+
83
105
  [:accepted, "Patient NPID, '#{order_dto[:patient][:id]}', matched"]
84
106
  rescue DuplicateNHID
85
107
  logger.warn("Failed to import order due to duplicate patient NHID: #{order_dto[:patient][:id]}")
@@ -90,8 +112,6 @@ module Lab
90
112
  rescue LimsException => e
91
113
  logger.warn("Failed to import order due to #{e.class} - #{e.message}")
92
114
  save_failed_import(order_dto, e.message)
93
- ensure
94
- update_last_seq(context.last_seq)
95
115
  end
96
116
  end
97
117
 
@@ -264,12 +284,12 @@ module Lab
264
284
  indicator: { concept_id: indicator.concept_id },
265
285
  value_type: value_type,
266
286
  value: value_type == 'numeric' ? value.to_f : value,
267
- value_modifier: value_modifier
287
+ value_modifier: value_modifier.blank? ? '=' : value_modifier
268
288
  )
269
289
  end
270
290
 
271
291
  def parse_lims_result_value(value)
272
- value = value['result_value']
292
+ value = value['result_value']&.strip
273
293
  return nil, nil, nil if value.blank?
274
294
 
275
295
  match = value&.match(/^(>|=|<|<=|>=)(.*)$/)
@@ -279,7 +299,7 @@ module Lab
279
299
  end
280
300
 
281
301
  def guess_result_datatype(result)
282
- return 'numeric' if result.match?(/^[+-]?(\d+(\.\d+)|\.\d+)?$/)
302
+ return 'numeric' if result.strip.match?(/^[+-]?(\d+(\.\d+)|\.\d+)?$/)
283
303
 
284
304
  'text'
285
305
  end
@@ -303,7 +323,7 @@ module Lab
303
323
  end
304
324
 
305
325
  def last_seq_path
306
- Rails.root.join('log/lims/last-seq.dat')
326
+ LIMS_LOG_PATH.join('last_seq.dat')
307
327
  end
308
328
  end
309
329
  end
@@ -5,44 +5,54 @@ module Lab
5
5
  module OrdersSearchService
6
6
  class << self
7
7
  def find_orders(filters)
8
- date = filters.delete(:date)
9
- status = filters.delete(:status)
8
+ extra_filters = pop_filters(filters, :date, :end_date, :status)
10
9
 
11
10
  orders = Lab::LabOrder.prefetch_relationships
12
11
  .where(filters)
13
12
  .order(start_date: :desc)
14
13
 
15
- orders = filter_orders_by_date(orders, date) if date
16
- orders = filter_orders_by_status(orders, status) if status
14
+ orders = filter_orders_by_status(orders, pop_filters(extra_filters, :status))
15
+ orders = filter_orders_by_date(orders, extra_filters)
17
16
 
18
17
  orders.map { |order| Lab::LabOrderSerializer.serialize_order(order) }
19
18
  end
20
19
 
21
- def filter_orders_by_date(orders, date)
22
- orders.where('start_date < DATE(?)', date.to_date + 1.day)
23
- end
20
+ def filter_orders_by_date(orders, date: nil, end_date: nil)
21
+ date = date&.to_date
22
+ end_date = end_date&.to_date
24
23
 
25
- def filter_orders_by_status(orders, status)
26
- case status.downcase
27
- when 'ordered' then orders.where(concept_id: unknown_concept_id)
28
- when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
24
+ if date && end_date
25
+ return orders.where('start_date BETWEEN ? AND ?', date, end_date + 1.day)
29
26
  end
30
- end
31
27
 
32
- def unknown_concept_id
33
- ConceptName.find_by_name!('Unknown').concept_id
28
+ if date
29
+ return orders.where('start_date BETWEEN ? AND ?', date, date + 1.day)
30
+ end
31
+
32
+ return orders.where('start_date < ?', end_date + 1.day) if end_date
33
+
34
+ orders
34
35
  end
35
36
 
36
- def filter_orders_by_status(orders, status)
37
- case status.downcase
37
+ def filter_orders_by_status(orders, status: nil)
38
+ case status&.downcase
38
39
  when 'ordered' then orders.where(concept_id: unknown_concept_id)
39
40
  when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
41
+ else orders
40
42
  end
41
43
  end
42
44
 
43
45
  def unknown_concept_id
44
46
  ConceptName.find_by_name!('Unknown').concept_id
45
47
  end
48
+
49
+ def pop_filters(params, *filters)
50
+ filters.each_with_object({}) do |filter, popped_params|
51
+ next unless params.key?(filter)
52
+
53
+ popped_params[filter.to_sym] = params.delete(filter)
54
+ end
55
+ end
46
56
  end
47
57
  end
48
58
  end
@@ -24,18 +24,21 @@ class CouchBum
24
24
  # within the passed block.
25
25
  def binge_changes(since: 0, limit: nil, include_docs: nil, &block)
26
26
  catch(:choke) do
27
- extra_params = stringify_params(limit: limit, include_docs: include_docs)
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?
28
30
 
29
- changes = couch_rest(:get, "_changes?since=#{since}&#{extra_params}")
31
+ changes = couch_rest(:get, "_changes?#{params}")
30
32
  context = BingeContext.new(changes)
31
- changes['results'].each { |change| context.instance_exec(change, &block) }
33
+ changes['results'].each do |change|
34
+ context.current_seq = change['seq']
35
+ context.instance_exec(change, &block)
36
+ end
32
37
  end
33
38
  end
34
39
 
35
40
  def couch_rest(method, route, *args, **kwargs)
36
41
  url = expand_route(route)
37
-
38
- logger.debug("CouchBum: Executing #{method} #{url}")
39
42
  CouchRest.send(method, url, *args, **kwargs)
40
43
  rescue CouchRest::Exception => e
41
44
  logger.error("Failed to communicate with CouchDB: Status: #{e.http_code} - #{e.http_body}")
@@ -46,6 +49,8 @@ class CouchBum
46
49
 
47
50
  # Context under which the callback passed to binge_changes is executed.
48
51
  class BingeContext
52
+ attr_accessor :current_seq
53
+
49
54
  def initialize(changes)
50
55
  @changes = changes
51
56
  end
@@ -175,6 +175,12 @@ paths:
175
175
  description: 'Filter by sample status: ordered, drawn'
176
176
  schema:
177
177
  type: string
178
+ - name: end_date
179
+ in: query
180
+ required: false
181
+ description: Select all results before this date
182
+ schema:
183
+ type: date
178
184
  responses:
179
185
  '200':
180
186
  description: Success
@@ -396,6 +402,32 @@ paths:
396
402
  responses:
397
403
  '204':
398
404
  description: No Content
405
+ "/api/v1/lab/reasons_for_test":
406
+ get:
407
+ summary: Reasons for test
408
+ description: Retrieve default reasons for test concept set
409
+ tags:
410
+ - Concepts
411
+ security:
412
+ - api_key: []
413
+ responses:
414
+ '200':
415
+ description: Success
416
+ content:
417
+ application/json:
418
+ schema:
419
+ type: array
420
+ items:
421
+ type: object
422
+ properties:
423
+ concept_id:
424
+ type: integer
425
+ name:
426
+ type: string
427
+ example: Routine
428
+ required:
429
+ - concept_id
430
+ - name
399
431
  "/api/v1/lab/tests/{test_id}/results":
400
432
  post:
401
433
  summary: Add results to order
data/lib/lab/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lab
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.8'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: his_emr_api_lab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elizabeth Glaser Pediatric Foundation Malawi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-18 00:00:00.000000000 Z
11
+ date: 2021-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: couchrest