mahis-dde 0.1.4
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 +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
|