Browse Source

Merge branch 'main' into CLDC-3627-problem-displaying-apostrophes-in-tab-title

pull/2643/head
Manny Dinssa 2 years ago committed by GitHub
parent
commit
70e2772f03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      Gemfile
  2. 7
      Gemfile.lock
  3. 90
      app/controllers/notifications_controller.rb
  4. 4
      app/helpers/application_helper.rb
  5. 2
      app/helpers/navigation_items_helper.rb
  6. 82
      app/helpers/notifications_helper.rb
  7. 21
      app/models/notification.rb
  8. 3
      app/presenters/homepage_presenter.rb
  9. 24
      app/services/postcode_service.rb
  10. 4
      app/views/home/index.html.erb
  11. 33
      app/views/notifications/_form.html.erb
  12. 8
      app/views/notifications/_notification_banner.html.erb
  13. 17
      app/views/notifications/_notification_home_section.html.erb
  14. 61
      app/views/notifications/check_answers.html.erb
  15. 27
      app/views/notifications/delete_confirmation.html.erb
  16. 3
      app/views/notifications/edit.html.erb
  17. 3
      app/views/notifications/new.html.erb
  18. 5
      app/views/notifications/show.html.erb
  19. 15
      config/locales/en.yml
  20. 5
      config/routes.rb
  21. 5
      db/migrate/20240904135306_add_show_additional_page_column_to_notifications.rb
  22. 15
      db/schema.rb
  23. 1
      spec/factories/notification.rb
  24. 22
      spec/features/notifications_spec.rb
  25. 7
      spec/features/start_page_spec.rb
  26. 2
      spec/lib/tasks/update_schemes_and_locations_from_csv_spec.rb
  27. 4
      spec/models/lettings_log_derived_fields_spec.rb
  28. 11
      spec/models/lettings_log_spec.rb
  29. 37
      spec/models/notification_spec.rb
  30. 11
      spec/models/sales_log_spec.rb
  31. 2
      spec/request_helper.rb
  32. 163
      spec/requests/notifications_controller_spec.rb
  33. 77
      spec/services/postcode_service_spec.rb

4
Gemfile

@ -23,6 +23,7 @@ gem "govuk-components", "~> 5.1"
gem "govuk_design_system_formbuilder", "~> 5.0"
# Convert Markdown into GOV.UK frontend-styled HTML
gem "govuk_markdown"
gem "redcarpet", "~> 3.6"
# GOV UK Notify
gem "notifications-ruby-client"
# A modest javascript framework for the html you already have
@ -38,7 +39,6 @@ gem "devise_two_factor_authentication"
# UK postcode parsing and validation
gem "uk_postcode"
# Get rich data from postcode lookups. Wraps postcodes.io
gem "postcodes_io"
# Use Ruby objects to build reusable markup. A React inspired evolution of the presenter pattern
gem "view_component", "~> 3.9"
# Use the AWS S3 SDK as storage mechanism
@ -111,3 +111,5 @@ end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "cssbundling-rails"
gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]
gem "excon", "~> 0.111.0"

7
Gemfile.lock

