Skip to content
Merged
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
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ gem "jwt"
gem "mechanize"
gem "notifications-ruby-client"
gem "okcomputer"
gem "omniauth_openid_connect"
gem "omniauth-rails_csrf_protection"
gem "omniauth_openid_connect"
gem "pagy"
gem "phonelib"
gem "pundit"
gem "rails_semantic_logger"
gem "rainbow"
gem "redis"
gem "ruby-progressbar"
gem "rubyzip"
gem "sentry-rails"
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ GEM
erb
psych (>= 4.0.0)
redcarpet (3.6.1)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.25.2)
connection_pool
redis-prescription (2.6.0)
Expand Down Expand Up @@ -819,6 +821,7 @@ DEPENDENCIES
rails (~> 8.0.2)
rails_semantic_logger
rainbow
redis
rladr
rspec
rspec-html-matchers
Expand Down
19 changes: 12 additions & 7 deletions app/components/app_imports_navigation_component.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# frozen_string_literal: true

class AppImportsNavigationComponent < ViewComponent::Base
def initialize(active:)
def initialize(active:, team:)
@active = active
@team = team
end

def call
Expand Down Expand Up @@ -31,18 +32,22 @@ def call

private

attr_reader :active
attr_reader :active, :team

delegate :import_issues_count, :policy, :policy_scope, to: :helpers
delegate :policy, :policy_scope, to: :helpers

def issues_text
safe_join(
["Import issues", " ", render(AppCountComponent.new(import_issues_count))]
)
count = TeamCachedCounts.new(team).import_issues
text_with_count("Import issues", count)
end

def notices_text
count = ImportantNotices.call(patient_scope: policy_scope(Patient)).length
safe_join(["Important notices", " ", render(AppCountComponent.new(count))])

text_with_count("Important notices", count)
end

def text_with_count(text, count)
safe_join([text, " ", render(AppCountComponent.new(count))])
end
end
2 changes: 2 additions & 0 deletions app/controllers/parent_interface/consent_forms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def record

session.delete(:consent_form_id)

TeamCachedCounts.new(@team).reset_unmatched_consent_responses!

send_consent_form_confirmation(@consent_form)

ConsentFormMatchingJob.perform_later(@consent_form)
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ def manifest_link_tag(name, **options)
def opengraph_image_tag(service_url, name)
tag.meta(property: "og:image", content: "#{service_url}#{asset_path(name)}")
end

def cached_counts = TeamCachedCounts.new(current_team)
end
9 changes: 0 additions & 9 deletions app/helpers/imports_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,6 @@ module ImportsHelper
%w[registration] => :registration
}.freeze

def import_issues_count
vaccination_records_with_issues =
policy_scope(VaccinationRecord).with_pending_changes.pluck(:patient_id)

patients_with_issues = policy_scope(Patient).with_pending_changes.pluck(:id)

(vaccination_records_with_issues + patients_with_issues).uniq.length
end

def issue_categories_for(pending_changes)
FIELD_GROUPS.filter_map do |(keys, group)|
group.to_s.humanize if (pending_changes & keys).any?
Expand Down
8 changes: 8 additions & 0 deletions app/jobs/commit_patient_changesets_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def perform(import)

import.postprocess_rows!

reset_counts(import)

import.update_columns(
processed_at: Time.zone.now,
status: :processed,
Expand Down Expand Up @@ -165,4 +167,10 @@ def has_auto_confirmable_school_move?(school_move, import)
academic_year: import.academic_year
) || school_move.patient.archived?(team: import.team)
end

def reset_counts(import)
cached_counts = TeamCachedCounts.new(import.team)
cached_counts.reset_import_issues!
cached_counts.reset_school_moves!
end
end
7 changes: 7 additions & 0 deletions app/jobs/consent_form_matching_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def match_with_exact_nhs_number
patient.update_from_pds!(pds_patient)
send_parental_contact_warning_if_needed(patient, @consent_form)
@consent_form.match_with_patient!(patient, current_user: nil)
reset_counts
true
end

def session_patients
Expand Down Expand Up @@ -95,5 +97,10 @@ def match_patient(patient)

send_parental_contact_warning_if_needed(patient, @consent_form)
@consent_form.match_with_patient!(patient, current_user: nil)
reset_counts
end

def reset_counts
TeamCachedCounts.new(@consent_form.team).reset_unmatched_consent_responses!
end
end
91 changes: 91 additions & 0 deletions app/lib/team_cached_counts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

class TeamCachedCounts
def initialize(team)
@team = team
end

def import_issues
return nil if current_user.nil?

