Skip to content

Commit

Permalink
Merge pull request #3068 from DFE-Digital/TEVA-2059-shortlist-applicant
Browse files Browse the repository at this point in the history
[TEVA-2059] Publishers can shortlist and reject applicants
  • Loading branch information
cpjmcquillan committed Mar 16, 2021
2 parents f67413f + e713a92 commit e4d420f
Show file tree
Hide file tree
Showing 48 changed files with 860 additions and 139 deletions.
5 changes: 2 additions & 3 deletions app/controllers/jobseekers/job_applications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ def new
end

def create
new_job_application = current_jobseeker.job_applications.find_or_initialize_by(vacancy: vacancy)
new_job_application.draft!
new_job_application = current_jobseeker.job_applications.create(vacancy: vacancy)
redirect_to jobseekers_job_application_build_path(new_job_application, :personal_details)
end

Expand All @@ -19,7 +18,7 @@ def submit
if params[:commit] == t("buttons.save_as_draft")
redirect_to jobseekers_job_applications_path, success: t("messages.jobseekers.job_applications.saved")
elsif review_form.valid?
job_application.update(status: :submitted, submitted_at: Time.zone.now)
job_application.submitted!
Jobseekers::JobApplicationMailer.application_submitted(job_application).deliver_later
@application_feedback_form = Jobseekers::JobApplication::FeedbackForm.new
else
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,54 @@
class Publishers::Vacancies::JobApplicationsController < Publishers::Vacancies::ApplicationController
helper_method :job_application, :vacancy
helper_method :form, :job_application, :vacancy

def reject
raise ActionController::RoutingError, "Cannot reject a draft or withdrawn application" if
job_application.draft? || job_application.withdrawn?
end

def shortlist
raise ActionController::RoutingError, "Cannot shortlist a draft or withdrawn application" if
job_application.draft? || job_application.withdrawn?
end

def show
raise ActionController::RoutingError, "Cannot view a draft or withdrawn application" if
job_application.draft? || job_application.withdrawn?
end

def update_status
raise ActionController::RoutingError, "Cannot shortlist or reject a draft or withdrawn application" if
job_application.draft? || job_application.withdrawn?

job_application.update(status: status, application_data: job_application.application_data.merge(form_params))
Jobseekers::JobApplicationMailer.send("application_#{status}".to_sym, job_application).deliver_now
# TODO: Update redirect when job applications index page exists (and update request/system specs)
redirect_to organisation_jobs_path,
success: t(".#{status}",
name: "#{job_application.application_data['first_name']} #{job_application.application_data['last_name']}")
end

private

def form
@form ||= Publishers::JobApplication::UpdateStatusForm.new
end

def form_params
params.require(:publishers_job_application_update_status_form).permit(:further_instructions, :rejection_reasons)
end

def job_application
@job_application ||= vacancy.job_applications.submitted.find(params[:id])
@job_application ||= vacancy.job_applications.find(params[:job_application_id] || params[:id])
end

def status
case params[:commit]
when t("buttons.shortlist")
"shortlisted"
when t("buttons.reject")
"unsuccessful"
end
end

def vacancy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Publishers::JobApplication::UpdateStatusForm
include ActiveModel::Model

attr_accessor :further_instructions, :rejection_reasons
end
6 changes: 6 additions & 0 deletions app/frontend/src/styles/base/_utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,9 @@
margin-bottom: 0;
}
}

.grey-border-box {
border: 5px solid govuk-colour('mid-grey');
margin-bottom: govuk-spacing(5);
padding: govuk-spacing(5);
}
19 changes: 19 additions & 0 deletions app/helpers/job_application_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module JobApplicationHelper
PUBLISHER_STATUS_MAPPINGS = { submitted: "review", unsuccessful: "rejected" }.freeze

JOB_APPLICATION_STATUS_TAG_COLOURS = {
draft: "pink", submitted: "blue", shortlisted: "green", unsuccessful: "red", withdrawn: "grey"
}.freeze

