dde_client 0.1.0

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