Skip to content

Commit

Permalink
Merge pull request #711 from bitzesty/nomination-statistics-report
Browse files Browse the repository at this point in the history
[KAVS0824] Nomination statistics report
  • Loading branch information
TheDancingClown authored Aug 22, 2024
2 parents 484b968 + 316aa5c commit 21328d0
Show file tree
Hide file tree
Showing 22 changed files with 559 additions and 7 deletions.
37 changes: 37 additions & 0 deletions app/assets/stylesheets/admin/tables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,40 @@
[role="region"][aria-labelledby][tabindex]:focus {
outline: .1em solid rgba(0,0,0,.1);
}

.kavs-table {
border-top: 1px solid #b1b4b6;
border-bottom: 1px solid #b1b4b6;
width: max-content;

thead {
background-color: #f2f2f2;

.govuk-table__header:last-child,
.govuk-table__cell:last-child,
.govuk-table__footer:last-child {
padding-right: 10px;
}
}

tfoot {
&.govuk-table__footer {
background-color: #f2f2f2;
font-weight: 700;
text-align: left;
}
}
}

.kavs-table__header--dense,
.kavs-table__cell--dense,
.kavs-table__footer--dense {
font-size: 1rem;
padding: 10px
}

.kavs-table__header--border-right,
.kavs-table__cell--border-right,
.kavs-table__footer--border-right {
border-right: 1px solid #b1b4b6;
}
3 changes: 3 additions & 0 deletions app/controllers/admin/protected_files_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Admin::ProtectedFilesController < Admin::BaseController
include ProtectedFileMixin
end
101 changes: 101 additions & 0 deletions app/controllers/admin/statistics/nominations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
class Admin::Statistics::NominationsController < Admin::BaseController
def index
authorize :statistics, :index?

@search = NominationStatsSearch.new(FormAnswer.all).search(permitted_params)
end

def create
authorize :statistics, :send?

@search = NominationStatsSearch.new(FormAnswer.all).search(permitted_params)

data = generate_csv(@search.results)
file = current_admin.protected_files.create_from_raw_data(data, "nomination-statistics-export.csv")

Admin::Statistics::NominationMailer.notify(current_admin.id, file.id).deliver_now

redirect_to admin_statistics_nominations_path(search: permitted_params), success: "CSV with nomination statistics has been sent to #{current_admin.email}."
end

private

def permitted_params
params.fetch(:search, NominationStatsSearch.default_search).permit!
end

def generate_csv(data)
CSV.generate(encoding: "UTF-8", force_quotes: true) do |csv|
csv << csv_mapping.map { |m| m[:label] }
data.each do |row|
csv << csv_mapping.map do |m|
func = m[:method]
row[func]
end
end

csv << csv_mapping.map do |m|
func = m[:method]

if func == :ceremonial_county_name
"Total"
else
data.sum(&func)
end
end
end
end

def csv_mapping
[
{
label: "Lieutenancy",
method: :ceremonial_county_name
},
{
label: "Nominations submitted",
method: :submitted_count
},
{
label: "Eligiblity - Admin eligible",
method: :admin_eligible_count
},
{
label: "Eligiblity - Not eligible nominator",
method: :admin_not_eligible_nominator_count
},
{
label: "Eligiblity - Not eligible group",
method: :admin_not_eligible_group_count
},
{
label: "Eligiblity - Withdrawn",
method: :withdrawn_count
},
{
label: "Local Assessment - Not recommended",
method: :local_assessment_not_recommended_count
},
{
label: "Local Assessment - Recommended",
method: :local_assessment_recommended_count
},
{
label: "National Assessment - Not recommended",
method: :not_recommended_count
},
{
label: "National Assessment - Recommended",
method: :shortlisted_count
},
{
label: "Royal Approval - Awarded",
method: :awarded_count
},
{
label: "Total",
method: :total_count
}
]
end
end
13 changes: 13 additions & 0 deletions app/controllers/concerns/protected_file_mixin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module ProtectedFileMixin
extend ActiveSupport::Concern

def self.included(base)
base.skip_after_action :verify_authorized
end

def show
file = current_subject.protected_files.find(params[:id])
file.mark_as_downloaded!
redirect_to file.file.url, allow_other_host: true
end
end
12 changes: 12 additions & 0 deletions app/mailers/admin/statistics/nomination_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Admin::Statistics::NominationMailer < ApplicationMailer
layout "mailer"

def notify(identifier, file_identifier)
@admin = Admin.find(identifier)
@file = @admin.protected_files.find(file_identifier)

subject = "Nomination statistics export - King's Award for Voluntary Service"

view_mail ENV["GOV_UK_NOTIFY_API_TEMPLATE_ID"], to: @admin.email, subject: subject_with_env_prefix(subject)
end
end
1 change: 1 addition & 0 deletions app/models/admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Admin < ApplicationRecord
validates :first_name, :last_name, presence: true

has_many :form_answer_attachments, as: :attachable
has_many :protected_files, as: :entity, dependent: :destroy

