dde_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|