Skip to content

Commit

Permalink
Merge pull request #8695 from DFE-Digital/33-ca-highlight-candidate-a…
Browse files Browse the repository at this point in the history
…pplication-edits-on-manage

Highlight candidate application edits on manage
  • Loading branch information
inulty-dfe authored Oct 19, 2023
2 parents 5bd4cc3 + d1d1dea commit e8c7fd7
Show file tree
Hide file tree
Showing 18 changed files with 481 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<section class="app-application-card">
<section id="<%= dom_id(application_choice) %>" class="app-application-card">
<header class="app-application-card__header">
<h3 class="govuk-heading-s">
<%= govuk_link_to candidate_name, provider_interface_application_choice_path(application_choice), no_visited_state: true %>
Expand All @@ -21,6 +21,11 @@
<span data-qa="provider"><%= course_provider_name %></span>
<%= "(#{accredited_provider.name})" if accredited_provider.present? %>
</dd>
<% if application_choice.updated_recently_since_submitted? %>
<dd class="govuk-hint govuk-body-s govuk-!-font-size-16">
<span><%= @application_choice.application_form.full_name %> updated on <%= @application_choice.application_form.updated_at.to_fs(:govuk_date) %> at <%= @application_choice.application_form.updated_at.to_fs(:govuk_time) %></span>
</dd>
<% end %>
</dl>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ def map_activity_log_events_for(attr)
end

def timeline_events
(status_change_events + note_events + feedback_events + change_offer_events + change_course_events + interview_events).sort_by(&:date).reverse
[status_change_events,
note_events,
feedback_events,
change_offer_events,
interview_preference_events,
personal_information_events,
disability_disclosure_events,
equality_diversity_events,
contact_information_events,
change_course_events,
interview_events].flatten.sort_by(&:date).reverse
end

def status_change_events
Expand Down Expand Up @@ -111,6 +121,83 @@ def interview_events
end
end

def interview_preference_events
interviews, @activity_log_events = @activity_log_events.partition { |e| e.audit.audited_changes['interview_preferences'] }
interviews.map do |event|
Event.new(
'Interview preference updated',
actor_for(event),
event.created_at,
'View Application',
provider_interface_application_choice_path(application_choice, anchor: 'interview-preferences-section'),
)
end
end

def personal_information_events
information, @activity_log_events = @activity_log_events.partition { |e| e.audit.audited_changes.keys.intersection(%w[first_name last_name date_of_birth]).present? }
information.map do |event|
Event.new(
'Personal information updated',
actor_for(event),
event.created_at,
'View Application',
provider_interface_application_choice_path(application_choice, anchor: 'personal-information-section'),
)
end
end

def contact_information_events
attributes = %w[last_name
phone_number
address_line1
address_line2
address_line3
address_line4
country
postcode]
information, @activity_log_events = @activity_log_events.partition { |e| e.audit.audited_changes.keys.intersection(attributes).present? }
information.map do |event|
Event.new(
'Contact information updated',
actor_for(event),
event.created_at,
'View Application',
provider_interface_application_choice_path(application_choice, anchor: 'contact-information-section'),
)
end
end

def disability_disclosure_events
disability_disclosure, @activity_log_events = @activity_log_events.partition do |e|
e.audit.audited_changes.key?('disability_disclosure')
end
disability_disclosure.map do |event|
Event.new(
'Disability disclosure updated',
actor_for(event),
event.created_at,
'View Application',
provider_interface_application_choice_path(application_choice, anchor: 'disability-disclosure-section'),
)
end
end

def equality_diversity_events
equality_diversity, @activity_log_events = @activity_log_events.partition do |e|
e.audit.audited_changes.key?('disability_disclosure')
end
equality_diversity.map do |event|
Event.new(
'Equality and Diversity updated',
actor_for(event),
event.created_at,
'View Application',
provider_interface_application_choice_path(application_choice, anchor: 'equality-diversity-section'),
)
end
end

def title_for(status)
TITLES[status]
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<section class="app-section govuk-!-width-two-thirds" data-qa="contact-information">
<section id="contact-information-section" class="app-section govuk-!-width-two-thirds" data-qa="contact-information">
<h2 class="govuk-heading-m govuk-!-font-size-27" id="contact-information">Contact information</h2>
<%= render SummaryListComponent.new(rows: rows) %>
</section>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<section class="app-section govuk-!-width-two-thirds">
<section id="diversity-section" class="app-section govuk-!-width-two-thirds">
<h2 class="govuk-heading-m govuk-!-font-size-27" id="diversity-information">Sex, disability and ethnicity</h2>

