Commit e94c1028 authored by Sean McGivern's avatar Sean McGivern

Deserialise existing custom notification settings

Create a post-deployment migration to update all existing notification settings
with at least one custom level enabled to the new format. Also handle the same
conversion when updating settings, to catch any stragglers.
parent f4b5fcbc
...@@ -42,6 +42,7 @@ class NotificationSetting < ActiveRecord::Base ...@@ -42,6 +42,7 @@ class NotificationSetting < ActiveRecord::Base
].freeze ].freeze
store :events, coder: JSON store :events, coder: JSON
before_save :convert_events
def self.find_or_create_for(source) def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source) setting = find_or_initialize_by(source: source)
...@@ -53,21 +54,42 @@ class NotificationSetting < ActiveRecord::Base ...@@ -53,21 +54,42 @@ class NotificationSetting < ActiveRecord::Base
setting setting
end end
EMAIL_EVENTS.each do |event| # 1. Check if this event has a value stored in its database column.
# 2. If it does, return that value.
# 3. If it doesn't (the value is nil), return the value from the serialized
# JSON hash in `events`.
(EMAIL_EVENTS - [:failed_pipeline]).each do |event|
define_method(event) do define_method(event) do
bool = super() bool = super()
bool.nil? ? !!events[event] : bool bool.nil? ? !!events[event] : bool
end end
alias_method :"#{event}?", event
end end
# Allow people to receive failed pipeline notifications if they already have # Allow people to receive failed pipeline notifications if they already have
# custom notifications enabled, as these are more like mentions than the other # custom notifications enabled, as these are more like mentions than the other
# custom settings. # custom settings.
def failed_pipeline def failed_pipeline
bool = read_attribute(:failed_pipeline) bool = super
bool = events[:failed_pipeline] if bool.nil? bool = events[:failed_pipeline] if bool.nil?
bool.nil? || bool bool.nil? || bool
end end
alias_method :failed_pipeline?, :failed_pipeline
def event_enabled?(event)
respond_to?(event) && public_send(event)
end
def convert_events
return if events_before_type_cast.nil?
EMAIL_EVENTS.each do |event|
write_attribute(event, public_send(event))
end
write_attribute(:events, nil)
end
end end
...@@ -8,7 +8,7 @@ class NotificationRecipientService ...@@ -8,7 +8,7 @@ class NotificationRecipientService
@project = project @project = project
end end
def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true) def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
recipients = target.participants(current_user) recipients = target.participants(current_user)
...@@ -59,7 +59,7 @@ class NotificationRecipientService ...@@ -59,7 +59,7 @@ class NotificationRecipientService
return [] if notification_setting.mention? || notification_setting.disabled? return [] if notification_setting.mention? || notification_setting.disabled?
return [] if notification_setting.custom? && !notification_setting.public_send(custom_action) return [] if notification_setting.custom? && !notification_setting.event_enabled?(custom_action)
return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
...@@ -176,7 +176,7 @@ class NotificationRecipientService ...@@ -176,7 +176,7 @@ class NotificationRecipientService
if notification_level if notification_level
settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
settings = settings.select { |setting| setting.events[action] } if action.present? settings = settings.select { |setting| setting.event_enabled?(action) } if action.present?
settings.map(&:user_id) settings.map(&:user_id)
else else
resource.notification_settings.pluck(:user_id) resource.notification_settings.pluck(:user_id)
...@@ -225,7 +225,7 @@ class NotificationRecipientService ...@@ -225,7 +225,7 @@ class NotificationRecipientService
def user_ids_with_global_level_custom(ids, action) def user_ids_with_global_level_custom(ids, action)
settings = settings_with_global_level_of(:custom, ids) settings = settings_with_global_level_of(:custom, ids)
settings = settings.select { |setting| setting.events[action] } settings = settings.select { |setting| setting.event_enabled?(action) }
settings.map(&:user_id) settings.map(&:user_id)
end end
......
...@@ -273,7 +273,7 @@ class NotificationService ...@@ -273,7 +273,7 @@ class NotificationService
end end
def issue_moved(issue, new_issue, current_user) def issue_moved(issue, new_issue, current_user)
recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user) recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user, action: 'moved')
recipients.map do |recipient| recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
......
class ConvertCustomNotificationSettingsToColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class NotificationSetting < ActiveRecord::Base
self.table_name = 'notification_settings'
store :events, coder: JSON
end
EMAIL_EVENTS = [
:new_note,
:new_issue,
:reopen_issue,
:close_issue,
:reassign_issue,
:new_merge_request,
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
:merge_merge_request,
:failed_pipeline,
:success_pipeline
]
# We only need to migrate (up or down) rows where at least one of these
# settings is set.
def up
NotificationSetting.where("events LIKE '%true%'").find_each do |notification_setting|
EMAIL_EVENTS.each do |event|
notification_setting[event] = notification_setting.events[event]
end
notification_setting[:events] = nil
notification_setting.save!
end
end
def down
NotificationSetting.where(EMAIL_EVENTS.join(' OR ')).find_each do |notification_setting|
events = {}
EMAIL_EVENTS.each do |event|
events[event] = !!notification_setting.public_send(event)
notification_setting[event] = nil
end
notification_setting[:events] = events
notification_setting.save!
end
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170606202615) do ActiveRecord::Schema.define(version: 20170607121233) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -60,7 +60,7 @@ describe NotificationSettingsController do ...@@ -60,7 +60,7 @@ describe NotificationSettingsController do
expect(notification_setting.level).to eq("custom") expect(notification_setting.level).to eq("custom")
custom_events.each do |event, value| custom_events.each do |event, value|
expect(notification_setting.send(event)).to eq(value) expect(notification_setting.event_enabled?(event)).to eq(value)
end end
end end
end end
...@@ -91,7 +91,7 @@ describe NotificationSettingsController do ...@@ -91,7 +91,7 @@ describe NotificationSettingsController do
expect(notification_setting.level).to eq("custom") expect(notification_setting.level).to eq("custom")
custom_events.each do |event, value| custom_events.each do |event, value|
expect(notification_setting.send(event)).to eq(value) expect(notification_setting.event_enabled?(event)).to eq(value)
end end
end end
end end
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns')
describe ConvertCustomNotificationSettingsToColumns, :migration do
let(:settings_params) do
[
{ level: 0, events: [:new_note] }, # disabled, single event
{ level: 3, events: [:new_issue, :reopen_issue, :close_issue, :reassign_issue] }, # global, multiple events
{ level: 5, events: described_class::EMAIL_EVENTS }, # custom, all events
{ level: 5, events: [] } # custom, no events
]
end
let(:notification_settings_before) do
settings_params.map do |params|
events = {}
params[:events].each do |event|
events[event] = true
end
user = create(:user)
create_params = { user_id: user.id, level: params[:level], events: events }
notification_setting = described_class::NotificationSetting.create(create_params)
[notification_setting, params]
end
end
let(:notification_settings_after) do
settings_params.map do |params|
events = {}
params[:events].each do |event|
events[event] = true
end
user = create(:user)
create_params = events.merge(user_id: user.id, level: params[:level])
notification_setting = described_class::NotificationSetting.create(create_params)
[notification_setting, params]
end
end
describe '#up' do
it 'migrates all settings where a custom event is enabled, even if they are not currently using the custom level' do
notification_settings_before
described_class.new.up
notification_settings_before.each do |(notification_setting, params)|
notification_setting.reload
expect(notification_setting.read_attribute_before_type_cast(:events)).to be_nil
expect(notification_setting.level).to eq(params[:level])
described_class::EMAIL_EVENTS.each do |event|
# We don't set the others to false, just let them default to nil
expected = params[:events].include?(event) || nil
expect(notification_setting.read_attribute(event)).to eq(expected)
end
end
end
end
describe '#down' do
it 'creates a custom events hash for all settings where at least one event is enabled' do
notification_settings_after
described_class.new.down
notification_settings_after.each do |(notification_setting, params)|
notification_setting.reload
expect(notification_setting.level).to eq(params[:level])
if params[:events].empty?
# We don't migrate empty settings
expect(notification_setting.events).to eq({})
else
described_class::EMAIL_EVENTS.each do |event|
expected = params[:events].include?(event)
expect(notification_setting.events[event]).to eq(expected)
expect(notification_setting.read_attribute(event)).to be_nil
end
end
end
end
it 'reverts the database to the state it was in before' do
notification_settings_before
described_class.new.up
described_class.new.down
notification_settings_before.each do |(notification_setting, params)|
notification_setting.reload
expect(notification_setting.level).to eq(params[:level])
if params[:events].empty?
# We don't migrate empty settings
expect(notification_setting.events).to eq({})
else
described_class::EMAIL_EVENTS.each do |event|
expected = params[:events].include?(event)
expect(notification_setting.events[event]).to eq(expected)
expect(notification_setting.read_attribute(event)).to be_nil
end
end
end
end
end
end
...@@ -55,4 +55,34 @@ RSpec.describe NotificationSetting, type: :model do ...@@ -55,4 +55,34 @@ RSpec.describe NotificationSetting, type: :model do
expect(user.notification_settings.for_projects.map(&:project)).to all(have_attributes(pending_delete: false)) expect(user.notification_settings.for_projects.map(&:project)).to all(have_attributes(pending_delete: false))
end end
end end
describe 'event_enabled?' do
before do
subject.update!(user: create(:user))
end
context 'for an event with a matching column name' do
before do
subject.update!(events: { new_note: true }.to_json)
end
it 'returns the value of the column' do
subject.update!(new_note: false)
expect(subject.event_enabled?(:new_note)).to be(false)
end
context 'when the column has a nil value' do
it 'returns the value from the events hash' do
expect(subject.event_enabled?(:new_note)).to be(false)
end
end
end
context 'for an event without a matching column name' do
it 'returns false' do
expect(subject.event_enabled?(:foo_event)).to be(false)
end
end
end
end end
...@@ -1539,8 +1539,7 @@ describe NotificationService, services: true do ...@@ -1539,8 +1539,7 @@ describe NotificationService, services: true do
# When resource is nil it means global notification # When resource is nil it means global notification
def update_custom_notification(event, user, resource: nil, value: true) def update_custom_notification(event, user, resource: nil, value: true)
setting = user.notification_settings_for(resource) setting = user.notification_settings_for(resource)
setting.events[event] = value setting.update!(event => value)
setting.save
end end
def add_users_with_subscription(project, issuable) def add_users_with_subscription(project, issuable)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment