diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 997e95390..0f1918fe4 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -72,6 +72,7 @@ Rails/BulkChangeTable:
- 'db/migrate/20221014140003_add_more_trust_fields_to_support_organisations.rb'
- 'db/migrate/20230210133230_refactor_notifications_into_one_table.rb'
- 'db/migrate/20230919092926_update_lot_frameworks_framework.rb'
+ - 'db/migrate/20230919144709_add_reference_sequence_to_frameworks_framework.rb'
# Offense count: 2
# Configuration parameters: Include.
diff --git a/Gemfile b/Gemfile
index 4bdb768f9..f3b49363f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,6 +6,7 @@ ruby "3.2.1"
gem "aasm"
gem "application_insights"
+gem "ar-sequence"
gem "aws-sdk-s3", require: false
#
# awaiting PR merge here: https://github.com/Azure/azure-storage-ruby/pull/228 due to need for faraday 2
diff --git a/Gemfile.lock b/Gemfile.lock
index ccf1e8bc2..db4a0db6b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -87,6 +87,8 @@ GEM
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
application_insights (0.5.6)
+ ar-sequence (0.2.1)
+ activerecord
ast (2.4.2)
attr_required (1.0.1)
aws-eventstream (1.2.0)
@@ -668,6 +670,7 @@ PLATFORMS
DEPENDENCIES
aasm
application_insights
+ ar-sequence
aws-sdk-s3
azure-storage-blob!
better_errors
diff --git a/app/assets/stylesheets/base/_misc.scss b/app/assets/stylesheets/base/_misc.scss
index 709dd8c77..6a6ef05f7 100644
--- a/app/assets/stylesheets/base/_misc.scss
+++ b/app/assets/stylesheets/base/_misc.scss
@@ -1,6 +1,11 @@
.flex-align-center {
display: flex;
align-items: center;
+ gap: 10px;
+
+ .govuk-body {
+ margin-bottom: 0;
+ }
}
.pull-up-10 {
diff --git a/app/controllers/frameworks/evaluation_contacts_controller.rb b/app/controllers/frameworks/evaluation_contacts_controller.rb
new file mode 100644
index 000000000..1ea0bd09f
--- /dev/null
+++ b/app/controllers/frameworks/evaluation_contacts_controller.rb
@@ -0,0 +1,2 @@
+class Frameworks::EvaluationContactsController < Frameworks::ApplicationController
+end
diff --git a/app/controllers/frameworks/evaluations_controller.rb b/app/controllers/frameworks/evaluations_controller.rb
new file mode 100644
index 000000000..db0620426
--- /dev/null
+++ b/app/controllers/frameworks/evaluations_controller.rb
@@ -0,0 +1,50 @@
+class Frameworks::EvaluationsController < Frameworks::ApplicationController
+ before_action :redirect_to_register_tab, unless: :turbo_frame_request?, only: :index
+ before_action :set_form_options, only: %i[new create]
+
+ def index
+ @filtering = Frameworks::Evaluation.filtering(filter_form_params)
+ @evaluations = @filtering.results.paginate(page: params[:evaluations_page])
+ end
+
+ def show
+ @evaluation = Frameworks::Evaluation.find(params[:id])
+ @activity_log_items = @evaluation.activity_log_items.paginate(page: params[:history_page])
+ end
+
+ def new
+ @evaluation = Frameworks::Evaluation.new(framework_id: params[:framework_id])
+ end
+
+ def create
+ @evaluation = Frameworks::Evaluation.new(evaluation_params)
+
+ if @evaluation.save
+ redirect_to @evaluation
+ else
+ render :new
+ end
+ end
+
+private
+
+ def set_form_options
+ @frameworks = Frameworks::Framework.for_evaluation
+ @agents = Support::Agent.framework_evaluators
+ end
+
+ def filter_form_params
+ params.fetch(:evaluations_filter, {}).permit(
+ :sort_by, :sort_order,
+ status: []
+ )
+ end
+
+ def evaluation_params
+ params.require(:frameworks_evaluation).permit(:framework_id, :assignee_id)
+ end
+
+ def redirect_to_register_tab
+ redirect_to frameworks_root_path(anchor: "evaluations", **request.params.except(:controller, :action))
+ end
+end
diff --git a/app/controllers/frameworks/categorisations_controller.rb b/app/controllers/frameworks/framework_categorisations_controller.rb
similarity index 66%
rename from app/controllers/frameworks/categorisations_controller.rb
rename to app/controllers/frameworks/framework_categorisations_controller.rb
index 69b8ac11e..da494859e 100644
--- a/app/controllers/frameworks/categorisations_controller.rb
+++ b/app/controllers/frameworks/framework_categorisations_controller.rb
@@ -1,4 +1,4 @@
-class Frameworks::CategorisationsController < Frameworks::ApplicationController
+class Frameworks::FrameworkCategorisationsController < Frameworks::ApplicationController
def edit
@framework = Frameworks::Framework.find(params[:framework_id])
end
@@ -8,7 +8,7 @@ def update
@framework.update!(category_params)
- redirect_to @framework
+ redirect_to frameworks_framework_path(@framework, back_to: params[:back_to])
end
private
diff --git a/app/models/concerns/filter_form.rb b/app/models/concerns/filter_form.rb
index 03ed2db05..bf2ff159f 100644
--- a/app/models/concerns/filter_form.rb
+++ b/app/models/concerns/filter_form.rb
@@ -33,6 +33,10 @@ def initial_scope(proc)
define_method :scoped_records do
proc.call
end
+
+ define_method :available_sort_options do
+ proc.call.available_sort_options
+ end
end
def sort_options(proc)
diff --git a/app/models/frameworks/activity_log_item/presentable.rb b/app/models/frameworks/activity_log_item/presentable.rb
index 07f2d670a..bc73d42dd 100644
--- a/app/models/frameworks/activity_log_item/presentable.rb
+++ b/app/models/frameworks/activity_log_item/presentable.rb
@@ -1,5 +1,14 @@
module Frameworks::ActivityLogItem::Presentable
extend ActiveSupport::Concern
+
+ def activity_type_identifier
+ activity_type.demodulize.underscore
+ end
+
+ def subject_type_identifier
+ subject_type.demodulize.underscore
+ end
+
def display_subject
"#{subject.try(:activity_log_display_type)} #{subject.try(:activity_log_display_short_name)}"
end
diff --git a/app/models/frameworks/activity_loggable/activity_log_presentable.rb b/app/models/frameworks/activity_loggable/activity_log_presentable.rb
index d8488928f..a4a73edde 100644
--- a/app/models/frameworks/activity_loggable/activity_log_presentable.rb
+++ b/app/models/frameworks/activity_loggable/activity_log_presentable.rb
@@ -5,6 +5,10 @@ module Frameworks::ActivityLoggable::ActivityLogPresentable
delegate :version_at, to: :paper_trail
end
+ def specific_change_template_for(_activity_loggable_version)
+ nil
+ end
+
def activity_log_display_name
try(:full_name) || try(:short_name) || try(:name) || try(:reference)
end
diff --git a/app/models/frameworks/activity_loggable_version.rb b/app/models/frameworks/activity_loggable_version.rb
index c2c315315..cdbea95ff 100644
--- a/app/models/frameworks/activity_loggable_version.rb
+++ b/app/models/frameworks/activity_loggable_version.rb
@@ -1,8 +1,34 @@
class Frameworks::ActivityLoggableVersion < PaperTrail::Version
include Presentable
+ after_create_commit :save_default_attributes_on_item_created
+
+ def field_changes
+ object_changes.except("id", "created_at", "updated_at")
+ end
+
+ def changed_fields_only?(*fields)
+ field_changes.keys == Array(fields)
+ end
+
+ def changed_fields_including?(*fields)
+ field_changes.keys.intersection(Array(fields)).any?
+ end
+
def item_association_at_version_or_current(association:, id:, version_at: created_at)
current_version_of_association = item.class.reflections[association].klass.find_by(id:)
current_version_of_association.try(:version_at, version_at).presence || current_version_of_association
end
+
+private
+
+ def save_default_attributes_on_item_created
+ return unless item.id_previously_changed?
+
+ object_changes_including_database_default_values = item.reload.attributes.each_with_object(object_changes) do |(attr, value), all_changes|
+ all_changes[attr] ||= [nil, value]
+ end
+
+ update!(object_changes: object_changes_including_database_default_values)
+ end
end
diff --git a/app/models/frameworks/activity_loggable_version/presentable.rb b/app/models/frameworks/activity_loggable_version/presentable.rb
index 3273294c7..1ea734912 100644
--- a/app/models/frameworks/activity_loggable_version/presentable.rb
+++ b/app/models/frameworks/activity_loggable_version/presentable.rb
@@ -5,6 +5,10 @@ def presentable_changes
Frameworks::ActivityLoggableVersion::PresentableChanges.new(self)
end
+ def specific_change_template
+ item.specific_change_template_for(self)
+ end
+
def display_field_version(field:, value:)
display_value =
if field.ends_with?("_id")
diff --git a/app/models/frameworks/activity_loggable_version/presentable_changes.rb b/app/models/frameworks/activity_loggable_version/presentable_changes.rb
index 1eec7cb73..f369c871d 100644
--- a/app/models/frameworks/activity_loggable_version/presentable_changes.rb
+++ b/app/models/frameworks/activity_loggable_version/presentable_changes.rb
@@ -10,10 +10,13 @@ def initialize(version)
def each
return to_enum(:each) unless block_given?
- @version.object_changes.except("id", "created_at", "updated_at").each do |field, changes|
+ @version.field_changes.each do |field, changes|
from = @version.display_field_version(field:, value: changes.first)
to = @version.display_field_version(field:, value: changes.last)
+ # Ignore empty -> empty changes
+ next if [String(to), String(from)].all?(&:empty?)
+
yield PresentableChange.new(field:, from:, to:)
end
end
diff --git a/app/models/frameworks/evaluation.rb b/app/models/frameworks/evaluation.rb
new file mode 100644
index 000000000..e99ff932b
--- /dev/null
+++ b/app/models/frameworks/evaluation.rb
@@ -0,0 +1,13 @@
+class Frameworks::Evaluation < ApplicationRecord
+ include Frameworks::ActivityLoggable
+ include Filterable
+ include Sortable
+ include StatusChangeable
+ include Presentable
+ include ActivityLogPresentable
+
+ belongs_to :framework
+ has_one :provider, through: :framework
+ belongs_to :assignee, class_name: "Support::Agent"
+ belongs_to :contact, class_name: "Frameworks::ProviderContact", optional: true
+end
diff --git a/app/models/frameworks/evaluation/activity_log_presentable.rb b/app/models/frameworks/evaluation/activity_log_presentable.rb
new file mode 100644
index 000000000..ef5bc1692
--- /dev/null
+++ b/app/models/frameworks/evaluation/activity_log_presentable.rb
@@ -0,0 +1,8 @@
+module Frameworks::Evaluation::ActivityLogPresentable
+ extend ActiveSupport::Concern
+
+ def specific_change_template_for(activity_loggable_version)
+ return "evaluations/status" if activity_loggable_version.changed_fields_only?("status")
+ return "evaluations/contact" if activity_loggable_version.changed_fields_only?("contact_id")
+ end
+end
diff --git a/app/models/frameworks/evaluation/filterable.rb b/app/models/frameworks/evaluation/filterable.rb
new file mode 100644
index 000000000..93efc0837
--- /dev/null
+++ b/app/models/frameworks/evaluation/filterable.rb
@@ -0,0 +1,13 @@
+module Frameworks::Evaluation::Filterable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :by_status, ->(statuses) { where(status: Array(statuses)) }
+ end
+
+ class_methods do
+ def filtering(params = {})
+ Frameworks::Evaluation::Filtering.new(params)
+ end
+ end
+end
diff --git a/app/models/frameworks/evaluation/filtering.rb b/app/models/frameworks/evaluation/filtering.rb
new file mode 100644
index 000000000..0c76c03bc
--- /dev/null
+++ b/app/models/frameworks/evaluation/filtering.rb
@@ -0,0 +1,7 @@
+class Frameworks::Evaluation::Filtering
+ include FilterForm
+
+ initial_scope -> { Frameworks::Evaluation }
+
+ filter_by :status, options: -> { Frameworks::Evaluation.statuses.map { |label, _id| [label.humanize, label] } }
+end
diff --git a/app/models/frameworks/evaluation/presentable.rb b/app/models/frameworks/evaluation/presentable.rb
new file mode 100644
index 000000000..4f177a3e9
--- /dev/null
+++ b/app/models/frameworks/evaluation/presentable.rb
@@ -0,0 +1,43 @@
+module Frameworks::Evaluation::Presentable
+ extend ActiveSupport::Concern
+
+ def framework_name
+ framework.try(:name)
+ end
+
+ def framework_reference_and_name
+ framework.try(:reference_and_name)
+ end
+
+ def framework_provider_name
+ framework.try(:provider_name)
+ end
+
+ def framework_category_names
+ framework.try(:category_names)
+ end
+
+ def display_status
+ ApplicationController.render(partial: "frameworks/evaluations/status", locals: { status: })
+ end
+
+ def display_assignee
+ assignee.try(:full_name)
+ end
+
+ def display_last_updated
+ "- TODO -"
+ end
+
+ def contact_name
+ contact.try(:name)
+ end
+
+ def contact_email
+ contact.try(:email)
+ end
+
+ def contact_phone
+ contact.try(:phone)
+ end
+end
diff --git a/app/models/frameworks/evaluation/sortable.rb b/app/models/frameworks/evaluation/sortable.rb
new file mode 100644
index 000000000..b44326ad3
--- /dev/null
+++ b/app/models/frameworks/evaluation/sortable.rb
@@ -0,0 +1,27 @@
+module Frameworks::Evaluation::Sortable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :sort_by_updated, ->(direction = "descending") { order("frameworks_evaluations.updated_at #{safe_direction(direction)}") }
+ scope :sort_by_reference, ->(direction = "descending") { order("frameworks_evaluations.reference #{safe_direction(direction)}") }
+ end
+
+ class_methods do
+ def sorted_by(sort_by:, sort_order:)
+ return sort_by_reference("descending") unless sort_by.present? && sort_order.present?
+
+ public_send("sort_by_#{sort_by}", sort_order)
+ end
+
+ def available_sort_options
+ [
+ %w[Reference reference],
+ %w[Updated updated],
+ ]
+ end
+
+ def safe_direction(direction)
+ direction == "descending" ? "DESC" : "ASC"
+ end
+ end
+end
diff --git a/app/models/frameworks/evaluation/status_changeable.rb b/app/models/frameworks/evaluation/status_changeable.rb
new file mode 100644
index 000000000..19099ebbd
--- /dev/null
+++ b/app/models/frameworks/evaluation/status_changeable.rb
@@ -0,0 +1,53 @@
+module Frameworks::Evaluation::StatusChangeable
+ extend ActiveSupport::Concern
+
+ included do
+ include AASM
+
+ enum status: {
+ draft: 0,
+ in_progress: 1,
+ approved: 2,
+ not_approved: 3,
+ cancelled: 4,
+ }, _scopes: false
+
+ aasm column: :status do
+ Frameworks::Evaluation.statuses.each { |status, _| state status.to_sym }
+
+ event :start do
+ transitions from: :draft, to: :in_progress, after: :after_starting_evaluation
+ end
+
+ event :approve do
+ transitions from: :in_progress, to: :approved, after: :after_approving
+ end
+
+ event :disapprove do
+ transitions from: :in_progress, to: :not_approved, after: :after_disapproving
+ end
+
+ event :cancel do
+ transitions from: :in_progress, to: :cancelled, after: :after_cancelling
+ end
+ end
+ end
+
+private
+
+ def after_starting_evaluation
+ framework.start_evaluation!(self)
+ end
+
+ def after_approving
+ framework.approve_evaluation!(self)
+ end
+
+ def after_disapproving
+ framework.disapprove_evaluation!(self)
+ end
+
+ def after_cancelling
+ framework.cancel_evaluation!(self)
+ end
+end
diff --git a/app/models/frameworks/framework.rb b/app/models/frameworks/framework.rb
index 3cfa9c606..7459f3271 100644
--- a/app/models/frameworks/framework.rb
+++ b/app/models/frameworks/framework.rb
@@ -8,19 +8,20 @@ class Frameworks::Framework < ApplicationRecord
include ActivityEventLoggable
include Filterable
include Sortable
+ include Evaluatable
belongs_to :provider
belongs_to :provider_contact, optional: true
belongs_to :proc_ops_lead, class_name: "Support::Agent", optional: true
belongs_to :e_and_o_lead, class_name: "Support::Agent", optional: true
+ has_many :evaluations
has_many :framework_categories
has_many :support_categories, through: :framework_categories,
after_add: :log_framework_category_added,
after_remove: :log_framework_category_removed
validates :provider_id, presence: { message: "Please select a provider" }, on: :creation_form
- validates :provider_contact_id, presence: { message: "Please select a contact" }, on: :creation_form
enum lot: {
single: 0,
diff --git a/app/models/frameworks/framework/activity_event_loggable.rb b/app/models/frameworks/framework/activity_event_loggable.rb
index 20c534b39..32f2efa30 100644
--- a/app/models/frameworks/framework/activity_event_loggable.rb
+++ b/app/models/frameworks/framework/activity_event_loggable.rb
@@ -3,18 +3,20 @@ module Frameworks::Framework::ActivityEventLoggable
def activity_event_data_for(activity_event)
case activity_event.event
- when "framework_category_added", "framework_category_removed"
+ when "category_added", "category_removed"
{ support_category: Support::Category.find(activity_event.data["support_category_id"]) }
+ when "evaluation_started", "evaluation_approved", "evaluation_not_approved", "evaluation_cancelled"
+ { evaluation: Frameworks::Evaluation.find(activity_event.data["evaluation_id"]) }
end
end
protected
def log_framework_category_added(category)
- log_activity_event("framework_category_added", support_category_id: category.id)
+ log_activity_event("category_added", support_category_id: category.id)
end
def log_framework_category_removed(category)
- log_activity_event("framework_category_removed", support_category_id: category.id)
+ log_activity_event("category_removed", support_category_id: category.id)
end
end
diff --git a/app/models/frameworks/framework/activity_log_presentable.rb b/app/models/frameworks/framework/activity_log_presentable.rb
index 031c6d229..677121358 100644
--- a/app/models/frameworks/framework/activity_log_presentable.rb
+++ b/app/models/frameworks/framework/activity_log_presentable.rb
@@ -1,6 +1,14 @@
module Frameworks::Framework::ActivityLogPresentable
extend ActiveSupport::Concern
+ def specific_change_template_for(activity_loggable_version)
+ "frameworks/status" if activity_loggable_version.changed_fields_only?("status")
+ end
+
+ def activity_log_display_name
+ reference_and_name
+ end
+
def display_field_version_dfe_start_date(date)
Date.parse(date).strftime("%d/%m/%y") if date.present?
end
diff --git a/app/models/frameworks/framework/evaluatable.rb b/app/models/frameworks/framework/evaluatable.rb
new file mode 100644
index 000000000..db5cae88f
--- /dev/null
+++ b/app/models/frameworks/framework/evaluatable.rb
@@ -0,0 +1,7 @@
+module Frameworks::Framework::Evaluatable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :for_evaluation, -> { by_status(%w[pending_evaluation not_approved]) }
+ end
+end
diff --git a/app/models/frameworks/framework/filtering.rb b/app/models/frameworks/framework/filtering.rb
index 67a3e2680..9bf0d8d4d 100644
--- a/app/models/frameworks/framework/filtering.rb
+++ b/app/models/frameworks/framework/filtering.rb
@@ -4,7 +4,6 @@ class Frameworks::Framework::Filtering
get_agents = ->(type_id) { Support::Agent.by_first_name.where(id: Frameworks::Framework.pluck(type_id)) }
initial_scope -> { Frameworks::Framework }
- sort_options -> { Frameworks::Framework.available_sort_options }
filter_by :status, options: -> { Frameworks::Framework.statuses.map { |label, _id| [label.humanize, label] } }
filter_by :provider, options: -> { Frameworks::Provider.order("short_name ASC").pluck(:short_name, :id) }
diff --git a/app/models/frameworks/framework/presentable.rb b/app/models/frameworks/framework/presentable.rb
index 6971d8b5f..f36465e13 100644
--- a/app/models/frameworks/framework/presentable.rb
+++ b/app/models/frameworks/framework/presentable.rb
@@ -5,6 +5,10 @@ def provider_name
provider.name || provider.short_name
end
+ def reference_and_name
+ [reference, name.presence || short_name].compact.join(" - ")
+ end
+
def reference_and_short_name
[reference, short_name].compact.join(" - ")
end
diff --git a/app/models/frameworks/framework/sortable.rb b/app/models/frameworks/framework/sortable.rb
index 42518e761..0cf360461 100644
--- a/app/models/frameworks/framework/sortable.rb
+++ b/app/models/frameworks/framework/sortable.rb
@@ -7,7 +7,8 @@ module Frameworks::Framework::Sortable
scope :sort_by_dfe_end_date, ->(direction = "descending") { order("dfe_end_date #{safe_direction(direction)}") }
scope :sort_by_provider_start_date, ->(direction = "descending") { order("provider_start_date #{safe_direction(direction)}") }
scope :sort_by_provider_end_date, ->(direction = "descending") { order("provider_end_date #{safe_direction(direction)}") }
- scope :sort_by_reference, ->(direction = "descending") { order("reference #{safe_direction(direction)}") }
+ scope :sort_by_reference, ->(direction = "descending") { order("frameworks_frameworks.reference #{safe_direction(direction)}") }
+ scope :sort_by_provider_reference, ->(direction = "descending") { order("provider_reference #{safe_direction(direction)}") }
end
class_methods do
diff --git a/app/models/frameworks/framework/spreadsheet_importable.rb b/app/models/frameworks/framework/spreadsheet_importable.rb
index 7ce2e063a..780b9230e 100644
--- a/app/models/frameworks/framework/spreadsheet_importable.rb
+++ b/app/models/frameworks/framework/spreadsheet_importable.rb
@@ -16,7 +16,7 @@ def import_from_spreadsheet(file)
end
def import_from_spreadsheet_row(row_mapping)
- framework = find_or_initialize_by(provider: row_mapping.provider, reference: row_mapping.reference)
+ framework = find_or_initialize_by(provider: row_mapping.provider, provider_reference: row_mapping.provider_reference)
framework.source = :spreadsheet_import
framework.attributes = row_mapping.attributes
framework.save!
diff --git a/app/models/frameworks/framework/spreadsheet_importable/row_mapping.rb b/app/models/frameworks/framework/spreadsheet_importable/row_mapping.rb
index b85027e62..c4591efbd 100644
--- a/app/models/frameworks/framework/spreadsheet_importable/row_mapping.rb
+++ b/app/models/frameworks/framework/spreadsheet_importable/row_mapping.rb
@@ -7,7 +7,7 @@ def initialize(row)
def provider = Frameworks::Provider.find_or_create_by!(short_name: row["FWK Provider"])
- def reference = row["Framework Reference "]
+ def provider_reference = row["Framework Reference "]
def attributes
{
@@ -16,7 +16,7 @@ def attributes
status:,
name:,
url:,
- reference:,
+ provider_reference:,
dfe_start_date:,
dfe_end_date:,
sct_framework_provider_lead:,
diff --git a/app/models/frameworks/framework/status_changeable.rb b/app/models/frameworks/framework/status_changeable.rb
index b268e5537..e425f6e68 100644
--- a/app/models/frameworks/framework/status_changeable.rb
+++ b/app/models/frameworks/framework/status_changeable.rb
@@ -2,12 +2,52 @@ module Frameworks::Framework::StatusChangeable
extend ActiveSupport::Concern
included do
+ include AASM
+
enum status: {
pending_evaluation: 0,
evaluating: 1,
not_approved: 2,
dfe_approved: 3,
cab_approved: 4,
- }
+ }, _scopes: false
+
+ aasm column: :status do
+ Frameworks::Framework.statuses.each { |status, _| state status.to_sym }
+
+ event :start_evaluation do
+ transitions from: %i[pending_evaluation not_approved], to: :evaluating, after: :after_starting_evaluation
+ end
+
+ event :approve_evaluation do
+ transitions from: :evaluating, to: :dfe_approved, after: :after_approving_evaluation
+ end
+
+ event :disapprove_evaluation do
+ transitions from: :evaluating, to: :not_approved, after: :after_disapproving_evaluation
+ end
+
+ event :cancel_evaluation do
+ transitions from: :evaluating, to: :not_approved, after: :after_cancelling_evaluation
+ end
+ end
+ end
+
+private
+
+ def after_starting_evaluation(evaluation)
+ log_activity_event("evaluation_started", evaluation_id: evaluation.id)
+ end
+
+ def after_approving_evaluation(evaluation)
+ log_activity_event("evaluation_approved", evaluation_id: evaluation.id)
+ end
+
+ def after_disapproving_evaluation(evaluation)
+ log_activity_event("evaluation_not_approved", evaluation_id: evaluation.id)
+ end
+
+ def after_cancelling_evaluation(evaluation)
+ log_activity_event("evaluation_cancelled", evaluation_id: evaluation.id)
end
end
diff --git a/app/models/support/agent.rb b/app/models/support/agent.rb
index 379501bae..06653e565 100644
--- a/app/models/support/agent.rb
+++ b/app/models/support/agent.rb
@@ -22,8 +22,18 @@ class Agent < ApplicationRecord
belongs_to :support_tower, class_name: "Support::Tower", optional: true
belongs_to :user, foreign_key: "dsi_uid", primary_key: "dfe_sign_in_uid", optional: true
- scope :caseworkers, -> { where("ARRAY['procops', 'procops_admin'] && roles::text[]") }
+ scope :caseworkers, -> { by_role(%w[procops procops_admin]) }
scope :by_first_name, -> { order("first_name ASC, last_name ASC") }
+ scope :by_role, ->(roles) { where("ARRAY[?] && roles::text[]", Array(roles)) }
+
+ scope :framework_evaluators, lambda {
+ by_role(%w[
+ procops_admin
+ procops
+ framework_evaluator
+ framework_evaluator_admin
+ ])
+ }
scope :omnisearch, lambda { |query|
sql = <<-SQL
diff --git a/app/views/frameworks/activity_log_items/_activity_log_item.html.erb b/app/views/frameworks/activity_log_items/_activity_log_item.html.erb
index ecee3c40f..4b069aaa5 100644
--- a/app/views/frameworks/activity_log_items/_activity_log_item.html.erb
+++ b/app/views/frameworks/activity_log_items/_activity_log_item.html.erb
@@ -1,4 +1,4 @@
-<%= render "frameworks/activity_log_items/activity/#{activity_log_item.activity_type.demodulize.underscore}",
+<%= render "frameworks/activity_log_items/activity/#{activity_log_item.activity_type_identifier}",
activity_log_item:,
activity: activity_log_item.activity,
subject: activity_log_item.subject %>
diff --git a/app/views/frameworks/activity_log_items/activity/_activity_event.html.erb b/app/views/frameworks/activity_log_items/activity/_activity_event.html.erb
index 0082550d6..4e9ee0e11 100644
--- a/app/views/frameworks/activity_log_items/activity/_activity_event.html.erb
+++ b/app/views/frameworks/activity_log_items/activity/_activity_event.html.erb
@@ -1,4 +1,4 @@
-<%= render "frameworks/activity_log_items/activity/activity_event/#{activity_log_item.activity.event}",
+<%= render "frameworks/activity_log_items/activity/activity_event/#{activity_log_item.subject_type_identifier}s/#{activity_log_item.activity.event}",
activity_log_item:,
- activity: activity_log_item.activity,
- subject: activity_log_item.subject %>
+ activity:,
+ subject: %>
diff --git a/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.html.erb b/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.html.erb
index c8451f02c..93b3b6e1f 100644
--- a/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.html.erb
+++ b/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.html.erb
@@ -1,20 +1,5 @@
-<%= activity.event.humanize %>d <%= activity.item.activity_log_display_type %>
-<%= link_to "[+]", "#", title: "See changes", class: "govuk-link", "data-component" => "toggle-panel-visibility", "data-panel" => activity_log_item.id %>
-
-
-
-
- <% activity.presentable_changes.each do |changes| %>
-
-
-
- <%= changes.from.presence || "empty" %>
- ➔
- <%= changes.to.presence || "empty" %>
- |
-
- <% end %>
-
-
-
+<% resolved_template = (activity.specific_change_template.present? \
+ ? "frameworks/activity_log_items/activity/activity_loggable_version/#{activity.specific_change_template}"
+ : "frameworks/activity_log_items/activity/activity_loggable_version/presentable_changes") %>
+<%= render resolved_template, activity_log_item:, activity:, subject: %>
diff --git a/app/views/frameworks/activity_log_items/activity/activity_event/evaluations/_evaluation_started.html.erb b/app/views/frameworks/activity_log_items/activity/activity_event/evaluations/_evaluation_started.html.erb
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/views/frameworks/activity_log_items/activity/activity_event/_framework_category_added.html.erb b/app/views/frameworks/activity_log_items/activity/activity_event/frameworks/_category_added.html.erb
similarity index 100%
rename from app/views/frameworks/activity_log_items/activity/activity_event/_framework_category_added.html.erb
rename to app/views/frameworks/activity_log_items/activity/activity_event/frameworks/_category_added.html.erb
diff --git a/app/views/frameworks/activity_log_items/activity/activity_event/_framework_category_removed.html.erb b/app/views/frameworks/activity_log_items/activity/activity_event/frameworks/_category_removed.html.erb
similarity index 100%
rename from app/views/frameworks/activity_log_items/activity/activity_event/_framework_category_removed.html.erb
rename to app/views/frameworks/activity_log_items/activity/activity_event/frameworks/_category_removed.html.erb
diff --git a/app/views/frameworks/activity_log_items/activity/activity_event/frameworks/_evaluation_started.html.erb b/app/views/frameworks/activity_log_items/activity/activity_event/frameworks/_evaluation_started.html.erb
new file mode 100644
index 000000000..4debb4fce
--- /dev/null
+++ b/app/views/frameworks/activity_log_items/activity/activity_event/frameworks/_evaluation_started.html.erb
@@ -0,0 +1 @@
+Framework Evaluation started - <%= link_to "View #{activity.loaded_data.evaluation.reference}", activity.loaded_data.evaluation, class: "govuk-link", "data-turbo" => false %>
diff --git a/app/views/frameworks/activity_log_items/activity/activity_loggable_version/_presentable_changes.html.erb b/app/views/frameworks/activity_log_items/activity/activity_loggable_version/_presentable_changes.html.erb
new file mode 100644
index 000000000..20bc61698
--- /dev/null
+++ b/app/views/frameworks/activity_log_items/activity/activity_loggable_version/_presentable_changes.html.erb
@@ -0,0 +1,22 @@
+<%= activity.event.humanize %>d <%= activity.item.activity_log_display_type %>
+<%= link_to "[+]", "#", title: "See changes", class: "govuk-link", "data-component" => "toggle-panel-visibility", "data-panel" => activity_log_item.id %>
+
+
+
+
+ <% activity.presentable_changes.each do |changes| %>
+
+
+
+ <% unless activity.event == "create" %>
+ <%= changes.from.presence || "empty" %>
+ ➔
+ <% end %>
+
+ <%= changes.to.presence || "empty" %>
+ |
+
+ <% end %>
+
+
+
diff --git a/app/views/frameworks/activity_log_items/activity/activity_loggable_version/evaluations/_contact.html.erb b/app/views/frameworks/activity_log_items/activity/activity_loggable_version/evaluations/_contact.html.erb
new file mode 100644
index 000000000..a9d937476
--- /dev/null
+++ b/app/views/frameworks/activity_log_items/activity/activity_loggable_version/evaluations/_contact.html.erb
@@ -0,0 +1 @@
+Assigned contact <%= link_to activity.item.contact_name, frameworks_provider_contact_path(activity.item.contact, back_to: current_url_b64(:history)), class: "govuk-link", "data-turbo" => false %> to the evaluation
diff --git a/app/views/frameworks/activity_log_items/activity/activity_loggable_version/evaluations/_status.html.erb b/app/views/frameworks/activity_log_items/activity/activity_loggable_version/evaluations/_status.html.erb
new file mode 100644
index 000000000..292c5393a
--- /dev/null
+++ b/app/views/frameworks/activity_log_items/activity/activity_loggable_version/evaluations/_status.html.erb
@@ -0,0 +1,4 @@
+Status changed from
+<%= render "frameworks/evaluations/status", status: activity.field_changes["status"].first %>
+to
+<%= render "frameworks/evaluations/status", status: activity.field_changes["status"].last %>
diff --git a/app/views/frameworks/activity_log_items/activity/activity_loggable_version/frameworks/_status.html.erb b/app/views/frameworks/activity_log_items/activity/activity_loggable_version/frameworks/_status.html.erb
new file mode 100644
index 000000000..208d61a97
--- /dev/null
+++ b/app/views/frameworks/activity_log_items/activity/activity_loggable_version/frameworks/_status.html.erb
@@ -0,0 +1,4 @@
+Status changed from
+<%= render "frameworks/frameworks/framework_status", status: activity.field_changes["status"].first %>
+to
+<%= render "frameworks/frameworks/framework_status", status: activity.field_changes["status"].last %>
diff --git a/app/views/frameworks/dashboards/index.html.erb b/app/views/frameworks/dashboards/index.html.erb
index 817f3a19e..47d4601a4 100644
--- a/app/views/frameworks/dashboards/index.html.erb
+++ b/app/views/frameworks/dashboards/index.html.erb
@@ -6,6 +6,10 @@
<%= link_to "Frameworks Register", "#frameworks-register", class: "govuk-tabs__tab" %>
+
+ <%= link_to "Framework Evaluations", "#evaluations", class: "govuk-tabs__tab" %>
+
+
<%= link_to "Provider Contacts", "#provider-contacts", class: "govuk-tabs__tab" %>
@@ -21,6 +25,12 @@
<% end %>
+ <%= turbo_frame_tag "evaluations-frame", src: frameworks_evaluations_path(**request.params.except(:controller, :action)) do %>
+
+ <% end %>
+
<%= turbo_frame_tag "provider-contacts-frame", src: frameworks_provider_contacts_path(**request.params.except(:controller, :action)) do %>
Loading...
diff --git a/app/views/frameworks/evaluations/_evaluation.html.erb b/app/views/frameworks/evaluations/_evaluation.html.erb
new file mode 100644
index 000000000..2d85d247d
--- /dev/null
+++ b/app/views/frameworks/evaluations/_evaluation.html.erb
@@ -0,0 +1,44 @@
+
+
+
+
+
- Case ID
+ - <%= link_to evaluation.reference, frameworks_evaluation_path(evaluation, back_to: current_url_b64(:evaluations)), class: "govuk-link", "data-turbo" => false %>
+
+
+
+
- Framework Name
+ - <%= evaluation.framework_name %>
+
+
+
+
- Framework Provider
+ - <%= evaluation.framework_provider_name %>
+
+
+
+
- Categories
+ - <%= evaluation.framework_category_names %>
+
+
+
+
+
+
+
+
- Status
+ - <%= evaluation.display_status %>
+
+
+
+
- Assignee
+ - <%= evaluation.display_assignee %>
+
+
+
+
- Updated
+ - <%= evaluation.display_last_updated %>
+
+
+
+
diff --git a/app/views/frameworks/evaluations/_index_filters.html.erb b/app/views/frameworks/evaluations/_index_filters.html.erb
new file mode 100644
index 000000000..f69a07655
--- /dev/null
+++ b/app/views/frameworks/evaluations/_index_filters.html.erb
@@ -0,0 +1,28 @@
+
+
<%= I18n.t("support.case.label.filters") %>
+
+ <%= form_with model: @filtering, scope: :evaluations_filter, method: :get, url: frameworks_evaluations_path, html: { "data-controller" => "case-filters" } do |form| %>
+
+
+ <%= link_to I18n.t("support.case.filter.buttons.clear"), frameworks_evaluations_path(viewing_from_entity: params[:viewing_from_entity]), class: "govuk-button govuk-button--secondary", role: "button" %>
+
+ <%= form.govuk_select :sort_by,
+ options_for_select(@filtering.available_sort_options, @filtering.sort_by),
+ label: { text: I18n.t("support.case.label.sort_by"), size: "s" } %>
+
+ <%= form.govuk_collection_radio_buttons :sort_order,
+ available_sort_orders,
+ :id,
+ :title,
+ legend: nil,
+ small: true %>
+
+ <%= expander title: "Status", subtitle: "#{@filtering.number_of_selected(:statuses)} selected", expanded: true do %>
+ <%= form.govuk_check_boxes_fieldset :status, legend: nil, small: true, form_group: { class: "govuk-!-margin-bottom-0" } do %>
+ <% @filtering.available_status_options.each do |status| %>
+ <%= form.govuk_check_box :status, status.last, exclusive: false, label: { text: status.first } %>
+ <% end %>
+ <% end %>
+ <% end %>
+ <% end %>
+
diff --git a/app/views/frameworks/evaluations/_status.html.erb b/app/views/frameworks/evaluations/_status.html.erb
new file mode 100644
index 000000000..7e5224b24
--- /dev/null
+++ b/app/views/frameworks/evaluations/_status.html.erb
@@ -0,0 +1,12 @@
+<%# Required local: either literal status or a evaluation record %>
+<% the_status = (defined?(status) ? status : evaluation.status).inquiry %>
+
+
+ <%= "govuk-tag--purple" if the_status.evaluating? %>
+ <%= "govuk-tag--grey" if the_status.not_approved? %>
+ <%= "govuk-tag--green" if the_status.approved? %>
+ <%= "govuk-tag--red" if the_status.cancelled? %>
+">
+ <%= the_status.humanize %>
+
diff --git a/app/views/frameworks/evaluations/index.html.erb b/app/views/frameworks/evaluations/index.html.erb
new file mode 100644
index 000000000..23d87e756
--- /dev/null
+++ b/app/views/frameworks/evaluations/index.html.erb
@@ -0,0 +1,24 @@
+<%= turbo_frame_tag "evaluations-frame" do %>
+
+
+
+ <% unless params[:viewing_from_entity] == "true" %>
+ <%= link_to "Add Evalutaion", new_frameworks_evaluation_path(back_to: current_url_b64(:evaluations)), class: "govuk-button", "data-turbo" => false %>
+ <% end %>
+
+ <%= render "index_filters" %>
+
+
+
+ <%= render @evaluations %>
+
+ <% unless @evaluations.any? %>
+
No framework evaluations found
+
Try clearing or changing your filters.
+ <% end %>
+
+ <%= render "components/pagination", records: @evaluations, page_param_name: :evaluations_page, pagination_params: { anchor: "evaluations" } %>
+
+
+
+<% end %>
diff --git a/app/views/frameworks/evaluations/new.html.erb b/app/views/frameworks/evaluations/new.html.erb
new file mode 100644
index 000000000..818343c60
--- /dev/null
+++ b/app/views/frameworks/evaluations/new.html.erb
@@ -0,0 +1,20 @@
+<%= content_for :title, "GHBS | Frameworks | New Evaluation" %>
+
+
New Framework Evaluation
+
+
+ <%= form_for @evaluation do |f| %>
+ <%= f.govuk_select :framework_id, @frameworks.map {|framework| [framework.reference_and_name, framework.id] },
+ options: { include_blank: "Select a framework" },
+ label: { text: "Framework" } %>
+
+ <%= f.govuk_select :assignee_id, @agents.map {|agent| [agent.full_name, agent.id] },
+ options: { include_blank: "Select an agent" },
+ label: { text: "Assignee" } %>
+
+
+ <%= f.govuk_submit "Create evaluation" %>
+ <%= link_to "Cancel", @back_url, class: "govuk-link govuk-link--no-visited-state" %>
+
+ <% end %>
+
diff --git a/app/views/frameworks/evaluations/show.html.erb b/app/views/frameworks/evaluations/show.html.erb
new file mode 100644
index 000000000..8ef9d7782
--- /dev/null
+++ b/app/views/frameworks/evaluations/show.html.erb
@@ -0,0 +1,58 @@
+<%= content_for :title, "GHBS | Frameworks | Evaluation #{@evaluation.reference}" %>
+
+<%= turbo_frame_tag dom_id(@evaluation) do %>
+
+
[<%= @evaluation.reference %>] Framework Evaluation
+
<%= @evaluation.framework_name %>
+
+
+ <%= @evaluation.display_status %>
+ Case Owner: <%= @evaluation.display_assignee %>
+
+
+
+
+ -
+ <%= link_to "Framework", "#framework", class: "govuk-tabs__tab" %>
+
+ -
+ <%= link_to "Messages", "#messages", class: "govuk-tabs__tab" %>
+
+ -
+ <%= link_to "History", "#history", class: "govuk-tabs__tab" %>
+
+ -
+ <%= link_to "Attachments", "#attachments", class: "govuk-tabs__tab" %>
+
+
+
+ <%= turbo_frame_tag "framework-frame" do %>
+
+ <%= render "frameworks/evaluations/show/framework" %>
+
+ <% end %>
+
+ <%= turbo_frame_tag "messages-frame" do %>
+
+ <%= render "frameworks/evaluations/show/messages" %>
+
+ <% end %>
+
+ <%= turbo_frame_tag "history-frame" do %>
+
+ <%= render "frameworks/evaluations/show/history" %>
+
+ <% end %>
+
+ <%= turbo_frame_tag "attachments-frame" do %>
+
+ <%= render "frameworks/evaluations/show/attachments" %>
+
+ <% end %>
+
+
+
Actions
+
+<% end %>
diff --git a/app/views/frameworks/evaluations/show/_attachments.html.erb b/app/views/frameworks/evaluations/show/_attachments.html.erb
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/views/frameworks/evaluations/show/_framework.html.erb b/app/views/frameworks/evaluations/show/_framework.html.erb
new file mode 100644
index 000000000..10fe57fc6
--- /dev/null
+++ b/app/views/frameworks/evaluations/show/_framework.html.erb
@@ -0,0 +1,27 @@
+
+
+
- Framework Provider
+ - <%= link_to @evaluation.framework_provider_name, frameworks_provider_path(@evaluation.provider, back_to: current_url_b64(:framework)), class: "govuk-link", "data-turbo" => false %>
+
+
+
+
- Framework Name
+ - <%= link_to @evaluation.framework_reference_and_name, frameworks_framework_path(@evaluation.framework, back_to: current_url_b64(:framework)), class: "govuk-link", "data-turbo" => false %>
+
+
+
+
- Contact
+ - <%= link_to @evaluation.contact_name, frameworks_provider_contact_path(@evaluation.contact, back_to: current_url_b64(:framework)), class: "govuk-link", "data-turbo" => false %>
+ - <%= link_to "Change", "#", class: "govuk-link", "data-turbo" => false %>
+
+
+
+
- Contact Email
+ - <%= @evaluation.contact_email %>
+
+
+
+
- Contact Phone
+ - <%= @evaluation.contact_phone %>
+
+
diff --git a/app/views/frameworks/evaluations/show/_history.html.erb b/app/views/frameworks/evaluations/show/_history.html.erb
new file mode 100644
index 000000000..63d780895
--- /dev/null
+++ b/app/views/frameworks/evaluations/show/_history.html.erb
@@ -0,0 +1,2 @@
+<%= render "frameworks/activity_log_items/list", activity_log_items: @activity_log_items %>
+<%= render "components/pagination", records: @activity_log_items, page_param_name: 'history_page', pagination_params: { anchor: "history" } %>
diff --git a/app/views/frameworks/evaluations/show/_messages.html.erb b/app/views/frameworks/evaluations/show/_messages.html.erb
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/views/frameworks/categorisations/edit.html.erb b/app/views/frameworks/framework_categorisations/edit.html.erb
similarity index 92%
rename from app/views/frameworks/categorisations/edit.html.erb
rename to app/views/frameworks/framework_categorisations/edit.html.erb
index c8d074cac..5b521e99b 100644
--- a/app/views/frameworks/categorisations/edit.html.erb
+++ b/app/views/frameworks/framework_categorisations/edit.html.erb
@@ -3,7 +3,7 @@
Edit Framework Categories
<%= @framework.name %>
-<%= form_for @framework, url: frameworks_framework_categorisations_path(@framework), method: :put do |f| %>
+<%= form_for @framework, url: frameworks_framework_categorisations_path(@framework, back_to: params[:back_to]), method: :put do |f| %>
<% procurement_category_grouped_options.each do |group, options| %>
<% next if group == "Or" %>
diff --git a/app/views/frameworks/frameworks/show.html.erb b/app/views/frameworks/frameworks/show.html.erb
index 1a2588fd7..1806c09d3 100644
--- a/app/views/frameworks/frameworks/show.html.erb
+++ b/app/views/frameworks/frameworks/show.html.erb
@@ -2,7 +2,7 @@
<%= turbo_frame_tag dom_id(@framework) do %>
-<%= @framework.reference_and_short_name %>
+[<%= @framework.reference %>] Framework
<%= @framework.name %>
<%= @framework.display_status %>
@@ -14,9 +14,12 @@
<%= link_to "Provider", "#framework-provider", class: "govuk-tabs__tab" %>
+
+
+ <%= link_to "Evaluations", "#framework-evaluations", class: "govuk-tabs__tab" %>
- <%= link_to "History", "#framework-activity", class: "govuk-tabs__tab" %>
+ <%= link_to "History", "#history", class: "govuk-tabs__tab" %>
@@ -32,9 +35,15 @@
<% end %>
- <%= turbo_frame_tag "framework-activity-frame" do %>
-
- <%= render "frameworks/frameworks/show/framework_activity" %>
+ <%= turbo_frame_tag "framework-evaluations-frame" do %>
+
+ <%= render "frameworks/frameworks/show/framework_evaluations" %>
+
+ <% end %>
+
+ <%= turbo_frame_tag "history-frame" do %>
+
+ <%= render "frameworks/frameworks/show/framework_history" %>
<% end %>
diff --git a/app/views/frameworks/frameworks/show/_framework_evaluations.html.erb b/app/views/frameworks/frameworks/show/_framework_evaluations.html.erb
new file mode 100644
index 000000000..04b5734c3
--- /dev/null
+++ b/app/views/frameworks/frameworks/show/_framework_evaluations.html.erb
@@ -0,0 +1,22 @@
+<%= link_to "Add Evaluation", new_frameworks_evaluation_path(framework_id: @framework.id, back_to: current_url_b64(:framework_evaluations)), class: "govuk-button", "data-turbo" => false %>
+
+
+
+
+
+
+
+
+
+
+
+ <% @framework.evaluations.each do |evaluation| %>
+
+ <%= link_to evaluation.reference, frameworks_evaluation_path(evaluation, back_to: current_url_b64(:framework_evaluations)), class: "govuk-link", "data-turbo" => false %> |
+ <%= evaluation.display_status %> |
+ <%= evaluation.display_assignee %> |
+ <%= evaluation.display_last_updated %> |
+
+ <% end %>
+
+
diff --git a/app/views/frameworks/frameworks/show/_framework_activity.html.erb b/app/views/frameworks/frameworks/show/_framework_history.html.erb
similarity index 100%
rename from app/views/frameworks/frameworks/show/_framework_activity.html.erb
rename to app/views/frameworks/frameworks/show/_framework_history.html.erb
diff --git a/app/views/frameworks/frameworks/show/_framework_provider.html.erb b/app/views/frameworks/frameworks/show/_framework_provider.html.erb
index 24a6a3d0c..d65fab083 100644
--- a/app/views/frameworks/frameworks/show/_framework_provider.html.erb
+++ b/app/views/frameworks/frameworks/show/_framework_provider.html.erb
@@ -4,6 +4,11 @@
<%= link_to @framework.provider_name, frameworks_provider_path(@framework.provider, back_to: current_url_b64(:framework_provider)), class: "govuk-link", "data-turbo" => false if @framework.provider.present? %>
+
+
Provider Reference
+ <%= @framework.provider_reference %>
+
+
SCT Provider Lead
<%= @framework.sct_framework_provider_lead %>
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index 1065b1d9b..e1175f4b1 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -25,7 +25,7 @@
{
"type": "template",
"name": "frameworks/frameworks/show",
- "line": 25,
+ "line": 28,
"file": "app/views/frameworks/frameworks/show.html.erb",
"rendered": {
"name": "frameworks/frameworks/show/_framework_details",
@@ -67,6 +67,40 @@
],
"note": ""
},
+ {
+ "warning_type": "Dynamic Render Path",
+ "warning_code": 15,
+ "fingerprint": "0bb9d316930182410224e235ece8dd8591c92966fb97a1f3520c82420bf7a066",
+ "check_name": "Render",
+ "message": "Render path contains parameter value",
+ "file": "app/views/frameworks/evaluations/index.html.erb",
+ "line": 13,
+ "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
+ "code": "render(action => Frameworks::Evaluation.filtering(filter_form_params).results.paginate(:page => params[:evaluations_page]), {})",
+ "render_path": [
+ {
+ "type": "controller",
+ "class": "Frameworks::EvaluationsController",
+ "method": "index",
+ "line": 8,
+ "file": "app/controllers/frameworks/evaluations_controller.rb",
+ "rendered": {
+ "name": "frameworks/evaluations/index",
+ "file": "app/views/frameworks/evaluations/index.html.erb"
+ }
+ }
+ ],
+ "location": {
+ "type": "template",
+ "template": "frameworks/evaluations/index"
+ },
+ "user_input": "params[:evaluations_page]",
+ "confidence": "Weak",
+ "cwe_id": [
+ 22
+ ],
+ "note": ""
+ },
{
"warning_type": "Dynamic Render Path",
"warning_code": 15,
@@ -265,7 +299,7 @@
"check_name": "MassAssignment",
"message": "Specify exact keys allowed for mass assignment instead of using `permit!` which allows any keys",
"file": "app/controllers/support/cases_controller.rb",
- "line": 140,
+ "line": 142,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.fetch(:tower, {}).fetch(tab, {}).permit!",
"render_path": null,
@@ -350,6 +384,6 @@
"note": ""
}
],
- "updated": "2023-09-14 15:05:19 +0100",
+ "updated": "2023-09-28 11:18:30 +0100",
"brakeman_version": "6.0.1"
}
diff --git a/config/routes.rb b/config/routes.rb
index c5ccfa9d7..2f866aef6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -301,11 +301,15 @@
# Frameworks portal
namespace :frameworks do
root to: "dashboards#index"
+
+ resources :evaluations do
+ resource :contacts, only: %i[edit update], controller: :evaluation_contacts
+ end
resources :frameworks do
- resource :categorisations, only: %w[edit update]
+ resource :categorisations, only: %i[edit update], controller: :framework_categorisations
end
- resources :provider_contacts
resources :providers
+ resources :provider_contacts
namespace :management do
get "/", to: "management#index"
diff --git a/db/migrate/20230919142106_create_frameworks_evaluations.rb b/db/migrate/20230919142106_create_frameworks_evaluations.rb
new file mode 100644
index 000000000..d76f8cfae
--- /dev/null
+++ b/db/migrate/20230919142106_create_frameworks_evaluations.rb
@@ -0,0 +1,14 @@
+class CreateFrameworksEvaluations < ActiveRecord::Migration[7.0]
+ def change
+ create_sequence :evaluation_refs
+ create_table :frameworks_evaluations, id: :uuid do |t|
+ t.string :reference, default: -> { "'FE' || nextval('evaluation_refs')" }
+ t.uuid :framework_id
+ t.uuid :assignee_id
+ t.uuid :contact_id
+ t.integer :status, default: 0
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20230919144709_add_reference_sequence_to_frameworks_framework.rb b/db/migrate/20230919144709_add_reference_sequence_to_frameworks_framework.rb
new file mode 100644
index 000000000..4afaf3e6a
--- /dev/null
+++ b/db/migrate/20230919144709_add_reference_sequence_to_frameworks_framework.rb
@@ -0,0 +1,7 @@
+class AddReferenceSequenceToFrameworksFramework < ActiveRecord::Migration[7.0]
+ def change
+ create_sequence :framework_refs
+ change_column_default :frameworks_frameworks, :reference, to: -> { "'F' || nextval('framework_refs')" }, from: nil
+ add_column :frameworks_frameworks, :provider_reference, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e2fdf998a..866247395 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,10 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_09_19_092926) do
+ActiveRecord::Schema[7.0].define(version: 2023_09_19_144709) do
+ create_sequence "evaluation_refs"
+ create_sequence "framework_refs"
+
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
enable_extension "pgcrypto"
@@ -225,6 +228,16 @@
t.datetime "updated_at", null: false
end
+ create_table "frameworks_evaluations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "reference", default: -> { "('FE'::text || nextval('evaluation_refs'::regclass))" }
+ t.uuid "framework_id"
+ t.uuid "assignee_id"
+ t.uuid "contact_id"
+ t.integer "status", default: 0
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "frameworks_framework_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "support_category_id", null: false
t.uuid "framework_id", null: false
@@ -238,7 +251,7 @@
t.string "name"
t.string "short_name"
t.string "url"
- t.string "reference"
+ t.string "reference", default: -> { "('F'::text || nextval('framework_refs'::regclass))" }
t.string "description"
t.uuid "provider_id", null: false
t.uuid "provider_contact_id"
@@ -254,6 +267,7 @@
t.datetime "updated_at", null: false
t.boolean "dps", default: true
t.integer "lot", default: 0
+ t.string "provider_reference"
end
create_table "frameworks_provider_contacts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
diff --git a/spec/factories/frameworks/evaluations.rb b/spec/factories/frameworks/evaluations.rb
new file mode 100644
index 000000000..167485d56
--- /dev/null
+++ b/spec/factories/frameworks/evaluations.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :frameworks_evaluation, class: "Frameworks::Evaluation" do
+ reference { "MyString" }
+ framework_id { "" }
+ status { 1 }
+ end
+end
diff --git a/spec/features/frameworks/evaluations/agent_can_create_framework_evaluation_spec.rb b/spec/features/frameworks/evaluations/agent_can_create_framework_evaluation_spec.rb
new file mode 100644
index 000000000..eeeefd8e0
--- /dev/null
+++ b/spec/features/frameworks/evaluations/agent_can_create_framework_evaluation_spec.rb
@@ -0,0 +1,15 @@
+require "rails_helper"
+
+xdescribe "Agent can create new framework evaluations", js: true do
+ include_context "with a framework evaluation agent"
+
+ let(:framework) { create(:frameworks_framework, status: "pending_evaluation") }
+
+ it "can create a new evaluation from the framework" do
+ visit frameworks_framework_path(framework)
+ click_on "Evaluations"
+ click_on "Add Evaluation"
+ select agent.full_name, from: "Assignee"
+ click_on "Create Evaluation"
+ end
+end
diff --git a/spec/models/frameworks/activity_loggable_version_spec.rb b/spec/models/frameworks/activity_loggable_version_spec.rb
new file mode 100644
index 000000000..63445413c
--- /dev/null
+++ b/spec/models/frameworks/activity_loggable_version_spec.rb
@@ -0,0 +1,11 @@
+require "rails_helper"
+
+describe Frameworks::ActivityLoggableVersion do
+ context "when the item is being created" do
+ it "also records the default fields added by enums, db etc" do
+ framework = create(:frameworks_framework)
+ expect(framework.versions.last.object_changes.keys).to include("reference") # added by db
+ expect(framework.versions.last.object_changes.keys).to include("status") # default enum value
+ end
+ end
+end
diff --git a/spec/models/frameworks/evaluation/activity_log_presentable_spec.rb b/spec/models/frameworks/evaluation/activity_log_presentable_spec.rb
new file mode 100644
index 000000000..c9705504f
--- /dev/null
+++ b/spec/models/frameworks/evaluation/activity_log_presentable_spec.rb
@@ -0,0 +1,19 @@
+require "rails_helper"
+
+describe Frameworks::Evaluation::ActivityLogPresentable do
+ subject(:presentable) { Frameworks::Evaluation.new }
+
+ describe "#specific_change_template_for" do
+ it "returns nil by default" do
+ template = presentable.specific_change_template_for(Frameworks::ActivityLoggableVersion.new(object_changes: {}))
+ expect(template).to be_nil
+ end
+
+ context "when the only changes are to the status field" do
+ it "returns evaluations/status" do
+ template = presentable.specific_change_template_for(Frameworks::ActivityLoggableVersion.new(object_changes: { status: [] }))
+ expect(template).to eq("evaluations/status")
+ end
+ end
+ end
+end
diff --git a/spec/models/frameworks/evaluation/status_changeable_spec.rb b/spec/models/frameworks/evaluation/status_changeable_spec.rb
new file mode 100644
index 000000000..c794d4af8
--- /dev/null
+++ b/spec/models/frameworks/evaluation/status_changeable_spec.rb
@@ -0,0 +1,45 @@
+require "rails_helper"
+
+describe Frameworks::Evaluation::StatusChangeable do
+ subject(:evaluation) { Frameworks::Evaluation.new(framework:, status: evaluation_status) }
+
+ describe "starting the evaluation" do
+ let(:framework) { create(:frameworks_framework, status: "pending_evaluation") }
+ let(:evaluation_status) { "draft" }
+
+ it "updates the frameworks status to evaluating" do
+ evaluation.start!
+ expect(framework.reload.status).to eq("evaluating")
+ end
+ end
+
+ describe "approving the evaluation" do
+ let(:framework) { create(:frameworks_framework, status: "evaluating") }
+ let(:evaluation_status) { "in_progress" }
+
+ it "updates the frameworks status to dfe-approved" do
+ evaluation.approve!
+ expect(framework.reload.status).to eq("dfe_approved")
+ end
+ end
+
+ describe "disaproving the evaluation" do
+ let(:framework) { create(:frameworks_framework, status: "evaluating") }
+ let(:evaluation_status) { "in_progress" }
+
+ it "updates the frameworks status to not approved" do
+ evaluation.disapprove!
+ expect(framework.reload.status).to eq("not_approved")
+ end
+ end
+
+ describe "cancelling the evaluation" do
+ let(:framework) { create(:frameworks_framework, status: "evaluating") }
+ let(:evaluation_status) { "in_progress" }
+
+ it "updates the frameworks status to not approved" do
+ evaluation.cancel!
+ expect(framework.reload.status).to eq("not_approved")
+ end
+ end
+end
diff --git a/spec/models/frameworks/evaluation_spec.rb b/spec/models/frameworks/evaluation_spec.rb
new file mode 100644
index 000000000..50aa5a9de
--- /dev/null
+++ b/spec/models/frameworks/evaluation_spec.rb
@@ -0,0 +1,5 @@
+require "rails_helper"
+
+RSpec.describe Frameworks::Evaluation, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/frameworks/framework/activity_log_presentable_spec.rb b/spec/models/frameworks/framework/activity_log_presentable_spec.rb
new file mode 100644
index 000000000..d328d9b39
--- /dev/null
+++ b/spec/models/frameworks/framework/activity_log_presentable_spec.rb
@@ -0,0 +1,19 @@
+require "rails_helper"
+
+describe Frameworks::Framework::ActivityLogPresentable do
+ subject(:presentable) { Frameworks::Framework.new }
+
+ describe "#specific_change_template_for" do
+ it "returns nil by default" do
+ template = presentable.specific_change_template_for(Frameworks::ActivityLoggableVersion.new(object_changes: {}))
+ expect(template).to be_nil
+ end
+
+ context "when the only changes are to the status field" do
+ it "returns frameworks/status" do
+ template = presentable.specific_change_template_for(Frameworks::ActivityLoggableVersion.new(object_changes: { status: [] }))
+ expect(template).to eq("frameworks/status")
+ end
+ end
+ end
+end