his_emr_api_lab 1.1.22 → 1.1.23
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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 +38 -0
- data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
- data/app/controllers/lab/results_controller.rb +19 -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 +26 -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_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/serializers/lab/lab_order_serializer.rb +55 -0
- data/app/serializers/lab/result_serializer.rb +36 -0
- data/app/serializers/lab/test_serializer.rb +29 -0
- data/app/services/lab/accession_number_service.rb +77 -0
- data/app/services/lab/concepts_service.rb +82 -0
- data/app/services/lab/labelling_service/order_label.rb +106 -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 +416 -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 +100 -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 +244 -0
- data/app/services/lab/lims/pull_worker.rb +289 -0
- data/app/services/lab/lims/push_worker.rb +149 -0
- data/app/services/lab/lims/utils.rb +91 -0
- data/app/services/lab/lims/worker.rb +86 -0
- data/app/services/lab/metadata.rb +24 -0
- data/app/services/lab/orders_search_service.rb +66 -0
- data/app/services/lab/orders_service.rb +212 -0
- data/app/services/lab/results_service.rb +149 -0
- data/app/services/lab/tests_service.rb +93 -0
- data/config/routes.rb +17 -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/his_emr_api_lab.rb +5 -0
- data/lib/lab/engine.rb +15 -0
- data/lib/lab/version.rb +5 -0
- data/lib/logger_multiplexor.rb +38 -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 +81 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 629116134d63ff0d2d3da97b80b0b9f4a3249483e5d57f43d4f332a645ce35e5
|
4
|
+
data.tar.gz: 414d3a1a4647e666599f71fca7e9026a034f210fbf6080da5cc501bb47c2e24f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5bf3f01293464ab6f0be71a8707ab6b57801a371375f1bd6a2fae9812da72d4cba323a5b783e68f9b770db0f6e21c9a39162d89d3004add6b6a8327f7033cf1
|
7
|
+
data.tar.gz: 6c09429e8520fc49459c40737214e1c538079cdacf2b715a9dc856d32bf2b562a8e68a4d5ad9f97bfbe3ead532484f510cc944986475978090755787d118c6f2
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2020 Walter Kaunda
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
![Ruby](https://github.com/EGPAFMalawiHIS/HIS-EMR-API-Lab/workflows/Ruby/badge.svg?branch=development)
|
2
|
+
# Lab
|
3
|
+
|
4
|
+
A Rails engine for [HIS-EMR-API](https://github.com/EGPAFMalawiHIS/HIS-EMR-API)
|
5
|
+
compatible applications that provides an API for managing various lab
|
6
|
+
activities.
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
The engine provides an API that provides the following functionalities:
|
11
|
+
|
12
|
+
- Search/Retrieve test types
|
13
|
+
- Search/Retrieve sample types for a given test
|
14
|
+
- Order lab tests
|
15
|
+
- Attach a result to a test
|
16
|
+
|
17
|
+
For details on how to perform these operations please see the
|
18
|
+
[API docs](https://raw.githack.com/EGPAFMalawiHIS/HIS-EMR-API-Lab/development/docs/api.html).
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'lab', git: 'https://github.com/EGPAFMalawiHIS/HIS-EMR-API-Lab', branch: 'development'
|
26
|
+
```
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
$ bundle install lab
|
32
|
+
```
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
```bash
|
37
|
+
$ gem install lab
|
38
|
+
```
|
39
|
+
|
40
|
+
Finally run:
|
41
|
+
|
42
|
+
```bash
|
43
|
+
$ bundle exec rails lab:install
|
44
|
+
```
|
45
|
+
|
46
|
+
## Configuration
|
47
|
+
|
48
|
+
This module in most cases should work without any configuration, however to enable
|
49
|
+
certain features some configuration may be required. Visit the
|
50
|
+
[configuration](./docs/configuration.md) page to learn how to configure the
|
51
|
+
application.
|
52
|
+
|
53
|
+
## Contributing
|
54
|
+
|
55
|
+
Fork this application create a branch for the contribution you want to make,
|
56
|
+
push your changes to the branch and then issue a pull request. You may want
|
57
|
+
to create a new first on our repository, so that your pull request references
|
58
|
+
this issue.
|
59
|
+
|
60
|
+
If you are fixing a bug, it will be nice to add a unit test that exposes
|
61
|
+
the bug. Although this is not a requirement in most cases.
|
62
|
+
|
63
|
+
Be sure to follow [this](https://github.com/rubocop/ruby-style-guide) Ruby
|
64
|
+
style guide. We don't necessarily look for strict adherence to the guidelines
|
65
|
+
but too much a departure from it is frowned upon. For example, you will be forgiven
|
66
|
+
for writing a method with 15 to 20 lines if you clearly justify why you couldn't
|
67
|
+
break that method into multiple smaller methods.
|
68
|
+
|
69
|
+
## License
|
70
|
+
|
71
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Lab'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'spec'
|
28
|
+
t.pattern = 'spec/**/*_spec.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class LabelsController < ApplicationController
|
5
|
+
skip_before_action :authenticate
|
6
|
+
|
7
|
+
def print_order_label
|
8
|
+
order_id = params.require(:order_id)
|
9
|
+
|
10
|
+
label = LabellingService::OrderLabel.new(order_id)
|
11
|
+
send_data(label.print, type: 'application/label; charset=utf-8',
|
12
|
+
stream: false,
|
13
|
+
filename: "#{SecureRandom.hex(24)}.lbl",
|
14
|
+
disposition: 'inline')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class OrdersController < ApplicationController
|
5
|
+
def create
|
6
|
+
order_params_list = params.require(:orders)
|
7
|
+
orders = order_params_list.map do |order_params|
|
8
|
+
OrdersService.order_test(order_params)
|
9
|
+
end
|
10
|
+
|
11
|
+
orders.each { |order| Lab::PushOrderJob.perform_later(order.fetch(:order_id)) }
|
12
|
+
|
13
|
+
render json: orders, status: :created
|
14
|
+
end
|
15
|
+
|
16
|
+
def update
|
17
|
+
specimen = params.require(:specimen).permit(:concept_id)
|
18
|
+
order = OrdersService.update_order(params[:id], specimen: specimen, force_update: params[:force_update])
|
19
|
+
Lab::PushOrderJob.perform_later(order.fetch(:order_id))
|
20
|
+
|
21
|
+
render json: order
|
22
|
+
end
|
23
|
+
|
24
|
+
def index
|
25
|
+
filters = params.permit(%i[patient_id accession_number date status])
|
26
|
+
|
27
|
+
Lab::UpdatePatientOrdersJob.perform_later(filters[:patient_id]) if filters[:patient_id]
|
28
|
+
render json: OrdersSearchService.find_orders(filters)
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy
|
32
|
+
OrdersService.void_order(params[:id], params[:reason])
|
33
|
+
Lab::VoidOrderJob.perform_later(params[:id])
|
34
|
+
|
35
|
+
render status: :no_content
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class ResultsController < ApplicationController
|
5
|
+
def create
|
6
|
+
result_params = params.require(:result)
|
7
|
+
.permit([:encounter_id,
|
8
|
+
:date,
|
9
|
+
{ measures: [:value,
|
10
|
+
:value_type,
|
11
|
+
:value_modifier,
|
12
|
+
{ indicator: [:concept_id] }] }])
|
13
|
+
|
14
|
+
result = Lab::ResultsService.create_results(params[:test_id], result_params, 'user entered')
|
15
|
+
|
16
|
+
render json: result, status: :created
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class SpecimenTypesController < ApplicationController
|
5
|
+
def index
|
6
|
+
filters = params.permit(%w[name test_type])
|
7
|
+
|
8
|
+
specimen_types = ConceptsService.specimen_types(name: filters['name'],
|
9
|
+
test_type: filters['test_type'])
|
10
|
+
.as_json(only: %w[concept_id name])
|
11
|
+
|
12
|
+
render json: specimen_types
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class TestTypesController < ApplicationController
|
5
|
+
def index
|
6
|
+
filters = params.permit(%w[name specimen_type])
|
7
|
+
|
8
|
+
test_types = ConceptsService.test_types(name: filters['name'],
|
9
|
+
specimen_type: filters['specimen_type'])
|
10
|
+
.as_json(only: %w[concept_id name])
|
11
|
+
|
12
|
+
render json: test_types
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Lab::TestsController < ::ApplicationController
|
4
|
+
def index
|
5
|
+
filters = params.permit(%i[order_date accession_number patient_id test_type_id specimen_type_id pending_results])
|
6
|
+
|
7
|
+
tests = service.find_tests(filters)
|
8
|
+
render json: tests
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a specimen to an existing order
|
12
|
+
def create
|
13
|
+
test_params = params.permit(:order_id, :date, tests: [:concept_id])
|
14
|
+
order_id, test_concepts = test_params.require(%i[order_id tests])
|
15
|
+
date = test_params[:date] || Date.today
|
16
|
+
|
17
|
+
tests = service.create_tests(Lab::LabOrder.find(order_id), date, test_concepts)
|
18
|
+
Lab::PushOrderJob.perform_later(order_id)
|
19
|
+
|
20
|
+
render json: tests, status: :created
|
21
|
+
end
|
22
|
+
|
23
|
+
def service
|
24
|
+
Lab::TestsService
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
##
|
5
|
+
# Push an order to LIMS.
|
6
|
+
class PushOrderJob < ApplicationJob
|
7
|
+
def perform(order_id)
|
8
|
+
push_worker = Lab::Lims::PushWorker.new(Lab::Lims::ApiFactory.create_api)
|
9
|
+
push_worker.push_order_by_id(order_id)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
##
|
5
|
+
# Fetches updates on a patient's orders from external sources.
|
6
|
+
class UpdatePatientOrdersJob < ApplicationJob
|
7
|
+
queue_as :default
|
8
|
+
|
9
|
+
def perform(patient_id)
|
10
|
+
Rails.logger.info('Initialising LIMS REST API...')
|
11
|
+
|
12
|
+
User.current = Lab::Lims::Utils.lab_user
|
13
|
+
Location.current = Location.find_by_name('ART clinic')
|
14
|
+
|
15
|
+
lockfile = Rails.root.join('tmp', "update-patient-orders-#{patient_id}.lock")
|
16
|
+
|
17
|
+
done = File.open(lockfile, File::RDWR | File::CREAT) do |lock|
|
18
|
+
unless lock.flock(File::LOCK_NB | File::LOCK_EX)
|
19
|
+
Rails.logger.info('Another update patient job is already running...')
|
20
|
+
break false
|
21
|
+
end
|
22
|
+
|
23
|
+
worker = Lab::Lims::PullWorker.new(Lab::Lims::ApiFactory.create_api)
|
24
|
+
worker.pull_orders(patient_id: patient_id)
|
25
|
+
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
File.unlink(lockfile) if done
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class VoidOrderJob < ApplicationJob
|
5
|
+
queue_as :default
|
6
|
+
|
7
|
+
def perform(order_id)
|
8
|
+
Rails.logger.info("Voiding order ##{order_id} in LIMS")
|
9
|
+
|
10
|
+
User.current = Lab::Lims::Utils.lab_user
|
11
|
+
Location.current = Location.find_by_name('ART clinic')
|
12
|
+
|
13
|
+
worker = Lab::Lims::PushWorker.new(Lab::Lims::ApiFactory.create_api)
|
14
|
+
worker.push_order(Lab::LabOrder.unscoped.find(order_id))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
# Used to keep track of counters for accession numbers.
|
5
|
+
#
|
6
|
+
# An accession number is just a prefix plus a running encounter
|
7
|
+
# that's reset at the end of the day
|
8
|
+
class LabAccessionNumberCounter < ApplicationRecord
|
9
|
+
self.table_name = :lab_accession_number_counters
|
10
|
+
|
11
|
+
validates_presence_of :date, :value
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class LabOrder < ::Order
|
5
|
+
class << self
|
6
|
+
def make_obs_concept_filter(concept_name)
|
7
|
+
concept = ConceptName.where(name: concept_name).select(:concept_id)
|
8
|
+
|
9
|
+
-> { where(concept: concept) }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
has_many :tests,
|
14
|
+
make_obs_concept_filter(Lab::Metadata::TEST_TYPE_CONCEPT_NAME),
|
15
|
+
class_name: '::Lab::LabTest',
|
16
|
+
foreign_key: :order_id
|
17
|
+
|
18
|
+
has_many :results,
|
19
|
+
make_obs_concept_filter(Lab::Metadata::TEST_RESULT_CONCEPT_NAME),
|
20
|
+
class_name: 'Observation',
|
21
|
+
foreign_key: :order_id
|
22
|
+
|
23
|
+
has_one :reason_for_test,
|
24
|
+
make_obs_concept_filter(Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME),
|
25
|
+
class_name: 'Observation',
|
26
|
+
foreign_key: :order_id
|
27
|
+
|
28
|
+
has_one :requesting_clinician,
|
29
|
+
make_obs_concept_filter(Lab::Metadata::REQUESTING_CLINICIAN_CONCEPT_NAME),
|
30
|
+
class_name: 'Observation',
|
31
|
+
foreign_key: :order_id
|
32
|
+
|
33
|
+
has_one :target_lab,
|
34
|
+
make_obs_concept_filter(Lab::Metadata::TARGET_LAB_CONCEPT_NAME),
|
35
|
+
class_name: 'Observation',
|
36
|
+
foreign_key: :order_id
|
37
|
+
|
38
|
+
has_one :mapping,
|
39
|
+
class_name: '::Lab::LimsOrderMapping',
|
40
|
+
foreign_key: :order_id
|
41
|
+
|
42
|
+
default_scope do
|
43
|
+
joins(:order_type)
|
44
|
+
.merge(OrderType.where(name: Lab::Metadata::ORDER_TYPE_NAME))
|
45
|
+
.where.not(concept_id: ConceptName.where(name: 'Tests ordered').select(:concept_id))
|
46
|
+
end
|
47
|
+
|
48
|
+
scope :drawn, -> { where.not(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
|
49
|
+
scope :not_drawn, -> { where(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
|
50
|
+
|
51
|
+
def self.prefetch_relationships
|
52
|
+
includes(:reason_for_test,
|
53
|
+
:requesting_clinician,
|
54
|
+
:target_lab,
|
55
|
+
tests: [:result])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class LabResult < Observation
|
5
|
+
alias measures children
|
6
|
+
|
7
|
+
default_scope do
|
8
|
+
lab_test_concept = ConceptName.where(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)
|
9
|
+
.select(:concept_id)
|
10
|
+
where(voided: 0, concept: lab_test_concept)
|
11
|
+
end
|
12
|
+
|
13
|
+
belongs_to :test,
|
14
|
+
(lambda do
|
15
|
+
where(concept: ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
|
16
|
+
.select(:concept_id))
|
17
|
+
end),
|
18
|
+
class_name: 'Observation',
|
19
|
+
foreign_key: :obs_group_id
|
20
|
+
|
21
|
+
def void(reason)
|
22
|
+
children.each do |measure|
|
23
|
+
# Need to have a LabResultMeasure model that privately handles it's children
|
24
|
+
measure.children.each { |provider| provider.void(reason) }
|
25
|
+
measure.void(reason)
|
26
|
+
end
|
27
|
+
|
28
|
+
super(reason)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class LabTest < ::Observation
|
5
|
+
default_scope do
|
6
|
+
where(concept: ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME))
|
7
|
+
end
|
8
|
+
|
9
|
+
has_one :result,
|
10
|
+
-> { where(concept: ConceptName.where(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)) },
|
11
|
+
class_name: '::Lab::LabResult',
|
12
|
+
foreign_key: :obs_group_id
|
13
|
+
|
14
|
+
def void(reason)
|
15
|
+
result&.void(reason)
|
16
|
+
super(reason)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class LimsOrderMapping < ApplicationRecord
|
5
|
+
belongs_to :order, class_name: 'LabOrder', foreign_key: 'order_id'
|
6
|
+
|
7
|
+
validates_presence_of :lims_id, :order_id
|
8
|
+
validates_uniqueness_of :lims_id, :order_id
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
module LabOrderSerializer
|
5
|
+
def self.serialize_order(order, tests: nil, requesting_clinician: nil, reason_for_test: nil, target_lab: nil)
|
6
|
+
tests ||= order.voided == 1 ? voided_tests(order) : order.tests
|
7
|
+
requesting_clinician ||= order.requesting_clinician
|
8
|
+
reason_for_test ||= order.reason_for_test
|
9
|
+
target_lab = target_lab&.value_text || order.target_lab&.value_text || Location.current_health_center&.name
|
10
|
+
|
11
|
+
ActiveSupport::HashWithIndifferentAccess.new(
|
12
|
+
{
|
13
|
+
id: order.order_id,
|
14
|
+
order_id: order.order_id, # Deprecated: Link to :id
|
15
|
+
encounter_id: order.encounter_id,
|
16
|
+
order_date: order.start_date,
|
17
|
+
patient_id: order.patient_id,
|
18
|
+
accession_number: order.accession_number,
|
19
|
+
specimen: {
|
20
|
+
concept_id: order.concept_id,
|
21
|
+
name: concept_name(order.concept_id)
|
22
|
+
},
|
23
|
+
requesting_clinician: requesting_clinician&.value_text,
|
24
|
+
target_lab: target_lab,
|
25
|
+
reason_for_test: {
|
26
|
+
concept_id: reason_for_test&.value_coded,
|
27
|
+
name: concept_name(reason_for_test&.value_coded)
|
28
|
+
},
|
29
|
+
tests: tests.map do |test|
|
30
|
+
result_obs = test.children.first
|
31
|
+
|
32
|
+
{
|
33
|
+
id: test.obs_id,
|
34
|
+
concept_id: test.value_coded,
|
35
|
+
name: concept_name(test.value_coded),
|
36
|
+
result: result_obs && ResultSerializer.serialize(result_obs)
|
37
|
+
}
|
38
|
+
end
|
39
|
+
}
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.concept_name(concept_id)
|
44
|
+
return concept_id unless concept_id
|
45
|
+
|
46
|
+
ConceptName.select(:name).find_by_concept_id(concept_id)&.name
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.voided_tests(order)
|
50
|
+
concept = ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
|
51
|
+
.select(:concept_id)
|
52
|
+
LabTest.unscoped.where(concept: concept, order: order, voided: true)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
##
|
5
|
+
# Serialize a Lab order result
|
6
|
+
module ResultSerializer
|
7
|
+
def self.serialize(result)
|
8
|
+
result.children.map do |measure|
|
9
|
+
value, value_type = read_value(measure)
|
10
|
+
concept_name = ConceptName.find_by_concept_id(measure.concept_id)
|
11
|
+
|
12
|
+
{
|
13
|
+
id: measure.obs_id,
|
14
|
+
indicator: {
|
15
|
+
concept_id: concept_name&.concept_id,
|
16
|
+
name: concept_name&.name
|
17
|
+
},
|
18
|
+
date: measure.obs_datetime,
|
19
|
+
value: value,
|
20
|
+
value_type: value_type,
|
21
|
+
value_modifier: measure.value_modifier
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.read_value(measure)
|
27
|
+
%w[value_numeric value_coded value_boolean value_text].each do |field|
|
28
|
+
value = measure.send(field)
|
29
|
+
|
30
|
+
return [value, field.split('_')[1]] if value
|
31
|
+
end
|
32
|
+
|
33
|
+
[nil, 'unknown']
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
module TestSerializer
|
5
|
+
def self.serialize(test, order: nil, result: nil)
|
6
|
+
order ||= test.order
|
7
|
+
result ||= test.result
|
8
|
+
|
9
|
+
{
|
10
|
+
id: test.obs_id,
|
11
|
+
concept_id: test.value_coded,
|
12
|
+
name: ConceptName.find_by_concept_id(test.value_coded)&.name,
|
13
|
+
order: {
|
14
|
+
id: order.order_id,
|
15
|
+
concept_id: order.concept_id,
|
16
|
+
name: ConceptName.find_by_concept_id(order.concept_id)&.name,
|
17
|
+
accession_number: order.accession_number
|
18
|
+
},
|
19
|
+
result: if result
|
20
|
+
{
|
21
|
+
id: result.obs_id,
|
22
|
+
modifier: result.value_modifier,
|
23
|
+
value: result.value_text
|
24
|
+
}
|
25
|
+
end
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|