diff --git a/app/controllers/frameworks/categorisations_controller.rb b/app/controllers/frameworks/categorisations_controller.rb new file mode 100644 index 000000000..69b8ac11e --- /dev/null +++ b/app/controllers/frameworks/categorisations_controller.rb @@ -0,0 +1,19 @@ +class Frameworks::CategorisationsController < Frameworks::ApplicationController + def edit + @framework = Frameworks::Framework.find(params[:framework_id]) + end + + def update + @framework = Frameworks::Framework.find(params[:framework_id]) + + @framework.update!(category_params) + + redirect_to @framework + end + +private + + def category_params + params.require(:frameworks_framework).permit(support_category_ids: []) + end +end diff --git a/app/models/frameworks/activity_event.rb b/app/models/frameworks/activity_event.rb new file mode 100644 index 000000000..cb19aba88 --- /dev/null +++ b/app/models/frameworks/activity_event.rb @@ -0,0 +1,7 @@ +class Frameworks::ActivityEvent < ApplicationRecord + include Frameworks::Activity + + def loaded_data + OpenStruct.new(**data, **activity_log_item.subject.try(:activity_event_data_for, self).presence || {}) + end +end diff --git a/app/models/frameworks/activity_log_item.rb b/app/models/frameworks/activity_log_item.rb index ac544a5fc..c3b1cd589 100644 --- a/app/models/frameworks/activity_log_item.rb +++ b/app/models/frameworks/activity_log_item.rb @@ -3,7 +3,9 @@ class Frameworks::ActivityLogItem < ApplicationRecord belongs_to :actor, polymorphic: true, optional: true belongs_to :subject, polymorphic: true, optional: true - delegated_type :activity, types: %w[Frameworks::ActivityLoggableVersion] + delegated_type :activity, types: %w[Frameworks::ActivityLoggableVersion Frameworks::ActivityEvent] + + default_scope { order("created_at DESC") } before_create do self.guid ||= Current.request_id diff --git a/app/models/frameworks/activity_loggable.rb b/app/models/frameworks/activity_loggable.rb index 349ae604d..531690cb7 100644 --- a/app/models/frameworks/activity_loggable.rb +++ b/app/models/frameworks/activity_loggable.rb @@ -14,6 +14,12 @@ module Frameworks::ActivityLoggable private def log_latest_version_in_activity_log + return unless previous_changes.any? + Frameworks::ActivityLogItem.create!(subject: self, activity: versions.last, activity_type: "Frameworks::ActivityLoggableVersion") end + + def log_activity_event(event, data = {}) + Frameworks::ActivityLogItem.create!(subject: self, activity: Frameworks::ActivityEvent.new(event:, data:)) + end end diff --git a/app/models/frameworks/framework.rb b/app/models/frameworks/framework.rb index f9623a18a..3cfa9c606 100644 --- a/app/models/frameworks/framework.rb +++ b/app/models/frameworks/framework.rb @@ -5,6 +5,7 @@ class Frameworks::Framework < ApplicationRecord include Sourceable include Presentable include ActivityLogPresentable + include ActivityEventLoggable include Filterable include Sortable @@ -12,7 +13,11 @@ class Frameworks::Framework < ApplicationRecord 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 - belongs_to :support_category, class_name: "Support::Category", optional: true + + 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 diff --git a/app/models/frameworks/framework/activity_event_loggable.rb b/app/models/frameworks/framework/activity_event_loggable.rb new file mode 100644 index 000000000..20c534b39 --- /dev/null +++ b/app/models/frameworks/framework/activity_event_loggable.rb @@ -0,0 +1,20 @@ +module Frameworks::Framework::ActivityEventLoggable + extend ActiveSupport::Concern + + def activity_event_data_for(activity_event) + case activity_event.event + when "framework_category_added", "framework_category_removed" + { support_category: Support::Category.find(activity_event.data["support_category_id"]) } + end + end + +protected + + def log_framework_category_added(category) + log_activity_event("framework_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) + end +end diff --git a/app/models/frameworks/framework/filterable.rb b/app/models/frameworks/framework/filterable.rb index c26f0c24f..024a1648f 100644 --- a/app/models/frameworks/framework/filterable.rb +++ b/app/models/frameworks/framework/filterable.rb @@ -4,7 +4,7 @@ module Frameworks::Framework::Filterable included do scope :by_status, ->(statuses) { where(status: Array(statuses)) } scope :by_provider, ->(provider_ids) { where(provider_id: Array(provider_ids)) } - scope :by_category, ->(category_ids) { where(support_category_id: Array(category_ids)) } + scope :by_category, ->(category_ids) { joins(:framework_categories).where(framework_categories: { support_category_id: Array(category_ids) }) } scope :by_e_and_o_lead, ->(e_and_o_lead_ids) { where(e_and_o_lead_id: Array(e_and_o_lead_ids)) } scope :by_proc_ops_lead, ->(proc_ops_lead_ids) { where(proc_ops_lead_id: Array(proc_ops_lead_ids)) } scope :by_provider_contact, ->(provider_contact_ids) { where(provider_contact_id: Array(provider_contact_ids)) } diff --git a/app/models/frameworks/framework/filtering.rb b/app/models/frameworks/framework/filtering.rb index e31eca699..e90906ccb 100644 --- a/app/models/frameworks/framework/filtering.rb +++ b/app/models/frameworks/framework/filtering.rb @@ -22,8 +22,7 @@ def results end def available_category_options - Support::Category.order("title ASC").where(id: Frameworks::Framework.pluck(:support_category_id)) - .map { |category| [category.title, category.id] } + Support::Category.order("title ASC").where(id: Frameworks::FrameworkCategory.pluck(:support_category_id)).pluck(:title, :id) end def available_provider_filter_options diff --git a/app/models/frameworks/framework/presentable.rb b/app/models/frameworks/framework/presentable.rb index a697c1b17..6971d8b5f 100644 --- a/app/models/frameworks/framework/presentable.rb +++ b/app/models/frameworks/framework/presentable.rb @@ -21,8 +21,8 @@ def provider_framework_owner_email provider_contact&.email end - def category_name - support_category&.title + def category_names + support_categories.pluck(:title).join(", ") end def proc_ops_lead_name diff --git a/app/models/frameworks/framework/sortable.rb b/app/models/frameworks/framework/sortable.rb index bfe56710c..42518e761 100644 --- a/app/models/frameworks/framework/sortable.rb +++ b/app/models/frameworks/framework/sortable.rb @@ -2,7 +2,7 @@ module Frameworks::Framework::Sortable extend ActiveSupport::Concern included do - scope :sort_by_updated, ->(direction = "descending") { order("updated_at #{safe_direction(direction)}") } + scope :sort_by_updated, ->(direction = "descending") { order("frameworks_frameworks.updated_at #{safe_direction(direction)}") } scope :sort_by_dfe_start_date, ->(direction = "descending") { order("dfe_start_date #{safe_direction(direction)}") } 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)}") } diff --git a/app/models/frameworks/framework_category.rb b/app/models/frameworks/framework_category.rb new file mode 100644 index 000000000..2536017b3 --- /dev/null +++ b/app/models/frameworks/framework_category.rb @@ -0,0 +1,4 @@ +class Frameworks::FrameworkCategory < ApplicationRecord + belongs_to :support_category, class_name: "Support::Category" + belongs_to :framework +end 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 new file mode 100644 index 000000000..0082550d6 --- /dev/null +++ b/app/views/frameworks/activity_log_items/activity/_activity_event.html.erb @@ -0,0 +1,4 @@ +<%= render "frameworks/activity_log_items/activity/activity_event/#{activity_log_item.activity.event}", + activity_log_item:, + activity: activity_log_item.activity, + subject: activity_log_item.subject %> diff --git a/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.erb b/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.html.erb similarity index 100% rename from app/views/frameworks/activity_log_items/activity/_activity_loggable_version.erb rename to app/views/frameworks/activity_log_items/activity/_activity_loggable_version.html.erb 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/_framework_category_added.html.erb new file mode 100644 index 000000000..8ae6f6f22 --- /dev/null +++ b/app/views/frameworks/activity_log_items/activity/activity_event/_framework_category_added.html.erb @@ -0,0 +1 @@ +Added category "<%= activity.loaded_data.support_category.title %>" to framework 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/_framework_category_removed.html.erb new file mode 100644 index 000000000..2667604a5 --- /dev/null +++ b/app/views/frameworks/activity_log_items/activity/activity_event/_framework_category_removed.html.erb @@ -0,0 +1 @@ +Removed category "<%= activity.loaded_data.support_category.title %>" from framework diff --git a/app/views/frameworks/categorisations/edit.html.erb b/app/views/frameworks/categorisations/edit.html.erb new file mode 100644 index 000000000..c8d074cac --- /dev/null +++ b/app/views/frameworks/categorisations/edit.html.erb @@ -0,0 +1,22 @@ +<%= content_for :title, "GHBS | Frameworks | Edit Framework Categories" %> + +Edit Framework Categories +

<%= @framework.name %>

+ +<%= form_for @framework, url: frameworks_framework_categorisations_path(@framework), method: :put do |f| %> + + <% procurement_category_grouped_options.each do |group, options| %> + <% next if group == "Or" %> + + <%= f.govuk_check_boxes_fieldset :support_category_ids, legend: { text: group, size: "s" } do %> + <% options.each do |category| %> + <%= f.govuk_check_box :support_category_ids, category[1], label: { text: category[0] } %> + <% end %> + <% end %> + <% end %> + +
+ <%= f.govuk_submit "Save changes" %> + <%= link_to "Cancel", @framework, class: "govuk-link govuk-link--no-visited-state" %> +
+<% end %> diff --git a/app/views/frameworks/frameworks/_framework.html.erb b/app/views/frameworks/frameworks/_framework.html.erb index 4f7d1fdc3..0e274f8f7 100644 --- a/app/views/frameworks/frameworks/_framework.html.erb +++ b/app/views/frameworks/frameworks/_framework.html.erb @@ -17,8 +17,8 @@
-
Category
-
<%= framework.category_name %>
+
Categories
+
<%= framework.category_names %>
diff --git a/app/views/frameworks/frameworks/show/_framework_details.html.erb b/app/views/frameworks/frameworks/show/_framework_details.html.erb index 77145b3b1..38c9d4564 100644 --- a/app/views/frameworks/frameworks/show/_framework_details.html.erb +++ b/app/views/frameworks/frameworks/show/_framework_details.html.erb @@ -6,8 +6,9 @@
-
Category
-
<%= @framework.category_name %>
+
Categories
+
<%= @framework.category_names %>
+
<%= link_to "Change", edit_frameworks_framework_categorisations_path(@framework, back_to: current_url_b64(:framework_details)), class: "govuk-link", "data-turbo" => false %>
diff --git a/config/routes.rb b/config/routes.rb index b208acd21..c5ccfa9d7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -301,7 +301,9 @@ # Frameworks portal namespace :frameworks do root to: "dashboards#index" - resources :frameworks + resources :frameworks do + resource :categorisations, only: %w[edit update] + end resources :provider_contacts resources :providers diff --git a/db/migrate/20230915092131_create_frameworks_framework_categories.rb b/db/migrate/20230915092131_create_frameworks_framework_categories.rb new file mode 100644 index 000000000..d5cf0ab13 --- /dev/null +++ b/db/migrate/20230915092131_create_frameworks_framework_categories.rb @@ -0,0 +1,12 @@ +class CreateFrameworksFrameworkCategories < ActiveRecord::Migration[7.0] + def change + create_table :frameworks_framework_categories, id: :uuid do |t| + t.uuid :support_category_id, null: false, foreign_key: true + t.uuid :framework_id, null: false, foreign_key: true + + t.timestamps + end + + remove_column :frameworks_frameworks, :support_category_id, :uuid + end +end diff --git a/db/migrate/20230915110928_create_frameworks_activity_events.rb b/db/migrate/20230915110928_create_frameworks_activity_events.rb new file mode 100644 index 000000000..01a5c669d --- /dev/null +++ b/db/migrate/20230915110928_create_frameworks_activity_events.rb @@ -0,0 +1,10 @@ +class CreateFrameworksActivityEvents < ActiveRecord::Migration[7.0] + def change + create_table :frameworks_activity_events, id: :uuid do |t| + t.string :event + t.jsonb :data + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index cabebdc09..801d7a338 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_13_084703) do +ActiveRecord::Schema[7.0].define(version: 2023_09_15_110928) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_trgm" @@ -207,6 +207,13 @@ t.index ["user_id"], name: "index_framework_requests_on_user_id" end + create_table "frameworks_activity_events", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "event" + t.jsonb "data" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "frameworks_activity_log_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "actor_id" t.string "actor_type" @@ -219,10 +226,16 @@ 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 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "frameworks_frameworks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.integer "source", default: 0 t.integer "status", default: 0 - t.uuid "support_category_id" t.string "name" t.string "short_name" t.string "url" diff --git a/spec/factories/frameworks/activity_events.rb b/spec/factories/frameworks/activity_events.rb new file mode 100644 index 000000000..ace9169ff --- /dev/null +++ b/spec/factories/frameworks/activity_events.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :frameworks_activity_event, class: "Frameworks::ActivityEvent" do + event { 1 } + data { "" } + end +end diff --git a/spec/factories/frameworks/framework_categories.rb b/spec/factories/frameworks/framework_categories.rb new file mode 100644 index 000000000..b0f6f4c45 --- /dev/null +++ b/spec/factories/frameworks/framework_categories.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :frameworks_framework_category, class: "Frameworks::FrameworkCategory" do + support_category { nil } + framework { nil } + end +end diff --git a/spec/features/frameworks/register/agent_can_categorise_framework_spec.rb b/spec/features/frameworks/register/agent_can_categorise_framework_spec.rb new file mode 100644 index 000000000..ab8fbee4a --- /dev/null +++ b/spec/features/frameworks/register/agent_can_categorise_framework_spec.rb @@ -0,0 +1,24 @@ +require "rails_helper" + +describe "Agent can categorise framework", js: true do + include_context "with a framework evaluation agent" + + let(:framework) { create(:frameworks_framework) } + + before { define_basic_categories } + + it "saves the categories" do + visit frameworks_framework_path(framework) + within ".govuk-summary-list__row", text: "Categories" do + click_on "Change" + end + check "Laptops" + check "Electricity" + click_on "Save changes" + + expect(page).to have_summary("Categories", "Laptops, Electricity") + click_on "History" + expect(page).to have_content('Added category "Laptops" to framework') + expect(page).to have_content('Added category "Electricity" to framework') + end +end diff --git a/spec/models/frameworks/activity_event_spec.rb b/spec/models/frameworks/activity_event_spec.rb new file mode 100644 index 000000000..e6ccc0849 --- /dev/null +++ b/spec/models/frameworks/activity_event_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe Frameworks::ActivityEvent, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/frameworks/activity_loggable_spec.rb b/spec/models/frameworks/activity_loggable_spec.rb index 8803c935a..d084da610 100644 --- a/spec/models/frameworks/activity_loggable_spec.rb +++ b/spec/models/frameworks/activity_loggable_spec.rb @@ -7,8 +7,7 @@ expect { provider.save! }.to change(Frameworks::ActivityLogItem, :count).from(0).to(1) - activity_log_item = Frameworks::ActivityLogItem.first - expect(activity_log_item.subject).to eq(provider) + activity_log_item = Frameworks::ActivityLogItem.where(subject: provider).reorder("created_at ASC").first expect(provider.versions.count).to eq(1) expect(activity_log_item.activity).to eq(provider.versions.last) end @@ -20,8 +19,7 @@ expect { provider.update(short_name: "TestProvider") }.to change(Frameworks::ActivityLogItem, :count).from(1).to(2) - activity_log_item = Frameworks::ActivityLogItem.last - expect(activity_log_item.subject).to eq(provider) + activity_log_item = Frameworks::ActivityLogItem.where(subject: provider).reorder("created_at ASC").last expect(provider.versions.count).to eq(2) expect(activity_log_item.activity).to eq(provider.versions.last) end diff --git a/spec/models/frameworks/framework/filterable_spec.rb b/spec/models/frameworks/framework/filterable_spec.rb index baec0dd89..541c5914f 100644 --- a/spec/models/frameworks/framework/filterable_spec.rb +++ b/spec/models/frameworks/framework/filterable_spec.rb @@ -14,11 +14,11 @@ let(:proc_ops_lead) { create(:support_agent) } before do - create(:frameworks_framework, name: "DfE Approved - Provider 1", status: :dfe_approved, provider: provider_1, support_category: category_1) - create(:frameworks_framework, name: "Not Approved - Provider 2", status: :not_approved, provider: provider_2, support_category: category_2) - create(:frameworks_framework, name: "Cab Approved - Provider 1", status: :cab_approved, provider: provider_1, support_category: category_1) - create(:frameworks_framework, name: "Evaluating - Provider 1", status: :evaluating, provider: provider_1, support_category: category_2, proc_ops_lead:) - create(:frameworks_framework, name: "Evaluating - Provider 2", status: :evaluating, provider: provider_2, support_category: category_1, e_and_o_lead:) + create(:frameworks_framework, name: "DfE Approved - Provider 1", status: :dfe_approved, provider: provider_1, support_category_ids: [category_1.id]) + create(:frameworks_framework, name: "Not Approved - Provider 2", status: :not_approved, provider: provider_2, support_category_ids: [category_2.id]) + create(:frameworks_framework, name: "Cab Approved - Provider 1", status: :cab_approved, provider: provider_1, support_category_ids: [category_1.id]) + create(:frameworks_framework, name: "Evaluating - Provider 1", status: :evaluating, provider: provider_1, support_category_ids: [category_2.id], proc_ops_lead:) + create(:frameworks_framework, name: "Evaluating - Provider 2", status: :evaluating, provider: provider_2, support_category_ids: [category_1.id], e_and_o_lead:) end describe "filtering by status" do