<%= render SummaryListComponent.new(rows: rows) %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<section class="app-section govuk-!-width-two-thirds" data-qa="personal-information">
<section id="personal-information-section" class="app-section govuk-!-width-two-thirds" data-qa="personal-information">
<h2 class="govuk-heading-m govuk-!-font-size-27" id="personal-information">Personal information</h2>
<%= render SummaryListComponent.new(rows: rows) %>
</section>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<section class="app-section govuk-!-width-two-thirds" id="disability-access-and-other-needs">
<section id="disability-disclosure-section" class="app-section govuk-!-width-two-thirds">
<h2 class="govuk-heading-m govuk-!-font-size-27">Disability support</h2>

<%= render SummaryListComponent.new(rows: rows) %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<section class="app-section govuk-!-width-two-thirds">
<section id="interview-preferences-section" class="app-section govuk-!-width-two-thirds">
<h2 class="govuk-heading-m govuk-!-font-size-27" id="interview"><%= t('page_titles.interview_preferences.heading') %></h2>

<%= render SummaryListComponent.new(rows: rows) %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def show
@available_training_providers = available_training_providers
@available_courses = available_courses
@available_course_options = available_course_options

@show_updated_recently_banner = @application_choice.updated_recently_since_submitted?
end

def timeline; end
Expand Down
4 changes: 4 additions & 0 deletions app/models/application_choice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ def supplementary_statuses
end
end

def updated_recently_since_submitted?
RecentlyUpdatedApplicationChoice.new(application_choice: self).call
end

private

def set_initial_status
Expand Down
33 changes: 33 additions & 0 deletions app/queries/get_activity_log_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ class GetActivityLogEvents
],
}.freeze

INCLUDE_APPLICATION_FORM_CHANGES_TO = [
'date_of_birth',
'first_name',
'last_name',

# Contact Information
'last_name',
'phone_number',
'address_line1',
'address_line2',
'address_line3',
'address_line4',
'country',
'postcode',

# Interview Preferences
'interview_preferences',

# Disability
'disability_disclosure',
].freeze