pg_search_scope :basic_search,
against: [
Expand Down
2 changes: 2 additions & 0 deletions app/models/assessor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class Assessor < ApplicationRecord
foreign_key: :sub_group,
primary_key: :sub_group

has_many :protected_files, as: :entity, dependent: :destroy

pg_search_scope :basic_search,
against: [
:first_name,
Expand Down
2 changes: 2 additions & 0 deletions app/models/group_leader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class GroupLeader < ApplicationRecord

belongs_to :form_answer, optional: true

has_many :protected_files, as: :entity, dependent: :destroy

scope :by_email, -> { order(:email) }
scope :confirmed, -> { where.not(confirmed_at: nil) }

Expand Down
3 changes: 2 additions & 1 deletion app/models/lieutenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Lieutenant < ApplicationRecord

belongs_to :ceremonial_county

has_many :protected_files, as: :entity, dependent: :destroy

validates :first_name, :last_name, :role, :ceremonial_county, presence: true

enumerize :role, in: %w(regular advanced)
Expand All @@ -30,7 +32,6 @@ class Lieutenant < ApplicationRecord
}
}


def assigned_nominations
nominations_scope
end
Expand Down
48 changes: 48 additions & 0 deletions app/models/protected_file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class ProtectedFile < ApplicationRecord
mount_uploader :file, ProtectedFileUploader

belongs_to :entity, polymorphic: true

validates :entity_type, inclusion: { in: %w(Admin Assessor GroupLeader Lieutenant User) }, presence: true
validates :entity_id, presence: true

after_create :cleanup_tempfile

def mark_as_downloaded!
touch(:last_downloaded_at)
end

def self.build_from_raw_data(data, filename, **attrs)
@file = Tempfile.new([get_basename(filename), get_extname(filename)])
@file.write(data)
@file.close

self.build(
file: @file,
**attrs
)
end

def self.create_from_raw_data(data, filename, **attrs)
build_from_raw_data(data, filename, **attrs).tap do |record|
record.save
end
end

private

def cleanup_tempfile
@file.unlink if @file
end

def self.get_basename(filename)
File.basename(filename, get_extname(filename))
end
private_class_method :get_basename


def self.get_extname(filename)
File.extname(filename)
end
private_class_method :get_extname
end
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class User < ApplicationRecord
belongs_to :account
has_many :form_answer_attachments, as: :attachable
has_many :support_letter_attachments, dependent: :destroy

has_many :protected_files, as: :entity, dependent: :destroy
end

begin :scopes
Expand Down
9 changes: 9 additions & 0 deletions app/policies/statistics_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class StatisticsPolicy < ApplicationPolicy
def index?
admin?
end

def send?
admin?
end
end
84 changes: 84 additions & 0 deletions app/search/nomination_stats_search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
class NominationStatsSearch < Search
TRACKED_STATES = %i[
submitted
admin_eligible
admin_not_eligible_nominator
admin_not_eligible_group
withdrawn
local_assessment_not_recommended
local_assessment_recommended
not_recommended
shortlisted
awarded
]

FETCH_QUERY = %Q{
ceremonial_counties.name AS ceremonial_county_name,
#{TRACKED_STATES.map { |s| "COUNT(CASE WHEN form_answers.state = '#{s}' THEN 1 END) AS #{s}_count" }.join(',')},
COUNT(CASE WHEN form_answers.state IN (#{TRACKED_STATES.map { |s| "'#{s}'" }.join(',')}) THEN 1 END) AS total_count
}.squish.freeze

def self.default_search
{
sort: "ceremonial_county_name",
search_filter: {
year: "all_years",
assigned_ceremonial_county: ceremonial_county_options.map(&:second)
}
}
end

def results
super

@search_results = @search_results
.select(FETCH_QUERY)
.joins(:ceremonial_county)
.group("ceremonial_counties.name")

@search_results
end

def filter_by_year(scoped_results, value)
if value == "all_years"
scoped_results
else
(year = AwardYear.find_by(year: value)) ? scoped_results.where(award_year: year) : scoped_results.none
end
end

def filter_by_assigned_ceremonial_county(scoped_results, value)
value = value.map do |v|
v == "not_assigned" ? nil : v
end
scoped_results.where(ceremonial_county_id: value)
end

def sort_by_ceremonial_county_name(scoped_results, desc = false)
scoped_results.order("ceremonial_counties.name #{sort_order(desc)}")
end

class << self
def ceremonial_county_options
collection_mapping(county_options)
end

private

def collection_mapping(options)
options.map do |k, v|
[v[:label], k]
end
end

def county_options
options = Hash[not_assigned: { label: "Not assigned" }]

CeremonialCounty.ordered.collect do |county|
options[county.id] = { label: county.name }
end

options
end
end
end
5 changes: 5 additions & 0 deletions app/uploaders/protected_file_uploader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class ProtectedFileUploader < FileUploader
def extension_allowlist
super + %w(csv)
end
end
18 changes: 18 additions & 0 deletions app/views/admin/statistics/nomination_mailer/notify.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
p.govuk-body
= "Dear #{@admin.first_name},"

p.govuk-body
' You requested an export of nomination statistics.

p.govuk-body
' Please find this report on the link below:

p.govuk-body
= link_to admin_protected_file_url(@file), admin_protected_file_url(@file)

p.govuk-body
' Kind Regards,

br

' The King's Awards Office
Loading

0 comments on commit 21328d0

Please sign in to comment.