def job_application_status_tag(status)
govuk_tag text: status,
colour: JOB_APPLICATION_STATUS_TAG_COLOURS[status.to_sym],
classes: "govuk-!-margin-bottom-2"
end

def publisher_job_application_status_tag(status)
govuk_tag text: PUBLISHER_STATUS_MAPPINGS[status.to_sym],
colour: JOB_APPLICATION_STATUS_TAG_COLOURS[status.to_sym],
classes: "govuk-!-margin-bottom-2"
end
end
30 changes: 28 additions & 2 deletions app/mailers/jobseekers/job_application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
class Jobseekers::JobApplicationMailer < Jobseekers::BaseMailer
def application_shortlisted(job_application)
@job_application = job_application
@vacancy = @job_application.vacancy
@organisation_name = @vacancy.parent_organisation.name
@contact_email = @vacancy.contact_email
@jobseeker = @job_application.jobseeker

@template = NOTIFY_JOBSEEKER_APPLICATION_SHORTLISTED_TEMPLATE
@to = @jobseeker.email

view_mail(@template, to: @to, subject: I18n.t("jobseekers.job_application_mailer.application_shortlisted.subject"))
end

def application_submitted(job_application)
@vacancy = job_application.vacancy
@organisation_name = @vacancy.parent_organisation.name
@contact_email = @vacancy.contact_email
@jobseeker = job_application.jobseeker

@template = NOTIFY_JOBSEEKER_APPLICATION_SUBMITTED_CONFIRMATION_TEMPLATE
@to = job_application.jobseeker.email
@template = NOTIFY_JOBSEEKER_APPLICATION_SUBMITTED_TEMPLATE
@to = @jobseeker.email

view_mail(@template, to: @to, subject: I18n.t("jobseekers.job_application_mailer.application_submitted.subject"))
end

def application_unsuccessful(job_application)
@job_application = job_application
@vacancy = @job_application.vacancy
@organisation_name = @vacancy.parent_organisation.name
@contact_email = @vacancy.contact_email
@jobseeker = @job_application.jobseeker

@template = NOTIFY_JOBSEEKER_APPLICATION_UNSUCCESSFUL_TEMPLATE
@to = @jobseeker.email

view_mail(@template, to: @to, subject: I18n.t("jobseekers.job_application_mailer.application_unsuccessful.subject"))
end
end
11 changes: 10 additions & 1 deletion app/models/job_application.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class JobApplication < ApplicationRecord
before_save :update_status_timestamp, if: :will_save_change_to_status?

extend ArrayEnum

array_enum completed_steps: {
Expand All @@ -12,12 +14,19 @@ class JobApplication < ApplicationRecord
declarations: 8,
}

enum status: { draft: 0, submitted: 1 }
# If you want to add a status, be sure to add a `status_at` column to the `job_applications` table
enum status: { draft: 0, submitted: 1, shortlisted: 2, unsuccessful: 3, withdrawn: 4 }, _default: 0

belongs_to :jobseeker
belongs_to :vacancy

has_many :job_application_details, dependent: :destroy
has_many :employment_history, -> { where(details_type: "employment_history") }, class_name: "JobApplicationDetail"
has_many :references, -> { where(details_type: "references") }, class_name: "JobApplicationDetail"

private

def update_status_timestamp
self["#{status}_at"] = Time.current
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<%= t(".intro", job_title: @vacancy.job_title, organisation_name: @organisation_name) %>

# <%= t(".heading") %>
<%- if @job_application.application_data["further_instructions"].present? %>
# <%= t(".instructions") %>
<%= @job_application.application_data["further_instructions"] %>
<% end %>

# <%= t(".more_info.heading") %>
<%= t(".more_info.description", email: notify_mail_to(@contact_email)) %>
<%= t(".search.intro") %>
<%= notify_link(root_url, t(".search.link_text")) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<%= t(".intro", job_title: @vacancy.job_title, organisation_name: @organisation_name) %>