INCLUDE_APPLICATION_CHOICE_CHANGES_TO = %w[
reject_by_default_feedback_sent_at
course_changed_at
Expand All @@ -37,6 +59,11 @@ def self.call(application_choices:, since: nil)
associated_type = 'ApplicationChoice'
AND associated_id = ac.id
AND NOT auditable_type = 'OfferCondition'
) OR (
auditable_type = 'ApplicationForm'
AND auditable_id = ac.application_form_id
AND action = 'update'
AND ( #{application_form_audits_filter_sql} )
)
COMBINE_AUDITS_WITH_APPLICATION_CHOICES_SCOPE_AND_FILTER

Expand All @@ -46,6 +73,12 @@ def self.call(application_choices:, since: nil)
.order('audits.created_at DESC')
end

def self.application_form_audits_filter_sql
INCLUDE_APPLICATION_FORM_CHANGES_TO.map do |change|
"jsonb_exists(audited_changes, '#{change}')"
end.join(' OR ')
end

def self.application_choice_audits_filter_sql
application_choice_audits_filter = INCLUDE_APPLICATION_CHOICE_CHANGES_TO.map do |change|
"jsonb_exists(audited_changes, '#{change}')"
Expand Down
57 changes: 57 additions & 0 deletions app/queries/recently_updated_application_choice.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Find audits for updates to editable sections on the application form
class RecentlyUpdatedApplicationChoice
UPDATED_RECENTLY_DAYS = 40

def initialize(application_choice:)
@application_choice = application_choice
end

def call
return false if @application_choice.sent_to_provider_at.blank?

since = [UPDATED_RECENTLY_DAYS.days.ago, @application_choice.sent_to_provider_at].max

Audited::Audit
.where('audits.created_at >= ? AND audits.action = \'update\'', since)
.where(auditable_type: 'ApplicationForm', auditable_id: @application_choice.application_form_id)
.where(application_form_audits_filter_sql)
.exists?
end

private

def application_form_audits_filter_sql
attributes.map do |attribute|
"jsonb_exists(audited_changes, '#{attribute}')"
end.join(' OR ')
end

attr_reader :application_choice

def attributes
[
# Personal Information
'date_of_birth',
'first_name',
'last_name',

# Contact Information
'phone_number',
'address_line1',
'address_line2',
'address_line3',
'address_line4',
'country',
'postcode',

# Interview Preferences
'interview_preferences',

# Disability
'disability_disclosure',

# Equality and diversity
'equality_and_diversity',
]
end
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<% content_for :browser_title, "#{@application_choice.application_form.full_name} – #{@application_choice.course.name_and_code}" %>

<% if @show_updated_recently_banner %>
<%= govuk_notification_banner(title_text: t('notification_banner.important')) do |notification_banner| %>
<% notification_banner.with_heading(text: "#{@application_choice.application_form.full_name} updated their application on #{@application_choice.application_form.updated_at.to_fs(:govuk_date)} at #{@application_choice.application_form.updated_at.to_fs(:govuk_time)}") %>
<p class="govuk-body"><%= govuk_link_to 'View the timeline for their updates', provider_interface_application_choice_timeline_path(@application_choice.id) %></p>
<% end %>
<% end %>

<%= render ProviderInterface::ApplicationChoiceHeaderComponent.new(
application_choice: @application_choice,
provider_can_respond: @provider_user_can_make_decisions,
Expand All @@ -11,6 +18,8 @@

<h2 class="govuk-heading-l govuk-!-margin-bottom govuk-!-font-size-36">Application</h2>

<p class="govuk-body govuk-!-display-none-print govuk-!-width-two-thirds"><%= @application_choice.application_form.full_name %> will be able to edit some sections of their application. You’ll get a notification if they add any new information.</p>

<p class="govuk-body govuk-!-display-none-print">
<% if FeatureFlag.active?(:withdraw_at_candidates_request) && application_withdrawable? %>
<%= govuk_link_to(
Expand Down
50 changes: 25 additions & 25 deletions config/brakeman.ignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
{
"ignored_warnings": [
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "094c4d4bd37df3a900e58709379ace20718201acf89d88108a2950dca8f861bc",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/queries/get_activity_log_events.rb",
"line": 71,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Audited::Audit.includes(:user => ([:provider_user, :support_user]), :auditable => ([:application_form, :course_option, :course, :site, :provider, :accredited_provider, :current_course_option])).joins(\"INNER JOIN (#{application_choices.to_sql}) ac\\n ON (\\n auditable_type = 'ApplicationChoice'\\n AND auditable_id = ac.id\\n AND action = 'update'\\n AND ( #{application_choice_audits_filter_sql} )\\n ) OR (\\n associated_type = 'ApplicationChoice'\\n AND associated_id = ac.id\\n AND NOT auditable_type = 'OfferCondition'\\n ) OR (\\n auditable_type = 'ApplicationForm'\\n AND auditable_id = ac.application_form_id\\n AND action = 'update'\\n AND ( #{application_form_audits_filter_sql} )\\n )\\n\".squish)",
"render_path": null,
"location": {
"type": "method",
"class": "GetActivityLogEvents",
"method": "s(:self).call"
},
"user_input": "application_choice_audits_filter_sql",
"confidence": "Weak",
"cwe_id": [
89
],
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
Expand Down Expand Up @@ -230,29 +253,6 @@
],
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "8d47ca53c31589141f357f2ee543042879b2512c30b261840f89cec9f6da0518",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/queries/get_activity_log_events.rb",
"line": 44,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Audited::Audit.includes(:user => ([:provider_user, :support_user]), :auditable => ([:application_form, :course_option, :course, :site, :provider, :accredited_provider, :current_course_option])).joins(\"INNER JOIN (#{application_choices.to_sql}) ac\\n ON (\\n auditable_type = 'ApplicationChoice'\\n AND auditable_id = ac.id\\n AND action = 'update'\\n AND ( #{application_choice_audits_filter_sql} )\\n ) OR (\\n associated_type = 'ApplicationChoice'\\n AND associated_id = ac.id\\n AND NOT auditable_type = 'OfferCondition'\\n )\\n\".squish)",
"render_path": null,
"location": {
"type": "method",
"class": "GetActivityLogEvents",
"method": "s(:self).call"
},
"user_input": "application_choice_audits_filter_sql",
"confidence": "Weak",
"cwe_id": [
89
],
"note": ""
},
{
"warning_type": "Unscoped Find",
"warning_code": 82,
Expand Down Expand Up @@ -415,6 +415,6 @@
"note": ""
}
],
"updated": "2023-02-23 15:37:29 +0000",
"brakeman_version": "5.4.1"
"updated": "2023-10-18 13:21:48 +0100",
"brakeman_version": "6.0.1"
}
19 changes: 19 additions & 0 deletions spec/factories/audit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,23 @@
audit.audited_changes = evaluator.changes
end
end

factory :application_form_audit, class: 'Audited::Audit' do
action { 'update' }
user { create(:provider_user) }
version { 1 }
request_uuid { SecureRandom.uuid }
created_at { Time.zone.now }

transient do
application_choice { build_stubbed(:application_choice) }
changes { {} }
end

after(:build) do |audit, evaluator|
audit.auditable_type = 'ApplicationForm'
audit.auditable_id = evaluator.application_choice.application_form.id
audit.audited_changes = evaluator.changes
end
end
end
Loading

0 comments on commit e8c7fd7

Please sign in to comment.