dde_client 0.1.0
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/README.md +37 -0
- data/Rakefile +12 -0
- data/app/assets/config/dde_client_manifest.js +1 -0
- data/app/assets/stylesheets/dde_client/application.css +15 -0
- data/app/controllers/dde_client/api/v1/dde_controller.rb +92 -0
- data/app/controllers/dde_client/api/v1/rollback_controller.rb +25 -0
- data/app/controllers/dde_client/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_client/dde_client.rb +162 -0
- data/app/services/dde_client/dde_service.rb +643 -0
- data/app/services/dde_client/matcher.rb +92 -0
- data/app/services/dde_client/merging_service.rb +769 -0
- data/app/services/dde_client/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/client_error.rb +3 -0
- data/lib/dde_client/engine.rb +5 -0
- data/lib/dde_client/version.rb +5 -0
- data/lib/dde_client.rb +9 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7996106a9df34e06acb594f8531a53c40d1e014fca7add081c4c4d545aea11e0
|
4
|
+
data.tar.gz: b5d3af559aa340dda33bd32266fc45e67fa7fc6d10dde5b841f8881b11782ad3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1f147e53a332201bbae823f6c2114002054932a743ed6c5b0415272f76934ededfc6824401b1158e34e8fc0f27fb2f706a24b7f89572521ea4ad86d9f6ae95ac
|
7
|
+
data.tar.gz: '009b7ef7c3ac8327563b2f09b460fe946f5170295e030b921970ac14cc3262d7e860e7a3aee6467e8aa14acb3e0531948f92bd3f94dc5c54f5e4bd192c1ed53d'
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# DdeClient
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/dde_client`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Install the gem and add to the application's Gemfile by executing:
|
10
|
+
|
11
|
+
$ bundle add dde_client
|
12
|
+
|
13
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
14
|
+
|
15
|
+
$ gem install dde_client
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
TODO: Write usage instructions here
|
20
|
+
|
21
|
+
## Development
|
22
|
+
|
23
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
24
|
+
|
25
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
26
|
+
|
27
|
+
## Contributing
|
28
|
+
|
29
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dde_client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/dde_client/blob/main/CODE_OF_CONDUCT.md).
|
30
|
+
|
31
|
+
## License
|
32
|
+
|
33
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
34
|
+
|
35
|
+
## Code of Conduct
|
36
|
+
|
37
|
+
Everyone interacting in the DdeClient project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/dde_client/blob/main/CODE_OF_CONDUCT.md).
|
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 DdeClient::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
|
+
DdeClient::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 DdeClient::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
|