diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 887b3c6d4..dfdfdaccd 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -4,3 +4,4 @@ $govuk-page-width: 1140px; @import "govuk_publishing_components/all_components"; @import "downtimes"; @import "summary-card"; +@import "sidebar_components"; diff --git a/app/assets/stylesheets/sidebar_components.scss b/app/assets/stylesheets/sidebar_components.scss new file mode 100644 index 000000000..b61b0b802 --- /dev/null +++ b/app/assets/stylesheets/sidebar_components.scss @@ -0,0 +1,6 @@ +.sidebar-components { + .govuk-button { + width: 100%; + } + border-top: 2px solid $govuk-brand-colour; +} diff --git a/app/controllers/homepage_controller.rb b/app/controllers/homepage_controller.rb index ff55c0168..743701685 100644 --- a/app/controllers/homepage_controller.rb +++ b/app/controllers/homepage_controller.rb @@ -1,8 +1,54 @@ class HomepageController < ApplicationController layout "design_system" + before_action :fetch_latest_popular_link def show - @latest_popular_links = PopularLinksEdition.last render "homepage/popular_links/show" end + + def create + @latest_popular_links = @latest_popular_links.create_draft_popular_links_from_last_record + render "homepage/popular_links/show" + end + + def edit + render "homepage/popular_links/edit" + end + + def update + update_link_items + flash[:success] = "Popular links draft saved.".html_safe + redirect_to show_popular_links_path + rescue StandardError + render "homepage/popular_links/edit" + end + + def publish + publish_latest_popular_links + render "homepage/popular_links/show" + end + +private + + def fetch_latest_popular_link + @latest_popular_links = PopularLinksEdition.last + end + + def update_link_items + @latest_popular_links.link_items = remove_leading_and_trailing_url_spaces(params[:popular_links].values) + @latest_popular_links.save! + end + + def remove_leading_and_trailing_url_spaces(links) + link_items = [] + links.each do |link| + link[:url] = link[:url].strip + link_items << link + end + link_items + end + + def publish_latest_popular_links + @latest_popular_links.publish_popular_links + end end diff --git a/app/helpers/errors_helper.rb b/app/helpers/errors_helper.rb index 72882f569..7bc1fddb8 100644 --- a/app/helpers/errors_helper.rb +++ b/app/helpers/errors_helper.rb @@ -1,14 +1,14 @@ module ErrorsHelper - def errors_for(errors, attribute) + def errors_for(errors, attribute, use_full_message: true) return nil if errors.blank? errors.filter_map { |error| if error.attribute == attribute { - text: error.full_message, + text: use_full_message ? error.full_message : error.message, } end } - .presence + .presence end end diff --git a/app/helpers/popular_links_helper.rb b/app/helpers/popular_links_helper.rb index 6c19a09ef..0bc89bb71 100644 --- a/app/helpers/popular_links_helper.rb +++ b/app/helpers/popular_links_helper.rb @@ -5,4 +5,31 @@ def popular_link_rows(item) rows << { key: "URL", value: item[:url] } rows.compact end + + def primary_button_for(model, url, text) + form_for model, url:, method: :post do + render "govuk_publishing_components/components/button", { + text:, + margin_bottom: 3, + } + end + end + + def secondary_button_for(model, url, text) + form_for model, url:, method: :post do + render "govuk_publishing_components/components/button", { + text:, + margin_bottom: 3, + secondary_solid: true, + } + end + end + + def primary_link_button_for(url, text) + render "govuk_publishing_components/components/button", { + text:, + href: url, + margin_bottom: 3, + } + end end diff --git a/app/models/action.rb b/app/models/action.rb index 45af101bb..c1dc908c5 100644 --- a/app/models/action.rb +++ b/app/models/action.rb @@ -17,6 +17,7 @@ class Action PUBLISH = "publish".freeze, ARCHIVE = "archive".freeze, NEW_VERSION = "new_version".freeze, + PUBLISH_POPULAR_LINKS = "publish_popular_links".freeze, ].freeze NON_STATUS_ACTIONS = [ diff --git a/app/models/edition.rb b/app/models/edition.rb index 8fd452196..f58dfd2c6 100644 --- a/app/models/edition.rb +++ b/app/models/edition.rb @@ -121,7 +121,7 @@ class ResurrectionError < RuntimeError validates :version_number, presence: true, uniqueness: { scope: :panopticon_id }, unless: :popular_links_edition? validates :panopticon_id, presence: true, unless: :popular_links_edition? validates_with SafeHtml, unless: :popular_links_edition? - validates_with LinkValidator, on: :update, unless: :archived? || :popular_links_edition? + validates_with LinkValidator, on: :update, unless: :archived_or_popular_links? validates_with ReviewerValidator validates :change_note, presence: { if: :major_change } @@ -519,4 +519,8 @@ def common_type_specific_field_keys(target_class) def popular_links_edition? instance_of?(::PopularLinksEdition) end + + def archived_or_popular_links? + archived? || popular_links_edition? + end end diff --git a/app/models/popular_links_edition.rb b/app/models/popular_links_edition.rb index 24848df7f..bc0dd2a6a 100644 --- a/app/models/popular_links_edition.rb +++ b/app/models/popular_links_edition.rb @@ -1,16 +1,36 @@ class PopularLinksEdition < Edition field :link_items, type: Array - validate :six_link_items_present? - validate :all_urls_and_titles_are_present? + validate :six_link_items_present + validate :all_valid_urls_and_titles_are_present - def six_link_items_present? + def six_link_items_present errors.add(:link_items, "6 links are required") if link_items.count != 6 end - def all_urls_and_titles_are_present? + def all_valid_urls_and_titles_are_present link_items.each_with_index do |item, index| - errors.add(:item, "A URL is required for Link #{index + 1}") unless item.key?(:url) - errors.add(:item, "A Title is required for Link #{index + 1}") unless item.key?(:title) + errors.add("url#{index + 1}", "URL is required for Link #{index + 1}") unless url_present?(item) + errors.add("title#{index + 1}", "Title is required for Link #{index + 1}") unless title_present?(item) + errors.add("url#{index + 1}", "URL is invalid for Link #{index + 1}, all URLs should have at least one '.' and no spaces.") if url_present?(item) && url_has_spaces_or_has_no_dot?(item[:url]) end end + + def url_has_spaces_or_has_no_dot?(url) + url.include?(" ") || url.exclude?(".") + end + + def title_present?(item) + item.key?(:title) && !item[:title].empty? + end + + def url_present?(item) + item.key?(:url) && !item[:url].empty? + end + + def create_draft_popular_links_from_last_record + last_popular_links = PopularLinksEdition.last + popular_links = PopularLinksEdition.new(title: last_popular_links.title, link_items: last_popular_links.link_items, version_number: last_popular_links.version_number.next) + popular_links.save! + popular_links + end end diff --git a/app/models/workflow.rb b/app/models/workflow.rb index 077a4b478..0ee9bc56c 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -93,6 +93,10 @@ class CannotDeletePublishedPublication < RuntimeError; end transition all => :archived, :unless => :archived? end + event :publish_popular_links do + transition %i[draft] => :published + end + state :in_review do validates :review_requested_at, presence: true end diff --git a/app/views/homepage/popular_links/_form.html.erb b/app/views/homepage/popular_links/_form.html.erb new file mode 100644 index 000000000..a29f333ab --- /dev/null +++ b/app/views/homepage/popular_links/_form.html.erb @@ -0,0 +1,25 @@ +<%= render "govuk_publishing_components/components/fieldset", { + legend_text: "Link #{index+1}", + heading_level: 2, + heading_size: "m", +} do %> + <%= render "govuk_publishing_components/components/input", { + label: { + text: "Title", + }, + name: "popular_links[#{index + 1}][title]", + id: "title#{index + 1}", + value: item[:title], + error_items: errors_for(form.object.errors, "title#{index + 1}".to_sym, use_full_message: false), + } %> + + <%= render "govuk_publishing_components/components/input", { + label: { + text: "URL", + }, + name: "popular_links[#{index + 1}][url]", + id: "url#{index + 1}", + value: item[:url], + error_items: errors_for(form.object.errors, "url#{index + 1}".to_sym, use_full_message: false), + } %> +<% end %> diff --git a/app/views/homepage/popular_links/_sidebar.html.erb b/app/views/homepage/popular_links/_sidebar.html.erb new file mode 100644 index 000000000..b64a7e852 --- /dev/null +++ b/app/views/homepage/popular_links/_sidebar.html.erb @@ -0,0 +1,13 @@ + diff --git a/app/views/homepage/popular_links/edit.html.erb b/app/views/homepage/popular_links/edit.html.erb new file mode 100644 index 000000000..174aa495f --- /dev/null +++ b/app/views/homepage/popular_links/edit.html.erb @@ -0,0 +1,32 @@ +<% content_for :page_title, 'Edit popular links' %> +<% content_for :title, 'Edit popular links' %> +<% content_for :title_context, 'Popular on GOV.UK' %> +<% unless @latest_popular_links.errors.empty? %> + <% content_for :error_summary do %> + <%= render("govuk_publishing_components/components/error_summary", { + id: "error-summary", + title: "There is a problem", + items: @latest_popular_links.errors.map do |error| + { + text: error.message, + href: "##{error.attribute.to_s}", + } + end + }) %> + <% end %> +<% end %> + +
+ <%= form_for @latest_popular_links, url: update_popular_links_path(@latest_popular_links), method: "patch" do |form| %> + <% @latest_popular_links.link_items.each_with_index do |item, index| %> + <%= render "homepage/popular_links/form", item:, index:, form: %> + <% end %> +
+ <%= render("govuk_publishing_components/components/button", { + text: "Save", + type: "submit", + }) %> + <%= link_to("Cancel", show_popular_links_path, class: "govuk-link govuk-link--no-visited-state") %> +
+ <% end %> +
diff --git a/app/views/homepage/popular_links/show.html.erb b/app/views/homepage/popular_links/show.html.erb index 88cd4ee54..5dfbf6253 100644 --- a/app/views/homepage/popular_links/show.html.erb +++ b/app/views/homepage/popular_links/show.html.erb @@ -7,3 +7,6 @@ <%= render "homepage/popular_links/link", item:, index: %> <% end %> +
+ <%= render "homepage/popular_links/sidebar" %> +
diff --git a/config/routes.rb b/config/routes.rb index 7f193bde8..769a9a3bc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -72,7 +72,11 @@ get "/govuk-sitemap.xml" => "sitemap#index" - get "/homepage/popular-links" => "homepage#show" + get "/homepage/popular-links" => "homepage#show", as: "show_popular_links" + post "/homepage/popular-links/create" => "homepage#create", as: "create_popular_links" + get "/homepage/popular-links/:id" => "homepage#edit", as: "edit_popular_links" + patch "/homepage/popular-links/:id" => "homepage#update", as: "update_popular_links" + post "/homepage/popular-links/:id/publish" => "homepage#publish", as: "publish_popular_links" mount GovukAdminTemplate::Engine, at: "/style-guide" mount Flipflop::Engine => "/flipflop" diff --git a/test/functional/homepage_controller_test.rb b/test/functional/homepage_controller_test.rb index f15fc5f80..75058352d 100644 --- a/test/functional/homepage_controller_test.rb +++ b/test/functional/homepage_controller_test.rb @@ -17,4 +17,101 @@ class HomepageControllerTest < ActionController::TestCase assert_template "homepage/popular_links/show" end end + + context "#create" do + setup do + @popular_links = FactoryBot.create(:popular_links, state: "published") + end + + should "render show template" do + post :create, params: { id: @popular_links.id } + + assert_response :ok + assert_template "homepage/popular_links/show" + end + + should "create a new draft popular links" do + assert_equal 1, PopularLinksEdition.count + assert_equal "published", PopularLinksEdition.last.state + + post :create, params: { id: @popular_links.id } + + assert_equal 2, PopularLinksEdition.count + assert_equal "draft", PopularLinksEdition.last.state + end + end + + context "#edit" do + should "render edit template" do + popular_links = FactoryBot.create(:popular_links, state: "published") + + post :edit, params: { id: popular_links.id } + + assert_response :ok + assert_template "homepage/popular_links/edit" + end + end + + context "#update" do + setup do + @popular_links = FactoryBot.create(:popular_links, state: "draft") + end + + should "update latest PopularLinksEdition with changed title and url" do + assert_equal "title1", @popular_links.link_items[0][:title] + assert_equal "https://www.url1.com", @popular_links.link_items[0][:url] + + new_title = "title has changed" + new_url = "https://www.changedurl.com" + patch :update, params: { id: @popular_links.id, + "popular_links" => + { "1" => { "title" => new_title, "url" => new_url }, + "2" => { "title" => "title2", "url" => "https://www.url2.com" }, + "3" => { "title" => "title3", "url" => "https://www.url3.com" }, + "4" => { "title" => "title4", "url" => "https://www.url4.com" }, + "5" => { "title" => "title5", "url" => "https://www.url5.com" }, + "6" => { "title" => "title6", "url" => "https://www.url6.com" } } } + + assert_equal new_title, PopularLinksEdition.last.link_items[0][:title] + assert_equal new_url, PopularLinksEdition.last.link_items[0][:url] + end + + should "redirect to show path on success" do + new_title = "title has changed" + patch :update, params: { id: @popular_links.id, + "popular_links" => + { "1" => { "title" => new_title, "url" => "https://www.url1.com" }, + "2" => { "title" => "title2", "url" => "https://www.url2.com" }, + "3" => { "title" => "title3", "url" => "https://www.url3.com" }, + "4" => { "title" => "title4", "url" => "https://www.url4.com" }, + "5" => { "title" => "title5", "url" => "https://www.url5.com" }, + "6" => { "title" => "title6", "url" => "https://www.url6.com" } } } + + assert_redirected_to show_popular_links_path + end + + should "render edit template on errors" do + patch :update, params: { id: @popular_links.id, + "popular_links" => + { "1" => { "title" => "title has changed", "url" => "https://www.url1.com" } } } + + assert_template "homepage/popular_links/edit" + end + end + + context "#publish" do + setup do + @popular_links = FactoryBot.create(:popular_links, state: "draft") + end + + should "publish latest draft popular links and render show template" do + assert_equal "draft", PopularLinksEdition.last.state + + post :publish, params: { id: @popular_links.id } + + assert_response :ok + assert_template "homepage/popular_links/show" + assert_equal "published", PopularLinksEdition.last.state + end + end end diff --git a/test/integration/homepage_popular_links_test.rb b/test/integration/homepage_popular_links_test.rb index c1932e88a..617dde2b3 100644 --- a/test/integration/homepage_popular_links_test.rb +++ b/test/integration/homepage_popular_links_test.rb @@ -3,30 +3,136 @@ class HomepagePopularLinksTest < JavascriptIntegrationTest setup do setup_users - @popular_links = FactoryBot.create(:popular_links) + @popular_links = FactoryBot.create(:popular_links, state: "published") visit_popular_links end - should "show page title" do - assert_title "Popular on GOV.UK" - end + context "#show" do + should "render page title" do + assert_title "Popular on GOV.UK" + end + + should "render 'Homepage' as a page title context" do + assert page.has_content?("Homepage") + end + + should "have 6 links with title and url" do + assert page.has_css?(".govuk-summary-card__title", count: 6) + assert page.has_text?("Title", count: 6) + assert page.has_text?("URL", count: 6) + end + + should "have popular links version and status" do + assert page.has_text?("Edition") + assert page.has_text?(@popular_links.version_number) + assert page.has_text?("Status") + assert page.has_text?("PUBLISHED") + assert page.has_css?(".govuk-tag--green") + end + + should "have 'Create new edition' button" do + assert page.has_text?("Create new edition") + end + + should "navigate to create path on click of 'Create new edition'" do + click_button("Create new edition") + assert_current_path create_popular_links_path + end - should "show 'Homepage' as a page title context" do - assert page.has_content?("Homepage") + should "render new draft popular links with edit option when 'Create new edition' button is clicked" do + click_button("Create new edition") + within(:css, ".govuk-tag--yellow") do + assert page.has_content?("DRAFT") + end + end end - should "have 6 links with title and url" do - assert page.has_css?(".govuk-summary-card__title", count: 6) - assert page.has_text?("Title", count: 6) - assert page.has_text?("URL", count: 6) + context "#create" do + should "create and show new edition with draft status and with an option to edit popular links" do + click_button("Create new edition") + + assert page.has_text?("Edition") + assert page.has_text?(@popular_links.version_number) + assert page.has_text?("Status") + assert page.has_text?("DRAFT") + assert page.has_css?(".govuk-tag--yellow") + assert page.has_text?("Edit popular links") + end + + should "create a new record with next version and 'draft' status" do + row = find_all(".govuk-summary-list__row") + assert row[0].has_text?("Edition") + assert row[0].has_text?("1") + assert row[1].has_text?("Status") + assert row[1].has_text?("PUBLISHED") + + click_button("Create new edition") + + row = find_all(".govuk-summary-list__row") + assert row[0].has_text?("Edition") + assert row[0].has_text?("2") + assert row[1].has_text?("Status") + assert row[1].has_text?("DRAFT") + end end - should "have popular links version and status" do - assert page.has_text?("Edition") - assert page.has_text?(@popular_links.version_number) - assert page.has_text?("Status") - assert page.has_text?("DRAFT") - assert page.has_css?(".govuk-tag--yellow") + context "#edit" do + setup do + click_button("Create new edition") + click_link("Edit popular links") + end + + should "render page title" do + assert_title "Edit popular links" + end + + should "render 'Popular on GOV.UK' as a page title context" do + assert page.has_content?("Popular on GOV.UK") + end + + should "have 6 links with title and url" do + assert page.has_css?(".govuk-input", count: 12) + assert page.has_text?("Title", count: 6) + assert page.has_text?("URL", count: 6) + end + + should "update record when 'Save' is clicked" do + fill_in "popular_links[1][title]", with: "new title 1" + click_button("Save") + + assert page.has_text?("Popular links draft saved.") + assert page.has_text?("new title 1") + end + + should "show validation errors for missing link and url" do + fill_in "popular_links[1][title]", with: "" + fill_in "popular_links[1][url]", with: "" + click_button("Save") + + assert page.has_text?("Title is required for Link 1") + assert page.has_text?("URL is required for Link 1") + end + + should "trim spaces from start and end of urls" do + fill_in "popular_links[1][url]", with: " www.abc.com " + click_button("Save") + + assert page.has_text?("www.abc.com") + assert_not page.has_text?(" www.abc.com ") + end + + should "render create page when 'Cancel' is clicked" do + click_link("Cancel") + + assert_current_path show_popular_links_path + end + + should "not save any changes when 'Cancel' is clicked" do + fill_in "popular_links[1][url]", with: "www.abc.com" + click_link("Cancel") + + assert_not page.has_text?("www.abc.com") + end end def visit_popular_links diff --git a/test/models/popular_links_edition_test.rb b/test/models/popular_links_edition_test.rb index 613503b16..d640be6d2 100644 --- a/test/models/popular_links_edition_test.rb +++ b/test/models/popular_links_edition_test.rb @@ -11,32 +11,46 @@ class PopularLinksEditionTest < ActiveSupport::TestCase popular_links = FactoryBot.build(:popular_links, link_items:) assert_not popular_links.valid? - assert popular_links.errors[:link_items].any? + assert_equal "6 links are required", popular_links.errors.messages[:link_items][0] end - should "validate all links have url" do - link_items = [{ title: "title1" }, - { url: "url2", title: "title2" }, - { url: "url3", title: "title3" }, - { url: "url4", title: "title4" }, - { url: "url5", title: "title5" }, - { url: "url6", title: "title6" }] + should "validate all links have url and title" do + link_items = [{ url: "https://www.url1.com", title: "" }, + { title: "title2" }, + { url: "https://www.url3.com", title: "title3" }, + { url: "https://www.url4.com", title: "title4" }, + { url: "https://www.url5.com", title: "title5" }, + { url: "https://www.url6.com", title: "title6" }] popular_links = FactoryBot.build(:popular_links, link_items:) assert_not popular_links.valid? - assert_equal popular_links.errors[:item][0], "A URL is required for Link 1" + assert popular_links.errors.messages[:title1].include?("Title is required for Link 1") + assert popular_links.errors.messages[:url2].include?("URL is required for Link 2") end - should "validate all links have title" do - link_items = [{ url: "url1" }, - { url: "url2", title: "title2" }, - { url: "url3", title: "title3" }, - { url: "url4", title: "title4" }, - { url: "url5", title: "title5" }, - { url: "url6", title: "title6" }] + should "validate all urls are valid" do + link_items = [{ url: "", title: "" }, + { url: "invalid", title: "title2" }, + { url: "www.abc.co.uk", title: "title3" }, + { url: "www.cde.co.uk", title: "title4" }, + { url: "www.efg.co.uk", title: "title5" }, + { url: "www.ijk.com", title: "title6" }] popular_links = FactoryBot.build(:popular_links, link_items:) assert_not popular_links.valid? - assert_equal popular_links.errors[:item][0], "A Title is required for Link 1" + assert popular_links.errors.messages[:url2].include?("URL is invalid for Link 2, all URLs should have at least one '.' and no spaces.") + assert popular_links.errors.messages[:url1].include?("URL is required for Link 1") + end + + should "create new record from last 'published' record with status as 'draft' and increased 'version_number'" do + popular_links = FactoryBot.create(:popular_links, state: "published") + + assert_equal "published", PopularLinksEdition.last.state + assert_equal 1, PopularLinksEdition.last.version_number + + popular_links.create_draft_popular_links_from_last_record + + assert_equal "draft", PopularLinksEdition.last.state + assert_equal 2, PopularLinksEdition.last.version_number end end diff --git a/test/support/factories.rb b/test/support/factories.rb index 0006628ae..77bdac0b6 100644 --- a/test/support/factories.rb +++ b/test/support/factories.rb @@ -183,7 +183,7 @@ factory :popular_links, class: "PopularLinksEdition" do title { "Homepage Popular Links" } - link_items { [{ url: "url1", title: "title1" }, { url: "url2", title: "title2" }, { url: "url3", title: "title3" }, { url: "url4", title: "title4" }, { url: "url5", title: "title5" }, { url: "url6", title: "title6" }] } + link_items { [{ url: "https://www.url1.com", title: "title1" }, { url: "https://www.url2.com", title: "title2" }, { url: "https://www.url3.com", title: "title3" }, { url: "https://www.url4.com", title: "title4" }, { url: "https://www.url5.com", title: "title5" }, { url: "https://www.url6.com", title: "title6" }] } end factory :programme_edition, parent: :edition, class: "ProgrammeEdition" do