# <%= t(".heading") %>
<%- if @job_application.application_data["rejection_reasons"].present? %>
# <%= t(".feedback") %>
<%= @job_application.application_data["rejection_reasons"] %>
<% end %>

# <%= t(".more_info.heading") %>
<%= t(".more_info.description", email: notify_mail_to(@contact_email)) %>
<%= t(".search.intro") %>
<%= notify_link(root_url, t(".search.link_text")) %>
6 changes: 3 additions & 3 deletions app/views/jobseekers/job_applications/index.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@
= tag.div(location(draft_application.vacancy.parent_organisation, job_location: draft_application.vacancy.job_location))

- card.body do
= govuk_tag(text: draft_application.status, colour: "pink", classes: "govuk-!-margin-bottom-2")
= job_application_status_tag(draft_application.status)
= tag.div(card.labelled_item(t(".last_edited"), format_date(draft_application.updated_at.to_date) + I18n.t("jobs.time_at") + format_time(draft_application.updated_at)))
= tag.div(card.labelled_item(t(".closing_date"), OrganisationVacancyPresenter.new(draft_application.vacancy).application_deadline))

- card.actions do
= tag.div(govuk_link_to(t(".continue_application"), jobseekers_job_application_review_path(draft_application)))
= tag.div(govuk_link_to(t("buttons.delete"), jobseekers_job_application_confirm_destroy_path(draft_application)))

- current_jobseeker.job_applications.includes(:vacancy).order(submitted_at: :desc).submitted.each do |submitted_application|
- current_jobseeker.job_applications.includes(:vacancy).order(submitted_at: :desc).not_draft.each do |submitted_application|
= render Shared::CardComponent.new do |card|
- card.header do
= tag.div(govuk_link_to(submitted_application.vacancy.job_title, job_path(submitted_application.vacancy), class: "govuk-link--no-visited-state"))
= tag.div(location(submitted_application.vacancy.parent_organisation, job_location: submitted_application.vacancy.job_location))

- card.body do
= govuk_tag(text: submitted_application.status, colour: "blue", classes: "govuk-!-margin-bottom-2")
= job_application_status_tag(submitted_application.status)
= tag.div(card.labelled_item(t(".submitted"), format_date(submitted_application.submitted_at.to_date) + I18n.t("jobs.time_at") + format_time(submitted_application.submitted_at)))
= tag.div(card.labelled_item(t(".closing_date"), OrganisationVacancyPresenter.new(submitted_application.vacancy).application_deadline))

Expand Down
41 changes: 33 additions & 8 deletions app/views/jobseekers/job_applications/show.html.slim
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
- content_for :page_title_prefix, t(".title")
- content_for :page_title_prefix, t(".page_title")

.jobs-banner
.govuk-width-container
.govuk-grid-row
.govuk-grid-column-full
= render(Shared::BreadcrumbComponent.new(collapse_on_mobile: false,
crumbs: [{ link_path: jobseekers_job_applications_path, link_text: t("breadcrumbs.job_applications") },
{ link_path: "#", link_text: vacancy.job_title }]))
= govuk_breadcrumbs breadcrumbs: { "#{t("breadcrumbs.job_applications")}": jobseekers_job_applications_path,
"#{vacancy.job_title}": "" }

.govuk-caption-l class="govuk-!-margin-top-5" = t("jobseekers.job_applications.heading_component.heading")

h2.govuk-heading-xl class="govuk-!-margin-bottom-5" = vacancy.job_title
h1.govuk-heading-xl class="govuk-!-margin-bottom-5 govuk-!-margin-top-0" = vacancy.job_title

div
span.govuk-body class="govuk-!-margin-right-5" = t(".status_heading")
= job_application_status_tag(job_application.status)

.govuk-grid-row
.govuk-grid-column-two-thirds
h1.govuk-heading-l = t(".heading")

