Skip to content

Commit e9187de

Browse files
authored
Merge pull request #1266 from Crown-Commercial-Service/feature/nrmi-11-add-stop-time-to-notifications
Feature/nrmi 11 add stop time to notifications
2 parents ca5eb11 + 3bb13ad commit e9187de

11 files changed

Lines changed: 171 additions & 19 deletions

File tree

app/controllers/admin/notifications_controller.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
class Admin::NotificationsController < AdminController
44
def index
55
markdown_parser = Redcarpet::Markdown.new(CustomMarkdownRenderer)
6-
@published_notification = Notification.published.first
6+
@published_notification = Notification.currently_active.first
77
if @published_notification
88
@published_notification_message = markdown_parser.render(@published_notification[:notification_message])
99
end
@@ -22,9 +22,7 @@ def show
2222
end
2323

2424
def create
25-
@notification = Notification.new(summary: notification_params[:summary],
26-
notification_message: notification_params[:notification_message],
27-
user: current_user['email'], published: true, published_at: Time.zone.now)
25+
@notification = create_notifications
2826
Notification.transaction do
2927
if @notification.save
3028
flash[:success] = 'Notification created successfully.'
@@ -55,6 +53,23 @@ def unpublish
5553
private
5654

5755
def notification_params
58-
params.require(:notification).permit(:summary, :notification_message)
56+
params.require(:notification).permit(:summary, :notification_message, :stop_datetime)
57+
end
58+
59+
def create_notifications
60+
Notification.new(summary: notification_params[:summary],
61+
notification_message: notification_params[:notification_message],
62+
user: current_user['email'],
63+
published: true,
64+
published_at: Time.zone.now,
65+
stop_datetime: parsed_stop_datetime)
66+
end
67+
68+
def parsed_stop_datetime
69+
return nil if notification_params[:stop_datetime].blank?
70+
71+
Time.use_zone('Europe/London') do
72+
Time.zone.parse(notification_params[:stop_datetime])
73+
end
5974
end
6075
end

app/controllers/v1/notifications_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
class V1::NotificationsController < ApiController
44
def index
5+
Notification.expire_past_due!
56
markdown_parser = Redcarpet::Markdown.new(CustomMarkdownRenderer)
67
notifications = Notification.published.first
78
notifications[:notification_message] = markdown_parser.render(notifications[:notification_message]) if notifications

app/models/notification.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
11
class Notification < ApplicationRecord
22
validates :summary, :notification_message, presence: true
3+
validate :stop_datetime_in_future
34

45
before_save :ensure_single_published_notification
56

67
scope :published, -> { where(published: true) }
8+
scope :currently_active, lambda {
9+
where(published: true)
10+
.where('stop_datetime IS NULL OR stop_datetime > ?', Time.current)
11+
}
712

813
def unpublish!
914
self.published = false
1015
self.unpublished_at = Time.zone.now
1116
save!
1217
end
1318

19+
def self.expire_past_due!
20+
expired = where('stop_datetime < ? AND published = ?', Time.current, true)
21+
# We use update_all for performance to expire records in a single
22+
# SQL query, bypassing validations for speed on every API request.
23+
# rubocop:disable Rails/SkipsModelValidations
24+
expired.update_all(published: false, unpublished_at: Time.zone.now)
25+
# rubocop:enable Rails/SkipsModelValidations
26+
end
27+
1428
private
1529

1630
def ensure_single_published_notification
1731
return unless published_changed? && published?
1832

1933
Notification.where.not(id: id).where(published: true).find_each(&:unpublish!)
2034
end
35+
36+
def stop_datetime_in_future
37+
return unless stop_datetime.present? && stop_datetime < Time.current
38+
39+
errors.add(:stop_datetime, 'must be in the future')
40+
end
2141
end

app/views/admin/notifications/index.html.haml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@
3636
%th.govuk-table__header Header
3737
%th.govuk-table__header Published
3838
%th.govuk-table__header Unpublished
39+
%th.govuk-table__header Date time to Unpublish
3940
%tbody.govuk-table__body
4041
- @notifications.each do |notification|
4142
%tr.govuk-table__row
4243
%td.govuk-table__cell= link_to notification.summary, admin_notification_path(notification.id)
4344
%td.govuk-table__cell= notification.published_at
4445
%td.govuk-table__cell= notification.unpublished_at
46+
%td.govuk-table__cell= notification.stop_datetime

app/views/admin/notifications/new.html.haml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
11
.govuk-grid-row
22
.govuk-grid-column-two-thirds
33
= link_to 'Back', admin_notifications_path, { class: 'govuk-back-link govuk-!-margin-bottom-5', title: 'Back to notifications' }
4-
54
= simple_form_for [:admin, @notification] do |form|
65
%fieldset.govuk-fieldset
76
%legend.govuk-fieldset__legend.govuk-fieldset__legend--xl
87
%h1.govuk-fieldset__heading
98
Create a new notification
109

11-
= form.input :summary, hide_optional: true, input_html: { value: @published_notification&.summary }
12-
= form.input :notification_message, label: 'Notification message', hide_optional: true, wrapper: :govuk_textarea_wrapper, input_html: { rows: '10', value: @published_notification&.notification_message }
10+
= form.input :summary,
11+
hide_optional: true,
12+
input_html: { value: @notification.summary || @published_notification&.summary }
13+
14+
= form.input :notification_message,
15+
label: 'Notification message',
16+
hide_optional: true,
17+
wrapper: :govuk_textarea_wrapper,
18+
input_html: { rows: '10', value: @notification.notification_message || @published_notification&.notification_message }
19+
20+
.govuk-form-group{class: ("govuk-form-group--error" if @notification.errors[:stop_datetime].any?)}
21+
%label.govuk-label{ for: "notification_stop_datetime" }
22+
Unpublish on
23+
24+
- if @notification.errors[:stop_datetime].any?
25+
%span.govuk-error-message
26+
%span.govuk-visually-hidden Error:
27+
= @notification.errors[:stop_datetime].join(', ')
28+
29+
- current_date = @notification.stop_datetime || @published_notification&.stop_datetime
30+
- current_date_london = current_date&.in_time_zone('Europe/London')
31+
= form.datetime_local_field :stop_datetime,
32+
class: "govuk-input",
33+
value: current_date_london&.strftime("%Y-%m-%dT%H:%M")
34+
%button.govuk-button.govuk-button--secondary{ type: "button", onclick: "document.getElementById('notification_stop_datetime').value = ''", class: "govuk-!-margin-top-1" }
35+
Clear date
36+
1337
%button#markdown-preview-btn{type: "button", class: 'govuk-button'} Preview
1438
= form.button :submit, value: 'Publish Notification', data: { disable_with: "Publish Notification" }
1539
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AddStopDatetimeToNotifications < ActiveRecord::Migration[6.0]
2+
def change
3+
add_column :notifications, :stop_datetime, :datetime, null: true
4+
add_index :notifications, :stop_datetime
5+
end
6+
end

db/schema.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,12 @@
161161
t.text "notification_message"
162162
t.boolean "published", default: false
163163
t.datetime "published_at"
164+
t.datetime "stop_datetime", precision: nil
164165
t.text "summary", null: false
165166
t.datetime "unpublished_at"
166167
t.string "user"
167168
t.index ["published"], name: "index_notifications_on_published"
169+
t.index ["stop_datetime"], name: "index_notifications_on_stop_datetime"
168170
end
169171

170172
create_table "release_notes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|

spec/factories/notifications.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
notification_message { 'Wear sunscreen' }
55
published { false }
66
published_at { Time.zone.now }
7+
stop_datetime { nil }
78
unpublished_at { nil }
89
user { 'testy.mctestface@example.com' }
910
end

spec/models/notification_spec.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,56 @@
55
subject { create(:notification, published: true) }
66
it { is_expected.to validate_presence_of(:summary) }
77
it { is_expected.to validate_presence_of(:notification_message) }
8+
9+
it 'is invalid if stop_datetime is in the past' do
10+
notification = build(:notification, stop_datetime: 1.day.ago)
11+
expect(notification).not_to be_valid
12+
expect(notification.errors[:stop_datetime]).to include('must be in the future')
13+
end
14+
15+
it 'is valid if stop_datetime is in the future' do
16+
notification = build(:notification, stop_datetime: 1.day.from_now)
17+
expect(notification).to be_valid
18+
end
19+
20+
it 'is valid if stop_datetime is nil' do
21+
notification = build(:notification, stop_datetime: nil)
22+
expect(notification).to be_valid
23+
end
24+
end
25+
26+
describe 'scopes' do
27+
describe '.currently_active' do
28+
it 'includes published notifications with future stop dates' do
29+
active = create(:notification, published: true, stop_datetime: 1.hour.from_now)
30+
expect(Notification.currently_active).to include(active)
31+
end
32+
33+
it 'includes published notifications with no stop date' do
34+
permanent = create(:notification, published: true, stop_datetime: nil)
35+
expect(Notification.currently_active).to include(permanent)
36+
end
37+
38+
it 'excludes notifications that have passed their stop date' do
39+
# Use save(validate: false) to simulate an old record that was valid when created
40+
expired = build(:notification, published: true, stop_datetime: 1.hour.ago)
41+
expired.save(validate: false)
42+
expect(Notification.currently_active).not_to include(expired)
43+
end
44+
end
45+
end
46+
47+
describe '.expire_past_due!' do
48+
it 'updates all past-due notifications to be unpublished' do
49+
expired = build(:notification, published: true, stop_datetime: 1.minute.ago)
50+
expired.save(validate: false)
51+
52+
Notification.expire_past_due!
53+
54+
expired.reload
55+
expect(expired.published).to be false
56+
expect(expired.unpublished_at).to be_within(1.second).of(Time.zone.now)
57+
end
858
end
959

1060
describe '#unpublish!' do

spec/requests/admin/notifications_spec.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,33 @@
99
get '/auth/google_oauth2/callback'
1010
end
1111

12+
describe '#create' do
13+
around do |example|
14+
travel_to(Time.utc(2026, 4, 20, 12, 0, 0)) { example.run }
15+
end
16+
17+
it 'stores stop_datetime as Europ/London local time converted to UTC' do
18+
london_input = '2026-04-20T14:15:00' # 2:15 PM London time on April 20, 2026
19+
20+
expect do
21+
post admin_notifications_path, params: {
22+
notification: {
23+
summary: 'Test Notification',
24+
notification_message: 'This is a test notification.',
25+
stop_datetime: london_input
26+
}
27+
}
28+
end.to change(Notification, :count).by(1)
29+
30+
notification = Notification.order(published_at: :desc).first
31+
32+
expected_time = ActiveSupport::TimeZone['Europe/London'].parse(london_input)
33+
34+
expect(notification.stop_datetime).to eq(expected_time)
35+
expect(notification.stop_datetime.utc).to eq(expected_time.utc)
36+
end
37+
end
38+
1239
describe '#preview' do
1340
it 'renders the Markdown content as HTML' do
1441
markdown_content = '**Bold Text**'

0 commit comments

Comments
 (0)