Data Migration Script for users_settings and users_notification_settings (#13390)

* schema file undelete description

* feat: v1 of the script

* Flesh out remaining enums under their categories

* complete UsersSettings data update script

* complete DUS for relevant attributes in users and profiles tables

* complete DUS for users_notification_settings

* alphabetize user_settings sql file

* safeguard against null values for "null: false" settings

* Set up actual UsersSettings DUS and specs files

* fix broken DUS script

* complete specs for UsersSetting DUS

* Address QA of specs

* complete specs for users_notification_settings DUS

* fix the typos (thanks Julianna!)

* add missing attribute "email_membership_newsletter"

Co-authored-by: Arit Amana <msarit@gmail.com>
This commit is contained in:
Ridhwana 2021-05-24 19:54:58 +02:00 committed by GitHub
parent a4c06d3207
commit bf02be9df8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 370 additions and 2 deletions

View file

@ -0,0 +1,7 @@
class RemoveUserIdIndexFromUsersSettings < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
remove_index :users_settings, column: :user_id, algorithm: :concurrently
end
end

View file

@ -0,0 +1,7 @@
class AddUniqueUserIdIndexToUsersSettings < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_index :users_settings, :user_id, unique: true, algorithm: :concurrently
end
end

View file

@ -0,0 +1,7 @@
class RemoveUserIdIndexFromUsersNotificationSettings < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
remove_index :users_notification_settings, column: :user_id, algorithm: :concurrently
end
end

View file

@ -0,0 +1,7 @@
class AddUniqueUserIdIndexToUsersNotificationSettings < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_index :users_notification_settings, :user_id, unique: true, algorithm: :concurrently
end
end

View file

@ -1445,7 +1445,7 @@ ActiveRecord::Schema.define(version: 2021_05_12_025422) do
t.datetime "updated_at", precision: 6, null: false
t.bigint "user_id", null: false
t.boolean "welcome_notifications", default: true, null: false
t.index ["user_id"], name: "index_users_notification_settings_on_user_id"
t.index ["user_id"], name: "index_users_notification_settings_on_user_id", unique: true
end
create_table "users_roles", id: false, force: :cascade do |t|
@ -1474,7 +1474,7 @@ ActiveRecord::Schema.define(version: 2021_05_12_025422) do
t.boolean "permit_adjacent_sponsors", default: true
t.datetime "updated_at", precision: 6, null: false
t.bigint "user_id", null: false
t.index ["user_id"], name: "index_users_settings_on_user_id"
t.index ["user_id"], name: "index_users_settings_on_user_id", unique: true
end
create_table "users_suspended_usernames", primary_key: "username_hash", id: :string, force: :cascade do |t|

View file

@ -0,0 +1,86 @@
module DataUpdateScripts
class MigrateRelevantFieldsFromUsersToUsersSettings
def run
# rubocop:disable Metrics/BlockLength(RuboCop)
ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.execute(
<<~SQL.squish,
WITH settings_data AS (
SELECT
data ->> 'brand_color1' AS brand_color1,
data ->> 'brand_color2' AS brand_color2,
CASE WHEN config_font='default' THEN 0
WHEN config_font='comic_sans' THEN 1
WHEN config_font='monospace' THEN 2
WHEN config_font='open_dyslexic' THEN 3
WHEN config_font='sans_serif' THEN 4
WHEN config_font='serif' THEN 5
ELSE 0
END
config_font,
CASE WHEN config_navbar='default_navbar' THEN 0
WHEN config_navbar='static_navbar' THEN 1
ELSE 0
END
config_navbar,
CASE WHEN config_theme='default_theme' THEN 0
WHEN config_theme='minimal_light_theme' THEN 1
WHEN config_theme='night_theme' THEN 2
WHEN config_theme='pink_theme' THEN 3
WHEN config_theme='ten_x_hacker_theme' THEN 4
ELSE 0
END
config_theme,
COALESCE(display_announcements, true),
COALESCE((data ->> 'display_email_on_profile')::boolean, false) as display_email_on_profile,
COALESCE(display_sponsors, true),
CASE WHEN editor_version='v2' THEN 0
WHEN editor_version='v1' THEN 1
ELSE 0
END
editor_version,
experience_level,
COALESCE(feed_mark_canonical, false),
COALESCE(feed_referential_link, true),
feed_url,
inbox_guidelines,
CASE WHEN inbox_type='private' THEN 0
WHEN inbox_type='open' THEN 1
ELSE 0
END
inbox_type,
COALESCE(permit_adjacent_sponsors, true),
users.id AS user_id,
NOW(),
NOW()
FROM users
JOIN profiles
ON profiles.user_id = users.id
)
INSERT INTO users_settings (brand_color1, brand_color2, config_font, config_navbar, config_theme, display_announcements, display_email_on_profile, display_sponsors, editor_version, experience_level, feed_mark_canonical, feed_referential_link, feed_url, inbox_guidelines, inbox_type, permit_adjacent_sponsors, user_id, created_at, updated_at)
SELECT * FROM settings_data
ON CONFLICT (user_id) DO UPDATE
SET brand_color1 = EXCLUDED.brand_color1,
brand_color2 = EXCLUDED.brand_color2,
config_font = EXCLUDED.config_font,
config_navbar = EXCLUDED.config_navbar,
config_theme = EXCLUDED.config_theme,
display_announcements = EXCLUDED.display_announcements,
display_email_on_profile = EXCLUDED.display_email_on_profile,
display_sponsors = EXCLUDED.display_sponsors,
editor_version = EXCLUDED.editor_version,
experience_level = EXCLUDED.experience_level,
feed_mark_canonical = EXCLUDED.feed_mark_canonical,
feed_referential_link = EXCLUDED.feed_referential_link,
feed_url = EXCLUDED.feed_url,
inbox_guidelines = EXCLUDED.inbox_guidelines,
inbox_type = EXCLUDED.inbox_type,
permit_adjacent_sponsors = EXCLUDED.permit_adjacent_sponsors,
updated_at = NOW();
SQL
)
end
# rubocop:enable Metrics/BlockLength(RuboCop)
end
end
end

View file

@ -0,0 +1,55 @@
module DataUpdateScripts
class MigrateRelevantFieldsFromUsersToUsersNotificationSettings
def run
# rubocop:disable Metrics/BlockLength(RuboCop)
ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.execute(
<<~SQL.squish,
WITH notification_settings_data AS (
SELECT
users.id AS user_id,
COALESCE(email_badge_notifications, true),
COALESCE(email_comment_notifications, true),
COALESCE(email_community_mod_newsletter, false),
COALESCE(email_connect_messages, true),
COALESCE(email_digest_periodic, false),
COALESCE(email_follower_notifications, true),
COALESCE(email_membership_newsletter, false),
COALESCE(email_mention_notifications, true),
COALESCE(email_newsletter, false),
COALESCE(email_tag_mod_newsletter, false),
COALESCE(email_unread_notifications, true),
COALESCE(mobile_comment_notifications, true),
COALESCE(mod_roundrobin_notifications, true),
COALESCE(reaction_notifications, true),
COALESCE(welcome_notifications, true),
NOW(),
NOW()
FROM users
)
INSERT INTO users_notification_settings (user_id, email_badge_notifications, email_comment_notifications, email_community_mod_newsletter, email_connect_messages, email_digest_periodic, email_follower_notifications, email_membership_newsletter, email_mention_notifications, email_newsletter, email_tag_mod_newsletter, email_unread_notifications, mobile_comment_notifications, mod_roundrobin_notifications, reaction_notifications, welcome_notifications, created_at, updated_at)
SELECT * FROM notification_settings_data
ON CONFLICT (user_id) DO UPDATE
SET email_badge_notifications = EXCLUDED.email_badge_notifications,
email_comment_notifications = EXCLUDED.email_comment_notifications,
email_community_mod_newsletter = EXCLUDED.email_community_mod_newsletter,
email_connect_messages = EXCLUDED.email_connect_messages,
email_digest_periodic = EXCLUDED.email_digest_periodic,
email_follower_notifications = EXCLUDED.email_follower_notifications,
email_membership_newsletter = EXCLUDED.email_membership_newsletter,
email_mention_notifications = EXCLUDED.email_mention_notifications,
email_newsletter = EXCLUDED.email_newsletter,
email_tag_mod_newsletter = EXCLUDED.email_tag_mod_newsletter,
email_unread_notifications = EXCLUDED.email_unread_notifications,
mobile_comment_notifications = EXCLUDED.mobile_comment_notifications,
mod_roundrobin_notifications = EXCLUDED.mod_roundrobin_notifications,
reaction_notifications = EXCLUDED.reaction_notifications,
welcome_notifications = EXCLUDED.welcome_notifications,
updated_at = NOW();
SQL
)
end
# rubocop:enable Metrics/BlockLength(RuboCop)
end
end
end

View file

@ -0,0 +1,76 @@
require "rails_helper"
require Rails.root.join(
"lib/data_update_scripts/20210503174302_migrate_relevant_fields_from_users_to_users_notification_settings.rb",
)
describe DataUpdateScripts::MigrateRelevantFieldsFromUsersToUsersNotificationSettings do
let(:users_notification_setting) { Users::NotificationSetting.last }
before do
Users::NotificationSetting.destroy_all
User.destroy_all
end
context "when migrating data" do
it "sets the expected number of records" do
create_list(:user, 3)
expect do
described_class.new.run
end.to change(Users::NotificationSetting, :count).from(0).to(3)
end
it "sets the correct User records" do
create(:user, welcome_notifications: true)
described_class.new.run
expect(users_notification_setting.welcome_notifications).to be(true)
end
it "sets the correct types" do
create(:user)
described_class.new.run
expect(users_notification_setting.reaction_notifications).to be_in([true, false])
end
it "sets a fallback value for values that are null" do
create(:user, email_newsletter: nil)
described_class.new.run
expect(users_notification_setting.email_newsletter).to be(false)
end
end
it "assigns updated_at and created_at timestamps that are more current than the original values" do
user = create(:user, created_at: 1.minute.ago, updated_at: 1.minute.ago)
described_class.new.run
expect(users_notification_setting.created_at).to be > user.created_at
expect(users_notification_setting.updated_at).to be > user.updated_at
end
context "when the user id exists in both the users_notification_settings and users tables" do
it "replaces the users_notification_settings values with values from the users table" do
user = create(:user, email_newsletter: true)
user_id = user.id
described_class.new.run
expect(users_notification_setting.user_id).to eq(user_id)
expect(users_notification_setting.email_newsletter).to be(true)
user.update_columns(email_newsletter: false)
described_class.new.run
users_notification_setting.reload
expect(users_notification_setting.user_id).to eq(user_id)
expect(users_notification_setting.email_newsletter).to be(false)
end
end
end

View file

@ -0,0 +1,123 @@
require "rails_helper"
require Rails.root.join(
"lib/data_update_scripts/20210423155327_migrate_relevant_fields_from_users_to_users_settings.rb",
)
describe DataUpdateScripts::MigrateRelevantFieldsFromUsersToUsersSettings do
let(:users_setting) { Users::Setting.last }
before do
Users::Setting.destroy_all
Profile.destroy_all
User.destroy_all
end
context "when migrating data" do
it "sets the expected number of records" do
create_list(:user, 3)
expect do
described_class.new.run
end.to change(Users::Setting, :count).from(0).to(3)
end
it "sets the correct User records" do
create(:user, config_font: "sans_serif")
described_class.new.run
expect(users_setting.config_font).to eq("sans_serif")
end
it "sets the correct Profile records" do
profile = create(
:profile,
:with_DEV_info,
user: create(:user, :without_profile),
display_email_on_profile: true,
)
described_class.new.run
expect(users_setting.display_email_on_profile).to eq(profile.display_email_on_profile)
end
it "sets the correct types" do
create(:user)
described_class.new.run
expect(users_setting.permit_adjacent_sponsors).to be_in([true, false])
end
it "casts data (display_email_on_profile) to the correct types (boolean)" do
create(
:profile,
:with_DEV_info,
user: create(:user, :without_profile),
display_email_on_profile: true,
)
described_class.new.run
expect(users_setting.display_email_on_profile).to be_in([true, false])
end
it "sets a fallback value for values that are null" do
create(:user, display_announcements: nil)
described_class.new.run
expect(users_setting.display_announcements).to be(true)
end
end
context "when migrating enum settings" do
it "assigns the correct mapping for enum settings" do
create(:user, config_font: "monospace")
described_class.new.run
expect(users_setting.monospace?).to be(true)
end
it "assigns the fallback value when the value passed does not have an enum defined" do
user = create(:user)
user.update_columns(config_font: "fake_font")
user.reload
described_class.new.run
expect(users_setting.default?).to be(true)
end
end
it "assigns updated_at and created_at timestamps that are more current than the original values" do
user = create(:user, created_at: 1.minute.ago, updated_at: 1.minute.ago)
described_class.new.run
expect(users_setting.created_at).to be > user.created_at
expect(users_setting.updated_at).to be > user.updated_at
end
context "when the user id exists in both the users_settings and users tables" do
it "replaces the users_settings values with values from the users table" do
user = create(:user, display_announcements: true)
user_id = user.id
described_class.new.run
expect(users_setting.user_id).to eq(user_id)
expect(users_setting.display_announcements).to be(true)
user.update_columns(display_announcements: false)
described_class.new.run
users_setting.reload
expect(users_setting.user_id).to eq(user_id)
expect(users_setting.display_announcements).to be(false)
end
end
end