@ -183,7 +183,7 @@ GEM
et-orbi (1.2.11)
tzinfo
event_stream_parser (1.0.0)
excon (0.109.0)
excon (0.111.0)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
@ -302,8 +302,6 @@ GEM
racc
pg (1.5.5)
possessive (1.0.1)
postcodes_io (0.4.0)
excon (~> 0.39)
propshaft (0.8.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
@ -533,6 +531,7 @@ DEPENDENCIES
devise_two_factor_authentication
dotenv-rails
erb_lint
excon (~> 0.111.0)
factory_bot_rails
faker
govuk-components (~> 5.1)
@ -549,7 +548,6 @@ DEPENDENCIES
parallel_tests
pg (~> 1.1)
possessive
postcodes_io
propshaft
pry-byebug
puma (~> 5.6)
@ -559,6 +557,7 @@ DEPENDENCIES
rack-mini-profiler (~> 2.0)
rails (~> 7.0.8.3)
rails_admin (~> 3.1)
redcarpet (~> 3.6)
redis (~> 4.8)
roo
rspec-rails

90
app/controllers/notifications_controller.rb

@ -1,19 +1,93 @@
class NotificationsController < ApplicationController
before_action :authenticate_user!, except: %i[show]
before_action :authenticate_scope!, except: %i[show dismiss]
before_action :find_notification, except: %i[new create]
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
def dismiss
if current_user.blank?
redirect_to root_path
else
current_user.newest_active_unread_notification.mark_as_read! for: current_user if current_user.newest_active_unread_notification.present?
redirect_back(fallback_location: root_path)
end
@notification.mark_as_read! for: current_user
redirect_back(fallback_location: root_path)
end
def show
@notification = current_user&.newest_active_unread_notification || Notification.newest_active_unauthenticated_notification
if @notification&.page_content
if !@notification.show_on_unauthenticated_pages && !current_user
render_not_found and return
end
if @notification.show_additional_page
render "show"
else
redirect_back(fallback_location: root_path)
end
end
def new
@notification = Notification.new
end
def create
@notification = Notification.new(notification_model_params)
if @notification.errors.empty? && @notification.save
redirect_to notification_check_answers_path(@notification)
else
render :new, status: :unprocessable_entity
end
end
def update
start_now = params[:notification][:start_now]
if @notification.errors.empty? && @notification.update(notification_model_params)
if start_now
flash[:notice] = "The notification has been created"
redirect_to root_path
else
redirect_to notification_check_answers_path(@notification)
end
elsif start_now
render :check_answers, status: :unprocessable_entity
else
render :edit, status: :unprocessable_entity
end
end
def delete
@notification.update!(end_date: Time.zone.now)
flash[:notice] = "The notification has been deleted"
redirect_to root_path
end
private
def notification_params
params.require(:notification).permit(:title, :show_on_unauthenticated_pages, :show_additional_page, :link_text, :page_content, :start_now)
end
def authenticate_scope!
render_not_found unless current_user.support?
end
def notification_model_params
model_params = notification_params.except(:start_now)
if notification_params[:show_additional_page] == "0"
model_params[:link_text] = nil
model_params[:page_content] = nil
end
model_params[:start_date] = Time.zone.now if notification_params[:start_now]
model_params
end
def find_notification
id = params[:id] || params[:notification_id]
@notification = current_user&.support? ? Notification.find_by(id:) : Notification.active.find_by(id:)
raise ActiveRecord::RecordNotFound unless @notification
@notification
end
end

4
app/helpers/application_helper.rb

@ -13,7 +13,7 @@ module ApplicationHelper
def govuk_header_classes(current_user)
if current_user&.support?
"app-header app-header--orange"
elsif ((current_user.blank? && Notification.active_unauthenticated_notifications.present?) || current_user&.active_unread_notifications.present?) && !current_page?(notifications_path)
elsif notifications_to_display?
"app-header app-header__no-border-bottom"
else
"app-header"
@ -29,7 +29,7 @@ module ApplicationHelper
end
def notifications_to_display?
!current_page?(notifications_path) && (authenticated_user_has_notifications? || unauthenticated_user_has_notifications?)
!request.path.match?(/\/notifications\/\d+$/) && (authenticated_user_has_notifications? || unauthenticated_user_has_notifications?)
end
private

2
app/helpers/navigation_items_helper.rb

@ -47,7 +47,7 @@ module NavigationItemsHelper
private
def home_current?(path)
path == root_path || path == notifications_path
path == root_path || path.match?(/\/notifications\/\d+$/)
end
def lettings_logs_current?(path)

82
app/helpers/notifications_helper.rb

@ -14,4 +14,86 @@ module NotificationsHelper
Notification.newest_active_unauthenticated_notification
end
end
def render_for_banner(title)
# rubocop:disable Rails/HelperInstanceVariable
@banner_renderer ||= NotificationRenderer.new({ invert_link_colour: true, bold_all_text: true })
@banner_markdown ||= Redcarpet::Markdown.new(@banner_renderer, no_intra_emphasis: true)
@banner_markdown.render(title)
# rubocop:enable Rails/HelperInstanceVariable
end
def render_for_summary(title)
render_normal_markdown(title)
end
def render_for_page(notification)
content_includes_own_title = /\A\s*#[^#]/.match?(notification.page_content)
return render_normal_markdown(notification.page_content) if content_includes_own_title
content = "# #{notification.title}\n#{notification.page_content}"
render_normal_markdown(content)
end
def render_for_home(notification)
return render_normal_markdown(notification.title) unless notification.show_additional_page
content = "#{notification.title} \n[#{notification.link_text}](#{notification_path(notification)})"
render_normal_markdown(content)
end
private
def render_normal_markdown(content)
# rubocop:disable Rails/HelperInstanceVariable
@on_page_renderer ||= NotificationRenderer.new({ invert_link_colour: false, bold_all_text: false })
@on_page_markdown ||= Redcarpet::Markdown.new(@on_page_renderer, no_intra_emphasis: true)
@on_page_markdown.render(content)
# rubocop:enable Rails/HelperInstanceVariable
end
end
class NotificationRenderer < Redcarpet::Render::HTML
def initialize(options = {})
link_class = "govuk-link"
link_class += " govuk-link--inverse" if options[:invert_link_colour]
@bold = options[:bold_all_text] # rubocop:disable Rails/HelperInstanceVariable
base_options = { escape_html: true, safe_links_only: true, link_attributes: { class: link_class } }
super base_options
end
def header(text, header_level)
header_size = case header_level
when 1
"xl"
when 2
"l"
when 3
"m"
else
"s"
end
%(<h#{header_level} class="govuk-heading-#{header_size}">#{text}</h#{header_level}>)
end
def paragraph(text)
return %(<p class="govuk-!-font-weight-bold">#{text}</p>) if @bold # rubocop:disable Rails/HelperInstanceVariable
%(<p class="govuk-body-m">#{text}</p>)
end
def list(contents, list_type)
return %(<ol class="govuk-list govuk-list--number">#{contents}</ol>) if list_type == :ordered
%(<ul class="govuk-list govuk-list--bullet">#{contents}</ul>)
end
def hrule
%(<hr class="govuk-section-break govuk-section-break--xl govuk-section-break--visible">)
end
def block_quote(quote)
%(<div class="govuk-inset-text">#{quote}</div>)
end
end

21
app/models/notification.rb

@ -1,7 +1,12 @@
class Notification < ApplicationRecord
acts_as_readable
scope :active, -> { where("start_date <= ? AND end_date >= ?", Time.zone.now, Time.zone.now) }
has_paper_trail
validates :title, presence: { message: I18n.t("activerecord.errors.models.notification.attributes.title.blank") }
validate :validate_additional_page_information
scope :active, -> { where("start_date <= ? AND (end_date >= ? OR end_date is null)", Time.zone.now, Time.zone.now) }
scope :unauthenticated, -> { where(show_on_unauthenticated_pages: true) }
def self.active_unauthenticated_notifications
@ -11,4 +16,18 @@ class Notification < ApplicationRecord
def self.newest_active_unauthenticated_notification
active_unauthenticated_notifications.last
end
private
def validate_additional_page_information
return unless show_additional_page
if link_text.blank?
errors.add :link_text, I18n.t("activerecord.errors.models.notification.attributes.link_text.blank_when_additional_page_set")
end
if page_content.blank?
errors.add :page_content, I18n.t("activerecord.errors.models.notification.attributes.page_content.blank_when_additional_page_set")
end
end
end

3
app/presenters/homepage_presenter.rb

@ -2,7 +2,7 @@ class HomepagePresenter
include Rails.application.routes.url_helpers
include CollectionTimeHelper
attr_reader :current_year_in_progress_lettings_data, :current_year_completed_lettings_data, :current_year_in_progress_sales_data, :current_year_completed_sales_data, :last_year_in_progress_lettings_data, :last_year_completed_lettings_data, :last_year_in_progress_sales_data, :last_year_completed_sales_data, :incomplete_schemes_data
attr_reader :current_year_in_progress_lettings_data, :current_year_completed_lettings_data, :current_year_in_progress_sales_data, :current_year_completed_sales_data, :last_year_in_progress_lettings_data, :last_year_completed_lettings_data, :last_year_in_progress_sales_data, :last_year_completed_sales_data, :incomplete_schemes_data, :active_notifications
def initialize(user)
@user = user
@ -25,6 +25,7 @@ class HomepagePresenter
path: schemes_path(status: [:incomplete], owning_organisation_select: "all"),
}
end
@active_notifications = Notification.active if @user.support?
end
def title_text_for_user

24
app/services/postcode_service.rb

@ -1,26 +1,24 @@
class PostcodeService
def initialize
@pio = Postcodes::IO.new
end
def lookup(postcode)
# Avoid network calls when postcode is invalid
return unless postcode.match(POSTCODE_REGEXP)
postcode_lookup = nil
result = nil
begin
# URI encoding only supports ASCII characters
ascii_postcode = self.class.clean(postcode)
Timeout.timeout(30) { postcode_lookup = @pio.lookup(ascii_postcode) }
rescue Timeout::Error
Rails.logger.warn("Postcodes.io lookup timed out")
end
if postcode_lookup && postcode_lookup.info.present?
{
location_code: postcode_lookup.codes["admin_district"],
location_admin_district: postcode_lookup&.admin_district,
response = Excon.get("https://api.postcodes.io/postcodes/#{ascii_postcode}", idempotent: true, timeout: 30, expects: [200])
parsed_response = JSON.parse(response.body)
result = {
location_code: parsed_response["result"]["codes"]["admin_district"],
location_admin_district: parsed_response["result"]["admin_district"],
}
rescue Excon::Error => e
Rails.logger.warn("Postcode lookup request was not successful: #{e} #{e.response.body}")
rescue StandardError => e
Rails.logger.error("Unexpected error with postcode lookup request: #{e}")
end
result
end
def self.clean(postcode)

4
app/views/home/index.html.erb

@ -6,6 +6,10 @@
<h1 class="govuk-heading-l"><%= @homepage_presenter.title_text_for_user %></h1>
</div>
<% if @current_user.support? %>
<%= render partial: "notifications/notification_home_section", locals: { active_notifications: @homepage_presenter.active_notifications } %>
<% end %>
<div class="app-data-box-section govuk-grid-row">
<% if @homepage_presenter.in_crossover_period? %>

33
app/views/notifications/_form.html.erb

@ -0,0 +1,33 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary %>
<% content_for :page_title, "Create a new notification" %>
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<% end %>
<h1 class="govuk-heading-l">
<%= content_for(:page_title) %>
</h1>
<span class="govuk-caption-m govuk-!-margin-bottom-6">This notification will be visible to all users until you delete it</span>
<%= f.govuk_text_field :title,
label: { text: "Title", size: "m" },
hint: { text: "Use markdown for links to existing pages" } %>
<%= f.govuk_check_boxes_fieldset :display_options, multiple: false, legend: { text: "Display Options" } do %>
<%= f.govuk_check_box :show_on_unauthenticated_pages, 1, 0, multiple: false, label: { text: "Show this notification on unauthenticated pages, for example the start page" } %>
<%= f.govuk_check_box :show_additional_page, 1, 0, multiple: false, label: { text: "Include a link to a separate page with additional information" } do %>
<%= f.govuk_text_field :link_text, label: { text: "Link text" }, hint: { text: "Use descriptive language and relevant terms. The link text should make sense out of context." } %>
<%= f.govuk_text_area :page_content, label: { text: "Page content" }, hint: { text: "Use markdown to format the page content. The page title will be the notification title by default. Use a heading level one if you want to override it." } %>
<% end %>
<% end %>
<span class="govuk-caption-m govuk-!-margin-bottom-6"><%= govuk_link_to "Find out more about using Markdown at Markdown Guide", "https://www.markdownguide.org/basic-syntax/", new_tab: true %></span>
<%= f.govuk_submit "Continue" %>
</div>
</div>

8
app/views/notifications/_notification_banner.html.erb

@ -6,16 +6,16 @@
<% if notification_count > 1 && current_user.present? %>
<p>Notification 1 of <%= notification_count %></p>
<% end %>
<p class="govuk-!-font-weight-bold"><%= notification.title.html_safe %></p>
<% if notification.page_content.present? %>
<%== render_for_banner(notification.title) %>
<% if notification.show_additional_page %>
<div class="govuk-body">
<%= govuk_link_to notification.link_text, notifications_path, class: "govuk-link--inverse govuk-!-font-weight-bold" %>
<%= govuk_link_to notification.link_text, notification_path(notification), class: "govuk-link--inverse govuk-!-font-weight-bold" %>
</div>
<% end %>
</div>
<% if current_user.present? %>
<p class="govuk-grid-column-one-quarter govuk-!-text-align-right ">
<%= govuk_link_to "Dismiss", dismiss_notifications_path, class: "govuk-link--inverse" %>
<%= govuk_link_to "Dismiss", notification_dismiss_path(notification), class: "govuk-link--inverse" %>
</p>
<% end %>
</div>

17
app/views/notifications/_notification_home_section.html.erb

@ -0,0 +1,17 @@
<div class="govuk-grid-row">
<p class="govuk-!-font-weight-bold"><%== I18n.t("active_notifications", count: active_notifications.count) %></p>
<% active_notifications.each do |notification| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-three-quarters">
<%== render_for_home(notification) %>
</div>
<div class="govuk-grid-column-one-quarter">
<%= govuk_link_to("Delete notification", notification_delete_confirmation_path(notification), class: "app-!-colour-red") %>
</div>
</div>
<% end %>
</div>
<div class="govuk-grid-row">
<%= govuk_button_link_to "Create a new notification", new_notification_path %>
</div>

61
app/views/notifications/check_answers.html.erb

@ -0,0 +1,61 @@
<% content_for :title, "Create a notification" %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-three-quarters-from-desktop">
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<% end %>
<h1 class="govuk-heading-l">
<span class="govuk-caption-l">Create a notification</span>
Check your answers
</h1>
<span class="govuk-caption-m govuk-!-margin-bottom-6">This notification will be visible to all users until you delete it</span>
<%= form_for(@notification, method: :patch) do |f| %>
<%= f.govuk_error_summary %>
<div class="govuk-summary-card">
<div class="govuk-summary-card__content">
<%= govuk_summary_list do |summary_list| %>
<% summary_list.with_row do |row| %>
<% row.with_key { "Title" } %>
<% row.with_value do %>
<%== render_for_summary(@notification.title) %>
<% end %>
<% row.with_action(text: "Change", href: edit_notification_path(@notification)) %>
<% end %>
<% summary_list.with_row do |row| %>
<% row.with_key { "Show on unauthenticated pages?" } %>
<% row.with_value { @notification.show_on_unauthenticated_pages ? "Yes" : "No" } %>
<% row.with_action(text: "Change", href: edit_notification_path(@notification)) %>
<% end %>
<% summary_list.with_row do |row| %>
<% row.with_key { "Link to additional information?" } %>
<% row.with_value { @notification.show_additional_page ? "Yes" : "No" } %>
<% row.with_action(text: "Change", href: edit_notification_path(@notification)) %>
<% end %>
<% if @notification.show_additional_page %>
<% summary_list.with_row do |row| %>
<% row.with_key { "Link text" } %>
<% row.with_value { @notification.link_text } %>
<% row.with_action(text: "Change", href: edit_notification_path(@notification)) %>
<% end %>
<% summary_list.with_row do |row| %>
<% row.with_key { "Page content" } %>
<% row.with_value do %>
<%= govuk_link_to "View page preview", notification_path(@notification), new_tab: true %>
<% end %>
<% row.with_action(text: "Change", href: edit_notification_path(@notification)) %>
<% end %>
<% end %>
<% end %>
</div>
</div>
<%= f.hidden_field :start_now, value: true %>
<%= f.govuk_submit "Create notification" %>
<% end %>
</div>
</div>

27
app/views/notifications/delete_confirmation.html.erb

@ -0,0 +1,27 @@
<% content_for :before_content do %>
<% content_for :title, "Are you sure you want to delete this notification?" %>
<%= govuk_back_link(href: :back) %>
<% end %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<h1 class="govuk-heading-xl">
<%= content_for(:title) %>
</h1>
<p class="govuk-body"><%== render_for_summary("**Notification:** #{@notification.title}") %></p>
<p class="govuk-body">Users will no longer see this notification.</p>
<%= govuk_warning_text(text: "You will not be able to undo this action.") %>
<div class="govuk-button-group">
<%= govuk_button_to(
"Delete notification",
notification_delete_path(@notification),
method: :delete,
) %>
<%= govuk_button_link_to "Cancel", root_path, html: { method: :get }, secondary: true %>
</div>
</div>
</div>

3
app/views/notifications/edit.html.erb

@ -0,0 +1,3 @@
<%= form_for(@notification, as: :notification, method: :patch) do |f| %>
<% render partial: "form", locals: { notification: @notification, f: } %>
<% end %>

3
app/views/notifications/new.html.erb

@ -0,0 +1,3 @@
<%= form_for(@notification, as: :notification, method: :post) do |f| %>
<% render partial: "form", locals: { notification: @notification, f: } %>
<% end %>

5
app/views/notifications/show.html.erb

@ -5,10 +5,7 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl"><%= @notification.title %></h1>
<p>
<%= sanitize @notification.page_content %>
</p>
<%== render_for_page(@notification) %>
</div>
</div>
<br>

15
config/locales/en.yml

@ -41,6 +41,11 @@ en:
create_password: "Create a password to finish setting up your account"
reset_password: "Reset your password"
active_notifications:
zero: "There are no active notifications"
one: "There is one active notification:"
other: "There are %{count} active notifications:"
activemodel:
errors:
models:
@ -180,11 +185,19 @@ en:
attributes:
absorbing_organisation_id:
blank: "Select the absorbing organisation"
merge_date:
merge_date:
blank: "Enter a merge date"
invalid: "Enter a valid merge date"
existing_absorbing_organisation:
blank: "You must answer absorbing organisation already active?"
notification:
attributes:
title:
blank: "Enter a title"
link_text:
blank_when_additional_page_set: "Enter the link text"
page_content:
blank_when_additional_page_set: "Enter the page content"
notification:
logs_deleted:

5
config/routes.rb

@ -142,8 +142,11 @@ Rails.application.routes.draw do
end
end
resource :notifications do
resources :notifications do
get "dismiss", to: "notifications#dismiss"
get "check-answers", to: "notifications#check_answers"
get "delete-confirmation", to: "notifications#delete_confirmation"
delete "delete", to: "notifications#delete"
end
resources :organisations do

5
db/migrate/20240904135306_add_show_additional_page_column_to_notifications.rb

@ -0,0 +1,5 @@
class AddShowAdditionalPageColumnToNotifications < ActiveRecord::Migration[7.0]
def change
add_column :notifications, :show_additional_page, :boolean
end
end

15
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: 2024_09_11_152702) do
ActiveRecord::Schema[7.0].define(version: 2024_09_18_112702) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -55,7 +55,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_11_152702) do
t.datetime "last_accessed"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.check_constraint "log_type::text = ANY (ARRAY['lettings'::character varying::text, 'sales'::character varying::text])", name: "log_type_check"
t.check_constraint "log_type::text = ANY (ARRAY['lettings'::character varying, 'sales'::character varying]::text[])", name: "log_type_check"
t.check_constraint "year >= 2000 AND year <= 2099", name: "year_check"
end
@ -206,14 +206,14 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_11_152702) do
t.integer "hb"
t.integer "hbrentshortfall"
t.integer "property_relet"
t.datetime "mrcdate", precision: nil
t.datetime "mrcdate"
t.integer "incref"
t.datetime "startdate", precision: nil
t.datetime "startdate"
t.integer "armedforces"
t.integer "first_time_property_let_as_social_housing"
t.integer "unitletas"
t.integer "builtype"
t.datetime "voiddate", precision: nil
t.datetime "voiddate"
t.bigint "owning_organisation_id"
t.bigint "managing_organisation_id"
t.integer "renttype"
@ -459,6 +459,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_11_152702) do
t.boolean "show_on_unauthenticated_pages"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "show_additional_page"
end
create_table "organisation_relationships", force: :cascade do |t|
@ -779,8 +780,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_11_152702) do
t.string "name"
t.bigint "organisation_id"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at", precision: nil
t.datetime "last_sign_in_at", precision: nil
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "role"

1
spec/factories/notification.rb

@ -6,5 +6,6 @@ FactoryBot.define do
start_date { Time.zone.yesterday }
end_date { Time.zone.tomorrow }
show_on_unauthenticated_pages { false }
show_additional_page { true }
end
end

22
spec/features/notifications_page_spec.rb → spec/features/notifications_spec.rb

@ -1,7 +1,7 @@
require "rails_helper"
require_relative "form/helpers"
RSpec.describe "Notifications Page Features" do
RSpec.describe "Notifications Features" do
include Helpers
context "when there are notifications" do
@ -9,32 +9,30 @@ RSpec.describe "Notifications Page Features" do
context "when the notifications are currently active" do
before do
create(:notification, title: "Notification title 1")
create(:notification, title: "Notification title 2")
create(:notification, title: "Notification title 3")
create(:notification, start_date: Time.zone.yesterday, title: "Notification title 1")
create(:notification, start_date: Time.zone.yesterday, end_date: Time.zone.tomorrow, title: "Notification title 2")
sign_in user
visit(notifications_path)
visit(root_path)
end
it "does not show the notification banner" do
expect(page).not_to have_content("Notification 1 of")
expect(page).not_to have_link("Dismiss")
expect(page).not_to have_link("Link text")
it "shows the notification banner" do
expect(page).to have_content("Notification 1 of")
expect(page).to have_link("Dismiss")
end
end
context "when the notifications are not currently active" do
before do
create(:notification, end_date: Time.zone.yesterday, title: "Notification title 1")
create(:notification, start_date: Time.zone.yesterday, end_date: Time.zone.yesterday, title: "Notification title 1")
create(:notification, start_date: Time.zone.tomorrow, title: "Notification title 2")
create(:notification, start_date: nil, title: "Notification title 3")
sign_in user
visit(notifications_path)
visit(root_path)
end
it "does not show the notifications banner" do
expect(page).not_to have_content("Notification 1 of")
expect(page).not_to have_link("Dismiss")
expect(page).not_to have_link("Link text")
end
end
end

7
spec/features/start_page_spec.rb

@ -30,14 +30,15 @@ RSpec.describe "Start Page Features" do
end
context "when the unauthenticated user clicks a notification link" do
let!(:notification) { create(:notification, title: "Notification title", link_text: "link", page_content: "Some html content", show_on_unauthenticated_pages: true) }
before do
create(:notification, show_on_unauthenticated_pages: true)
visit(root_path)
click_link("Link text")
click_link("link")
end
it "takes them to the notification details page" do
expect(page).to have_current_path(notifications_path)
expect(page).to have_current_path(notification_path(notification))
expect(page).to have_content("Notification title")
expect(page).to have_content("Some html content")
expect(page).to have_link("Back to Start")

2
spec/lib/tasks/update_schemes_and_locations_from_csv_spec.rb

@ -25,7 +25,7 @@ RSpec.describe "bulk_update" do
allow(ENV).to receive(:[]).with("BULK_UPLOAD_BUCKET").and_return(instance_name)
WebMock.stub_request(:get, /api\.postcodes\.io/)
.to_return(status: 200, body: "{\"status\":404,\"error\":\"Postcode not found\"}", headers: {})
.to_return(status: 404, body: "{\"status\":404,\"error\":\"Postcode not found\"}", headers: {})
WebMock.stub_request(:get, /api\.postcodes\.io\/postcodes\/B11BB/)
.to_return(status: 200, body: '{"status":200,"result":{"admin_district":"Westminster","codes":{"admin_district":"E09000033"}}}', headers: {})
end

4
spec/models/lettings_log_derived_fields_spec.rb

@ -981,9 +981,9 @@ RSpec.describe LettingsLog, type: :model do
end
describe "deriving voiddate from startdate" do
let(:startdate) { Time.zone.now }
let(:startdate) { Time.zone.now.beginning_of_day }
it "correctly derives voiddate if the letting is a renewal and clears it if it is not" do
it "correctly derives voiddate if the letting is a renewal" do
log.assign_attributes(renewal: 1, startdate:)
expect { log.set_derived_fields! }.to change(log, :voiddate).to startdate

11
spec/models/lettings_log_spec.rb

@ -329,17 +329,6 @@ RSpec.describe LettingsLog do
.to raise_error(ActiveRecord::RecordInvalid, /#{I18n.t("validations.postcode")}/)
end
context "when the local authority lookup times out" do
before do
allow(Timeout).to receive(:timeout).and_raise(Timeout::Error)
end
it "logs a warning" do
expect(Rails.logger).to receive(:warn).with("Postcodes.io lookup timed out")
address_lettings_log.update!({ postcode_known: 1, postcode_full: "M1 1AD" })
end
end
it "correctly resets all fields if property postcode not known" do
address_lettings_log.update!({ postcode_known: 0 })

37
spec/models/notification_spec.rb

@ -0,0 +1,37 @@
require "rails_helper"
RSpec.describe Notification, type: :model do
describe "#valid?" do
context "when show additional page is true" do
context "and page_content is blank" do
let(:notification) { build(:notification, show_additional_page: true, page_content: "") }
it "adds an error to page_content" do
notification.valid?
expect(notification.errors[:page_content]).to include("Enter the page content")
end
end
context "and link_text is blank" do
let(:notification) { build(:notification, show_additional_page: true, link_text: nil) }
it "adds an error to link_text" do
notification.valid?
expect(notification.errors[:link_text]).to include("Enter the link text")
end
end
end
context "when show additional page is false" do
context "and page_content and link_text are blank" do
let(:notification) { build(:notification, show_additional_page: false, link_text: nil, page_content: nil) }
it "is valid" do
expect(notification).to be_valid
end
end
end
end
end

11
spec/models/sales_log_spec.rb

@ -628,17 +628,6 @@ RSpec.describe SalesLog, type: :model do
.to raise_error(ActiveRecord::RecordInvalid, /#{I18n.t("validations.postcode")}/)
end
context "when the local authority lookup times out" do
before do
allow(Timeout).to receive(:timeout).and_raise(Timeout::Error)
end
it "logs a warning" do
expect(Rails.logger).to receive(:warn).with("Postcodes.io lookup timed out")
address_sales_log.update!({ pcodenk: 1, postcode_full: "M1 1AD" })
end
end
it "correctly resets all fields if property postcode not known" do
address_sales_log.update!({ pcodenk: 1 })

2
spec/request_helper.rb

@ -4,7 +4,7 @@ module RequestHelper
def self.stub_http_requests
WebMock.disable_net_connect!(allow_localhost: true)
WebMock.stub_request(:get, /api\.postcodes\.io/)
.to_return(status: 200, body: "{\"status\":404,\"error\":\"Postcode not found\"}", headers: {})
.to_return(status: 404, body: "{\"status\":404,\"error\":\"Postcode not found\"}", headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA11AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 1AA\",\"admin_district\":\"Westminster\",\"codes\":{\"admin_district\":\"E09000033\"}}}", headers: {})

163
spec/requests/notifications_controller_spec.rb

@ -0,0 +1,163 @@
require "rails_helper"
RSpec.describe NotificationsController, type: :request do
context "when user is signed in as a support user" do
let(:support_user) { create(:user, :support) }
before do
allow(support_user).to receive(:need_two_factor_authentication?).and_return(false)
sign_in support_user
end
describe "#create" do
let(:request) { post notifications_path, params: params }
context "with valid parameters" do
let(:params) { { "notification": { title: "Test Create", show_on_unauthenticated_pages: "1", show_additional_page: "1", link_text: "link", page_content: "page" } } }
it "creates a new notification with no start date set" do
request
notification = Notification.find_by(title: "Test Create")
expect(notification.show_on_unauthenticated_pages).to be(true)
expect(notification.show_additional_page).to be(true)
expect(notification.link_text).to eq("link")
expect(notification.page_content).to eq("page")
expect(notification.start_date).to be_nil
end
it "redirects to check answers page" do
request
notification = Notification.find_by(title: "Test Create")
expect(response).to redirect_to(notification_check_answers_path(notification))
end
end
context "with invalid parameters" do
let(:params) { { "notification": { title: "", show_on_unauthenticated_pages: "1" } } }
it "gives an error response" do
request
expect(response).to have_http_status(:unprocessable_entity)
end
end
context "when show additional page is false" do
let(:params) { { "notification": { title: "No Additional Page", show_on_unauthenticated_pages: "1", show_additional_page: "0", link_text: "text", page_content: "content" } } }
it "ignores values for link_text and page_content" do
request
notification = Notification.find_by(title: "No Additional Page")
expect(notification.link_text).to be_nil
expect(notification.page_content).to be_nil
end
end
end
describe "#update" do
let(:notification) { create(:notification, title: "Initial Title", start_date: nil, end_date: nil) }
let(:request) { patch notification_path(notification), params: params }
context "when start_now is set to true" do
let(:params) { { "notification": { start_now: true } } }
it "sets the start date on the notification" do
request
notification.reload
expect(notification.start_date).not_to be_nil
expect(notification.start_date).to be < Time.zone.now
end
it "redirects to the home page" do
request
expect(response).to redirect_to(root_path)
end
end
context "when start_now is not set" do
let(:params) { { "notification": { title: "Updated Title", show_on_unauthenticated_pages: "1" } } }
it "sets the relevant values on the notification" do
request
notification.reload
expect(notification.title).to eql("Updated Title")
expect(notification.start_date).to be_nil
end
it "redirects to check answers" do
request
expect(response).to redirect_to(notification_check_answers_path(notification))
end
end
context "when show additional page is false" do
let(:notification) { create(:notification, show_additional_page: "0", link_text: "link", page_content: "page") }
let(:params) { { "notification": { show_additional_page: "0", link_text: "text", page_content: "content" } } }
it "removes values for link_text and page_content" do
request
notification.reload
expect(notification.link_text).to be_nil
expect(notification.page_content).to be_nil
end
end
end
describe "#delete" do
let(:notification) { create(:notification, end_date: nil) }
let(:request) { delete notification_delete_path(notification) }
it "sets end_date on the notification" do
request
notification.reload
expect(notification.end_date).to be < Time.zone.now
end
end
end
context "when user is signed in as a non-support user" do
let(:user) { create(:user, :data_coordinator) }
before do
sign_in user
end
describe "#create" do
let(:request) { post notifications_path, params: { "notification": { title: "Test Create" } } }
it "returns not found" do
request
expect(response).to have_http_status(:not_found)
end
it "does not create a notification" do
expect { request }.not_to change(Notification, :count)
end
end
describe "#update" do
let(:notification) { create(:notification) }
let(:request) { patch notification_path(notification), params: { "notification": { title: "Test Update" } } }
it "returns not found" do
request
expect(response).to have_http_status(:not_found)
end
end
describe "#delete" do
let(:notification) { create(:notification, end_date: nil) }
let(:request) { delete notification_delete_path(notification) }
it "returns not found" do
request
expect(response).to have_http_status(:not_found)
end
it "does not set end_date on the notification" do
request
notification.reload
expect(notification.end_date).to be_nil
end
end
end
end

77
spec/services/postcode_service_spec.rb

@ -1,9 +1,80 @@
require "rails_helper"
describe PostcodeService do
let(:postcode) { "s r81LS\u00A0" }
let(:service) { described_class.new }
it "returns clean postcode" do
expect(described_class.clean(postcode)).to eq "SR81LS"
describe "clean" do
let(:postcode) { "s r81LS\u00A0" }
it "returns clean postcode" do
expect(described_class.clean(postcode)).to eq "SR81LS"
end
end
describe "lookup" do
before do
Excon.defaults[:mock] = true
Excon.defaults[:stubs] = :local
end
context "when the request returns a success response" do
before do
Excon.stub({}, { body: '{"result": { "admin_district": "District", "codes": { "admin_district": "123" } } }', status: 200 })
end
it "returns the admin district and admin district code" do
result = service.lookup("A00 0AA")
expect(result[:location_code]).to eq("123")
expect(result[:location_admin_district]).to eq("District")
end
end
context "when the request returns a not found response" do
before do
Excon.stub({}, { status: 404 })
end
it "returns nil" do
result = service.lookup("A00 0AA")
expect(result).to be_nil
end
it "logs the error at warning level" do
expect(Rails.logger).to receive(:warn).with(match "404 Not Found")
service.lookup("A00 0AA")
end
end
context "when the request returns an error response" do
before do
Excon.stub({}, { body: "This is an error message that is not valid json", status: 500 })
end
it "returns nil" do
result = service.lookup("A00 0AA")
expect(result).to be_nil
end
it "logs the error at warning level" do
expect(Rails.logger).to receive(:warn).with(match "This is an error message that is not valid json")
service.lookup("A00 0AA")
end
end
context "when the request returns a success response that causes later errors" do
before do
Excon.stub({}, { body: '{"result": { "admin_district": "District" } }', status: 200 })
end
it "returns nil" do
result = service.lookup("A00 0AA")
expect(result).to be_nil
end
it "logs the error at error level" do
expect(Rails.logger).to receive(:error).with(match "Unexpected error with postcode lookup request")
service.lookup("A00 0AA")
end
end
end
end

Loading…
Cancel
Save