diff --git a/app/components/programme_status_component.rb b/app/components/programme_status_component.rb new file mode 100644 index 000000000..e88a42dc7 --- /dev/null +++ b/app/components/programme_status_component.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class ProgrammeStatusComponent < ViewComponent::Base + def initialize(user_programme_enrolment:) + @user_programme_enrolment = user_programme_enrolment + @cms_programme_model = Cms::Collections::Programme.get(@user_programme_enrolment.programme.slug) + end + + def render? + @user_programme_enrolment.in_state?(:pending) || @user_programme_enrolment.in_state?(:complete) + end + + def title + if @user_programme_enrolment.in_state?(:pending) + @cms_programme_model.status_pending_title.value + else + @cms_programme_model.status_completed_title.value + end + end + + def content + if @user_programme_enrolment.in_state?(:pending) + @cms_programme_model.status_pending_text + else + @cms_programme_model.status_completed_text + end + end +end diff --git a/app/components/programme_status_component/programme_status_component.html.erb b/app/components/programme_status_component/programme_status_component.html.erb new file mode 100644 index 000000000..72acac5bc --- /dev/null +++ b/app/components/programme_status_component/programme_status_component.html.erb @@ -0,0 +1,20 @@ + +<%= render GovGridRowComponent.new(additional_classes: "programme-status-wrapper", background_color: "white") do |row| %> + <%= row.with_column("two-thirds") do %> +
+

<%= title %>

+ <% if @user_programme_enrolment.in_state?(:complete) %> + <%= image_pack_tag "media/images/icons/programme_completed_icon.svg" %> + <% end %> +
+ <%= render content.render %> + <% end %> + <%= row.with_column("one-third") do %> + <% if @user_programme_enrolment.in_state?(:complete) %> +
+ <%= link_to 'Download your certificate', @user_programme_enrolment.programme.certificate_path, + class: 'govuk-button button', role: 'button' %>

