his_emr_api_lab 0.0.2
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 +52 -0
- data/Rakefile +32 -0
- data/app/controllers/lab/application_controller.rb +6 -0
- data/app/controllers/lab/orders_controller.rb +34 -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 +25 -0
- data/app/jobs/lab/application_job.rb +4 -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 +47 -0
- data/app/models/lab/lab_result.rb +21 -0
- data/app/models/lab/lab_test.rb +14 -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 +49 -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/lims/api.rb +46 -0
- data/app/services/lab/lims/config.rb +56 -0
- data/app/services/lab/lims/order_dto.rb +177 -0
- data/app/services/lab/lims/order_serializer.rb +112 -0
- data/app/services/lab/lims/utils.rb +27 -0
- data/app/services/lab/lims/worker.rb +121 -0
- data/app/services/lab/metadata.rb +23 -0
- data/app/services/lab/orders_search_service.rb +48 -0
- data/app/services/lab/orders_service.rb +194 -0
- data/app/services/lab/results_service.rb +92 -0
- data/app/services/lab/tests_service.rb +93 -0
- data/config/routes.rb +15 -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/lib/couch_bum/couch_bum.rb +77 -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 +682 -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 +32 -0
- data/lib/tasks/lab_tasks.rake +25 -0
- data/lib/tasks/loaders/data/reasons-for-test.csv +6 -0
- data/lib/tasks/loaders/data/test-measures.csv +224 -0
- data/lib/tasks/loaders/data/tests.csv +142 -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 +296 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8cc795d01dc073e4556a1070e8e14199e49adff9419dfe02837008adb1aa6556
|
4
|
+
data.tar.gz: 9a61181925a0df93287b43446f0ace3b552972859f5f61248f7b8fb0fb085f56
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4a571f53b3c2216c9bdb60bfc98f8b02ecef5053d72ac91d9a4680d8adb893a4085cd4b91c9fb873552f4e0e84b7e6990321140e0fe9795e1c64bf45c2bf5fb2
|
7
|
+
data.tar.gz: e169300a7f5e7b4c3755107895522275eaa3b71d17495cd14cac3cd5227d6b1f2529f8a27695f5301cbd7d92f20917c9e37dbd66be0babf49cdd98c3b3be3674
|
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,52 @@
|
|
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
|
+
## Contributing
|
47
|
+
|
48
|
+
Contribution directions go here.
|
49
|
+
|
50
|
+
## License
|
51
|
+
|
52
|
+
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,34 @@
|
|
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
|
+
render json: orders, status: :created
|
12
|
+
end
|
13
|
+
|
14
|
+
def update
|
15
|
+
specimen = params.require(:specimen).permit(:concept_id)
|
16
|
+
|
17
|
+
order = OrdersService.update_order(params[:id], specimen: specimen)
|
18
|
+
|
19
|
+
render json: order
|
20
|
+
end
|
21
|
+
|
22
|
+
def index
|
23
|
+
filters = params.permit(%i[patient_id accession_number date status])
|
24
|
+
|
25
|
+
render json: OrdersSearchService.find_orders(filters)
|
26
|
+
end
|
27
|
+
|
28
|
+
def destroy
|
29
|
+
OrdersService.void_order(params[:id], params[:reason])
|
30
|
+
|
31
|
+
render status: :no_content
|
32
|
+
end
|
33
|
+
end
|
34
|
+
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)
|
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,25 @@
|
|
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(Order.find(order_id), date, test_concepts)
|
18
|
+
|
19
|
+
render json: tests, status: :created
|
20
|
+
end
|
21
|
+
|
22
|
+
def service
|
23
|
+
Lab::TestsService
|
24
|
+
end
|
25
|
+
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,47 @@
|
|
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
|
+
default_scope { joins(:order_type).merge(OrderType.where(name: Lab::Metadata::ORDER_TYPE_NAME)) }
|
39
|
+
|
40
|
+
def self.prefetch_relationships
|
41
|
+
includes(:reason_for_test,
|
42
|
+
:requesting_clinician,
|
43
|
+
:target_lab,
|
44
|
+
tests: [:result])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,21 @@
|
|
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
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
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
|
+
end
|
14
|
+
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,49 @@
|
|
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.tests
|
7
|
+
requesting_clinician ||= order.requesting_clinician
|
8
|
+
reason_for_test ||= order.reason_for_test
|
9
|
+
target_lab ||= order.target_lab
|
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&.value_text,
|
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
|
+
end
|
49
|
+
end
|