- if job_application.shortlisted?
= render Shared::NotificationComponent.new style: "notice", content: { title: t(".shortlist_alert.title"), body: t(".shortlist_alert.body", organisation: vacancy.parent_organisation_name) }, alert: "info", dismiss: false, background: true

// TODO: Complete in later ticket
.grey-border-box
h3.govuk-heading-m = t(".school_details.heading")
= govuk_summary_list classes: "govuk-!-margin-bottom-0" do |component|
- component.slot(:row, key: t(".school_details.name"), value: vacancy.parent_organisation_name)
- component.slot(:row, key: t(".school_details.number"), value: vacancy.contact_number)
- component.slot(:row, key: t(".school_details.email"), value: vacancy.contact_email)

- if job_application.unsuccessful? && job_application.application_data["rejection_reasons"]
.grey-border-box
h3.govuk-heading-m = t(".feedback")
p.govuk-body class="govuk-!-margin-bottom-0" = job_application.application_data["rejection_reasons"]

ol.app-task-list
li = render "jobseekers/job_applications/review/personal_details", allow_edit: false
Expand All @@ -28,7 +46,14 @@

.govuk-grid-column-one-third
.account-sidebar
h2.account-sidebar__heading = t(".assistance.heading")
h2.account-sidebar__heading = t(".timeline")

= render Shared::TimelineComponent.new do |timeline|
- timeline.date(key: t("jobseekers.job_applications.steps.application_submitted"), value: format_date(job_application.submitted_at.to_date) + I18n.t("jobs.time_at") + format_time(job_application.submitted_at))
- if job_application.unsuccessful?
- timeline.date(key: t("jobseekers.job_applications.status_timestamps.unsuccessful"),
value: format_date(job_application.unsuccessful_at.to_date))
- elsif job_application.shortlisted?
- timeline.date(key: t("jobseekers.job_applications.status_timestamps.shortlisted"),
value: format_date(job_application.shortlisted_at.to_date))
- timeline.date(key: t("jobseekers.job_applications.status_timestamps.submitted"),
value: format_date(job_application.submitted_at.to_date) + I18n.t("jobs.time_at") + format_time(job_application.submitted_at))
19 changes: 19 additions & 0 deletions app/views/publishers/vacancies/job_applications/reject.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
- content_for :page_title_prefix, t(".page_title")

.govuk-grid-row
.govuk-grid-column-two-thirds
h2.govuk-heading-m class="govuk-!-margin-bottom-0"
= "#{job_application.application_data['first_name']} #{job_application.application_data['last_name']}"
.govuk-caption-l
= t("jobseekers.job_applications.heading_component.caption",
job_title: vacancy.job_title, organisation_name: vacancy.parent_organisation_name)

h1.govuk-heading-xl class="govuk-!-margin-top-5 govuk-!-margin-bottom-5" = t(".heading")

= render Shared::NotificationComponent.new style: "danger", content: t(".alert"), alert: "info", dismiss: false

= form_for form, url: organisation_job_job_application_update_status_path(vacancy.id, job_application.id) do |f|
= f.govuk_text_area :rejection_reasons, label: { size: "s" }, rows: 10, form_group: { classes: "optional-field" }
= f.govuk_submit t("buttons.reject")

