Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions app/controllers/admin/deduplicate_patients_controller.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
class Admin::DeduplicatePatientsController < AdminController
skip_before_action :verify_authenticity_token
before_action :set_filter_options, only: [:show]
DUPLICATE_LIMIT = 250

def show
facilities = current_admin.accessible_facilities(:manage)
authorize { facilities.any? }

# Apply facility filter if selected
filtered_facilities = if @selected_facility.present?
facilities.where(id: @selected_facility.id)
elsif @selected_district.present?
facilities.where(id: @selected_district.facilities)
else
facilities
end

# Scoping by facilities is costly for users who have a lot of facilities
duplicate_patient_ids = if current_admin.accessible_organizations(:manage).any?
duplicate_patient_ids = if current_admin.accessible_organizations(:manage).any? && !@selected_district.present? && !@selected_facility.present?
PatientDeduplication::Strategies.identifier_excluding_full_name_match(limit: DUPLICATE_LIMIT)
else
PatientDeduplication::Strategies.identifier_excluding_full_name_match_for_facilities(
limit: DUPLICATE_LIMIT,
facilities: facilities
facilities: filtered_facilities
)
end

Expand Down Expand Up @@ -44,4 +54,38 @@ def can_admin_deduplicate_patients?(patients)
.where(id: patients.pluck(:assigned_facility_id))
.any?
end

private

def set_filter_options
@accessible_facilities = current_admin.accessible_facilities(:manage)
populate_districts
set_selected_district
populate_facilities
set_selected_facility
end

def populate_districts
@districts = Region.district_regions
.joins("INNER JOIN regions facility_region ON regions.path @> facility_region.path")
.where("facility_region.source_id" => @accessible_facilities.map(&:id))
.distinct(:slug)
.order(:name)
end

def set_selected_district
@selected_district = if params[:district_slug].present?
@districts.find_by(slug: params[:district_slug])
elsif @districts.present?
@districts.first
end
end

def populate_facilities
@facilities = @accessible_facilities.where(id: @selected_district&.facilities).order(:name)
end

def set_selected_facility
@selected_facility = @facilities.find_by(id: params[:facility_id]) if params[:facility_id].present?
end
end
31 changes: 31 additions & 0 deletions app/controllers/api/v4/patient_scores_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class Api::V4::PatientScoresController < Api::V4::SyncController
def sync_to_user
__sync_to_user__("patient_scores")
end

def current_facility_records
@current_facility_records ||=
PatientScore
.for_sync
.where(patient: current_facility.prioritized_patients.select(:id))
.updated_on_server_since(current_facility_processed_since, limit)
end

def other_facility_records
other_facilities_limit = limit - current_facility_records.size
@other_facility_records ||=
PatientScore
.for_sync
.where(patient_id: current_sync_region
.syncable_patients
.where.not(registration_facility: current_facility)
.select(:id))
.updated_on_server_since(other_facilities_processed_since, other_facilities_limit)
end

private

def transform_to_response(patient_score)
Api::V4::PatientScoreTransformer.to_response(patient_score)
end
end
13 changes: 13 additions & 0 deletions app/models/patient_score.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class PatientScore < ApplicationRecord
include Mergeable
include Discard::Model

belongs_to :patient, optional: true

validates :device_created_at, presence: true
validates :device_updated_at, presence: true
validates :score_type, presence: true
validates :score_value, presence: true, numericality: true

scope :for_sync, -> { with_discarded }
end
16 changes: 16 additions & 0 deletions app/schema/api/v4/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ def patient_attribute
required: %w[id patient_id height weight created_at updated_at]}
end

def patient_score
{type: :object,
properties: {
id: {"$ref" => "#/definitions/uuid"},
patient_id: {"$ref" => "#/definitions/uuid"},
score_type: {"$ref" => "#/definitions/non_empty_string"},
score_value: {type: :number},
deleted_at: {"$ref" => "#/definitions/nullable_timestamp"},
created_at: {"$ref" => "#/definitions/timestamp"},
updated_at: {"$ref" => "#/definitions/timestamp"}
},
required: %w[id patient_id score_type score_value created_at updated_at]}
end

