mahis-dde 0.1.4
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +20 -0
- data/Rakefile +8 -0
- data/app/assets/config/dde_manifest.js +1 -0
- data/app/assets/stylesheets/dde/application.css +15 -0
- data/app/controllers/dde/api/v1/dde_controller.rb +92 -0
- data/app/controllers/dde/api/v1/rollback_controller.rb +25 -0
- data/app/controllers/dde/application_controller.rb +4 -0
- data/app/helpers/dde/application_helper.rb +4 -0
- data/app/jobs/dde/application_job.rb +4 -0
- data/app/mailers/dde/application_mailer.rb +6 -0
- data/app/models/dde/application_record.rb +5 -0
- data/app/services/dde/dde_client.rb +162 -0
- data/app/services/dde/dde_service.rb +643 -0
- data/app/services/dde/matcher.rb +92 -0
- data/app/services/dde/merging_service.rb +769 -0
- data/app/services/dde/rollback_service.rb +320 -0
- data/app/services/merge_audit_service.rb +56 -0
- data/app/utils/model_utils.rb +62 -0
- data/app/views/layouts/dde/application.html.erb +15 -0
- data/config/routes.rb +14 -0
- data/lib/dde/client_error.rb +3 -0
- data/lib/dde/engine.rb +5 -0
- data/lib/dde/version.rb +3 -0
- data/lib/dde.rb +6 -0
- data/lib/tasks/dde_tasks.rake +4 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7505492de7f9f3257c9b711cf894ec77cf708fee5562d060c415e341caf2ad65
|
4
|
+
data.tar.gz: 322ae74b18e2f6e1c73b48a0c5bed16374c945172370388e982b8c327c621a21
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4896294257cd71d17c9a60841643dce73f2c7a72bd7f882984ba6db1687399f1141cfacd98aa7c78fc1f56c5a97b07614769755ec3106cdf1748583f15b4516a
|
7
|
+
data.tar.gz: 6b079c31529e9cec84a2e90a5fda87e1756c642a0c5f9568ff9401ca96b91d97ea1352046f7848e1d5300115dbd0c86bf1a423b639ec4d94b33a55c172150710
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2024 bryan
|
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,20 @@
|
|
1
|
+
# Dde
|
2
|
+
DDE stands for Demographics Data Exchange. Its main purpose is to manage patient IDs
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'dde', git: 'https://github.com/Malawi-Ministry-of-Health/dde-client', branch: 'main'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
## License
|
20
|
+
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 @@
|
|
1
|
+
//= link_directory ../stylesheets/dde .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class Dde::Api::V1::DdeController < ApplicationController
|
2
|
+
# GET /dde/patients
|
3
|
+
def find_patients_by_npid
|
4
|
+
npid = params.require(:npid)
|
5
|
+
render json: service.find_patients_by_npid(npid)
|
6
|
+
end
|
7
|
+
|
8
|
+
def find_patients_by_name_and_gender
|
9
|
+
given_name, family_name, gender = params.require(%i[given_name family_name gender])
|
10
|
+
render json: service.find_patients_by_name_and_gender(given_name, family_name, gender)
|
11
|
+
end
|
12
|
+
|
13
|
+
def import_patients_by_npid
|
14
|
+
npid = params.require(:npid)
|
15
|
+
render json: service.import_patients_by_npid(npid)
|
16
|
+
end
|
17
|
+
|
18
|
+
def import_patients_by_doc_id
|
19
|
+
doc_id = params.require(:doc_id)
|
20
|
+
render json: service.import_patients_by_doc_id(doc_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
def remaining_npids
|
24
|
+
render json: service.remaining_npids
|
25
|
+
end
|
26
|
+
|
27
|
+
# GET /api/v1/dde/match
|
28
|
+
#
|
29
|
+
# Returns Dde patients matching demographics passed
|
30
|
+
def match_patients_by_demographics
|
31
|
+
render json: service.match_patients_by_demographics(
|
32
|
+
given_name: match_params[:given_name],
|
33
|
+
family_name: match_params[:family_name],
|
34
|
+
gender: match_params[:gender],
|
35
|
+
birthdate: match_params[:birthdate],
|
36
|
+
home_traditional_authority: match_params[:home_traditional_authority],
|
37
|
+
home_district: match_params[:home_district],
|
38
|
+
home_village: match_params[:home_village]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def reassign_patient_npid
|
43
|
+
patient_ids = params.permit(:doc_id, :patient_id)
|
44
|
+
render json: service.reassign_patient_npid(patient_ids)
|
45
|
+
end
|
46
|
+
|
47
|
+
def merge_patients
|
48
|
+
primary_patient_ids = params.require(:primary)
|
49
|
+
secondary_patient_ids_list = params.require(:secondary)
|
50
|
+
|
51
|
+
render json: service.merge_patients(primary_patient_ids, secondary_patient_ids_list)
|
52
|
+
end
|
53
|
+
|
54
|
+
def patient_diff
|
55
|
+
patient_id = params.require(:patient_id)
|
56
|
+
diff = service.find_patient_updates(patient_id)
|
57
|
+
|
58
|
+
render json: { diff: diff }
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Updates local patient with demographics in Dde.
|
63
|
+
def refresh_patient
|
64
|
+
patient_id = params.require(:patient_id)
|
65
|
+
update_npid = params[:update_npid]&.casecmp?('true') || false
|
66
|
+
|
67
|
+
patient = service.update_local_patient(Patient.find(patient_id), update_npid: update_npid)
|
68
|
+
|
69
|
+
render json: patient
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
MATCH_PARAMS = %i[given_name family_name gender birthdate home_village
|
75
|
+
home_traditional_authority home_district].freeze
|
76
|
+
|
77
|
+
def match_params
|
78
|
+
MATCH_PARAMS.each_with_object({}) do |param, params_hash|
|
79
|
+
raise "param #{param} is required" if params[param].blank?
|
80
|
+
|
81
|
+
params_hash[param] = params[param]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def service
|
86
|
+
Dde::DdeService.new(visit_type: visit_type)
|
87
|
+
end
|
88
|
+
|
89
|
+
def visit_type
|
90
|
+
Program.find(params.require(:visit_type_id))
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# controller for managing merge rollback
|
4
|
+
class Api::V1::RollbackController < ApplicationController
|
5
|
+
def merge_history
|
6
|
+
identifier = params.require(:identifier)
|
7
|
+
render json: merge_service.get_patient_audit(identifier), status: :ok
|
8
|
+
end
|
9
|
+
|
10
|
+
def rollback_patient
|
11
|
+
patient_id = params.require(:patient_id)
|
12
|
+
visit_type_id = params.require(:visit_type_id)
|
13
|
+
render json: rollback_service.rollback_merged_patient(patient_id, visit_type_id), status: :ok
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def merge_service
|
19
|
+
MergeAuditService.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def rollback_service
|
23
|
+
RollbackService.new
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'restclient'
|
5
|
+
|
6
|
+
class Dde::DdeClient
|
7
|
+
def initialize
|
8
|
+
@auto_login = true # If logged out, automatically login on next request
|
9
|
+
@base_url = nil
|
10
|
+
@connection = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# Connect to Dde Web Service using either a configuration file
|
14
|
+
# or an old Connection.
|
15
|
+
#
|
16
|
+
# @return A Connection object that can be used to re-connect to Dde
|
17
|
+
def connect(url:, username:, password:)
|
18
|
+
@connection = establish_connection(url: url, username: username, password: password)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Reconnect to Dde using previous connection
|
22
|
+
#
|
23
|
+
# @see: DdeClient#connect
|
24
|
+
def restore_connection(connection)
|
25
|
+
@connection = reload_connection(connection)
|
26
|
+
end
|
27
|
+
|
28
|
+
def get(resource)
|
29
|
+
exec_request resource do |url, headers|
|
30
|
+
RestClient.get url, headers
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def post(resource, data)
|
35
|
+
exec_request resource do |url, headers|
|
36
|
+
RestClient.post url, data.to_json, headers
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def put(resource, data)
|
41
|
+
exec_request resource do |url, headers|
|
42
|
+
RestClient.put url, data.to_json, headers
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(resource)
|
47
|
+
exec_request resource do |url, headers|
|
48
|
+
RestClient.delete url, headers
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
JSON_CONTENT_TYPE = 'application/json'
|
55
|
+
LOGGER = Logger.new(STDOUT)
|
56
|
+
Dde_API_KEY_VALIDITY_PERIOD = 3600 * 12
|
57
|
+
Dde_VERSION = 'v1'
|
58
|
+
|
59
|
+
# Reload old connection to Dde
|
60
|
+
def reload_connection(connection)
|
61
|
+
LOGGER.debug 'Loading Dde connection'
|
62
|
+
if connection[:expires] < Time.now
|
63
|
+
LOGGER.debug 'Dde connection expired'
|
64
|
+
establish_connection(connection[:config])
|
65
|
+
else
|
66
|
+
@base_url = connection[:config][:url]
|
67
|
+
connection
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Establish a connection to Dde
|
72
|
+
#
|
73
|
+
# NOTE: This simply involves logging into Dde
|
74
|
+
def establish_connection(url:, username:, password:)
|
75
|
+
LOGGER.debug 'Establishing new connection to Dde from configuration'
|
76
|
+
|
77
|
+
# Block any automatic logins when processing request to avoid infinite loop
|
78
|
+
# in request execution below... Under normal circumstances request execution
|
79
|
+
# will attempt a login if 401 is met. Not pretty, I know but it does the job
|
80
|
+
# for now!!!
|
81
|
+
@auto_login = false
|
82
|
+
|
83
|
+
# HACK: Globally save base_url as a connection object may not currently
|
84
|
+
# be available to the build_url method right now
|
85
|
+
@base_url = url
|
86
|
+
|
87
|
+
response, status = post('login', username: username, password: password)
|
88
|
+
|
89
|
+
@auto_login = true
|
90
|
+
|
91
|
+
if status != 200
|
92
|
+
raise StandardError, "Unable to establish connection to Dde: #{response}"
|
93
|
+
end
|
94
|
+
|
95
|
+
LOGGER.info('Connection to Dde established :)')
|
96
|
+
@connection = {
|
97
|
+
key: response['access_token'],
|
98
|
+
expires: Time.now + Dde_API_KEY_VALIDITY_PERIOD,
|
99
|
+
config: { url: url, username: username, password: password }
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns a URI object with API host attached
|
104
|
+
def build_uri(resource)
|
105
|
+
"#{@base_url}/#{Dde_VERSION}/#{resource}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def headers
|
109
|
+
{
|
110
|
+
'Content-type' => JSON_CONTENT_TYPE,
|
111
|
+
'Authorization' => @connection ? @connection[:key] : nil
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def exec_request(resource)
|
116
|
+
LOGGER.debug "Executing Dde request (#{resource})"
|
117
|
+
response = yield build_uri(resource), headers
|
118
|
+
LOGGER.debug "Handling Dde response:\n\tStatus - #{response.code}\n\tBody - #{response.body}"
|
119
|
+
handle_response response
|
120
|
+
rescue RestClient::Unauthorized => e
|
121
|
+
LOGGER.error "DdeClient suppressed exception: #{e}"
|
122
|
+
return handle_response e.response unless @auto_login
|
123
|
+
|
124
|
+
LOGGER.debug 'Auto-logging into Dde...'
|
125
|
+
establish_connection(@connection[:config])
|
126
|
+
LOGGER.debug "Reset connection: #{@connection}"
|
127
|
+
retry # Retry last request...
|
128
|
+
rescue RestClient::BadRequest => e
|
129
|
+
LOGGER.error "DdeClient suppressed exception: #{e}"
|
130
|
+
handle_response e.response
|
131
|
+
rescue RestClient::UnprocessableEntity => e
|
132
|
+
LOGGER.error "DdeClient suppressed exception: #{e}"
|
133
|
+
handle_response e.response
|
134
|
+
rescue RestClient::NotFound => e
|
135
|
+
LOGGER.error "DdeClient suppressed exception: #{e}"
|
136
|
+
handle_response e.response
|
137
|
+
rescue RestClient::InternalServerError => e
|
138
|
+
LOGGER.error "DdeClient suppressed exceptionnnn: #{e}"
|
139
|
+
handle_response e.response
|
140
|
+
end
|
141
|
+
|
142
|
+
def handle_response(response)
|
143
|
+
# 204 is no content response, no further processing required.
|
144
|
+
return nil, 204 if response.code.to_i == 204
|
145
|
+
|
146
|
+
# NOTE: Following is commented out as Dde at the moment is quite liberal
|
147
|
+
# in how it responds to various requests. It seems to know no difference
|
148
|
+
# between 'application/json' and 'text/plain'.
|
149
|
+
#
|
150
|
+
# unless response["content-type"].include? JSON_CONTENT_TYPE
|
151
|
+
# puts "Invalid response from API: content-type: " + response["content-type"]
|
152
|
+
# return nil, 0
|
153
|
+
# end
|
154
|
+
|
155
|
+
# Dde is somewhat undecided on how it reports back its status code.
|
156
|
+
# Sometimes we get a proper HTTP status code and sometimes it is within
|
157
|
+
# the response body.
|
158
|
+
# response_status = response.code || response.body['status']
|
159
|
+
response_status = response.code
|
160
|
+
[JSON.parse(response.body), response_status&.to_i]
|
161
|
+
end
|
162
|
+
end
|