= govuk_link_to t("buttons.cancel"), organisation_job_job_application_path(vacancy.id, job_application.id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
- content_for :page_title_prefix, t(".page_title")

.govuk-grid-row
.govuk-grid-column-two-thirds
h2.govuk-heading-m class="govuk-!-margin-bottom-0"
= "#{job_application.application_data['first_name']} #{job_application.application_data['last_name']}"
.govuk-caption-l
= t("jobseekers.job_applications.heading_component.caption",
job_title: vacancy.job_title, organisation_name: vacancy.parent_organisation_name)

h1.govuk-heading-xl class="govuk-!-margin-top-5 govuk-!-margin-bottom-5" = t(".heading")

= render Shared::NotificationComponent.new style: "danger", content: t(".alert"), alert: "info", dismiss: false

= form_for form, url: organisation_job_job_application_update_status_path(vacancy.id, job_application.id) do |f|
= f.govuk_text_area :further_instructions, label: { size: "s" }, rows: 10, form_group: { classes: "optional-field" }
= f.govuk_submit t("buttons.shortlist")

= govuk_link_to t("buttons.cancel"), organisation_job_job_application_path(vacancy.id, job_application.id)
24 changes: 18 additions & 6 deletions app/views/publishers/vacancies/job_applications/show.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,38 @@
= govuk_breadcrumbs breadcrumbs: { "#{t("publishers.vacancies_component.published.tab_heading")}": jobs_with_type_organisation_path(:published),
"#{vacancy.job_title}": "",
"#{job_application.application_data["first_name"]} #{job_application.application_data["last_name"]}": "" }

.govuk-caption-l class="govuk-!-margin-top-5"
= t("jobseekers.job_applications.heading_component.caption",
job_title: vacancy.job_title, organisation_name: vacancy.parent_organisation_name)
h2.govuk-heading-xl class="govuk-!-margin-bottom-5"

h1.govuk-heading-xl class="govuk-!-margin-bottom-5 govuk-!-margin-top-0"
// TODO: Implement reference numbers
= "TV12345 - #{job_application.application_data['first_name']} #{job_application.application_data['last_name']}"

div
span.govuk-body class="govuk-!-margin-right-5" = t(".status_heading")
= govuk_tag text: "review", colour: "blue"
= publisher_job_application_status_tag(job_application.status)

.govuk-grid-row
.govuk-grid-column-two-thirds
= govuk_link_to t("buttons.shortlist"), "#", button: true, class: "govuk-!-margin-right-3"
= govuk_link_to t("buttons.reject"), "#", button: true, class: "govuk-button--warning govuk-!-margin-right-3"
= govuk_link_to t("buttons.download_application"), "#", button: true, class: "govuk-button--secondary"
.job-application-actions
- if job_application.submitted?
= govuk_link_to t("buttons.shortlist"), organisation_job_job_application_shortlist_path(vacancy.id, job_application.id), button: true, class: "govuk-!-margin-right-3"
- if job_application.submitted? || job_application.shortlisted?
= govuk_link_to t("buttons.reject"), organisation_job_job_application_reject_path(vacancy.id, job_application.id), button: true, class: "govuk-button--warning govuk-!-margin-right-3"
= govuk_link_to t("buttons.download_application"), "#", button: true, class: "govuk-button--secondary"

.govuk-grid-column-one-third
.account-sidebar
h2.account-sidebar__heading = t(".timeline")

= render Shared::TimelineComponent.new do |timeline|
- timeline.date(key: t("jobseekers.job_applications.steps.application_submitted"),
- if job_application.unsuccessful?
- timeline.date(key: t("jobseekers.job_applications.status_timestamps.rejected"),
value: format_date(job_application.unsuccessful_at.to_date) + I18n.t("jobs.time_at") + format_time(job_application.unsuccessful_at))
- elsif job_application.shortlisted?
- timeline.date(key: t("jobseekers.job_applications.status_timestamps.shortlisted"),
value: format_date(job_application.shortlisted_at.to_date) + I18n.t("jobs.time_at") + format_time(job_application.shortlisted_at))
- timeline.date(key: t("jobseekers.job_applications.status_timestamps.submitted"),
value: format_date(job_application.submitted_at.to_date) + I18n.t("jobs.time_at") + format_time(job_application.submitted_at))
5 changes: 5 additions & 0 deletions config/analytics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ shared:
- status
- created_at
- updated_at
- submitted_at
- draft_at
- shortlisted_at
- unsuccessful_at
- withdrawn_at
- jobseeker_id
- vacancy_id
- application_data
Expand Down
Loading

0 comments on commit e4d420f

Please sign in to comment.