def patient_phone_number
{
type: :object,
Expand Down Expand Up @@ -458,6 +472,8 @@ def definitions
patient: patient,
patient_attribute: patient_attribute,
patient_attributes: Api::CommonDefinitions.array_of("patient_attribute"),
patient_score: patient_score,
patient_scores: Api::CommonDefinitions.array_of("patient_score"),
patient_business_identifier: Api::V3::Models.patient_business_identifier,
patient_business_identifiers: Api::CommonDefinitions.array_of("patient_business_identifier"),
phone_number: Api::V3::Models.phone_number,
Expand Down
22 changes: 22 additions & 0 deletions app/transformers/api/v4/patient_score_transformer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class Api::V4::PatientScoreTransformer < Api::V4::Transformer
class << self
def to_response(payload)
current_time = Time.current.iso8601
super(payload)
.merge({
"score_type" => payload["score_type"],
"score_value" => payload["score_value"].to_f,
"created_at" => current_time,
"updated_at" => current_time
})
end

def from_request(payload)
super(payload)
.merge({
"score_type" => payload["score_type"],
"score_value" => payload["score_value"].to_f
})
end
end
end
32 changes: 32 additions & 0 deletions app/views/admin/deduplicate_patients/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
<h1 class="page-title mb-3">Merge duplicate patients</h1>

<%= bootstrap_form_with(url: admin_deduplication_path, method: :get, layout: :horizontal, class: "mb-4") do |form| %>
<% html_select_options = { onchange: "this.form.submit();" }
searchable_select_options = html_select_options.merge(class: "selectpicker", data: {live_search: true}) %>
<div class="form-row">
<div id="district-selector" class="form-group col-md-4">
<%= form.select :district_slug,
@districts.order(:name).map { |district| [district.name, district.slug] },
{
hide_label: true,
selected: @selected_district&.slug,
wrapper: false
},
searchable_select_options
%>
</div>
<div id="facility-selector" class="form-group col-md-4">
<%= form.select :facility_id,
@facilities.order(:name).map { |facility| [facility.label_with_district, facility.id] },
{
hide_label: true,
include_blank: "All facilities",
selected: @selected_facility&.id,
wrapper: false
},
searchable_select_options
%>
</div>
</div>
<%= form.submit "Filter", style: "display: none" %>
<% end %>

<div class="card">
<h3>
<% if @duplicate_count == Admin::DeduplicatePatientsController::DUPLICATE_LIMIT %>
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
get "sync", to: "cvd_risks#sync_to_user"
post "sync", to: "cvd_risks#sync_from_user"
end

scope :patient_scores do
get "sync", to: "patient_scores#sync_to_user"
end
end

namespace :webview do
Expand Down
24 changes: 24 additions & 0 deletions db/migrate/20260209112204_create_patient_scores.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class CreatePatientScores < ActiveRecord::Migration[6.1]
def change
unless table_exists?(:patient_scores)
create_table :patient_scores, id: :uuid do |t|
t.references :patient, null: false, foreign_key: true, type: :uuid
t.string :score_type, null: false, limit: 100
t.decimal :score_value, precision: 5, scale: 2, null: false
t.datetime :device_created_at, null: false
t.datetime :device_updated_at, null: false
t.datetime :deleted_at

t.timestamps
end
end

unless index_exists?(:patient_scores, [:patient_id, :score_type])
add_index :patient_scores, [:patient_id, :score_type]
end

unless index_exists?(:patient_scores, :updated_at)
add_index :patient_scores, :updated_at
end
end
end
48 changes: 48 additions & 0 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2683,6 +2683,23 @@ CREATE TABLE public.patient_phone_numbers (
);


--
-- Name: patient_scores; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE public.patient_scores (
id uuid NOT NULL,
patient_id uuid NOT NULL,
score_type character varying(100) NOT NULL,
score_value numeric(5,2) NOT NULL,
device_created_at timestamp without time zone NOT NULL,
device_updated_at timestamp without time zone NOT NULL,
deleted_at timestamp without time zone,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);


--
-- Name: prescription_drugs; Type: TABLE; Schema: public; Owner: -
--
Expand Down Expand Up @@ -7398,6 +7415,14 @@ ALTER TABLE ONLY public.patient_phone_numbers
ADD CONSTRAINT patient_phone_numbers_pkey PRIMARY KEY (id);


--
-- Name: patient_scores patient_scores_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.patient_scores
ADD CONSTRAINT patient_scores_pkey PRIMARY KEY (id);


--
-- Name: patients patients_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
Expand Down Expand Up @@ -8501,6 +8526,20 @@ CREATE INDEX index_patient_phone_numbers_on_dnd_status ON public.patient_phone_n
CREATE INDEX index_patient_phone_numbers_on_patient_id ON public.patient_phone_numbers USING btree (patient_id);


--
-- Name: index_patient_scores_on_patient_id_and_score_type; Type: INDEX; Schema: public; Owner: -
--

CREATE INDEX index_patient_scores_on_patient_id_and_score_type ON public.patient_scores USING btree (patient_id, score_type);


--
-- Name: index_patient_scores_on_updated_at; Type: INDEX; Schema: public; Owner: -
--

CREATE INDEX index_patient_scores_on_updated_at ON public.patient_scores USING btree (updated_at);


--
-- Name: index_patient_registrations_per_day_per_facilities; Type: INDEX; Schema: public; Owner: -
--
Expand Down Expand Up @@ -9121,6 +9160,14 @@ ALTER TABLE ONLY public.patient_phone_numbers
ADD CONSTRAINT fk_rails_0145dd0b05 FOREIGN KEY (patient_id) REFERENCES public.patients(id);


--
-- Name: patient_scores fk_rails_0209112204; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.patient_scores
ADD CONSTRAINT fk_rails_0209112204 FOREIGN KEY (patient_id) REFERENCES public.patients(id);


--
-- Name: facility_groups fk_rails_0ba9e6af98; Type: FK CONSTRAINT; Schema: public; Owner: -
--
Expand Down Expand Up @@ -9674,6 +9721,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20251215113615'),
('20251219061210'),
('20260128094448'),
('20260209112204'),
('20260212195326'),
('20260205110957'),
('20260224063659'),
Expand Down
27 changes: 27 additions & 0 deletions spec/controllers/api/v4/patient_scores_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require "rails_helper"

describe Api::V4::PatientScoresController, type: :controller do
let(:request_user) { create(:user) }
let(:request_facility_group) { request_user.facility.facility_group }
let(:request_facility) { create(:facility, facility_group: request_facility_group) }
let(:model) { PatientScore }

def create_record(options = {})
facility = create(:facility, facility_group: request_facility_group)
patient = create(:patient, registration_facility: facility)
create(:patient_score, options.merge(patient: patient))
end

def create_record_list(n, options = {})
facility = create(:facility, facility_group_id: request_facility_group.id)
patient = create(:patient, registration_facility_id: facility.id)
create_list(:patient_score, n, options.merge(patient: patient))
end

it_behaves_like "a sync controller that authenticates user requests: sync_to_user"
it_behaves_like "a sync controller that audits the data access: sync_to_user"

describe "GET sync: send data from server to device;" do
it_behaves_like "a working V3 sync controller sending records"
end
end
10 changes: 10 additions & 0 deletions spec/factories/patient_scores.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FactoryBot.define do
factory :patient_score do
id { SecureRandom.uuid }
patient
score_type { "risk_score" }
score_value { 75.50 }
device_created_at { Time.current }
device_updated_at { Time.current }
end
end