From bf02be9df89837a1eff43cff5469198e495e7a8f Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 24 May 2021 19:54:58 +0200 Subject: [PATCH] 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 --- ...emove_user_id_index_from_users_settings.rb | 7 + ..._unique_user_id_index_to_users_settings.rb | 7 + ..._index_from_users_notification_settings.rb | 7 + ...id_index_to_users_notification_settings.rb | 7 + db/schema.rb | 4 +- ...ant_fields_from_users_to_users_settings.rb | 86 ++++++++++++ ...om_users_to_users_notification_settings.rb | 55 ++++++++ ...ers_to_users_notification_settings_spec.rb | 76 +++++++++++ ...ields_from_users_to_users_settings_spec.rb | 123 ++++++++++++++++++ 9 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20210419171609_remove_user_id_index_from_users_settings.rb create mode 100644 db/migrate/20210419171746_add_unique_user_id_index_to_users_settings.rb create mode 100644 db/migrate/20210419174909_remove_user_id_index_from_users_notification_settings.rb create mode 100644 db/migrate/20210419174931_add_unique_user_id_index_to_users_notification_settings.rb create mode 100644 lib/data_update_scripts/20210423155327_migrate_relevant_fields_from_users_to_users_settings.rb create mode 100644 lib/data_update_scripts/20210503174302_migrate_relevant_fields_from_users_to_users_notification_settings.rb create mode 100644 spec/lib/data_update_scripts/migrate_relevant_fields_from_users_to_users_notification_settings_spec.rb create mode 100644 spec/lib/data_update_scripts/migrate_relevant_fields_from_users_to_users_settings_spec.rb diff --git a/db/migrate/20210419171609_remove_user_id_index_from_users_settings.rb b/db/migrate/20210419171609_remove_user_id_index_from_users_settings.rb new file mode 100644 index 000000000..40e8eca1c --- /dev/null +++ b/db/migrate/20210419171609_remove_user_id_index_from_users_settings.rb @@ -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 diff --git a/db/migrate/20210419171746_add_unique_user_id_index_to_users_settings.rb b/db/migrate/20210419171746_add_unique_user_id_index_to_users_settings.rb new file mode 100644 index 000000000..b62f057f6 --- /dev/null +++ b/db/migrate/20210419171746_add_unique_user_id_index_to_users_settings.rb @@ -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 diff --git a/db/migrate/20210419174909_remove_user_id_index_from_users_notification_settings.rb b/db/migrate/20210419174909_remove_user_id_index_from_users_notification_settings.rb new file mode 100644 index 000000000..f3ce658e3 --- /dev/null +++ b/db/migrate/20210419174909_remove_user_id_index_from_users_notification_settings.rb @@ -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 diff --git a/db/migrate/20210419174931_add_unique_user_id_index_to_users_notification_settings.rb b/db/migrate/20210419174931_add_unique_user_id_index_to_users_notification_settings.rb new file mode 100644 index 000000000..b56737bf6 --- /dev/null +++ b/db/migrate/20210419174931_add_unique_user_id_index_to_users_notification_settings.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 24280fd9d..200174819 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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| diff --git a/lib/data_update_scripts/20210423155327_migrate_relevant_fields_from_users_to_users_settings.rb b/lib/data_update_scripts/20210423155327_migrate_relevant_fields_from_users_to_users_settings.rb new file mode 100644 index 000000000..fbca81fb8 --- /dev/null +++ b/lib/data_update_scripts/20210423155327_migrate_relevant_fields_from_users_to_users_settings.rb @@ -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 diff --git a/lib/data_update_scripts/20210503174302_migrate_relevant_fields_from_users_to_users_notification_settings.rb b/lib/data_update_scripts/20210503174302_migrate_relevant_fields_from_users_to_users_notification_settings.rb new file mode 100644 index 000000000..12cb3c0ce --- /dev/null +++ b/lib/data_update_scripts/20210503174302_migrate_relevant_fields_from_users_to_users_notification_settings.rb @@ -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 diff --git a/spec/lib/data_update_scripts/migrate_relevant_fields_from_users_to_users_notification_settings_spec.rb b/spec/lib/data_update_scripts/migrate_relevant_fields_from_users_to_users_notification_settings_spec.rb new file mode 100644 index 000000000..c51668508 --- /dev/null +++ b/spec/lib/data_update_scripts/migrate_relevant_fields_from_users_to_users_notification_settings_spec.rb @@ -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 diff --git a/spec/lib/data_update_scripts/migrate_relevant_fields_from_users_to_users_settings_spec.rb b/spec/lib/data_update_scripts/migrate_relevant_fields_from_users_to_users_settings_spec.rb new file mode 100644 index 000000000..4e68cea7f --- /dev/null +++ b/spec/lib/data_update_scripts/migrate_relevant_fields_from_users_to_users_settings_spec.rb @@ -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