+
+ <% end %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/components/programme_status_component/programme_status_component.scss b/app/components/programme_status_component/programme_status_component.scss new file mode 100644 index 000000000..a21886a8b --- /dev/null +++ b/app/components/programme_status_component/programme_status_component.scss @@ -0,0 +1,19 @@ +.programme-status-wrapper { + + &__title-row { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 15px; + gap: 20px; + + h2 { + margin-bottom: 0; + } + } + + &__button-container { + padding-top: 50px; + text-align: center; + } +} \ No newline at end of file diff --git a/app/models/programme.rb b/app/models/programme.rb index 003ecd389..6a0888b24 100644 --- a/app/models/programme.rb +++ b/app/models/programme.rb @@ -151,10 +151,6 @@ def user_is_eligible?(user) true end - def enrichment_enabled? - false - end - def programme_objectives programme_activity_groupings.includes(:programme_activities).order(:sort_key) end diff --git a/app/models/programmes/a_level.rb b/app/models/programmes/a_level.rb index f852f51a0..7b4544825 100644 --- a/app/models/programmes/a_level.rb +++ b/app/models/programmes/a_level.rb @@ -42,5 +42,9 @@ def achievement_type def enrolment_confirmation_required? false end + + def certificate_path + certificate_a_level_path + end end end diff --git a/app/models/programmes/cs_accelerator.rb b/app/models/programmes/cs_accelerator.rb index 61cd957c4..844c70439 100644 --- a/app/models/programmes/cs_accelerator.rb +++ b/app/models/programmes/cs_accelerator.rb @@ -135,5 +135,9 @@ def achievement_type def enrolment_confirmation_required? false end + + def certificate_path + certificate_cs_accelerator_certificate_path + end end end diff --git a/app/models/programmes/i_belong.rb b/app/models/programmes/i_belong.rb index 39c06e816..3c1425fab 100644 --- a/app/models/programmes/i_belong.rb +++ b/app/models/programmes/i_belong.rb @@ -58,5 +58,9 @@ def achievement_type def enrolment_confirmation_required? false end + + def certificate_path + certificate_i_belong_path + end end end diff --git a/app/models/programmes/primary_certificate.rb b/app/models/programmes/primary_certificate.rb index f9ed6d764..4a7234e2d 100644 --- a/app/models/programmes/primary_certificate.rb +++ b/app/models/programmes/primary_certificate.rb @@ -38,10 +38,6 @@ def pathways? true end - def enrichment_enabled? - true - end - def send_pending_mail? true end @@ -57,5 +53,9 @@ def achievement_type def enrolment_confirmation_required? true end + + def certificate_path + certificate_primary_certificate_path + end end end diff --git a/app/models/programmes/secondary_certificate.rb b/app/models/programmes/secondary_certificate.rb index a1b474afc..b5b971148 100644 --- a/app/models/programmes/secondary_certificate.rb +++ b/app/models/programmes/secondary_certificate.rb @@ -38,10 +38,6 @@ def pathways? true end - def enrichment_enabled? - true - end - def programme_objectives [ ProgrammeObjectives::ProgrammeCompletionRequired.new( @@ -68,5 +64,9 @@ def achievement_type def enrolment_confirmation_required? false end + + def certificate_path + certificate_secondary_certificate_path + end end end diff --git a/app/services/cms/collections/programme.rb b/app/services/cms/collections/programme.rb new file mode 100644 index 000000000..1cbdbe8b2 --- /dev/null +++ b/app/services/cms/collections/programme.rb @@ -0,0 +1,33 @@ +module Cms + module Collections + class Programme < Resource + def to_search_record(index_time) + raise NotImplementedError + end + + def self.is_collection = true + + def self.collection_attribute_mappings + [] + end + + def self.resource_attribute_mappings + [ + {model: Models::Slug, key: nil, param_name: :slug}, + {model: Models::TextField, key: :statusPendingTitle}, + {model: Models::TextBlock, key: :statusPendingText, with_wrapper: false}, + {model: Models::TextField, key: :statusCompletedTitle}, + {model: Models::TextBlock, key: :statusCompletedText, with_wrapper: false} + ] + end + + def self.cache_expiry + 15.minutes + end + + def self.resource_key + "programmes" + end + end + end +end diff --git a/app/services/cms/models/text_field.rb b/app/services/cms/models/text_field.rb new file mode 100644 index 000000000..9ecf6ffd1 --- /dev/null +++ b/app/services/cms/models/text_field.rb @@ -0,0 +1,15 @@ +module Cms + module Models + class TextField + attr_accessor :value + + def initialize(value:) + @value = value + end + + def render + # has no render method, this exists just to provide data to model + end + end + end +end diff --git a/app/services/cms/providers/strapi/factories/model_factory.rb b/app/services/cms/providers/strapi/factories/model_factory.rb index 1e9917648..686837c2b 100644 --- a/app/services/cms/providers/strapi/factories/model_factory.rb +++ b/app/services/cms/providers/strapi/factories/model_factory.rb @@ -15,10 +15,17 @@ def self.process_model(mapping, all_data) to_seo(strapi_data) elsif model_class == Models::Slug model_class.new(slug: strapi_data[:slug]) + elsif model_class == Models::TextField + model_class.new(value: strapi_data) elsif model_class == Models::FeaturedImage to_featured_image(strapi_data[:data][:attributes]) if strapi_data[:data] elsif model_class == Models::TextBlock - to_content_block(strapi_data) + with_wrapper = if mapping.key?(:with_wrapper) + mapping[:with_wrapper] + else + true + end + to_content_block(strapi_data, with_wrapper:) elsif model_class == Models::SimpleTitle model_class.new(title: strapi_data) elsif model_class == Models::Aside diff --git a/app/services/cms/providers/strapi/mocks/programme.rb b/app/services/cms/providers/strapi/mocks/programme.rb new file mode 100644 index 000000000..b029c6c12 --- /dev/null +++ b/app/services/cms/providers/strapi/mocks/programme.rb @@ -0,0 +1,15 @@ +module Cms + module Providers + module Strapi + module Mocks + class Programme < StrapiMock + attribute(:slug) { Faker::Internet.slug } + attribute(:statusPendingTitle) { Faker::Internet.slug } + attribute(:statusPendingText) { RichBlocks.generate_data } + attribute(:statusCompletedTitle) { Faker::Internet.slug } + attribute(:statusCompletedText) { RichBlocks.generate_data } + end + end + end + end +end diff --git a/app/services/cms/resource.rb b/app/services/cms/resource.rb index bd66cc440..6b2edbffc 100644 --- a/app/services/cms/resource.rb +++ b/app/services/cms/resource.rb @@ -14,6 +14,26 @@ def to_search_record(index_time) raise NotImplementedError end + def respond_to_missing?(method_name, include_private = false) + self.class.param_indexes.key?(method_name) || super + end + + def method_missing(method_name) + index = self.class.param_indexes[method_name] + data_models[index] + end + + def self.param_name(mapping) + return mapping[:param_name] if mapping.has_key?(:param_name) + mapping[:key].to_s.underscore.to_sym + end + + def self.param_indexes + resource_attribute_mappings.each_with_object({}).with_index do |(mapping, indexes), index| + indexes[param_name(mapping)] = index + end + end + def self.is_collection = false def self.resource_attribute_mappings diff --git a/app/webpacker/images/icons/programme_completed_icon.svg b/app/webpacker/images/icons/programme_completed_icon.svg new file mode 100644 index 000000000..95bf94469 --- /dev/null +++ b/app/webpacker/images/icons/programme_completed_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/previews/components/cms_split_horizontal_card_component_preview.rb b/previews/components/cms_split_horizontal_card_component_preview.rb index a153d1598..597e92aa0 100644 --- a/previews/components/cms_split_horizontal_card_component_preview.rb +++ b/previews/components/cms_split_horizontal_card_component_preview.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -class CmsSplitHorizontialCardComponentPreview < ViewComponent::Preview +class CmsSplitHorizontalCardComponentPreview < ViewComponent::Preview def default - render(CmsSplitHorizontialCardComponent.new( + render(CmsSplitHorizontalCardComponent.new( section_title: Faker::Lorem.sentence, card_content: Cms::Mocks::RichBlocks.as_model, aside_content: Cms::Mocks::RichBlocks.as_model, diff --git a/previews/components/programme_status_component_preview.rb b/previews/components/programme_status_component_preview.rb new file mode 100644 index 000000000..fc877c55f --- /dev/null +++ b/previews/components/programme_status_component_preview.rb @@ -0,0 +1,27 @@ +class ProgrammeStatusComponentPreview < ViewComponent::Preview + class UPEMock + def initialize(status) + @status = status + end + + def in_state?(status) + @status == status + end + + def programme = Programme.primary_certificate + end + + def pending + user_programme_enrolment = UPEMock.new(:pending) + render( + ProgrammeStatusComponent.new(user_programme_enrolment:) + ) + end + + def complete + user_programme_enrolment = UPEMock.new(:complete) + render( + ProgrammeStatusComponent.new(user_programme_enrolment:) + ) + end +end diff --git a/spec/components/programme_status_component_spec.rb b/spec/components/programme_status_component_spec.rb new file mode 100644 index 000000000..2662f4dd2 --- /dev/null +++ b/spec/components/programme_status_component_spec.rb @@ -0,0 +1,60 @@ +require "rails_helper" + +RSpec.describe ProgrammeStatusComponent, type: :component do + let(:programme) { create(:primary_certificate) } + + before do + stub_strapi_programme(programme.slug, programme: Cms::Mocks::Programme.generate_raw_data( + status_pending_title: "Pending title", + status_completed_title: "Completed title" + )) + end + + context "Enrolment enrolled" do + let(:user_programme_enrolment) { create(:user_programme_enrolment, programme:) } + + before do + render_inline(described_class.new(user_programme_enrolment:)) + end + + it "should not render" do + expect(page).not_to have_css(".programme-status-wrapper") + end + end + + context "Enrolment Pending" do + let(:user_programme_enrolment) { create(:pending_enrolment, programme:) } + + before do + render_inline(described_class.new(user_programme_enrolment:)) + end + + it "should render the wrapper" do + expect(page).to have_css(".programme-status-wrapper") + end + + it "should render the correct heading" do + expect(page).to have_css("h2.govuk-heading-m", text: "Pending title") + end + end + + context "Enrolment completed" do + let(:user_programme_enrolment) { create(:completed_enrolment, programme:) } + + before do + render_inline(described_class.new(user_programme_enrolment:)) + end + + it "should render the wrapper" do + expect(page).to have_css(".programme-status-wrapper") + end + + it "should render the correct heading" do + expect(page).to have_css("h2.govuk-heading-m", text: "Completed title") + end + + it "should render cerficate button" do + expect(page).to have_link("Download your certificate", href: "/certificate/primary-certificate/view-certificate") + end + end +end diff --git a/spec/models/programme_spec.rb b/spec/models/programme_spec.rb index 1398d0ea4..c3d223d3d 100644 --- a/spec/models/programme_spec.rb +++ b/spec/models/programme_spec.rb @@ -345,14 +345,6 @@ end end - describe "#enrichment_enabled?" do - let(:programme) { create(:programme) } - - it "should return false" do - expect(programme.enrichment_enabled?).to be false - end - end - describe "#certificate_name" do it "should return not implemented error" do expect { generic_programme.certificate_name }.to raise_error(NotImplementedError) diff --git a/spec/models/programmes/a_level_spec.rb b/spec/models/programmes/a_level_spec.rb index 9dba19356..db0f4052a 100644 --- a/spec/models/programmes/a_level_spec.rb +++ b/spec/models/programmes/a_level_spec.rb @@ -24,6 +24,12 @@ end end + describe "#enrolment_confirmation_required?" do + it "should return false" do + expect(subject.enrolment_confirmation_required?).to be false + end + end + describe "#enrol_path" do it "returns the path for the enrol" do expect(subject.enrol_path(user_programme_enrolment: {user_id: "user_id", @@ -52,4 +58,10 @@ expect(subject.programme_objectives[1..]).to match_array(pags) end end + + describe "#certificate_path" do + it "should return its certificate path" do + expect(subject.certificate_path).to eq "/certificate/a-level-certificate/view-certificate" + end + end end diff --git a/spec/models/programmes/cs_accelerator_spec.rb b/spec/models/programmes/cs_accelerator_spec.rb index fd22e9489..7bc097fdb 100644 --- a/spec/models/programmes/cs_accelerator_spec.rb +++ b/spec/models/programmes/cs_accelerator_spec.rb @@ -520,4 +520,16 @@ expect(programme.auto_enrollable?).to be true end end + + describe "#enrolment_confirmation_required?" do + it "should return false" do + expect(programme.enrolment_confirmation_required?).to be false + end + end + + describe "#certificate_path" do + it "should return its certificate path" do + expect(programme.certificate_path).to eq "/certificate/subject-knowledge/view-certificate" + end + end end diff --git a/spec/models/programmes/i_belong_spec.rb b/spec/models/programmes/i_belong_spec.rb index f1168ef26..93e688a22 100644 --- a/spec/models/programmes/i_belong_spec.rb +++ b/spec/models/programmes/i_belong_spec.rb @@ -58,4 +58,16 @@ expect(programme.minimum_character_required_community_evidence).to eq(50) end end + + describe "#enrolment_confirmation_required?" do + it "should return false" do + expect(programme.enrolment_confirmation_required?).to be false + end + end + + describe "#certificate_path" do + it "should return its certificate path" do + expect(programme.certificate_path).to eq "/certificate/i-belong/view-certificate" + end + end end diff --git a/spec/models/programmes/primary_certificate_spec.rb b/spec/models/programmes/primary_certificate_spec.rb index c77234919..bc5266659 100644 --- a/spec/models/programmes/primary_certificate_spec.rb +++ b/spec/models/programmes/primary_certificate_spec.rb @@ -105,4 +105,10 @@ expect(programme.enrolment_confirmation_required?).to be true end end + + describe "#certificate_path" do + it "should return its certificate path" do + expect(programme.certificate_path).to eq "/certificate/primary-certificate/view-certificate" + end + end end diff --git a/spec/models/programmes/secondary_certificate_spec.rb b/spec/models/programmes/secondary_certificate_spec.rb index a8a464c92..0ddccd8bf 100644 --- a/spec/models/programmes/secondary_certificate_spec.rb +++ b/spec/models/programmes/secondary_certificate_spec.rb @@ -111,4 +111,16 @@ expect(secondary_certificate.auto_enrollable?).to be true end end + + describe "#enrolment_confirmation_required?" do + it "should return false" do + expect(secondary_certificate.enrolment_confirmation_required?).to be false + end + end + + describe "#certificate_path" do + it "should return its certificate path" do + expect(secondary_certificate.certificate_path).to eq "/certificate/secondary-certificate/view-certificate" + end + end end diff --git a/spec/services/cms/collections/programme_spec.rb b/spec/services/cms/collections/programme_spec.rb new file mode 100644 index 000000000..2cc1a1c01 --- /dev/null +++ b/spec/services/cms/collections/programme_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +RSpec.describe Cms::Collections::Programme do + it "should have correct resource_key" do + expect(described_class.resource_key).to eq("programmes") + end + + it "should have 15 minute cache expiry" do + expect(described_class.cache_expiry).to eq(15.minutes) + end + + it "defines not collection attribute mappings" do + expect(described_class.collection_attribute_mappings).to eq([]) + end + + context "slug" do + let(:slug) { "programme-resource-test" } + let!(:programme) { create(:programme, slug:) } + before do + mock_data = Cms::Mocks::Programme.generate_raw_data(slug:) + stub_strapi_programme(slug, programme: mock_data) + end + + it "should return correctly for single page" do + cms_programme = described_class.get(slug) + expect(cms_programme.slug.slug).to eq(slug) + end + + it "should raise error for search records" do + cms_programme = described_class.get(slug) + expect { + cms_programme.to_search_record(DateTime.now) + }.to raise_exception(NotImplementedError) + end + end +end diff --git a/spec/services/cms/resource_spec.rb b/spec/services/cms/resource_spec.rb index c94237bc4..74b2085a8 100644 --- a/spec/services/cms/resource_spec.rb +++ b/spec/services/cms/resource_spec.rb @@ -99,8 +99,12 @@ def self.resource_key end end - it "calling all gets collection object" do + before do stub_strapi_get_collection_entity("cms-collection-resource-test") + stub_strapi_get_single_blog_post("cms-collection-resource-test/test-post") + end + + it "calling all gets collection object" do response = collection_class.all(1, 10) expect(response).to be_a Cms::Collection end @@ -109,19 +113,54 @@ def self.resource_key expect(collection_class.collection_attribute_mappings).to be_a Array end + describe "#param_name" do + it "should use key as param_name" do + expect(collection_class.param_name({key: :title, model: Cms::Models::SimpleTitle})).to eq(:title) + end + + it "should use param_name as name when specified" do + expect(collection_class.param_name({key: :title, model: Cms::Models::SimpleTitle, param_name: :something})).to eq(:something) + end + end + + describe "#param_indexes" do + it "should include key" do + expect(collection_class.param_indexes).to have_key(:title) + end + + it "should set correct index" do + expect(collection_class.param_indexes[:title]).to eq(0) + end + end + it "calling collection_view should create component" do - stub_strapi_get_collection_entity("cms-collection-resource-test") response = collection_class.all(1, 10) resource = response.resources.first expect(resource.data_models.first).to be_a Cms::Models::SimpleTitle end it "calling get should return single record" do - stub_strapi_get_single_blog_post("cms-collection-resource-test/test-post") response = collection_class.get("test-post") expect(response).to be_a described_class end + it "should raise error for search_record" do + response = collection_class.get("test-post") + expect { + response.to_search_record(DateTime.now) + }.to raise_exception(NotImplementedError) + end + + it "should respond_to mapping key" do + response = collection_class.get("test-post") + expect(response).to respond_to(:title) + end + + it "should respond data model for mapping key" do + response = collection_class.get("test-post") + expect(response.title).to be_a Cms::Models::SimpleTitle + end + context "should cache results" do let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } diff --git a/spec/support/cms/providers/strapi/strapi_stubs.rb b/spec/support/cms/providers/strapi/strapi_stubs.rb index aabbfc88d..0d6841663 100644 --- a/spec/support/cms/providers/strapi/strapi_stubs.rb +++ b/spec/support/cms/providers/strapi/strapi_stubs.rb @@ -111,6 +111,10 @@ def stub_strapi_web_page_not_found(key) stub_request(:get, /^https:\/\/strapi.teachcomputing.org\/api\/web-pages\/#{key}/).to_return_json(body: not_found_response, status: 404) end + def stub_strapi_programme(key, programme: Cms::Mocks::Programme.generate_raw_data) + stub_request(:get, /^https:\/\/strapi.teachcomputing.org\/api\/programmes\/#{key}/).to_return_json(body: {data: programme}) + end + private def to_strapi_collection(records, page: 1, page_size: 10, page_count: 1)