diff --git a/app/assets/javascripts/frontend/custom_questions/support_letters.js.coffee b/app/assets/javascripts/frontend/custom_questions/support_letters.js.coffee index af3e491b8..997ec6639 100644 --- a/app/assets/javascripts/frontend/custom_questions/support_letters.js.coffee +++ b/app/assets/javascripts/frontend/custom_questions/support_letters.js.coffee @@ -5,6 +5,10 @@ window.SupportLetters = $(document).on 'change', '.js-trigger-autosave', debounce(SupportLetters.submit, 1000) + $(document).on 'click', '.js-remove-support-letter-attachment', (e) -> + e.preventDefault() + SupportLetters.removeFile($(this).closest('.govuk-form-group').find('input:first'), e) + new_item_init: (el) -> SupportLetters.clean_up_system_tags(el) SupportLetters.enable_item_fields_and_controls(el) @@ -25,12 +29,14 @@ window.SupportLetters = parent.find('.govuk-error-message').html('') parent.find('.govuk-error-message').closest('.govuk-form-group').removeClass('govuk-form-group--error') - - label = $('
') + + textContainer = parent.find('.support-letter-attachment-container') + textContainer.removeClass('govuk-!-display-none') + scanningText = '(File uploaded and is being scanned for viruses. Preview available once the scan is complete.)
' + textContainer.prepend(' ') hiddenInput = $("") - - parent.append(label) parent.append(hiddenInput) + parent.find('.js-support-letter-attachment').addClass('govuk-!-display-none') SupportLetters.autosave() SupportLetters.submit(e) @@ -61,6 +67,15 @@ window.SupportLetters = parent.find('input[type="hidden"]').remove() parent.find('.support-letter-attachment-filename').remove() + removeFile: (el, e) -> + $el = $(el) + $el.val('') + $el.siblings('.js-support-letter-attachment-id').first().val('') + $el.siblings('.support-letter-attachment-container').addClass('govuk-!-display-none') + $el.removeClass('govuk-!-display-none') + SupportLetters.autosave() + SupportLetters.submit(e) + enable_item_fields_and_controls: (parent) -> parent.find('.govuk-error-message').html('') prefixed = parent.find('.js-system-tag').data('new-hidden-input-name') diff --git a/app/assets/javascripts/frontend/form-validation.js.coffee b/app/assets/javascripts/frontend/form-validation.js.coffee index fd40f0a05..9cf164baa 100644 --- a/app/assets/javascripts/frontend/form-validation.js.coffee +++ b/app/assets/javascripts/frontend/form-validation.js.coffee @@ -12,7 +12,7 @@ window.FormValidation = clearErrors: (container) -> if container.closest(".question-financial").length > 0 container.closest("label").find(".govuk-error-message").empty() - else if container.closest('.question-block').data('answer').indexOf('address') > -1 + else if container.closest('.question-block').data('answer') && container.closest('.question-block').data('answer').indexOf('address') > -1 container.closest(".govuk-form-group").find(".govuk-error-message").empty() else container.closest(".question-block").find(".govuk-error-message").empty() @@ -56,6 +56,9 @@ window.FormValidation = isCheckboxQuestion: (question) -> question.find("input[type='checkbox']").length + isSupportLetterAttachment: (question) -> + question.find(".js-support-letter-attachment").length + toDate: (str) -> moment(str, "DD/MM/YYYY") @@ -81,6 +84,9 @@ window.FormValidation = if @isCheckboxQuestion(question) return question.find("input[type='checkbox']").filter(":checked").length + if @isSupportLetterAttachment(question) + return (question.find(".js-support-letter-attachment-id").val() || '').toString().trim().length + validateRequiredQuestion: (question) -> # if it's a conditional question, but condition was not satisfied conditional = true @@ -352,13 +358,6 @@ window.FormValidation = @addErrorMessage(question, errorMessage) return - validateSupportLetters: (question) -> - lettersReceived = $(".js-support-letter-received").length - if lettersReceived < 2 - @logThis(question, "validateSupportLetters", "Upload two letters of support") - @appendMessage(question, "Upload two letters of support") - @addErrorClass(question) - validateSelectionLimit: (question) -> selection_limit = question.data("selection-limit") current_selection_count = question.find("input[type=checkbox]:checked").length @@ -455,11 +454,6 @@ window.FormValidation = # console.log "validateDropBlockCondition" @validateDropBlockCondition(question) - if question.hasClass("question-support-requests") || - question.hasClass("question-support-uploads") - # console.log "validateSupportLetters" - @validateSupportLetters(question) - if question.hasClass("question-limited-selections") @validateSelectionLimit(question) @@ -489,7 +483,6 @@ window.FormValidation = stepContainer.find(".govuk-form-group--error").removeClass("govuk-form-group--error") stepContainer.find(".govuk-error-message").empty() $(".steps-progress-bar .js-step-link[data-step='" + currentStep + "']").removeClass("step-errors") - for question in stepContainer.find(".question-block") question = $(question) @validateIndividualQuestion(question) diff --git a/app/assets/stylesheets/admin/tables.scss b/app/assets/stylesheets/admin/tables.scss index 8d8b975f9..5c817ce6b 100644 --- a/app/assets/stylesheets/admin/tables.scss +++ b/app/assets/stylesheets/admin/tables.scss @@ -5,3 +5,44 @@ [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; + + .text-center { + text-align: center; + } + + 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; +} \ No newline at end of file diff --git a/app/assets/stylesheets/frontend/forms.scss b/app/assets/stylesheets/frontend/forms.scss index d17d03335..46a4363a9 100644 --- a/app/assets/stylesheets/frontend/forms.scss +++ b/app/assets/stylesheets/frontend/forms.scss @@ -660,6 +660,11 @@ input[type="file"] { padding: 20px !important; border: 2px solid #000 !important; + &.borderless { + border: 0 !important; + padding: 0 !important; + } + .lte-ie7 & { display: inline; width: 100%; diff --git a/app/assets/stylesheets/frontend/views/award_form.scss b/app/assets/stylesheets/frontend/views/award_form.scss index 0b32cc711..6de7f48b7 100644 --- a/app/assets/stylesheets/frontend/views/award_form.scss +++ b/app/assets/stylesheets/frontend/views/award_form.scss @@ -151,15 +151,28 @@ margin: 0.75em 0; } -.support-letter-attachment-filename, label > .visible-read-only, .view-value { margin-bottom: 10px; font-weight: normal; } -.support-letter-attachment-filename { - display: block; +.support-letter-attachment-container { + padding: 15px; + display: flex; + justify-content: space-between; + align-items: start; + background: $govuk-light-grey; + + .js-remove-support-letter-attachment{ + color: $govuk-red; + word-break: keep-all; + margin-top: 0 !important; + } + + .non-js-remove-support-letter-attachment { + color: $govuk-red; + } } .view-only, diff --git a/app/controllers/admin/protected_files_controller.rb b/app/controllers/admin/protected_files_controller.rb new file mode 100644 index 000000000..e07e900ba --- /dev/null +++ b/app/controllers/admin/protected_files_controller.rb @@ -0,0 +1,3 @@ +class Admin::ProtectedFilesController < Admin::BaseController + include ProtectedFileMixin +end \ No newline at end of file diff --git a/app/controllers/admin/statistics/nominations_controller.rb b/app/controllers/admin/statistics/nominations_controller.rb new file mode 100644 index 000000000..fc0e67529 --- /dev/null +++ b/app/controllers/admin/statistics/nominations_controller.rb @@ -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 diff --git a/app/controllers/concerns/protected_file_mixin.rb b/app/controllers/concerns/protected_file_mixin.rb new file mode 100644 index 000000000..bedce6075 --- /dev/null +++ b/app/controllers/concerns/protected_file_mixin.rb @@ -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 diff --git a/app/controllers/form/support_letter_attachments_controller.rb b/app/controllers/form/support_letter_attachments_controller.rb new file mode 100644 index 000000000..1be939752 --- /dev/null +++ b/app/controllers/form/support_letter_attachments_controller.rb @@ -0,0 +1,29 @@ +class Form::SupportLetterAttachmentsController < Form::BaseController + include FormAnswerSubmissionMixin + before_action :set_support_letter + + def show; end + + def destroy + attachment = SupportLetterAttachment.find(params[:id]) + form_answer = attachment.form_answer + + if attachment.destroy + updated_list = form_answer.document['supporter_letters_list'].reject { |letter| letter['letter_of_support'] == attachment.id } + form_answer.update(document: form_answer.document.merge(supporter_letters_list: updated_list)) + + flash[:notice] = 'Attachment successfully deleted.' + else + flash[:alert] = 'Failed to delete attachment.' + end + + redirect_to form_form_answer_supporters_path(form_answer) + end + + private + + def set_support_letter + @support_letter = SupportLetter.find(params[:support_letter_id]) + @form_answer = @support_letter.form_answer + end +end diff --git a/app/controllers/form/support_letters_controller.rb b/app/controllers/form/support_letters_controller.rb index fa581277a..0e422180a 100644 --- a/app/controllers/form/support_letters_controller.rb +++ b/app/controllers/form/support_letters_controller.rb @@ -49,7 +49,7 @@ def add_support_letters_to_document! h[:first_name] = support_letter.first_name h[:last_name] = support_letter.last_name h[:relationship_to_nominee] = support_letter.relationship_to_nominee - h[:letter_of_support] = support_letter.support_letter_attachment.id + h[:letter_of_support] = support_letter.support_letter_attachment&.id end end diff --git a/app/mailers/admin/statistics/nomination_mailer.rb b/app/mailers/admin/statistics/nomination_mailer.rb new file mode 100644 index 000000000..0477cf90c --- /dev/null +++ b/app/mailers/admin/statistics/nomination_mailer.rb @@ -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 diff --git a/app/models/admin.rb b/app/models/admin.rb index 532f6861e..fb2f24096 100644 --- a/app/models/admin.rb +++ b/app/models/admin.rb @@ -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: [ diff --git a/app/models/assessor.rb b/app/models/assessor.rb index 4a69cf018..69cfed63e 100644 --- a/app/models/assessor.rb +++ b/app/models/assessor.rb @@ -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, diff --git a/app/models/group_leader.rb b/app/models/group_leader.rb index 874cb95d1..92081be88 100644 --- a/app/models/group_leader.rb +++ b/app/models/group_leader.rb @@ -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) } diff --git a/app/models/lieutenant.rb b/app/models/lieutenant.rb index 3f56165ef..a335f7ac4 100644 --- a/app/models/lieutenant.rb +++ b/app/models/lieutenant.rb @@ -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) @@ -30,7 +32,6 @@ class Lieutenant < ApplicationRecord } } - def assigned_nominations nominations_scope end diff --git a/app/models/protected_file.rb b/app/models/protected_file.rb new file mode 100644 index 000000000..31403a0cb --- /dev/null +++ b/app/models/protected_file.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 5c50b2a24..c0a19b002 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/policies/statistics_policy.rb b/app/policies/statistics_policy.rb new file mode 100644 index 000000000..0a40f801f --- /dev/null +++ b/app/policies/statistics_policy.rb @@ -0,0 +1,9 @@ +class StatisticsPolicy < ApplicationPolicy + def index? + admin? + end + + def send? + admin? + end +end diff --git a/app/search/nomination_stats_search.rb b/app/search/nomination_stats_search.rb new file mode 100644 index 000000000..cd05edb16 --- /dev/null +++ b/app/search/nomination_stats_search.rb @@ -0,0 +1,61 @@ +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{ + CASE WHEN ceremonial_counties.name IS NULL THEN 'Not assigned' ELSE ceremonial_counties.name END 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: FormAnswerStatus::AdminFilter.values('assigned county') + } + } + end + + def results + super + + @search_results = @search_results + .select(FETCH_QUERY) + .left_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 + +end diff --git a/app/uploaders/protected_file_uploader.rb b/app/uploaders/protected_file_uploader.rb new file mode 100644 index 000000000..d58837c3b --- /dev/null +++ b/app/uploaders/protected_file_uploader.rb @@ -0,0 +1,5 @@ +class ProtectedFileUploader < FileUploader + def extension_allowlist + super + %w(csv) + end +end diff --git a/app/views/admin/statistics/nomination_mailer/notify.text.erb b/app/views/admin/statistics/nomination_mailer/notify.text.erb new file mode 100644 index 000000000..62c1a7e9c --- /dev/null +++ b/app/views/admin/statistics/nomination_mailer/notify.text.erb @@ -0,0 +1,10 @@ +Dear <%= @admin.first_name %>, + +You requested an export of nomination statistics. + +Please find this report on the link below: +<%= admin_protected_file_url(@file) %> + +Kind regards, + +The King's Awards Office diff --git a/app/views/admin/statistics/nominations/_list.html.slim b/app/views/admin/statistics/nominations/_list.html.slim new file mode 100644 index 000000000..62d71c2cb --- /dev/null +++ b/app/views/admin/statistics/nominations/_list.html.slim @@ -0,0 +1,106 @@ +ruby: + results = object.results + +div role="region" aria-labelledby="table-list-nomination-statistics-caption" tabindex="0" + table.govuk-table.kavs-table#table-list-nomination-statistics + caption.govuk-visually-hidden#table-list-nomination-statistics-caption + | nomination statistics report + colgroup + col style="width: 135px;" + col style="width: 110px;" + col style="width: 55px;" + col style="width: 90px;" + col style="width: 90px;" + col style="width: auto;" + col style="width: 110px;" + col style="width: auto;" + col style="width: 110px;" + col style="width: auto;" + col style="width: 95px;" + col style="width: auto;" + thead.govuk-table__head + tr.govuk-table__row + th.sortable.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" rowspan="2" + = sort_link f, 'Lieutenancy', object, :ceremonial_county_name + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" rowspan="2" + | Submitted nominations + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right.text-center scope="col" colspan="4" + | Eligibility + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right.text-center scope="col" colspan="2" + | Local Assessment + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right.text-center scope="col" colspan="2" + | National Assessment + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" rowspan="2" + | Royal Approval - Awarded + th.govuk-table__header.kavs-table__header--dense scope="col" rowspan="2" + | Total + tr.govuk-table__row + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" + | Admin eligible + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" + | Not eligible nominator + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" + | Not eligible group + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" + | Withdrawn + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" + | Not recommended + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" + | Recommended + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" + | Not recommended + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right scope="col" + | Recommended + tbody.govuk-table__body + - results.each do |row| + tr.govuk-table__row + th.govuk-table__header.kavs-table__header--dense.kavs-table__header--border-right + = row[:ceremonial_county_name] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:submitted_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:admin_eligible_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:admin_not_eligible_nominator_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:admin_not_eligible_group_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:withdrawn_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:local_assessment_not_recommended_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:local_assessment_recommended_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:not_recommended_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:shortlisted_count] + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = row[:awarded_count] + td.govuk-table__cell.kavs-table__cell--dense + = row[:total_count] + tfoot.govuk-table__footer + tr.govuk-table__row + th.kavs-table__footer--dense.kavs-table__footer--border-right + | Total + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:submitted_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:admin_eligible_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:admin_not_eligible_nominator_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:admin_not_eligible_group_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:withdrawn_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:local_assessment_not_recommended_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:local_assessment_recommended_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:not_recommended_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:shortlisted_count) + td.govuk-table__cell.kavs-table__cell--dense.kavs-table__cell--border-right + = results.sum(&:awarded_count) + td.govuk-table__cell.kavs-table__cell--dense + = results.sum(&:total_count) diff --git a/app/views/admin/statistics/nominations/index.html.slim b/app/views/admin/statistics/nominations/index.html.slim new file mode 100644 index 000000000..5dc75466c --- /dev/null +++ b/app/views/admin/statistics/nominations/index.html.slim @@ -0,0 +1,44 @@ +- title "Nomination statistics" + +h1.govuk-heading-xl + | Nomination statistics + += simple_form_for @search, url: "#", method: :get, as: :search, html: { id: "nomination-statistics-form", class: "search-form" } do |f| + = f.simple_fields_for [:filters, @search.filters] do |g| + div[class="govuk-grid-row"] + div[class="govuk-grid-column-one-quarter"] + div[class="govuk-form-group" aria-label="Award year"] + label[class="govuk-label" for="award-year-select"] + ' Award year + = g.select :year, award_years_collection, {}, { id: "award-year-select", class: "govuk-select custom-select", style: "height: 40px;", aria: { label: "award year select" } } + + = render "shared/form_answers/filters/assigned_lieutenancy_filter", g: g, options: FormAnswerStatus::AdminFilter.collection('assigned county') + + div[class="govuk-button-group"] + = f.submit "Apply filters", + class: "govuk-button", + id: "apply-nomination-filters" + + = link_to "Remove filters", + admin_statistics_nominations_path, + class: "govuk-button govuk-button--secondary", + role: "button" + + div[class="govuk-button-group"] + = link_to "Receive CSV with nomination statistics via email", + admin_statistics_nominations_path(params.to_unsafe_h), + class: "govuk-link", + method: :post + + = render partial: "list", locals: { f: f, object: @search } + +scss: + .autocomplete__input { + min-height: 2.5rem; + } + + @media (max-width: 991px) { + .autocomplete__input { + min-height: 2rem; + } + } diff --git a/app/views/assessor/form_answers/_question.html.slim b/app/views/assessor/form_answers/_question.html.slim index 9078c0863..bb0f616dd 100644 --- a/app/views/assessor/form_answers/_question.html.slim +++ b/app/views/assessor/form_answers/_question.html.slim @@ -9,9 +9,7 @@ fieldset class=question.fieldset_classes data=question.fieldset_data_hash - if question.label_as_legend? legend.govuk-label aria-label="#{ref.to_s.gsub(' ', '-')} #{question.title.blank? ? '' : ':' + question.title}" - if question.ref || question.sub_ref - span class="steps step-#{ref.to_s.parameterize} #{'if-js-hide' if question.sub_ref && !question.display_sub_ref_on_js_form}" - span.todo - = ref.to_s + = render "qae_form/question_ref", question: question, ref: ref - unless question.title.blank? - font_size = question.delegate_obj.is_a?(QaeFormBuilder::HeaderQuestion) ? "govuk-!-font-size-36" : "govuk-!-font-size-24" span class="govuk-body #{ font_size } govuk-!-font-weight-bold govuk-!-display-block" @@ -23,9 +21,7 @@ fieldset class=question.fieldset_classes data=question.fieldset_data_hash legend.govuk-label aria-label="#{ref.to_s.gsub(' ', '-')} #{question.title.blank? ? '' : ':' + question.title}" label.govuk-label for="q_#{question.key}" id="q_#{question.key}_label" aria-label="#{ref.to_s.gsub(' ', '-')}: #{question.title}" - if question.ref || question.sub_ref - span class="steps step-#{ref.to_s.parameterize} #{'if-js-hide' if question.sub_ref && !question.display_sub_ref_on_js_form}" - span.todo - = ref.to_s + = render "qae_form/question_ref", question: question, ref: ref - unless question.title.blank? span class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block" == question.title @@ -36,10 +32,6 @@ fieldset class=question.fieldset_classes data=question.fieldset_data_hash - if question.ref || question.sub_ref .if-js-hide label.govuk-label for="q_#{question.key}" aria-label="#{ref.to_s.gsub(' ', '-')}: #{question.title}" - span class="steps step-#{ref.to_s.parameterize} #{'if-js-hide' if question.sub_ref && !question.display_sub_ref_on_js_form}" - span.visuallyhidden - = ref.to_s - span.todo - = ref.to_s + = render "qae_form/question_ref", question: question, ref: ref = render "assessor/form_answers/questions/#{question.delegate_obj.class.name.demodulize.underscore}", question: question, answers: answers, attachments: attachments diff --git a/app/views/form/support_letter_attachments/show.html.slim b/app/views/form/support_letter_attachments/show.html.slim new file mode 100644 index 000000000..10b3b01ac --- /dev/null +++ b/app/views/form/support_letter_attachments/show.html.slim @@ -0,0 +1,20 @@ +- content_for :page_title, "Support Letter Attachment" + +.govuk-grid-row + .govuk-grid-column-two-thirds + h1.govuk-heading-xl Support Letter Attachment + + - if @support_letter.support_letter_attachment.present? + .govuk-summary-list + .govuk-summary-list__row + dt.govuk-summary-list__key + | File Name + dd.govuk-summary-list__value + = @support_letter.support_letter_attachment.original_filename + + = button_to "Delete Attachment", + form_form_answer_support_letter_support_letter_attachment_path(@form_answer, @support_letter, @support_letter.support_letter_attachment), + method: :delete, + class: "govuk-button govuk-button--warning" + - else + p.govuk-body No attachment found for this support letter. diff --git a/app/views/form/support_letters/_form.html.slim b/app/views/form/support_letters/_form.html.slim index 3d11283b7..e5f3cdac2 100644 --- a/app/views/form/support_letters/_form.html.slim +++ b/app/views/form/support_letters/_form.html.slim @@ -1,26 +1,51 @@ = f.simple_fields_for :support_letters do |ff| - idx = ff.options[:child_index] + 1 - li - .govuk-grid-row - .govuk-grid-column-one-half - label[class="govuk-label"] - span[class="govuk-body govuk-!-font-size-20 govuk-!-font-weight-bold govuk-!-display-block"] - = "Letter of Support #{idx}" - = ff.input :first_name, label: "First Name:", input_html: { class: "form-control" } - = ff.input :last_name, label: "Surname:", input_html: { class: "form-control" } - = ff.input :relationship_to_nominee, label: "Relationship to Group:", input_html: { class: "form-control" } - - = ff.input :manual, as: :hidden, input_html: { value: true } - = ff.input :user_id, as: :hidden, input_html: { value: current_user.id } - - = ff.simple_fields_for :support_letter_attachment, (ff.object.support_letter_attachment || ff.object.build_support_letter_attachment) do |fff| - = fff.input :attachment, as: :file, label: "Upload Letter of Support #{idx}", input_html: { class: "form-control" }, wrapper_html: { style: "margin-bottom: -1rem;" } - - if fff.object.attachment.present? - p.govuk-body.support-letter-attachment-filename - = render "shared/attachment_with_virus_check_status", item: fff.object, mount_name: :attachment - - = fff.input :attachment_cache, as: :hidden - - = fff.input :form_answer_id, as: :hidden, input_html: { value: @form_answer.id } - = fff.input :user_id, as: :hidden, input_html: { value: current_user.id } + - first_or_second = idx == 1 ? "first" : "second" + li.borderless + .question-block + label[class="govuk-label"] + legend.govuk-label + = render "qae_form/question_ref", question: question, ref: "C #{idx}" + span[class="govuk-body govuk-!-font-size-36 govuk-!-font-weight-bold govuk-!-display-block"] + = "The #{first_or_second} letter of support" + legend.govuk-label aria-label="C #{idx}.1: Name" + = render "qae_form/question_ref", question: question, ref: "C #{idx}.1" + span[class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block"] + = "Name of the person who wrote the #{first_or_second} letter of support" + = ff.input :first_name, label: "First name:", input_html: { class: "form-control medium" } + = ff.input :last_name, label: "Surname:", input_html: { class: "form-control medium" } + + legend.govuk-label aria-label="C #{idx}.2: Relationship to Group" + = render "qae_form/question_ref", question: question, ref: "C #{idx}.2" + label for="form_answer_support_letters_attributes_#{ff.options[:child_index]}_relationship_to_nominee" class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block" + = "Relationship to group" + span.question-context.question-debug.govuk-hint + ' For example, a beneficiary of the group, local resident or member of a partner charity. + = ff.input :relationship_to_nominee, label: false, input_html: { class: "form-control medium" } + + = ff.input :manual, as: :hidden, input_html: { value: true } + = ff.input :user_id, as: :hidden, input_html: { value: current_user.id } + + legend.govuk-label aria-label="C #{idx}.3: Upload Letter of Support" + = render "qae_form/question_ref", question: question, ref: "C #{idx}.3" + label for="form_answer_support_letters_attributes_#{ff.options[:child_index]}_support_letter_attachment" class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block" + = "Upload the #{first_or_second} letter of support" + span.question-context.question-debug.govuk-hint + ' If you upload the wrong file, click the 'Remove' link next to the file name to delete it. The file upload button will reappear, allowing you to select the correct file. + + = ff.simple_fields_for :support_letter_attachment, (ff.object.support_letter_attachment || ff.object.build_support_letter_attachment) do |fff| + + - if ff.object.support_letter_attachment.id.present? + p.govuk-body.support-letter-attachment-container class="govuk-!-font-size-19" + = render "shared/attachment_with_virus_check_status", item: fff.object, mount_name: :attachment + - if ff.object.support_letter_attachment.id.present? + = link_to 'Remove', form_form_answer_support_letter_support_letter_attachment_path(@form_answer, ff.object, ff.object.support_letter_attachment), method: :delete, class: 'govuk-link non-js-remove-support-letter-attachment govuk-!-font-size-19' + + = fff.input :attachment_cache, as: :hidden + + = fff.input :form_answer_id, as: :hidden, input_html: { value: @form_answer.id } + = fff.input :user_id, as: :hidden, input_html: { value: current_user.id } + + - unless fff.object.id.present? + = fff.input :attachment, as: :file, label: false, input_html: { class: "form-control" }, wrapper_html: { style: "margin-bottom: -1rem;" } diff --git a/app/views/form/supporters/index.html.slim b/app/views/form/supporters/index.html.slim index e5d73b67f..27f6e9caa 100644 --- a/app/views/form/supporters/index.html.slim +++ b/app/views/form/supporters/index.html.slim @@ -15,19 +15,10 @@ h1.govuk-heading-xl article.group role="article" div - letters_intro_question = @step.questions.detect { |q| q.key == :supporter_letters_list } + - letters_context = @step.questions.detect { |q| q.key == :supporter_letters_list_context} - checkbox_questions = @step.questions.select { |q| q.key.in?(%i(independent_individual not_nominator)) } - .question-block - label.govuk-label - span class="steps step-d-1" - span.visuallyhidden - ' C 1 - span.todo - ' C 1 - span class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block" - ' Upload your letters of support - - == letters_intro_question&.context + == letters_context&.context - for help in (letters_intro_question&.hint || []) details.govuk-details data-module="govuk-details" @@ -45,7 +36,7 @@ h1.govuk-heading-xl - f.object.support_letters.build unless f.object.support_letters[n].present? ul.list-add.supporters-list - = render partial: "form/support_letters/form", locals: { f: f } + = render partial: "form/support_letters/form", locals: { f: f, question: letters_intro_question } - checkbox_questions.each do |question| .govuk-form-group diff --git a/app/views/layouts/govuk_template.html.erb b/app/views/layouts/govuk_template.html.erb index e536f6ee1..aff39db55 100644 --- a/app/views/layouts/govuk_template.html.erb +++ b/app/views/layouts/govuk_template.html.erb @@ -31,7 +31,7 @@ <%= stylesheet_pack_tag 'application' %> - + diff --git a/app/views/layouts/navbar/_admin.html.slim b/app/views/layouts/navbar/_admin.html.slim index ffbd9c17c..45575f594 100644 --- a/app/views/layouts/navbar/_admin.html.slim +++ b/app/views/layouts/navbar/_admin.html.slim @@ -5,6 +5,10 @@ li.govuk-header__navigation-item class=('active' if controller_name == 'form_answers') = link_to "Nominations", admin_form_answers_path, class: 'govuk-header__link' +- if policy(:statistics).index? + li.govuk-header__navigation-item class=('active' if controller_name == 'nominations') + = link_to "Statistics", admin_statistics_nominations_path, class: 'govuk-header__link' + - if policy(:user).index? li.govuk-header__navigation-item class=('active' if %w[users admins assessors].include?(controller_name)) = link_to "Users", admin_users_path, class: 'govuk-header__link' diff --git a/app/views/qae_form/_question.html.slim b/app/views/qae_form/_question.html.slim index d916491bc..dd0e9eb5d 100644 --- a/app/views/qae_form/_question.html.slim +++ b/app/views/qae_form/_question.html.slim @@ -11,9 +11,7 @@ fieldset class=question.fieldset_classes data=question.fieldset_data_hash - if question.label_as_legend? legend.govuk-label aria-label="#{ref.to_s.gsub(' ', '-')} #{question.title.blank? ? '' : ':' + question.title}" - if question.ref || question.sub_ref - span class="steps step-#{ref.to_s.parameterize} #{'if-js-hide' if question.sub_ref && !question.display_sub_ref_on_js_form}" - span.todo - = ref.to_s + = render "qae_form/question_ref", question: question, ref: ref - unless question.title.blank? - font_size = question.delegate_obj.is_a?(QaeFormBuilder::HeaderQuestion) ? "govuk-!-font-size-36" : "govuk-!-font-size-24" span class="govuk-body #{ font_size } govuk-!-font-weight-bold govuk-!-display-block" @@ -25,9 +23,7 @@ fieldset class=question.fieldset_classes data=question.fieldset_data_hash legend.govuk-label aria-label="#{ref.to_s.gsub(' ', '-')} #{question.title.blank? ? '' : ':' + question.title}" label.govuk-label for="q_#{question.key}" id="q_#{question.key}_label" aria-label="#{ref.to_s.gsub(' ', '-')}: #{question.title}" - if question.ref || question.sub_ref - span class="steps step-#{ref.to_s.parameterize} #{'if-js-hide' if question.sub_ref && !question.display_sub_ref_on_js_form}" - span.todo - = ref.to_s + = render "qae_form/question_ref", question: question, ref: ref - unless question.title.blank? span class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block" == question.title @@ -38,11 +34,7 @@ fieldset class=question.fieldset_classes data=question.fieldset_data_hash - if question.ref || question.sub_ref .if-js-hide label.govuk-label for="q_#{question.key}" aria-label="#{ref.to_s.gsub(' ', '-')}: #{question.title}" - span class="steps step-#{ref.to_s.parameterize} #{'if-js-hide' if question.sub_ref && !question.display_sub_ref_on_js_form}" - span.visuallyhidden - = ref.to_s - span.todo - = ref.to_s + = render "qae_form/question_ref", question: question, ref: ref - if question.delegate_obj.is_a?(QaeFormBuilder::HeaderQuestion) - if question.ref || question.sub_ref diff --git a/app/views/qae_form/_question_ref.html.slim b/app/views/qae_form/_question_ref.html.slim new file mode 100644 index 000000000..16b3acf99 --- /dev/null +++ b/app/views/qae_form/_question_ref.html.slim @@ -0,0 +1,3 @@ +span class="steps step-#{ref.to_s.parameterize} #{'if-js-hide' if question.sub_ref && !question.display_sub_ref_on_js_form}" + span.todo + = ref.to_s diff --git a/app/views/qae_form/_supporter_fields.html.slim b/app/views/qae_form/_supporter_fields.html.slim index aee0fad3b..e345cd1e9 100644 --- a/app/views/qae_form/_supporter_fields.html.slim +++ b/app/views/qae_form/_supporter_fields.html.slim @@ -1,37 +1,51 @@ +- first_or_second = index == 0 ? "first" : "second" +- idx = index + 1 - persisted = supporter["support_letter_id"].present? || supporter["supporter_id"].present? - create_url = users_form_answer_support_letters_url(@form_answer) - update_url = users_form_answer_support_letter_path(@form_answer, supporter["support_letter_id"]) if persisted -li[class=class_names("js-add-example", "js-support-letter-received" => persisted) data-create-url=create_url data-update-url=update_url] + +li.borderless[class=class_names("js-add-example", "js-support-letter-received" => persisted) data-create-url=create_url data-update-url=update_url] + legend.govuk-label + = render "qae_form/question_ref", question: question, ref: "C #{idx}" label[class="govuk-label"] - span[class="govuk-body govuk-!-font-size-20 govuk-!-font-weight-bold govuk-!-display-block"] - = "Letter of Support #{index + 1}" + span[class="govuk-body govuk-!-font-size-36 govuk-!-font-weight-bold govuk-!-display-block"] + = "The #{first_or_second} letter of support" input.js-support-entry-id type="hidden" name="form[#{question.key}][#{index}][support_letter_id]" value=supporter["support_letter_id"] *possible_read_only_ops(question.step.opts[:id]) .js-system-tag data-new-hidden-input-name="form[#{question.key}][#{index}][support_letter_id]" - .govuk-form-group + legend.govuk-label aria-label="C #{index + 1}.1: Name" + = render "qae_form/question_ref", question: question, ref: "C #{idx}.1" + span class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block" + ' Name of the person who wrote the #{first_or_second} letter of support + .govuk-form-group.question-block.question-required label.govuk-label for="form[#{question.key}][#{index}][first_name]" - ' First Name: + ' First name: span.govuk-error-message input.js-support-letter-field.js-support-letter-first-name.js-trigger-autosave.govuk-input autocomplete="off" class="js-trigger-autosave medium" name="form[#{question.key}][#{index}][first_name]" id="form[#{question.key}][#{index}][first_name]" type="text" value=supporter["first_name"] *possible_read_only_ops(question.step.opts[:id]) - - .govuk-form-group + .govuk-form-group.question-block.question-required label.govuk-label for="form[#{question.key}][#{index}][last_name]" ' Surname: span.govuk-error-message input.js-support-letter-field.js-support-letter-last-name.js-trigger-autosave.govuk-input autocomplete="off" class="js-trigger-autosave medium" name="form[#{question.key}][#{index}][last_name]" id="form[#{question.key}][#{index}][last_name]" type="text" value=supporter["last_name"] *possible_read_only_ops(question.step.opts[:id]) - .govuk-form-group - label.govuk-label for="form[#{question.key}][#{index}][relationship_to_nominee]" - ' Relationship to Group: + legend.govuk-label aria-label="C #{idx}.2: Relationship to Group" + = render "qae_form/question_ref", question: question, ref: "C #{idx}.2" + .govuk-form-group.question-block.question-required + label class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block" for="form[#{question.key}][#{index}][relationship_to_nominee]" + ' Relationship to group + span.question-context.question-debug.govuk-hint + ' For example, a beneficiary of the group, local resident or member of a partner charity. span.govuk-error-message input.js-support-letter-field.js-support-letter-relationship-to-nominee.js-trigger-autosave.govuk-input autocomplete="off" class="js-trigger-autosave medium" name="form[#{question.key}][#{index}][relationship_to_nominee]" id="form[#{question.key}][#{index}][relationship_to_nominee]" type="text" value=supporter["relationship_to_nominee"] *possible_read_only_ops(question.step.opts[:id]) - .govuk-form-group - label.govuk-label for="form[#{question.key}][#{index}][letter_of_support]" - = "Upload Letter of Support #{index + 1}" + legend.govuk-label aria-label="C #{idx}.3: Relationship to Group" + = render "qae_form/question_ref", question: question, ref: "C #{idx}.3" + .govuk-form-group.question-block.question-required + label class="govuk-body govuk-!-font-size-24 govuk-!-font-weight-bold govuk-!-display-block" for="form[#{question.key}][#{index}][letter_of_support]" + ' Upload the #{first_or_second} letter of support + span.question-context.question-debug.govuk-hint + ' If you upload the wrong file, click the 'Remove' link next to the file name to delete it. The file upload button will reappear, allowing you to select the correct file. span.govuk-error-message - - input class="js-support-letter-field js-trigger-autosave js-support-letter-attachment govuk-input medium" name="form[#{question.key}][#{index}][letter_of_support]" id="form[#{question.key}][#{index}][letter_of_support]" type='file' *possible_read_only_ops(question.step.opts[:id]) - - if supporter['letter_of_support'].present? - = render "support_letters/attachment", question: question, index: index, attachment_id: supporter['letter_of_support'] + input class="js-support-letter-field js-trigger-autosave js-support-letter-attachment govuk-input medium #{'govuk-!-display-none' if supporter['letter_of_support'].present?}" name="form[#{question.key}][#{index}][letter_of_support]" id="form[#{question.key}][#{index}][letter_of_support]" type='file' *possible_read_only_ops(question.step.opts[:id]) + = render "support_letters/attachment", question: question, index: index, supporter: supporter, attachment_id: supporter['letter_of_support'] diff --git a/app/views/shared/_attachment_with_virus_check_status.html.slim b/app/views/shared/_attachment_with_virus_check_status.html.slim index bef7d0bb7..4b9bcb9e5 100644 --- a/app/views/shared/_attachment_with_virus_check_status.html.slim +++ b/app/views/shared/_attachment_with_virus_check_status.html.slim @@ -7,12 +7,14 @@ class: 'govuk-link' - elsif scan_results == "scanning" || scan_results == "pending" = item.try(:original_filename) - | (scanning on viruses) + br + .govuk-hint + | (File uploaded and is being scanned for viruses. Preview available once the scan is complete.) - elsif scan_results == "infected" = item.try(:original_filename) - | has been blocked (virus detected), please remove. + | has been blocked (virus detected), please upload another file. - else - = item.try(:original_filename) - | didn't pass virus scanner check, please remove + = item.try(:original_filename) || "File" + | didn't pass virus scanner check, please upload another file. - else = item.try(:original_filename) diff --git a/app/views/support_letters/_attachment.html.slim b/app/views/support_letters/_attachment.html.slim index 611b93a17..2d5dd4906 100644 --- a/app/views/support_letters/_attachment.html.slim +++ b/app/views/support_letters/_attachment.html.slim @@ -1,5 +1,8 @@ - file = support_letter_attachments[attachment_id.to_i] -p.govuk-body.support-letter-attachment-filename - = render "shared/attachment_with_virus_check_status", item: file, mount_name: :attachment +.support-letter-attachment-container class="#{'govuk-!-display-none' unless attachment_id.present?}" + .support-letter-attachment-filename class="govuk-body govuk-!-font-size-19" + = render "shared/attachment_with_virus_check_status", item: file, mount_name: :attachment + button.govuk-link.js-remove-support-letter-attachment class="govuk-!-font-size-19" data-module="govuk-button" type="button" + | Remove input.js-support-letter-attachment-id type="hidden" name="form[#{question.key}][#{index}][letter_of_support]" value=attachment_id *possible_read_only_ops diff --git a/config/routes.rb b/config/routes.rb index e9bb73c10..8c3d0638e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -104,7 +104,9 @@ namespace :form do resources :form_answers do resources :supporters, only: [:new, :create, :index, :destroy] - resources :support_letters, only: [:create] + resources :support_letters, only: [:create] do + resources :support_letter_attachments, only: [:show, :destroy] + end resources :form_attachments, only: [:index, :new, :create, :destroy] end end @@ -268,6 +270,12 @@ patch 'update_password' end end + + namespace :statistics do + resources :nominations, only: [:index, :create], path_names: { create: :send } + end + + resources :protected_files, path: "/files", only: :show end namespace :lieutenant do diff --git a/db/migrate/20240819123818_enable_extension_pgcrypto.rb b/db/migrate/20240819123818_enable_extension_pgcrypto.rb new file mode 100644 index 000000000..5c1efa5ad --- /dev/null +++ b/db/migrate/20240819123818_enable_extension_pgcrypto.rb @@ -0,0 +1,5 @@ +class EnableExtensionPgcrypto < ActiveRecord::Migration[7.1] + def change + enable_extension("pgcrypto") + end +end diff --git a/db/migrate/20240819143818_create_protected_files.rb b/db/migrate/20240819143818_create_protected_files.rb new file mode 100644 index 000000000..df5ae61a9 --- /dev/null +++ b/db/migrate/20240819143818_create_protected_files.rb @@ -0,0 +1,11 @@ +class CreateProtectedFiles < ActiveRecord::Migration[7.1] + def change + create_table :protected_files, id: :uuid do |t| + t.belongs_to :entity, polymorphic: true + t.string :file + + t.datetime :created_at, precision: 6, null: false + t.datetime :last_downloaded_at, precision: 6 + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 0fae4c66c..a5798ab4c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10,24 +10,31 @@ SET client_min_messages = warning; SET row_security = off; -- --- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- Name: hstore; Type: EXTENSION; Schema: -; Owner: - -- --- *not* creating schema, since initdb creates it +CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public; -- --- Name: hstore; Type: EXTENSION; Schema: -; Owner: - +-- Name: EXTENSION hstore; Type: COMMENT; Schema: -; Owner: - -- -CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public; +COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs'; -- --- Name: EXTENSION hstore; Type: COMMENT; Schema: -; Owner: - +-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - -- -COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs'; +CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; + + +-- +-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; SET default_tablespace = ''; @@ -1049,6 +1056,20 @@ CREATE SEQUENCE public.previous_wins_id_seq ALTER SEQUENCE public.previous_wins_id_seq OWNED BY public.previous_wins.id; +-- +-- Name: protected_files; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.protected_files ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + entity_type character varying, + entity_id bigint, + file character varying, + created_at timestamp(6) without time zone NOT NULL, + last_downloaded_at timestamp(6) without time zone +); + + -- -- Name: scans; Type: TABLE; Schema: public; Owner: - -- @@ -3052,6 +3073,14 @@ ALTER TABLE ONLY public.previous_wins ADD CONSTRAINT previous_wins_pkey PRIMARY KEY (id); +-- +-- Name: protected_files protected_files_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.protected_files + ADD CONSTRAINT protected_files_pkey PRIMARY KEY (id); + + -- -- Name: scans scans_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -3432,6 +3461,13 @@ CREATE INDEX index_palace_attendees_on_palace_invite_id ON public.palace_attende CREATE INDEX index_palace_invites_on_form_answer_id ON public.palace_invites USING btree (form_answer_id); +-- +-- Name: index_protected_files_on_entity; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_protected_files_on_entity ON public.protected_files USING btree (entity_type, entity_id); + + -- -- Name: index_scans_on_uuid; Type: INDEX; Schema: public; Owner: - -- @@ -3654,6 +3690,8 @@ ALTER TABLE ONLY public.support_letters SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('20240819143818'), +('20240819123818'), ('20240216144428'), ('20211214111643'), ('20211104074415'), diff --git a/forms/award_years/v2025/qavs/qavs_step3.rb b/forms/award_years/v2025/qavs/qavs_step3.rb index d8b99639f..acfb87a1e 100644 --- a/forms/award_years/v2025/qavs/qavs_step3.rb +++ b/forms/award_years/v2025/qavs/qavs_step3.rb @@ -6,36 +6,35 @@ def qavs_step3Please note your answers are being saved automatically in the background.
) - supporters :supporter_letters_list, "" do - ref "C 1" + header :supporter_letters_list_context, "" do context %(Letters of support are an essential part of your nomination, as they help to clarify and explain the impact of the nominated group's work in the local community. You will need to provide 2 letters of support alongside your nomination.
For more information on what letters can cover, please see the Letters of Support page on our website.
-Key criteria:
+Key criteria:
For each letter uploaded below, please note the letter writer's relationship to the group, for example: a beneficiary of the group, local resident or member of a partner charity.
-Once uploaded, all files are saved automatically. If you make a mistake and upload the wrong letter, please use the same button to upload the correct file as it will automatically replace the previous version.
) pdf_context_with_header_blocks [ [:normal, %(Letters of support are an essential part of your nomination, as they help to clarify and explain the impact of the nominated group's work in the local community. You will need to provide 2 letters of support alongside your nomination.)], [:normal, %(For more information on what letters can cover, please see the Letters of Support page on our website.)], - [:normal, %(Key criteria: + [:bold, %(Key criteria: - 1. letters must be written by individuals who are familiar with the group's work, for example: a beneficiary, local resident or member of a partner charity - 2. letters must not be written by a volunteer, employee, trustee, or anyone involved in the running of the group - 3. letters written by the nominator will be ineligible - 4. each letter should be no more than 500 words - 5. only 2 letters of support can be submitted + 1. Letters must be written by individuals who are familiar with the group's work, for example: a beneficiary, local resident or member of a partner charity. + 2. Letters must not be written by a volunteer, employee, trustee, or anyone involved in the running of the group. + 3. Letters written by the nominator will be ineligible. + 4. Each letter should be no more than 500 words. + 5. Only 2 letters of support can be submitted. )], - [:normal, %(For each letter uploaded below, please note the letter writer's relationship to the group, for example: a beneficiary of the group, local resident or member of a partner charity.)], - [:bold, %(Once uploaded, all files are saved automatically. If you make a mistake and upload the wrong letter, please use the same button to upload the correct file as it will automatically replace the previous version.)] ] + end + + supporters :supporter_letters_list, "" do + ref "C 1" classes "question-support-uploads" limit 2 default 2