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 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -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,4 @@
1
+ module Dde
2
+ class ApplicationController < ActionController::API
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Dde
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Dde
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Dde
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Dde
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ 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