Rails
.cache
.fetch(import_issues_key) do
vaccination_records_with_issues =
VaccinationRecordPolicy::Scope
.new(current_user, VaccinationRecord)
.resolve
.with_pending_changes
.pluck(:patient_id)

patients_with_issues =
PatientPolicy::Scope
.new(current_user, Patient)
.resolve
.with_pending_changes
.pluck(:id)

(vaccination_records_with_issues + patients_with_issues).uniq.length
end
end

def reset_import_issues!
Rails.cache.delete(import_issues_key)
end

def school_moves
return nil if current_user.nil?

Rails
.cache
.fetch(school_moves_key) do
SchoolMovePolicy::Scope.new(current_user, SchoolMove).resolve.count
end
end

def reset_school_moves!
Rails.cache.delete(school_moves_key)
end

def unmatched_consent_responses
return nil if current_user.nil?

Rails
.cache
.fetch(unmatched_consent_responses_key) do
ConsentFormPolicy::Scope
.new(current_user, ConsentForm)
.resolve
.unmatched
.recorded
.not_archived
.count
end
end

def reset_unmatched_consent_responses!
Rails.cache.delete(unmatched_consent_responses_key)
end

private

attr_reader :team

def import_issues_key = cache_key("import-issues")

def school_moves_key = cache_key("school-moves")

def unmatched_consent_responses_key = cache_key("unmatched-consent-responses")

def current_user
# We can't use the policy_scope helper here as we're not in a controller.
# Instead, we can mock what a `User` looks like from the perspective of a
# controller to satisfy the policy scopes.
@current_user ||=
if team && (organisation = team.organisation)
OpenStruct.new(selected_team: team, selected_organisation: organisation)
end
end

def cache_key(type) = "cached-counts/#{type}/#{team.id}"
end
2 changes: 1 addition & 1 deletion app/views/imports/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

<%= govuk_button_to "Import records", imports_path, secondary: true, class: "nhsuk-u-margin-bottom-4" %>

<%= render AppImportsNavigationComponent.new(active: :index) %>
<%= render AppImportsNavigationComponent.new(active: :index, team: current_team) %>

<%= render AppImportsTableComponent.new(team: current_team) %>
2 changes: 1 addition & 1 deletion app/views/imports/issues/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<%= govuk_button_to "Import records", imports_path, secondary: true, class: "nhsuk-u-margin-bottom-4" %>

<%= render AppImportsNavigationComponent.new(active: :issues) %>
<%= render AppImportsNavigationComponent.new(active: :issues, team: current_team) %>

<% if @import_issues.any? %>
<div class="nhsuk-table__panel-with-heading-tab">
Expand Down
2 changes: 1 addition & 1 deletion app/views/imports/notices/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<%= govuk_button_to "Import records", imports_path, secondary: true, class: "nhsuk-u-margin-bottom-4" %>

<%= render AppImportsNavigationComponent.new(active: :notices) %>
<%= render AppImportsNavigationComponent.new(active: :notices, team: current_team) %>

<% if @notices.present? %>
<%= render AppNoticesTableComponent.new(@notices) %>
Expand Down
6 changes: 3 additions & 3 deletions app/views/layouts/_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@
t("consent_forms.index.title_short"),
consent_forms_path,
request_path: request.path,
count: policy_scope(ConsentForm).unmatched.recorded.not_archived.count
count: cached_counts.unmatched_consent_responses
) %>

<%= render AppHeaderNavigationItemComponent.new(
t("school_moves.index.title"),
school_moves_path,
request_path: request.path,
count: policy_scope(SchoolMove).count
count: cached_counts.school_moves
) %>

<%= render AppHeaderNavigationItemComponent.new(t("vaccines.index.title"), vaccines_path, request_path: request.path) %>
Expand All @@ -62,7 +62,7 @@
t("imports.index.title_short"),
imports_path,
request_path: request.path,
count: import_issues_count
count: cached_counts.import_issues
) %>

<%= render AppHeaderNavigationItemComponent.new(t("teams.show.title"), team_path, request_path: request.path) %>
Expand Down
16 changes: 15 additions & 1 deletion config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,21 @@
)

# Replace the default in-process memory cache store with a durable alternative.
# config.cache_store = :mem_cache_store
config.cache_store =
:redis_cache_store,
{
url: ENV["REDIS_CACHE_URL"],
error_handler: ->(method:, returning:, exception:) do
Sentry.capture_exception(
exception,
level: "warning",
tags: {
method:,
returning:
}
)
end
}

# Replace the default in-process and non-durable queuing backend for Active Job.
# config.active_job.queue_adapter = :resque
Expand Down