malawi_hiv_program_reports 1.0.1 → 1.0.2
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 +4 -4
- data/app/services/malawi_hiv_program_reports/README.md +16 -0
- data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
- data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
- data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
- data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
- data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
- data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
- data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
- data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
- data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
- data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
- data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
- data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
- data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
- data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
- data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
- data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
- data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
- data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
- data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
- data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
- data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
- data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
- data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
- data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
- data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2340 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
- data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
- data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
- data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
- data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
- data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
- data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
- data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
- data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +202 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
- data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
- data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
- data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
- data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
- data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
- data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
- data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
- data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
- data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
- data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
- data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
- data/lib/malawi_hiv_program_reports/version.rb +1 -1
- metadata +74 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MalawiHivProgramReports
|
|
4
|
+
module Pepfar
|
|
5
|
+
## Viral Load Coverage Report
|
|
6
|
+
# 1. given the start and end dates, this report will go back 12 months using the end date
|
|
7
|
+
# 2. pick all clients that are due in the mentioned period
|
|
8
|
+
# 3. the picked clients should also include those that are new on ART 6 months before the end date
|
|
9
|
+
# 4. for the sample drawns available pick the latest sample drawn within the reporting period
|
|
10
|
+
# 5. for the results pick the latest result within the reporting period
|
|
11
|
+
class ViralLoadCoverage2
|
|
12
|
+
|
|
13
|
+
include Utils
|
|
14
|
+
include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
|
|
15
|
+
include MalawiHivProgramReports::Adapters::Moh::Custom
|
|
16
|
+
include MalawiHivProgramReports::Utils::ModelUtils
|
|
17
|
+
attr_reader :start_date, :end_date, :location
|
|
18
|
+
|
|
19
|
+
def initialize(start_date:, end_date:, **kwargs)
|
|
20
|
+
@start_date = start_date&.to_date
|
|
21
|
+
raise InvalidParameterError, 'start_date is required' unless @start_date
|
|
22
|
+
|
|
23
|
+
@end_date = end_date&.to_date || @start_date + 12.months
|
|
24
|
+
raise InvalidParameterError, "start_date can't be greater than end_date" if @start_date > @end_date
|
|
25
|
+
|
|
26
|
+
@occupation = kwargs.delete(:occupation)
|
|
27
|
+
@type = kwargs.delete(:application)
|
|
28
|
+
@location = kwargs.delete(:location)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_report
|
|
32
|
+
report = init_report
|
|
33
|
+
build_report(report)
|
|
34
|
+
report
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def vl_maternal_status(patient_list)
|
|
38
|
+
return { FP: [], FBf: [] } if patient_list.blank?
|
|
39
|
+
|
|
40
|
+
pregnant = pregnant_women(patient_list).map { |woman| woman['person_id'].to_i }
|
|
41
|
+
return { FP: pregnant, FBf: [] } if (patient_list - pregnant).blank?
|
|
42
|
+
|
|
43
|
+
feeding = breast_feeding(patient_list - pregnant).map { |woman| woman['person_id'].to_i }
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
FP: pregnant,
|
|
47
|
+
FBf: feeding
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# rubocop:disable Metrics/AbcSize
|
|
52
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
53
|
+
# rubocop:disable Metrics/MethodLength
|
|
54
|
+
def process_due_people
|
|
55
|
+
@clients = []
|
|
56
|
+
start = Time.now
|
|
57
|
+
results = clients_on_art
|
|
58
|
+
# get all clients that are females from results
|
|
59
|
+
@maternal_status = vl_maternal_status(results.map do |patient|
|
|
60
|
+
patient['patient_id'] if patient['gender'] == 'F'
|
|
61
|
+
end.compact)
|
|
62
|
+
if @type.blank? || @type == 'poc'
|
|
63
|
+
Parallel.each(results, in_threads: 20) do |patient|
|
|
64
|
+
process_client_eligibility(patient)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
results.each { |patient| process_client_eligibility(patient) } if @type == 'emastercard'
|
|
68
|
+
end_time = Time.now
|
|
69
|
+
Rails.logger.info "Time taken to process #{results.length} clients: #{end_time - start} seconds.
|
|
70
|
+
These are the clients returned: #{@clients.length}"
|
|
71
|
+
@clients
|
|
72
|
+
end
|
|
73
|
+
# rubocop:enable Metrics/AbcSize
|
|
74
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
75
|
+
# rubocop:enable Metrics/MethodLength
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# rubocop:disable Metrics/AbcSize
|
|
80
|
+
# rubocop:disable Metrics/MethodLength
|
|
81
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
82
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
83
|
+
# rubocop:disable Layout/LineLength
|
|
84
|
+
def process_client_eligibility(patient)
|
|
85
|
+
result = extra_information(patient['patient_id'])
|
|
86
|
+
patient['defaulter_date'] = result['defaulter_date']
|
|
87
|
+
patient['current_regimen'] = result['current_regimen']
|
|
88
|
+
patient['art_start_date'] = result['art_start_date']
|
|
89
|
+
patient['maternal_status'] =
|
|
90
|
+
if @maternal_status[:FP].include?(patient['patient_id'])
|
|
91
|
+
'FP'
|
|
92
|
+
else
|
|
93
|
+
(@maternal_status[:FBf].include?(patient['patient_id']) ? 'FBf' : nil)
|
|
94
|
+
end
|
|
95
|
+
return if !patient['defaulter_date'].blank? && (patient['defaulter_date'].to_date < end_date - 12.months)
|
|
96
|
+
return if result['art_start_date'].blank?
|
|
97
|
+
return if result['art_start_date'].to_date > end_date - 6.months
|
|
98
|
+
return if remove_adverse_outcome_patient?(patient)
|
|
99
|
+
|
|
100
|
+
@clients << patient
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def remove_adverse_outcome_patient?(patient)
|
|
104
|
+
return false unless adverse_outcomes.include?(patient['state'].to_i)
|
|
105
|
+
|
|
106
|
+
last_date = patient['vl_order_date'] || patient['art_start_date']
|
|
107
|
+
if patient['vl_order_date'].present? && last_date.to_date >= start_date && last_date.to_date <= end_date
|
|
108
|
+
return false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
length = 12
|
|
112
|
+
length = 6 if patient['maternal_status'] == 'FP'
|
|
113
|
+
length = 6 if patient['maternal_status'] == 'FBf'
|
|
114
|
+
length = 6 if patient['current_regimen'].to_s.match(/P/i)
|
|
115
|
+
|
|
116
|
+
if patient['vl_order_date'] && patient['vl_order_date'].to_date >= end_date - 12.months && patient['vl_order_date'].to_date <= end_date
|
|
117
|
+
return false
|
|
118
|
+
end
|
|
119
|
+
return false if last_date.to_date + length.months < patient['outcome_date'].to_date
|
|
120
|
+
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pregnant_women(patient_list)
|
|
125
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
|
126
|
+
SELECT o.person_id, o.value_coded
|
|
127
|
+
FROM obs o
|
|
128
|
+
INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.encounter_type IN (#{encounter_types.to_sql})
|
|
129
|
+
INNER JOIN person p ON o.person_id = e.patient_id AND LEFT(p.gender, 1) = 'F'
|
|
130
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
131
|
+
INNER JOIN (
|
|
132
|
+
SELECT person_id, MAX(obs_datetime) AS obs_datetime
|
|
133
|
+
FROM obs
|
|
134
|
+
INNER JOIN encounter ON encounter.encounter_id = obs.encounter_id AND encounter.encounter_type IN (#{encounter_types.to_sql}) AND encounter.voided = 0
|
|
135
|
+
WHERE obs.concept_id IN (#{pregnant_concepts.to_sql})
|
|
136
|
+
AND obs.obs_datetime BETWEEN DATE(#{ActiveRecord::Base.connection.quote(start_date)}) AND DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
|
|
137
|
+
AND obs.voided = 0
|
|
138
|
+
#{site_manager(operator: 'AND', column: 'obs.site_id', location: @location)}
|
|
139
|
+
GROUP BY person_id
|
|
140
|
+
) AS max_obs ON max_obs.person_id = o.person_id AND max_obs.obs_datetime = o.obs_datetime
|
|
141
|
+
WHERE o.concept_id IN (#{pregnant_concepts.to_sql})
|
|
142
|
+
AND o.voided = 0
|
|
143
|
+
AND o.value_coded IN (#{yes_concepts.join(',')})
|
|
144
|
+
AND o.person_id IN (#{patient_list.join(',')})
|
|
145
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
146
|
+
GROUP BY o.person_id
|
|
147
|
+
SQL
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def breast_feeding(patient_list)
|
|
151
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
|
152
|
+
SELECT o.person_id, o.value_coded
|
|
153
|
+
FROM obs o
|
|
154
|
+
INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.encounter_type IN (#{encounter_types.to_sql})
|
|
155
|
+
INNER JOIN person p ON o.person_id = e.patient_id AND LEFT(p.gender, 1) = 'F'
|
|
156
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
157
|
+
INNER JOIN (
|
|
158
|
+
SELECT person_id, MAX(obs_datetime) AS obs_datetime
|
|
159
|
+
FROM obs
|
|
160
|
+
INNER JOIN encounter ON encounter.encounter_id = obs.encounter_id AND encounter.encounter_type IN (#{encounter_types.to_sql}) AND encounter.voided = 0
|
|
161
|
+
WHERE obs.concept_id IN (#{breast_feeding_concepts.to_sql})
|
|
162
|
+
AND obs.obs_datetime BETWEEN DATE(#{ActiveRecord::Base.connection.quote(start_date)}) AND DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
|
|
163
|
+
AND obs.voided = 0
|
|
164
|
+
#{site_manager(operator: 'AND', column: 'obs.site_id', location: @location)}
|
|
165
|
+
GROUP BY person_id
|
|
166
|
+
) AS max_obs ON max_obs.person_id = o.person_id AND max_obs.obs_datetime = o.obs_datetime
|
|
167
|
+
WHERE o.concept_id IN (#{breast_feeding_concepts.to_sql})
|
|
168
|
+
AND o.voided = 0
|
|
169
|
+
AND o.value_coded IN (#{yes_concepts.join(',')})
|
|
170
|
+
AND o.person_id IN (#{patient_list.join(',')})
|
|
171
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
172
|
+
GROUP BY o.person_id
|
|
173
|
+
SQL
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def build_report(report)
|
|
177
|
+
refresh_outcomes_table
|
|
178
|
+
load_tx_curr_into_report(report, create_patients_alive_and_on_art_query)
|
|
179
|
+
clients = process_due_people
|
|
180
|
+
clients.each do |patient|
|
|
181
|
+
report[patient['age_group']][patient['gender'].to_sym][:due_for_vl] << patient['patient_id']
|
|
182
|
+
end
|
|
183
|
+
load_patient_tests_into_report(report, clients.map { |patient| patient['patient_id'] })
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def refresh_outcomes_table
|
|
187
|
+
MalawiHivProgramReports::Moh::CohortBuilder.new(outcomes_definition: 'pepfar', location: @location)
|
|
188
|
+
.init_temporary_tables(@start_date, @end_date, nil)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def create_patients_alive_and_on_art_query
|
|
192
|
+
ActiveRecord::Base.connection.select_all(
|
|
193
|
+
<<~SQL
|
|
194
|
+
SELECT tpo.patient_id, LEFT(tesd.gender, 1) AS gender, disaggregated_age_group(tesd.birthdate, DATE('#{end_date.to_date}')) age_group
|
|
195
|
+
FROM temp_patient_outcomes tpo
|
|
196
|
+
INNER JOIN temp_earliest_start_date tesd ON tesd.patient_id = tpo.patient_id
|
|
197
|
+
WHERE tpo.cum_outcome = 'On antiretrovirals'
|
|
198
|
+
SQL
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def load_tx_curr_into_report(report, patients)
|
|
203
|
+
report.each do |age_group, _data|
|
|
204
|
+
%i[M F].each do |gender|
|
|
205
|
+
report[age_group][gender][:tx_curr] ||= []
|
|
206
|
+
report[age_group][gender][:tx_curr] = populate_tx_curr(patients, age_group, gender) || []
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def populate_tx_curr(patients, age_group, gender)
|
|
212
|
+
patients.select do |patient|
|
|
213
|
+
(patient['age_group'] == age_group && patient['gender'].to_sym == gender) && patient['patient_id']
|
|
214
|
+
end&.map {|a| a['patient_id']}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# rubocop:disable Metrics/AbcSize
|
|
218
|
+
# rubocop:disable Metrics/MethodLength
|
|
219
|
+
def load_patient_tests_into_report(report, clients)
|
|
220
|
+
find_patients_with_viral_load(clients).each do |patient|
|
|
221
|
+
age_group = patient['age_group']
|
|
222
|
+
gender = patient['gender'].to_sym
|
|
223
|
+
reason_for_test = (patient['reason_for_test'] || 'Routine').match?(/Routine/i) ? :routine : :targeted
|
|
224
|
+
|
|
225
|
+
report[age_group][gender][:drawn][reason_for_test] << patient['patient_id']
|
|
226
|
+
next unless patient['result_value']
|
|
227
|
+
|
|
228
|
+
if patient['result_value'].casecmp?('LDL')
|
|
229
|
+
report[age_group][gender][:low_vl][reason_for_test] << patient['patient_id']
|
|
230
|
+
elsif patient['result_value'].to_i < 1000
|
|
231
|
+
report[age_group][gender][:low_vl][reason_for_test] << patient['patient_id']
|
|
232
|
+
else
|
|
233
|
+
report[age_group][gender][:high_vl][reason_for_test] << patient['patient_id']
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
# rubocop:enable Metrics/AbcSize
|
|
238
|
+
# rubocop:enable Metrics/MethodLength
|
|
239
|
+
|
|
240
|
+
## This method prepares the response structure for the report
|
|
241
|
+
def init_report
|
|
242
|
+
pepfar_age_groups.each_with_object({}) do |age_group, report|
|
|
243
|
+
report[age_group] = %i[F M].each_with_object({}) do |gender, hash|
|
|
244
|
+
hash[gender] = {
|
|
245
|
+
due_for_vl: [],
|
|
246
|
+
drawn: { routine: [], targeted: [] },
|
|
247
|
+
high_vl: { routine: [], targeted: [] },
|
|
248
|
+
low_vl: { routine: [], targeted: [] }
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def due_for_viral_load
|
|
255
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
|
256
|
+
(#{find_patients_with_overdue_viral_load}) UNION (#{find_patients_due_for_initial_viral_load})
|
|
257
|
+
SQL
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def adverse_outcomes
|
|
261
|
+
@adverse_outcomes ||= ActiveRecord::Base.connection.select_all(
|
|
262
|
+
<<~SQL
|
|
263
|
+
SELECT pws.program_workflow_state_id state
|
|
264
|
+
FROM program_workflow pw
|
|
265
|
+
INNER JOIN concept_name pcn ON pcn.concept_id = pw.concept_id AND pcn.concept_name_type = 'FULLY_SPECIFIED' AND pcn.voided = 0
|
|
266
|
+
INNER JOIN program_workflow_state pws ON pws.program_workflow_id = pw.program_workflow_id AND pws.retired = 0
|
|
267
|
+
INNER JOIN concept_name cn ON cn.concept_id = pws.concept_id AND cn.concept_name_type = 'FULLY_SPECIFIED' AND cn.voided = 0
|
|
268
|
+
WHERE pw.program_id = 1 AND pw.retired = 0 AND pws.terminal = 1
|
|
269
|
+
SQL
|
|
270
|
+
).map { |state| state['state'] }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def clients_on_art
|
|
274
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
|
275
|
+
SELECT
|
|
276
|
+
ab.patient_id,
|
|
277
|
+
disaggregated_age_group(p.birthdate, DATE(#{ActiveRecord::Base.connection.quote(end_date)})) AS age_group,
|
|
278
|
+
p.birthdate,
|
|
279
|
+
p.gender,
|
|
280
|
+
pid.identifier AS arv_number,
|
|
281
|
+
current_state.state,
|
|
282
|
+
current_state.start_date outcome_date,
|
|
283
|
+
current_order.start_date vl_order_date
|
|
284
|
+
FROM orders ab
|
|
285
|
+
INNER JOIN person p ON p.person_id = ab.patient_id AND p.voided = 0
|
|
286
|
+
#{site_manager(operator: 'AND', column: 'p.site_id', location: @location)}
|
|
287
|
+
INNER JOIN drug_order dor ON dor.order_id = ab.order_id AND dor.quantity > 0
|
|
288
|
+
INNER JOIN arv_drug ad ON dor.drug_inventory_id = ad.drug_id
|
|
289
|
+
INNER JOIN patient_program pp ON pp.patient_id = ab.patient_id AND pp.voided = 0 AND pp.program_id = 1
|
|
290
|
+
INNER JOIN (
|
|
291
|
+
SELECT a.patient_program_id, a.state, a.start_date, a.end_date
|
|
292
|
+
FROM patient_state a
|
|
293
|
+
LEFT OUTER JOIN patient_state b ON a.patient_program_id = b.patient_program_id
|
|
294
|
+
AND a.start_date < b.start_date
|
|
295
|
+
AND b.voided = 0
|
|
296
|
+
WHERE b.patient_program_id IS NULL AND a.end_date IS NULL AND a.voided = 0
|
|
297
|
+
) current_state ON current_state.patient_program_id = pp.patient_program_id
|
|
298
|
+
LEFT OUTER JOIN orders b ON ab.patient_id = b.patient_id
|
|
299
|
+
AND ab.order_id = b.order_id
|
|
300
|
+
AND ab.auto_expire_date < b.auto_expire_date
|
|
301
|
+
AND b.voided = 0 AND b.order_type_id = 1
|
|
302
|
+
#{site_manager(operator: 'AND', column: 'b.site_id', location: @location)}
|
|
303
|
+
LEFT JOIN (#{current_occupation_query}) a ON a.person_id = ab.patient_id
|
|
304
|
+
#{site_manager(operator: 'AND', column: 'a.site_id', location: @location)}
|
|
305
|
+
LEFT JOIN patient_identifier pid ON pid.patient_id = pp.patient_id AND pid.identifier_type IN (#{pepfar_patient_identifier_type.to_sql}) AND pid.voided = 0
|
|
306
|
+
#{site_manager(operator: 'AND', column: 'pid.site_id', location: @location)}
|
|
307
|
+
LEFT JOIN (
|
|
308
|
+
SELECT ab.patient_id, MAX(ab.start_date) start_date
|
|
309
|
+
FROM orders ab
|
|
310
|
+
INNER JOIN concept_name
|
|
311
|
+
ON concept_name.concept_id = ab.concept_id
|
|
312
|
+
AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)', '50:50 Normal Plasma')
|
|
313
|
+
AND concept_name.voided = 0
|
|
314
|
+
#{site_manager(operator: 'AND', column: 'ab.site_id', location: @location)}
|
|
315
|
+
LEFT OUTER JOIN orders b ON ab.patient_id = b.patient_id
|
|
316
|
+
AND ab.order_id = b.order_id
|
|
317
|
+
AND ab.start_date < b.start_date
|
|
318
|
+
AND b.voided = 0
|
|
319
|
+
#{site_manager(operator: 'AND', column: 'b.site_id', location: @location)}
|
|
320
|
+
WHERE b.patient_id IS NULL AND ab.voided = 0 AND ab.order_type_id = 4 AND ab.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
|
|
321
|
+
GROUP BY ab.patient_id
|
|
322
|
+
) current_order ON current_order.patient_id = ab.patient_id
|
|
323
|
+
WHERE b.patient_id IS NULL
|
|
324
|
+
AND ab.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
|
|
325
|
+
AND ab.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
|
|
326
|
+
AND p.person_id NOT IN (#{drug_refills_and_external_consultation_list})
|
|
327
|
+
#{site_manager(operator: 'AND', column: 'ab.site_id', location: @location)}
|
|
328
|
+
AND ((current_state.state IN (#{adverse_outcomes.join(',')}) AND current_state.start_date >= (DATE(#{ActiveRecord::Base.connection.quote(end_date)}) - INTERVAL 12 MONTH)) OR current_state.state IN (7, 1, 87, 120, 136))
|
|
329
|
+
GROUP BY ab.patient_id;
|
|
330
|
+
SQL
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def extra_information(patient_id)
|
|
334
|
+
ActiveRecord::Base.connection.select_one <<~SQL
|
|
335
|
+
SELECT #{function_manager(function: 'patient_current_regimen', location: @location, args: "#{patient_id}, DATE(#{ActiveRecord::Base.connection.quote(end_date)}), #{@location}")} AS current_regimen,
|
|
336
|
+
#{function_manager(function: 'date_antiretrovirals_started', location: @location, args: "#{patient_id}, DATE(#{ActiveRecord::Base.connection.quote(end_date)}), #{@location}")} AS art_start_date,
|
|
337
|
+
#{function_manager(function: 'current_pepfar_defaulter_date', location: @location, args: "#{patient_id}, DATE(#{ActiveRecord::Base.connection.quote(end_date)}), #{@location}")} AS defaulter_date
|
|
338
|
+
SQL
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
##
|
|
342
|
+
# Find all patients that are on treatment with at least one VL before end of reporting period.
|
|
343
|
+
def find_patients_with_viral_load(clients)
|
|
344
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
|
345
|
+
SELECT orders.patient_id,
|
|
346
|
+
disaggregated_age_group(patient.birthdate,
|
|
347
|
+
DATE(#{ActiveRecord::Base.connection.quote(end_date)})) AS age_group,
|
|
348
|
+
patient.birthdate,
|
|
349
|
+
patient.gender,
|
|
350
|
+
patient_identifier.identifier AS arv_number,
|
|
351
|
+
orders.start_date AS order_date,
|
|
352
|
+
COALESCE(orders.discontinued_date, orders.start_date) AS sample_draw_date,
|
|
353
|
+
COALESCE(reason_for_test_value.name, reason_for_test.value_text) AS reason_for_test,
|
|
354
|
+
result.value_modifier AS result_modifier,
|
|
355
|
+
COALESCE(result.value_numeric, result.value_text) AS result_value
|
|
356
|
+
FROM orders
|
|
357
|
+
INNER JOIN person patient ON patient.person_id = orders.patient_id AND patient.voided = 0
|
|
358
|
+
#{site_manager(operator: 'AND', column: 'patient.site_id', location: @location)}
|
|
359
|
+
INNER JOIN order_type
|
|
360
|
+
ON order_type.order_type_id = orders.order_type_id
|
|
361
|
+
AND order_type.name = 'Lab'
|
|
362
|
+
AND order_type.retired = 0
|
|
363
|
+
INNER JOIN concept_name
|
|
364
|
+
ON concept_name.concept_id = orders.concept_id
|
|
365
|
+
AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)', 'Plasma')
|
|
366
|
+
AND concept_name.voided = 0
|
|
367
|
+
LEFT JOIN obs AS reason_for_test
|
|
368
|
+
ON reason_for_test.order_id = orders.order_id
|
|
369
|
+
AND reason_for_test.concept_id IN (SELECT concept_id FROM concept_name WHERE name LIKE 'Reason for test' AND voided = 0)
|
|
370
|
+
AND reason_for_test.voided = 0
|
|
371
|
+
LEFT JOIN concept_name AS reason_for_test_value
|
|
372
|
+
ON reason_for_test_value.concept_id = reason_for_test.value_coded
|
|
373
|
+
AND reason_for_test_value.voided = 0
|
|
374
|
+
LEFT JOIN obs AS result
|
|
375
|
+
ON result.order_id = orders.order_id
|
|
376
|
+
AND result.concept_id IN (SELECT concept_id FROM concept_name WHERE name LIKE 'HIV Viral load' AND voided = 0)
|
|
377
|
+
AND result.voided = 0
|
|
378
|
+
AND (result.value_text IS NOT NULL OR result.value_numeric IS NOT NULL)
|
|
379
|
+
INNER JOIN (
|
|
380
|
+
/* Get the latest order dates for each patient */
|
|
381
|
+
SELECT orders.patient_id, MAX(orders.start_date) AS start_date
|
|
382
|
+
FROM orders
|
|
383
|
+
INNER JOIN order_type
|
|
384
|
+
ON order_type.order_type_id = orders.order_type_id
|
|
385
|
+
AND order_type.name = 'Lab'
|
|
386
|
+
AND order_type.retired = 0
|
|
387
|
+
INNER JOIN concept_name
|
|
388
|
+
ON concept_name.concept_id = orders.concept_id
|
|
389
|
+
AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)', 'Plasma')
|
|
390
|
+
AND concept_name.voided = 0
|
|
391
|
+
WHERE orders.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
|
|
392
|
+
#{site_manager(operator: 'AND', column: 'orders.site_id', location: @location)}
|
|
393
|
+
AND orders.start_date >= DATE(#{ActiveRecord::Base.connection.quote(start_date)}) - INTERVAL 12 MONTH
|
|
394
|
+
AND orders.voided = 0
|
|
395
|
+
GROUP BY orders.patient_id
|
|
396
|
+
) AS latest_patient_order_date
|
|
397
|
+
ON latest_patient_order_date.patient_id = orders.patient_id
|
|
398
|
+
AND latest_patient_order_date.start_date = orders.start_date
|
|
399
|
+
LEFT JOIN patient_identifier
|
|
400
|
+
ON patient_identifier.patient_id = orders.patient_id
|
|
401
|
+
AND patient_identifier.identifier_type IN (#{pepfar_patient_identifier_type.to_sql})
|
|
402
|
+
AND patient_identifier.voided = 0
|
|
403
|
+
WHERE orders.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
|
|
404
|
+
AND orders.start_date >= DATE(#{ActiveRecord::Base.connection.quote(start_date)}) - INTERVAL 12 MONTH
|
|
405
|
+
AND orders.voided = 0
|
|
406
|
+
AND orders.patient_id IN (#{clients.push(0).join(',')})
|
|
407
|
+
GROUP BY orders.patient_id
|
|
408
|
+
SQL
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def yes_concepts
|
|
412
|
+
@yes_concepts ||= ::ConceptName.where(name: 'Yes').select(:concept_id).map do |record|
|
|
413
|
+
record['concept_id'].to_i
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def pregnant_concepts
|
|
418
|
+
@pregnant_concepts ||= ::ConceptName.where(name: ['Is patient pregnant?', 'patient pregnant'])
|
|
419
|
+
.select(:concept_id)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def breast_feeding_concepts
|
|
423
|
+
@breast_feeding_concepts ||= ::ConceptName.where(name: ['Breast feeding?', 'Breast feeding', 'Breastfeeding'])
|
|
424
|
+
.select(:concept_id)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def encounter_types
|
|
428
|
+
@encounter_types ||= ::EncounterType.where(name: ['HIV CLINIC CONSULTATION', 'HIV STAGING'])
|
|
429
|
+
.select(:encounter_type_id)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MalawiHivProgramReports
|
|
4
|
+
module ReportMap
|
|
5
|
+
REPORTS = {
|
|
6
|
+
'ARCHIVING_CANDIDATES' => MalawiHivProgramReports::ArchivingCandidates,
|
|
7
|
+
'APPOINTMENTS' => MalawiHivProgramReports::Clinic::AppointmentsReport,
|
|
8
|
+
'ARV_REFILL_PERIODS' => MalawiHivProgramReports::ArvRefillPeriods,
|
|
9
|
+
'COHORT' => MalawiHivProgramReports::Moh::Cohort,
|
|
10
|
+
'COHORT_DISAGGREGATED' => MalawiHivProgramReports::Moh::CohortDisaggregated,
|
|
11
|
+
'COHORT_DISAGGREGATED_ADDITIONS' => MalawiHivProgramReports::Moh::CohortDisaggregatedAdditions,
|
|
12
|
+
'COHORT_SURVIVAL_ANALYSIS' => MalawiHivProgramReports::Moh::CohortSurvivalAnalysis,
|
|
13
|
+
'DRUG_DISPENSATIONS' => MalawiHivProgramReports::Clinic::DrugDispensations,
|
|
14
|
+
'HIGH_VL_PATIENTS' => MalawiHivProgramReports::Clinic::ViralLoadResults,
|
|
15
|
+
'IPT' => MalawiHivProgramReports::Clinic::IptReport,
|
|
16
|
+
'PATIENTS_WITH_OUTDATED_DEMOGRAPHICS' => MalawiHivProgramReports::Clinic::PatientsWithOutdatedDemographics,
|
|
17
|
+
'PATIENTS_ON_DTG' => MalawiHivProgramReports::Clinic::PatientsOnDtg,
|
|
18
|
+
'PREGNANT_PATIENTS' => MalawiHivProgramReports::Clinic::PregnantPatients,
|
|
19
|
+
'REGIMENS_AND_FORMULATIONS' => MalawiHivProgramReports::Clinic::RegimensAndFormulations,
|
|
20
|
+
'REGIMENS_BY_WEIGHT_AND_GENDER' => MalawiHivProgramReports::Clinic::RegimensByWeightAndGender,
|
|
21
|
+
'REGIMEN_SWITCH' => MalawiHivProgramReports::Clinic::RegimenSwitch,
|
|
22
|
+
'REGIMEN_REPORT' => MalawiHivProgramReports::Clinic::RegimenDispensationData,
|
|
23
|
+
'PEPFAR_REGIMEN_SWITCH' => MalawiHivProgramReports::Pepfar::RegimenSwitch,
|
|
24
|
+
'RETENTION' => MalawiHivProgramReports::Clinic::Retention,
|
|
25
|
+
'LIMS_ELECTRONIC_RESULTS' => MalawiHivProgramReports::Clinic::LimsResults,
|
|
26
|
+
'TPT_OUTCOME' => MalawiHivProgramReports::Clinic::TptOutcome,
|
|
27
|
+
'CLINIC_TX_RTT' => MalawiHivProgramReports::Clinic::TxRtt,
|
|
28
|
+
'TB_PREV2' => MalawiHivProgramReports::Pepfar::TbPrev3,
|
|
29
|
+
'TPT_NEWLY_INITIATED' => MalawiHivProgramReports::Moh::TptNewlyInitiated,
|
|
30
|
+
'TX_CURR' => MalawiHivProgramReports::Clinic::PatientsAliveAndOnTreatment,
|
|
31
|
+
'TX_ML' => MalawiHivProgramReports::Pepfar::TxMl,
|
|
32
|
+
'TX_RTT' => MalawiHivProgramReports::Pepfar::TxRtt,
|
|
33
|
+
'IPT_COVERAGE' => MalawiHivProgramReports::Clinic::IptCoverage,
|
|
34
|
+
'VISITS' => MalawiHivProgramReports::Clinic::VisitsReport,
|
|
35
|
+
'VL_DUE' => MalawiHivProgramReports::Clinic::PatientsDueForViralLoad,
|
|
36
|
+
'DEFAULTER_LIST' => MalawiHivProgramReports::Pepfar::DefaulterList,
|
|
37
|
+
'VL_DISAGGREGATED' => MalawiHivProgramReports::Clinic::ViralLoadDisaggregated,
|
|
38
|
+
'TB_PREV' => MalawiHivProgramReports::Pepfar::TbPrev,
|
|
39
|
+
'OUTCOME_LIST' => MalawiHivProgramReports::Clinic::OutcomeList,
|
|
40
|
+
'VIRAL_LOAD' => MalawiHivProgramReports::Clinic::ViralLoad,
|
|
41
|
+
'VIRAL_LOAD_COVERAGE' => MalawiHivProgramReports::Pepfar::ViralLoadCoverage2,
|
|
42
|
+
'VL_MATERNAL_STATUS' => MalawiHivProgramReports::Pepfar::MaternalStatus,
|
|
43
|
+
'EXTERNAL_CONSULTATION_CLIENTS' => MalawiHivProgramReports::Clinic::ExternalConsultationClients,
|
|
44
|
+
'SC_ARVDISP' => MalawiHivProgramReports::Pepfar::ScArvdisp,
|
|
45
|
+
'SC_CURR' => MalawiHivProgramReports::Pepfar::ScCurr,
|
|
46
|
+
'PATIENT_ART_VL_DATES' => MalawiHivProgramReports::Pepfar::PatientStartVl,
|
|
47
|
+
'MOH_TPT' => MalawiHivProgramReports::Moh::MohTpt,
|
|
48
|
+
'TX_TB' => MalawiHivProgramReports::Pepfar::TxTb,
|
|
49
|
+
'VL_COLLECTION' => MalawiHivProgramReports::Clinic::VlCollection,
|
|
50
|
+
'DISCREPANCY_REPORT' => MalawiHivProgramReports::Clinic::DiscrepancyReport,
|
|
51
|
+
'STOCK_CARD' => MalawiHivProgramReports::Clinic::StockCardReport,
|
|
52
|
+
'HYPERTENSION_REPORT' => MalawiHivProgramReports::Clinic::HypertensionReport,
|
|
53
|
+
'TX_NEW' => MalawiHivProgramReports::Pepfar::TxNew
|
|
54
|
+
}.freeze
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# About UTILS
|
|
2
|
+
This folder contains all the utility functions that are used in the project. The functions are divided into different files based on their functionality. The files are as follows:
|
|
3
|
+
|
|
4
|
+
- [Common SQL Queries Utils](docs/common_sql_query_utils.md) - This file contains all the common SQL queries that are used in the project.
|
|
5
|
+
- [Concurrency Utils]() - This file contains all the functions that are used to handle concurrency in the project.
|
|
6
|
+
- [Model Utils]() - This file contains all the functions that are used to handle the models in the project.
|
|
7
|
+
- [Parameter Utils]() - This file contains all the functions that are used to handle the parameters in the project.
|
|
8
|
+
- [Time Utils]() - This file contains all the functions that are used to handle the time in the project.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This is a module that can be included in any class that needs to use the methods defined here.
|
|
4
|
+
module MalawiHivProgramReports
|
|
5
|
+
module Utils
|
|
6
|
+
module CommonSqlQueryUtils
|
|
7
|
+
def process_occupation(start_date:, end_date:, occupation:, location:, definition: 'moh')
|
|
8
|
+
return if occupation.blank?
|
|
9
|
+
|
|
10
|
+
MalawiHivProgramReports::Moh::CohortBuilder.new(outcomes_definition: definition, location:).init_temporary_tables(start_date, end_date,
|
|
11
|
+
occupation)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def occupation_filter(occupation:, field_name:, table_name: '', include_clause: true)
|
|
15
|
+
clause = 'WHERE' if include_clause
|
|
16
|
+
table_name = "#{table_name}." unless table_name.blank?
|
|
17
|
+
return '' if occupation.blank?
|
|
18
|
+
return '' if occupation == 'All'
|
|
19
|
+
if occupation == 'Military'
|
|
20
|
+
return "#{clause} #{table_name}#{field_name} IN ('#{occupation}', 'MDF Reserve', 'MDF Retired', 'Soldier', 'Soldier/Police')"
|
|
21
|
+
end
|
|
22
|
+
return unless occupation == 'Civilian'
|
|
23
|
+
|
|
24
|
+
"#{clause} #{table_name}#{field_name} NOT IN ('Military', 'MDF Reserve', 'MDF Retired', 'Soldier', 'Soldier/Police')"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def external_client_query(end_date:)
|
|
28
|
+
end_date = ActiveRecord::Base.connection.quote(end_date)
|
|
29
|
+
<<~SQL
|
|
30
|
+
SELECT obs.person_id FROM obs,
|
|
31
|
+
(SELECT person_id, Max(obs_datetime) AS obs_datetime, concept_id FROM obs
|
|
32
|
+
WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name = 'Type of patient' AND voided = 0)
|
|
33
|
+
AND DATE(obs_datetime) <= #{end_date}
|
|
34
|
+
AND voided = 0
|
|
35
|
+
GROUP BY person_id,concept_id) latest_record
|
|
36
|
+
WHERE obs.person_id = latest_record.person_id
|
|
37
|
+
AND obs.concept_id = latest_record.concept_id
|
|
38
|
+
AND obs.obs_datetime = latest_record.obs_datetime
|
|
39
|
+
AND obs.value_coded IN (SELECT concept_id FROM concept_name WHERE name = 'Drug refill' || name = 'External consultation')
|
|
40
|
+
AND obs.voided = 0
|
|
41
|
+
#{site_manager(operator: 'AND', column: 'obs.site_id', location: @location)}
|
|
42
|
+
SQL
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def current_occupation_query
|
|
46
|
+
ActiveRecord::Base.connection.adapter_name.downcase
|
|
47
|
+
<<~SQL
|
|
48
|
+
SELECT a.person_id, a.value, a.site_id
|
|
49
|
+
FROM person_attribute a
|
|
50
|
+
LEFT OUTER JOIN person_attribute b
|
|
51
|
+
ON a.person_attribute_id = b.person_attribute_id #{site_manager(operator: 'AND', column: 'b.site_id', location: @location)}
|
|
52
|
+
AND a.date_created < b.date_created
|
|
53
|
+
AND b.voided = 0 #{site_manager(operator: 'AND', column: 'a.site_id', location: @location)}
|
|
54
|
+
WHERE b.person_attribute_id IS NULL AND a.person_attribute_type_id = 13 AND a.voided = 0
|
|
55
|
+
#{site_manager(operator: 'AND', column: 'a.site_id', location: @location)}
|
|
56
|
+
SQL
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MalawiHivProgramReports
|
|
4
|
+
module Utils
|
|
5
|
+
module ConcurrencyUtils
|
|
6
|
+
LOCK_FILES_DIR_PATH = Rails.root.join('tmp', 'locks')
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# Acquire a lock and run the given block of code.
|
|
10
|
+
#
|
|
11
|
+
# The locking mechanism uses files to allow for blocking across processes.
|
|
12
|
+
# This is useful for example in situations where you only want to run one
|
|
13
|
+
# instance of a report.
|
|
14
|
+
#
|
|
15
|
+
# Parameters:
|
|
16
|
+
# lock_file_path: A relative path to the lock file (allows for namespacing your locks, eg art_service/regimens.lock)
|
|
17
|
+
# blocking: If lock can not be acquired wait else throw an error (defaults to true)
|
|
18
|
+
#
|
|
19
|
+
# Raises:
|
|
20
|
+
# FailedToAcquireLock: When lock couldn't be acquired and blocking is set to false
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# class Someclass
|
|
24
|
+
# include ModelUtils
|
|
25
|
+
#
|
|
26
|
+
# def do_something
|
|
27
|
+
# with_lock('mylockfile.lock') do
|
|
28
|
+
# # Run some task requiring exclusive access to some resource
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# SomeClass.new.do_something
|
|
34
|
+
def with_lock(lock_file_path, blocking: true)
|
|
35
|
+
path = LOCK_FILES_DIR_PATH.join(lock_file_path)
|
|
36
|
+
|
|
37
|
+
unless Dir.exist?(path.dirname)
|
|
38
|
+
Rails.logger.debug("Creating lock file directory: #{path.dirname}")
|
|
39
|
+
::FileUtils.mkdir_p(path.dirname)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
File.open(path, 'w') do |lock_file|
|
|
43
|
+
Rails.logger.debug("Attempting to acquire lock: #{lock_file_path}")
|
|
44
|
+
locked = lock_file.flock(blocking ? File::LOCK_EX : File::LOCK_NB | File::LOCK_EX)
|
|
45
|
+
raise ::FailedToAcquireLock, "Lock #{lock_file_path} is locked by another process" if !locked && !blocking
|
|
46
|
+
|
|
47
|
+
lock_file.write("Locked by process ##{Process.pid} at #{Time.now}")
|
|
48
|
+
yield
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Code Documentation
|
|
2
|
+
## Summary
|
|
3
|
+
The documentation is for a module called ```CommonSqlQueryUtils``` that provides two methods: ```occupation_filter``` and ```external_client_query```. The ```occupation_filter``` method takes in parameters such as occupation, field_name, table_name, and include_clause, and returns a SQL clause based on the occupation value. The ```external_client_query``` method takes in the end_date parameter and returns a SQL query that retrieves person_ids from the ```obs``` table based on certain conditions.
|
|
4
|
+
|
|
5
|
+
Example Usage
|
|
6
|
+
### Example usage of the ```occupation_filter``` method
|
|
7
|
+
```ruby
|
|
8
|
+
include CommonSqlQueryUtils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
occupation_filter(occupation: 'Military', field_name: 'occupation', table_name: 'users', include_clause: true)
|
|
12
|
+
```
|
|
13
|
+
#### Output: "WHERE users.occupation = 'Military'"
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
occupation_filter(occupation: 'Civilian', field_name: 'occupation', table_name: 'users', include_clause: false)
|
|
17
|
+
```
|
|
18
|
+
#### Output: "users.occupation != 'Military'"
|
|
19
|
+
|
|
20
|
+
### Example usage of the ```external_client_query``` method
|
|
21
|
+
```ruby
|
|
22
|
+
include CommonSqlQueryUtils
|
|
23
|
+
|
|
24
|
+
external_client_query(end_date: '2021-01-01')
|
|
25
|
+
```
|
|
26
|
+
#### Output: SQL query string
|
|
27
|
+
|
|
28
|
+
## Code Analysis
|
|
29
|
+
### Inputs
|
|
30
|
+
- ```occupation``` (string): The occupation value to filter on.
|
|
31
|
+
- ```field_name``` (string): The name of the field to filter on.
|
|
32
|
+
- ```table_name``` (string): The name of the table to include in the SQL clause.
|
|
33
|
+
- ```include_clause``` (boolean): Whether to include the ```WHERE``` clause in the SQL statement.
|
|
34
|
+
- ```end_date``` (string): The end date to use in the SQL query.
|
|
35
|
+
|
|
36
|
+
### Flow
|
|
37
|
+
The ```occupation_filter``` method checks if the ```include_clause``` parameter is true and sets the ```clause``` variable to ```WHERE``` if it is.
|
|
38
|
+
The ```table_name``` variable is modified to include a trailing dot if it is not blank.
|
|
39
|
+
The method then checks if the ```occupation``` parameter is blank or equal to ```All``` and returns an empty string in those cases.
|
|
40
|
+
If the ```occupation``` parameter is ```Military```, the method returns a SQL clause string with the ```occupation``` field equal to ```Military```.
|
|
41
|
+
If the ```occupation``` parameter is ```Civilian```, the method returns a SQL clause string with the ```occupation``` field not equal to ```Military```.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
The ``external_client_query`` method quotes the ```end_date``` parameter using ActiveRecord::Base.connection.quote.
|
|
46
|
+
The method then returns a multi-line SQL query string that retrieves person_ids from the ```obs``` table based on certain conditions.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Outputs
|
|
50
|
+
The ``occupation_filter`` method returns a SQL clause string based on the occupation value.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
The ``external_client_query`` method returns a multi-line SQL query string.
|