Use new UserSetting and UserNotificationSettings and Ignore Related User Table Fields (#14121)

* 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!)

* begin implementation

* still building

* add missing attribute "email_membership_newsletter"

* complete sync code (except race condition for user profile)

* complete implementation, remains tests

* Address PR review and fix Travis fails

* remove superfluous Profile.new

* fix travis fails

* feat: update the users_notification_setting attributes from the user model

* feat: use the config fonts enums to display the fonts

* feat: loop through the keys

* fix profile = nil blowing up; add specs for notification_setting model

* remove unneeded spec

* remove feed validation until after sync code removed; fixes feed_import spec failures

* remove spec associated with feed_url validation in user_setting model

* fix failing spec 😅

* add TODO

* feat: set the user settings in the user controller  and use it in the customization form

* feat: move some update logic to the users settings controller thats being used from customization

* feat: show the updated values form the users_settingd and not the user instance

* Generalize redirect back to current tab

* still trying to reflect changed theme upon refresh

* customizations take effect on refresh

* remove 'with_feed' scope from user model

Co-authored-by: Jamie Gaskins <jamie@forem.com>

* start with takeover for fields previously in profiles table

* Takeover code for `publishing_from_rss` section in Settings (#13914)

* implement takeover code part 1

* implement takeover code

* fix feed fetch

* need rhymes help

* complete implementation; specs pending

* fix STUPID omission that caused so many headaches 😫

* implement profile fields pointing to users_settings 🎉

* run migrations

* implement inbox type & guidelines takeover code; specs pending (#13911)

* Point changes in notification settings to `users_notification_settings` table (#13910)

* implement takeover code; remains specs

* address PR feedback; remove related sync code

* address PR review feedback

* need help with routing and specs

* address pr review

* addressing pr review

* Treat implementation edge cases and omissions 😅

* fix uncommented comment

* fixing implementation cases

* address more PR review feedback

* fixing notifications use-cases

* refactor settings controller

* more pr review changes

* solving bugs

* fix broken onboarding

* handle eperience_level calls

* more fixes

* remove unneeded mappings

* add To-dos for quety updates

* remove done TODO

* purge done TODOs

* update notification_settings-related queries

* start fixing specs

* fixing specs

* fix notification and lrg_forem specs

* fixing broken specs

* still fixing

* fix line dif and remove reloads from user.rb

* run specs

* silence bullet and other fixes

* remove setting migration scripts and specs, fix more settings for specs

* handle missing user for article builder and fix notification specs

* fix some final controller specs and re-add incorrectly removed specs

* remove deprecated data update scripts and related workers, put travis back

* refactor admin tags mods controller, write/move specs for users notifications settings controller

* schema cleanup and other small refactors for consistency

* set field we can invalidate in spec via active record instead of at the db level

* remove I think an uneccessary hook call from subscribe_to_mailchimp_newsletter

* use bnefore_create to setup settings, please dont blow up the test suite

* mailchimp bot fix

* remove decorator in favor of single model method

Co-authored-by: Arit Amana <msarit@gmail.com>
Co-authored-by: Ridhwana <ridhwana.khan16@gmail.com>
Co-authored-by: Arit Amana <32520970+msarit@users.noreply.github.com>
Co-authored-by: Jamie Gaskins <jamie@forem.com>
This commit is contained in:
Molly Struve 2021-07-08 10:31:34 -04:00 committed by GitHub
parent e99e803867
commit e87dead7ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
103 changed files with 795 additions and 870 deletions

View file

@ -11,25 +11,37 @@ module Admin
def create
user = User.find_by(id: tag_params[:user_id])
if user&.update(email_tag_mod_newsletter: true)
unless user
flash[:error] = "Error: User ID ##{tag_params[:user_id]} was not found"
return redirect_to edit_admin_tag_path(params[:tag_id])
end
notification_setting = user.notification_setting
if notification_setting.update(email_tag_mod_newsletter: true)
TagModerators::Add.call([user.id], [params[:tag_id]])
flash[:success] = "#{user.username} was added as a tag moderator!"
else
flash[:error] = "Error: User ID ##{tag_params[:user_id]} was not found,
or their account has errors: #{user&.errors_as_sentence}"
or their account has errors: #{notification_setting.errors_as_sentence}"
end
redirect_to edit_admin_tag_path(params[:tag_id])
end
def destroy
user = User.find_by(id: tag_params[:user_id])
unless user
flash[:error] = "Error: User ID ##{tag_params[:user_id]} was not found"
return redirect_to edit_admin_tag_path(params[:tag_id])
end
notification_setting = user.notification_setting
tag = Tag.find_by(id: params[:tag_id])
if user&.update(email_tag_mod_newsletter: false)
if notification_setting.update(email_tag_mod_newsletter: false)
TagModerators::Remove.call(user, tag)
flash[:success] = "@#{user.username} - ID ##{user.id} was removed as a tag moderator."
else
flash[:error] = "Error: User ID ##{tag_params[:user_id]} was not found,
or their account has errors: #{user&.errors_as_sentence}"
or their account has errors: #{notification_setting.errors_as_sentence}"
end
redirect_to edit_admin_tag_path(tag.id)
end

View file

@ -218,7 +218,7 @@ class ArticlesController < ApplicationController
def base_editor_assigments
@user = current_user
@version = @user.editor_version if @user
@version = @user.setting.editor_version if @user
@organizations = @user&.organizations
@tag = Tag.find_by(name: params[:template])
@prefill = params[:prefill].to_s.gsub("\\n ", "\n").gsub("\\n", "\n")

View file

@ -137,7 +137,7 @@ class ChatChannelsController < ApplicationController
valid_listing = Listing.where(user_id: params[:user_id], contact_via_connect: true).limit(1)
authorize ChatChannel
if chat_recipient.inbox_type == "open" || valid_listing.length == 1
if chat_recipient.setting.inbox_type == "open" || valid_listing.length == 1
chat = ChatChannels::CreateWithUsers.call(users: [current_user, chat_recipient], channel_type: "direct")
message_markdown = params[:message]
message = Message.new(

View file

@ -4,7 +4,7 @@ class EmailSubscriptionsController < ApplicationController
if verified_params[:expires_at] > Time.current
user = User.find(verified_params[:user_id])
user.update(verified_params[:email_type] => false)
user.notification_setting.update(verified_params[:email_type] => false)
@email_type = preferred_email_name.fetch(verified_params[:email_type], "this list")
else
render "invalid_token"

View file

@ -18,7 +18,7 @@ module IncomingWebhooks
def create
not_authorized unless valid_secret?
user = User.find_by!(email: params.dig(:data, :email))
user.update(email_type => false)
user.notification_setting.update(email_type => false)
end
private

View file

@ -1,6 +1,8 @@
class ProfilesController < ApplicationController
before_action :authenticate_user!
ALLOWED_USER_PARAMS = %i[name email username profile_image].freeze
ALLOWED_USERS_SETTING_PARAMS = %i[display_email_on_profile brand_color1 brand_color2].freeze
def update
update_result = Profiles::Update.call(current_user, update_params)
@ -13,7 +15,8 @@ class ProfilesController < ApplicationController
flash[:error] = "Error: #{update_result.errors_as_sentence}"
render template: "users/edit", locals: {
user: update_params[:user],
profile: update_params[:profile]
profile: update_params[:profile],
users_setting: update_params[:users_setting]
}
end
end
@ -21,6 +24,8 @@ class ProfilesController < ApplicationController
private
def update_params
params.permit(profile: Profile.attributes + Profile.static_fields, user: ALLOWED_USER_PARAMS)
params.permit(profile: Profile.attributes + Profile.static_fields,
user: ALLOWED_USER_PARAMS,
users_setting: ALLOWED_USERS_SETTING_PARAMS)
end
end

View file

@ -90,7 +90,7 @@ class ReactionsController < ApplicationController
result = "create"
if category == "readinglist" && current_user.experience_level
if category == "readinglist" && current_user.setting.experience_level
rate_article(reaction)
end
@ -146,7 +146,7 @@ class ReactionsController < ApplicationController
group: "experience_level",
user_id: current_user.id,
context: "readinglist_reaction",
rating: current_user.experience_level)
rating: current_user.setting.experience_level)
end
def clear_moderator_reactions(id, type, mod, category)

View file

@ -80,6 +80,7 @@ class RegistrationsController < Devise::RegistrationsController
def build_devise_resource
build_resource(sign_up_params)
resource.registered_at = Time.current
resource.build_setting(editor_version: "v2")
resource.remote_profile_image_url = Users::ProfileImageGenerator.call if resource.remote_profile_image_url.blank?
check_allowed_email(resource) if resource.email.present?
resource.save if resource.email.present?

View file

@ -344,7 +344,7 @@ class StoriesController < ApplicationController
sameAs: user_same_as,
image: Images::Profile.call(@user.profile_image_url, length: 320),
name: @user.name,
email: @user.email_public ? @user.email : nil,
email: @user.setting.display_email_on_profile ? @user.email : nil,
jobTitle: @user.employment_title.presence,
description: @user.summary.presence || "404 bio not found",
worksFor: [user_works_for].compact,

View file

@ -0,0 +1,63 @@
module Users
class NotificationSettingsController < ApplicationController
before_action :raise_suspended
before_action :authenticate_user!
after_action :verify_authorized
ALLOWED_PARAMS = %i[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].freeze
def update
authorize current_user, policy_class: UserPolicy
if current_user.notification_setting.update(users_notification_setting_params)
flash[:settings_notice] = "Your notification settings have been updated."
else
Honeycomb.add_field("error", current_user.notification_setting.errors.messages.compact_blank)
Honeycomb.add_field("errored", true)
flash[:error] = current_user.notification_setting.errors_as_sentence
end
redirect_to user_settings_path(:notifications)
end
def onboarding_notifications_checkbox_update
authorize User
if params[:notifications]
permitted_params = %i[email_newsletter email_digest_periodic]
current_user.notification_setting.assign_attributes(params[:notifications].permit(permitted_params))
end
current_user.saw_onboarding = true
success = current_user.notification_setting.save
render_update_response(success, current_user.notification_setting.errors_as_sentence)
end
private
def render_update_response(success, errors = nil)
status = success ? 200 : 422
respond_to do |format|
format.json { render json: { errors: errors }, status: status }
end
end
def users_notification_setting_params
params.require(:users_notification_setting).permit(ALLOWED_PARAMS)
end
end
end

View file

@ -0,0 +1,55 @@
module Users
class SettingsController < ApplicationController
before_action :raise_suspended
before_action :authenticate_user!
after_action :verify_authorized
ALLOWED_PARAMS = %i[config_theme
config_font
config_navbar
display_announcements
display_sponsors
editor_version
experience_level
feed_fetched_at
feed_mark_canonical
feed_referential_link
feed_url
inbox_guidelines
inbox_type
permit_adjacent_sponsors].freeze
def update
authorize current_user, policy_class: UserPolicy
users_setting = current_user.setting
tab = params["users_setting"]["tab"] || "profile"
if users_setting.update(users_setting_params)
import_articles_from_feed(users_setting)
if users_setting.experience_level.present?
cookies.permanent[:user_experience_level] = users_setting.experience_level.to_s
end
current_user.touch(:profile_updated_at)
flash[:settings_notice] = "Your config has been updated. Refresh to see all changes."
else
Honeycomb.add_field("error", users_setting.errors.messages.compact_blank)
Honeycomb.add_field("errored", true)
flash[:error] = users_setting.errors_as_sentence
end
redirect_to user_settings_path(tab)
end
private
def import_articles_from_feed(users_setting)
return if users_setting.feed_url.blank?
Feeds::ImportArticlesWorker.perform_async(users_setting.user_id)
end
def users_setting_params
params.require(:users_setting).permit(ALLOWED_PARAMS)
end
end
end

View file

@ -11,6 +11,7 @@ class UsersController < ApplicationController
before_action :initialize_stripe, only: %i[edit]
ALLOWED_USER_PARAMS = %i[last_onboarding_page username].freeze
ALLOWED_ONBOARDING_PARAMS = %i[checked_code_of_conduct checked_terms_and_conditions].freeze
INDEX_ATTRIBUTES_FOR_SERIALIZATION = %i[id name username summary profile_image].freeze
private_constant :INDEX_ATTRIBUTES_FOR_SERIALIZATION
REMOVE_IDENTITY_ERROR = "An error occurred. Please try again or send an email to: %<email>s".freeze
@ -35,6 +36,7 @@ class UsersController < ApplicationController
return redirect_to sign_up_path
end
set_user
set_users_setting_and_notification_setting
set_current_tab(params["tab"] || "profile")
handle_settings_tab
end
@ -42,6 +44,7 @@ class UsersController < ApplicationController
# PATCH/PUT /users/:id.:format
def update
set_current_tab(params["user"]["tab"])
set_users_setting_and_notification_setting
@user.assign_attributes(permitted_attributes(@user))
@ -51,14 +54,13 @@ class UsersController < ApplicationController
import_articles_from_feed(@user)
notice = "Your profile was successfully updated."
if config_changed?
notice = "Your config has been updated. Refresh to see all changes."
end
if @user.export_requested?
notice += " The export will be emailed to you shortly."
ExportContentWorker.perform_async(@user.id, @user.email)
end
cookies.permanent[:user_experience_level] = @user.experience_level.to_s if @user.experience_level.present?
if @user.setting.experience_level.present?
cookies.permanent[:user_experience_level] = @user.setting.experience_level.to_s
end
flash[:settings_notice] = notice
@user.touch(:profile_updated_at)
redirect_to "/settings/#{@tab}"
@ -185,10 +187,7 @@ class UsersController < ApplicationController
def onboarding_checkbox_update
if params[:user]
permitted_params = %i[
checked_code_of_conduct checked_terms_and_conditions email_newsletter email_digest_periodic
]
current_user.assign_attributes(params[:user].permit(permitted_params))
current_user.assign_attributes(params[:user].permit(ALLOWED_ONBOARDING_PARAMS))
end
current_user.saw_onboarding = true
@ -360,12 +359,15 @@ class UsersController < ApplicationController
authorize @user
end
def set_current_tab(current_tab = "profile")
@tab = current_tab
def set_users_setting_and_notification_setting
return unless @user
@users_setting = @user.setting
@users_notification_setting = @user.notification_setting
end
def config_changed?
params[:user].include?(:config_theme)
def set_current_tab(current_tab = "profile")
@tab = current_tab
end
def destroy_request_in_progress?
@ -373,7 +375,7 @@ class UsersController < ApplicationController
end
def import_articles_from_feed(user)
return if user.feed_url.blank?
return if user.setting.feed_url.blank?
Feeds::ImportArticlesWorker.perform_async(user.id)
end

View file

@ -36,35 +36,31 @@ class UserDecorator < ApplicationDecorator
end
def enriched_colors
if bg_color_hex.blank? || text_color_hex.blank?
if setting.brand_color1.blank? || setting.brand_color2.blank?
{
bg: assigned_color[:bg],
text: assigned_color[:text]
}
else
{
bg: bg_color_hex,
text: text_color_hex
bg: setting.brand_color1,
text: setting.brand_color2
}
end
end
def config_font_name
config_font.gsub("default", Settings::UserExperience.default_font)
end
def config_body_class
body_class = [
config_theme.tr("_", "-"),
"#{config_font_name.tr('_', '-')}-article-body",
setting.config_theme.tr("_", "-"),
"#{setting.resolved_font_name.tr('_', '-')}-article-body",
"trusted-status-#{trusted}",
"#{config_navbar.tr('_', '-')}-header",
"#{setting.config_navbar.tr('_', '-')}-header",
]
body_class.join(" ")
end
def dark_theme?
config_theme == "night_theme" || config_theme == "ten_x_hacker_theme"
setting.config_theme == "night_theme" || setting.config_theme == "ten_x_hacker_theme"
end
def assigned_color
@ -113,4 +109,8 @@ class UserDecorator < ApplicationDecorator
created_at.after?(min_days.days.ago)
end
delegate :display_sponsors, to: :setting
delegate :display_announcements, to: :setting
end

View file

@ -24,13 +24,13 @@ export class EmailPreferencesForm extends Component {
onSubmit() {
const csrfToken = getContentOfToken('csrf-token');
fetch('/onboarding_checkbox_update', {
fetch('/onboarding_notifications_checkbox_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user: this.state }),
body: JSON.stringify({ notifications: this.state }),
credentials: 'same-origin',
}).then((response) => {
if (response.ok) {

View file

@ -42,7 +42,7 @@ class BadgeAchievement < ApplicationRecord
def send_email_notification
return unless user.is_a?(User)
return unless user.email && user.email_badge_notifications
return unless user.email && user.notification_setting.email_badge_notifications
BadgeAchievements::SendEmailNotificationWorker.perform_async(id)
end

View file

@ -299,7 +299,7 @@ class Comment < ApplicationRecord
parent_exists? &&
parent_user.class.name != "Podcast" &&
parent_user != user &&
parent_user.email_comment_notifications &&
parent_user.notification_setting.email_comment_notifications &&
parent_user.email &&
parent_or_root_article.receive_notifications
end

View file

@ -17,7 +17,7 @@ class Mention < ApplicationRecord
def send_email_notification
user = User.find(user_id)
return unless user.email.present? && user.email_mention_notifications
return unless user.email.present? && user.notification_setting.email_mention_notifications
Mentions::SendEmailNotificationWorker.perform_async(id)
end

View file

@ -13,7 +13,7 @@ class Message < ApplicationRecord
after_create :update_all_has_unopened_messages_statuses
def preferred_user_color
color_options = [user.bg_color_hex || "#000000", user.text_color_hex || "#000000"]
color_options = [user.setting.brand_color1 || "#000000", user.setting.brand_color2 || "#000000"]
Color::CompareHex.new(color_options).brightness(0.9)
end
@ -217,7 +217,7 @@ class Message < ApplicationRecord
recipient.chat_channel_memberships.order(last_opened_at: :desc)
.first.last_opened_at > 15.hours.ago ||
chat_channel.last_message_at > 30.minutes.ago ||
recipient.email_connect_messages == false
recipient.notification_setting.email_connect_messages == false
NotifyMailer.with(message: self).new_message_email.deliver_now
end

View file

@ -26,9 +26,6 @@ class Profile < ApplicationRecord
# NOTE: @citizen428 This is a temporary mapping so we don't break DEV during
# profile migration/generalization work.
MAPPED_ATTRIBUTES = {
brand_color1: :bg_color_hex,
brand_color2: :text_color_hex,
display_email_on_profile: :email_public,
education: :education,
skills_languages: :mostly_work_with
}.with_indifferent_access.freeze

View file

@ -36,7 +36,47 @@ class User < ApplicationRecord
youtube_url
].freeze
self.ignored_columns = PROFILE_COLUMNS
COLUMNS_NOW_IN_USERS_SETTINGS = %w[
config_theme
config_font
config_navbar
display_announcements
display_sponsors
editor_version
experience_level
feed_mark_canonical
feed_referential_link
feed_url
inbox_guidelines
inbox_type
permit_adjacent_sponsors
].freeze
COLUMNS_NOW_IN_USERS_NOTIFICATION_SETTINGS = %w[
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
].freeze
INACTIVE_PROFILE_COLUMNS = %w[
bg_color_hex
text_color_hex
email_public
].freeze
self.ignored_columns = PROFILE_COLUMNS + COLUMNS_NOW_IN_USERS_SETTINGS + COLUMNS_NOW_IN_USERS_NOTIFICATION_SETTINGS
# NOTE: @citizen428 This is temporary code during profile migration and will
# be removed.
@ -53,6 +93,8 @@ class User < ApplicationRecord
# Getters and setters for unmapped profile attributes
(PROFILE_COLUMNS - Profile::MAPPED_ATTRIBUTES.values).each do |column|
next if INACTIVE_PROFILE_COLUMNS.include?(column)
delegate column, "#{column}=", to: :profile, allow_nil: true
end
@ -67,18 +109,9 @@ class User < ApplicationRecord
end
ANY_ADMIN_ROLES = %i[admin super_admin].freeze
EDITORS = %w[v1 v2].freeze
FONTS = %w[serif sans_serif monospace comic_sans open_dyslexic].freeze
INBOXES = %w[open private].freeze
NAVBARS = %w[default static].freeze
THEMES = %w[default night_theme pink_theme minimal_light_theme ten_x_hacker_theme].freeze
USERNAME_MAX_LENGTH = 30
USERNAME_REGEXP = /\A[a-zA-Z0-9_]+\z/.freeze
MESSAGES = {
invalid_config_font: "%<value>s is not a valid font selection",
invalid_config_navbar: "%<value>s is not a valid navbar value",
invalid_config_theme: "%<value>s is not a valid theme",
invalid_editor_version: "%<value>s must be either v1 or v2",
reserved_username: "username is reserved"
}.freeze
# follow the syntax in https://interledger.org/rfcs/0026-payment-pointers/#payment-pointer-syntax
@ -90,57 +123,6 @@ class User < ApplicationRecord
\z
}x.freeze
# Relevant Fields for migration from Users table to Users_Settings table
USER_FIELDS_TO_MIGRATE_TO_USERS_SETTINGS_TABLE = %w[
config_font
config_navbar
config_theme
display_announcements
display_sponsors
editor_version
experience_level
feed_mark_canonical
feed_referential_link
feed_url
inbox_guidelines
inbox_type
permit_adjacent_sponsors
].to_set.freeze
# Relevant Fields for migration from Profiles table to Users_Settings table
PROFILE_FIELDS_TO_MIGRATE_TO_USERS_SETTINGS_TABLE = %w[
brand_color1
brand_color2
display_email_on_profile
].to_set.freeze
# Relevant Fields for migration from Users table to Users_Notification_Settings table
USER_FIELDS_TO_MIGRATE_TO_USERS_NOTIFICATION_SETTINGS_TABLE = %w[
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
].to_set.freeze
USER_SETTINGS_ENUM_FIELDS = %w[
config_font
config_navbar
config_theme
editor_version
inbox_type
].to_set.freeze
attr_accessor :scholar_email, :new_note, :note_for_current_role, :user_status, :merge_user_id,
:add_credits, :remove_credits, :add_org_credits, :remove_org_credits, :ip_address,
:current_password
@ -236,25 +218,12 @@ class User < ApplicationRecord
validates :blocked_by_count, presence: true
validates :blocking_others_count, presence: true
validates :comments_count, presence: true
validates :config_font, inclusion: { in: FONTS + ["default".freeze], message: MESSAGES[:invalid_config_font] }
validates :config_font, presence: true
validates :config_navbar, inclusion: { in: NAVBARS, message: MESSAGES[:invalid_config_navbar] }
validates :config_navbar, presence: true
validates :config_theme, inclusion: { in: THEMES, message: MESSAGES[:invalid_config_theme] }
validates :config_theme, presence: true
validates :credits_count, presence: true
validates :editor_version, inclusion: { in: EDITORS, message: MESSAGES[:invalid_editor_version] }
validates :email, length: { maximum: 50 }, email: true, allow_nil: true
validates :email, uniqueness: { allow_nil: true, case_sensitive: false }, if: :email_changed?
validates :email_digest_periodic, inclusion: { in: [true, false] }
validates :experience_level, numericality: { less_than_or_equal_to: 10 }, allow_blank: true
validates :feed_referential_link, inclusion: { in: [true, false] }
validates :feed_url, length: { maximum: 500 }, allow_nil: true
validates :following_orgs_count, presence: true
validates :following_tags_count, presence: true
validates :following_users_count, presence: true
validates :inbox_guidelines, length: { maximum: 250 }, allow_nil: true
validates :inbox_type, inclusion: { in: INBOXES }
validates :name, length: { in: 1..100 }
validates :password, length: { in: 8..100 }, allow_nil: true
validates :payment_pointer, format: PAYMENT_POINTER_REGEXP, allow_blank: true
@ -269,7 +238,6 @@ class User < ApplicationRecord
validates :username, uniqueness: { case_sensitive: false, message: lambda do |_obj, data|
"#{data[:value]} is taken."
end }, if: :username_changed?
validates :welcome_notifications, inclusion: { in: [true, false] }
# add validators for provider related usernames
Authentication::Providers.username_fields.each do |username_field|
@ -285,7 +253,6 @@ class User < ApplicationRecord
validate :non_banished_username, :username_changed?
validate :unique_including_orgs_and_podcasts, if: :username_changed?
validate :validate_feed_url, if: :feed_url_changed?
validate :can_send_confirmation_email
validate :update_rate_limit
# NOTE: when updating the password on a Devise enabled model, the :encrypted_password
@ -293,9 +260,6 @@ class User < ApplicationRecord
validate :password_matches_confirmation, if: :encrypted_password_changed?
alias_attribute :public_reactions_count, :reactions_count
alias_attribute :subscribed_to_welcome_notifications?, :welcome_notifications
alias_attribute :subscribed_to_mod_roundrobin_notifications?, :mod_roundrobin_notifications
alias_attribute :subscribed_to_email_follower_notifications?, :email_follower_notifications
scope :eager_load_serialized_data, -> { includes(:roles) }
scope :registered, -> { where(registered: true) }
@ -332,15 +296,14 @@ class User < ApplicationRecord
),
)
}
scope :with_feed, -> { where.not(feed_url: [nil, ""]) }
before_validation :check_for_username_change
before_validation :downcase_email
before_validation :set_config_input
# make sure usernames are not empty, to be able to use the database unique index
before_validation :verify_email
before_validation :set_username
before_validation :strip_payment_pointer
before_create :create_users_settings_and_notification_settings_records
before_destroy :unsubscribe_from_newsletters, prepend: true
before_destroy :destroy_follows, prepend: true
@ -350,7 +313,6 @@ class User < ApplicationRecord
after_create_commit :send_welcome_notification
after_commit :subscribe_to_mailchimp_newsletter
after_commit :sync_users_settings_table, :sync_users_notification_settings_table, on: %i[create update]
after_commit :bust_cache
def self.dev_account
@ -548,7 +510,7 @@ class User < ApplicationRecord
return unless registered && email.present?
return if Settings::General.mailchimp_api_key.blank?
return if saved_changes.key?(:unconfirmed_email) && saved_changes.key?(:confirmation_sent_at)
return unless saved_changes.key?(:email) || saved_changes.key?(:email_newsletter)
return unless saved_changes.key?(:email)
Users::SubscribeToMailchimpNewsletterWorker.perform_async(id)
end
@ -621,6 +583,18 @@ class User < ApplicationRecord
"User:#{id}"
end
def subscribed_to_welcome_notifications?
notification_setting.welcome_notifications
end
def subscribed_to_mod_roundrobin_notifications?
notification_setting.mod_roundrobin_notifications
end
def subscribed_to_email_follower_notifications?
notification_setting.email_follower_notifications
end
protected
# Send emails asynchronously
@ -632,43 +606,9 @@ class User < ApplicationRecord
private
def sync_relevant_profile_fields_to_user_settings_table(users_setting_record)
PROFILE_FIELDS_TO_MIGRATE_TO_USERS_SETTINGS_TABLE.each do |field|
# rubocop:disable Layout/LineLength
users_setting_record.assign_attributes(field => profile.public_send(field)) if profile&.public_send(field).present?
# rubocop:enable Layout/LineLength
end
end
def migrate_users_and_profile_fields_to_users_settings(users_setting_record)
USER_FIELDS_TO_MIGRATE_TO_USERS_SETTINGS_TABLE.each do |field|
if USER_SETTINGS_ENUM_FIELDS.include?(field)
field_enums = Users::Setting.defined_enums[field]
users_setting_record.assign_attributes(field => field_enums[public_send(field).to_sym])
else
users_setting_record.assign_attributes(field => public_send(field))
end
end
sync_relevant_profile_fields_to_user_settings_table(users_setting_record)
users_setting_record.save
end
def sync_users_settings_table
users_setting_record = Users::Setting.create_or_find_by(user_id: id)
migrate_users_and_profile_fields_to_users_settings(users_setting_record)
end
def sync_users_notification_settings_table
users_notification_setting_record = Users::NotificationSetting.create_or_find_by(user_id: id)
USER_FIELDS_TO_MIGRATE_TO_USERS_NOTIFICATION_SETTINGS_TABLE.each do |field|
users_notification_setting_record.assign_attributes(field => public_send(field))
end
users_notification_setting_record.save
def create_users_settings_and_notification_settings_records
self.setting = Users::Setting.create
self.notification_setting = Users::NotificationSetting.create
end
def send_welcome_notification
@ -711,12 +651,6 @@ class User < ApplicationRecord
self.email = email.downcase if email
end
def set_config_input
self.config_theme = config_theme&.tr(" ", "_")
self.config_font = config_font&.tr(" ", "_")
self.config_navbar = config_navbar&.tr(" ", "_")
end
def check_for_username_change
return unless username_changed?
@ -736,16 +670,6 @@ class User < ApplicationRecord
Users::BustCacheWorker.perform_async(id)
end
def validate_feed_url
return if feed_url.blank?
valid = Feeds::ValidateUrl.call(feed_url)
errors.add(:feed_url, "is not a valid RSS/Atom feed") unless valid
rescue StandardError => e
errors.add(:feed_url, e.message)
end
def tag_keywords_for_search
"#{employer_name}#{mostly_work_with}#{available_for}"
end

View file

@ -1,10 +1,21 @@
module Users
class NotificationSetting < ApplicationRecord
self.table_name_prefix = "users_"
belongs_to :user, touch: true
validates :email_digest_periodic, inclusion: { in: [true, false] }
alias_attribute :subscribed_to_welcome_notifications?, :welcome_notifications
alias_attribute :subscribed_to_mod_roundrobin_notifications?, :mod_roundrobin_notifications
alias_attribute :subscribed_to_email_follower_notifications?, :email_follower_notifications
after_commit :subscribe_to_mailchimp_newsletter
def subscribe_to_mailchimp_newsletter
return unless email_newsletter
Users::SubscribeToMailchimpNewsletterWorker.perform_async(user.id)
end
end
end

View file

@ -2,9 +2,9 @@ module Users
class Setting < ApplicationRecord
self.table_name_prefix = "users_"
belongs_to :user
belongs_to :user, touch: true
scope :with_feed, -> { where.not(feed_url: [nil, ""]) }
# TODO: @msarit Double-check how these suffixes have impacted the rest of the codebase
enum editor_version: { v2: 0, v1: 1 }, _suffix: :editor
enum config_font: { default: 0, comic_sans: 1, monospace: 2, open_dyslexic: 3, sans_serif: 4, serif: 5 },
_suffix: :font
@ -18,7 +18,23 @@ module Users
validates :feed_referential_link, inclusion: { in: [true, false] }
validates :feed_url, length: { maximum: 500 }, allow_nil: true
validates :inbox_guidelines, length: { maximum: 250 }, allow_nil: true
end
# TODO: @msarit Re-add feed_url validation after updates are pointed directly to users_settings table
validate :validate_feed_url, if: :feed_url_changed?
def resolved_font_name
config_font.gsub("default", Settings::UserExperience.default_font)
end
private
def validate_feed_url
return if feed_url.blank?
valid = Feeds::ValidateUrl.call(feed_url)
errors.add(:feed_url, "is not a valid RSS/Atom feed") unless valid
rescue StandardError => e
errors.add(:feed_url, e.message)
end
end
end

View file

@ -62,6 +62,10 @@ class UserPolicy < ApplicationPolicy
true
end
def onboarding_notifications_checkbox_update?
true
end
def update?
current_user?
end

View file

@ -28,7 +28,7 @@ class ArticleWithVideoCreationService
end
def initial_article_with_params(article)
if @current_user.editor_version == "v1"
if @current_user.setting.editor_version == "v1"
title = "Unpublished Video ~ #{rand(100_000).to_s(26)}"
article.body_markdown = "---\ntitle: #{title}\npublished: false\ndescription: \ntags: \n---\n\n"
else

View file

@ -7,7 +7,7 @@ module Articles
@tag = tag
@prefill = prefill
@editor_version2 = @user&.editor_version == "v2"
@editor_version2 = @user&.setting&.editor_version == "v2"
end
def self.call(...)

View file

@ -105,7 +105,8 @@ module Articles
end
def score_experience_level(article)
- (((article.experience_level_rating - (@user&.experience_level || 5)).abs / 2) * @experience_level_weight)
user_experience_level = @user&.setting&.experience_level || 5
- (((article.experience_level_rating - user_experience_level).abs / 2) * @experience_level_weight)
end
def score_comments(article)

View file

@ -25,6 +25,8 @@ class EmailDigest
private
def get_users
User.registered.where(email_digest_periodic: true).where.not(email: "")
User.registered.joins(:notification_setting)
.where(notification_setting: { email_digest_periodic: true })
.where.not(email: "")
end
end

View file

@ -13,7 +13,7 @@ class EmailDigestArticleCollector
return [] unless should_receive_email?
articles = if user_has_followings?
experience_level_rating = (@user.experience_level || 5)
experience_level_rating = (@user.setting.experience_level || 5)
experience_level_rating_min = experience_level_rating - 3.6
experience_level_rating_max = experience_level_rating + 3.6

View file

@ -20,7 +20,7 @@ module Feeds
published: false
date: #{@item.published}
tags: #{get_tags}
canonical_url: #{@user.feed_mark_canonical ? @feed_source_url : ''}
canonical_url: #{@user.setting.feed_mark_canonical ? @feed_source_url : ''}
---
#{assemble_body_markdown}
@ -64,7 +64,7 @@ module Feeds
def thorough_parsing(content, feed_url)
html_doc = Nokogiri::HTML(content)
find_and_replace_possible_links!(html_doc) if @user.feed_referential_link
find_and_replace_possible_links!(html_doc) if @user.setting.feed_referential_link
find_and_replace_picture_tags_with_img!(html_doc)
if feed_url&.include?("medium.com")

View file

@ -6,7 +6,7 @@ module Feeds
def initialize(users: nil, earlier_than: nil)
# using nil here to avoid an unnecessary table count to check presence
@users = users || User.with_feed
@users = users || User.where(id: Users::Setting.with_feed.select(:user_id))
@earlier_than = earlier_than
# NOTE: should these be configurable? Currently they are the result of empiric
@ -64,7 +64,7 @@ module Feeds
# TODO: put this in separate service object
def fetch_feeds(batch_of_users)
data = batch_of_users.pluck(:id, :feed_url)
data = batch_of_users.joins(:setting).pluck(:id, "users_settings.feed_url")
result = Parallel.map(data, in_threads: num_fetchers) do |user_id, url|
cleaned_url = url.to_s.strip
@ -149,7 +149,7 @@ module Feeds
e,
feeds_import_info: {
username: user.username,
feed_url: user.feed_url,
feed_url: user.setting&.feed_url,
item_count: item_count_error(feed),
error: "Feeds::Import::CreateArticleError:#{item.url}"
},

View file

@ -25,7 +25,7 @@ module Mailchimp
gibbon.lists(Settings::General.mailchimp_newsletter_id).members(target_md5_email).upsert(
body: {
email_address: user.email,
status: user.email_newsletter ? "subscribed" : "unsubscribed",
status: user.notification_setting.email_newsletter ? "subscribed" : "unsubscribed",
merge_fields: {
NAME: user.name.to_s,
USERNAME: user.username.to_s,
@ -35,7 +35,7 @@ module Mailchimp
ARTICLES: user.articles.size,
COMMENTS: user.comments.size,
ONBOARD_PK: user.onboarding_package_requested.to_s,
EXPERIENCE: user.experience_level || 666
EXPERIENCE: user.setting.experience_level || 666
}
},
)
@ -68,7 +68,7 @@ module Mailchimp
return false unless Settings::General.mailchimp_community_moderators_id.present? && user.has_role?(:trusted)
success = false
status = user.email_community_mod_newsletter ? "subscribed" : "unsubscribed"
status = user.notification_setting.email_community_mod_newsletter ? "subscribed" : "unsubscribed"
begin
gibbon.lists(Settings::General.mailchimp_community_moderators_id).members(target_md5_email).upsert(
body: {
@ -98,7 +98,7 @@ module Mailchimp
tag_ids = user.roles.where(name: "tag_moderator").pluck(:resource_id)
tag_names = Tag.where(id: tag_ids).pluck(:name)
status = user.email_tag_mod_newsletter ? "subscribed" : "unsubscribed"
status = user.notification_setting.email_tag_mod_newsletter ? "subscribed" : "unsubscribed"
begin
gibbon.lists(Settings::General.mailchimp_tag_moderators_id).members(target_md5_email).upsert(

View file

@ -33,9 +33,9 @@ module Moderator
def remove_mod_roles
@user.remove_role(:trusted)
@user.remove_role(:tag_moderator)
@user.update(email_tag_mod_newsletter: false)
@user.notification_setting.update(email_tag_mod_newsletter: false)
Mailchimp::Bot.new(user).manage_tag_moderator_list
@user.update(email_community_mod_newsletter: false)
@user.notification_setting.update(email_community_mod_newsletter: false)
Mailchimp::Bot.new(user).manage_community_moderator_list
end

View file

@ -4,9 +4,9 @@ module Notifications
SUPPORTED = [Comment].freeze
def self.available_moderators
User.with_role(:trusted)
.where("last_moderation_notification < ?", MODERATORS_AVAILABILITY_DELAY.ago)
.where(mod_roundrobin_notifications: true)
User.with_role(:trusted).joins(:notification_setting)
.where("last_moderation_notification < ?", 3.days.ago)
.where(notification_setting: { mod_roundrobin_notifications: true })
end
end
end

View file

@ -35,7 +35,9 @@ module Notifications
end
# Send PNs using Rpush - respecting users' notificaton delivery settings
targets = User.where(id: user_ids, mobile_comment_notifications: true).ids
targets = User.joins(:notification_setting)
.where(notification_setting: { mobile_comment_notifications: true }).ids
PushNotifications::Send.call(
user_ids: targets,
title: "@#{comment.user.username}",

View file

@ -24,8 +24,10 @@ module Profiles
def initialize(user, updated_attributes)
@user = user
@profile = user.profile
@users_setting = user.setting
@updated_profile_attributes = updated_attributes[:profile] || {}
@updated_user_attributes = updated_attributes[:user].to_h || {}
@updated_users_setting_attributes = updated_attributes[:users_setting].to_h || {}
@errors = []
@success = false
end
@ -38,6 +40,7 @@ module Profiles
else
errors.concat(@profile.errors.full_messages)
errors.concat(@user.errors.full_messages)
errors.concat(@users_setting.errors.full_messages)
Honeycomb.add_field("error", errors_as_sentence)
Honeycomb.add_field("errored", true)
end
@ -62,6 +65,7 @@ module Profiles
Profile.transaction do
update_profile
@user.update!(@updated_user_attributes)
@users_setting.update!(@updated_users_setting_attributes)
end
true
rescue ActiveRecord::RecordInvalid

View file

@ -49,7 +49,9 @@ module TagModerators
end
def add_tag_mod_role(user, tag)
user.update(email_tag_mod_newsletter: true) unless user.email_tag_mod_newsletter?
unless user.notification_setting.email_tag_mod_newsletter?
user.notification_setting.update(email_tag_mod_newsletter: true)
end
user.add_role(:tag_moderator, tag)
Rails.cache.delete("user-#{user.id}/tag_moderators_list")
return unless tag_mod_newsletter_enabled?

View file

@ -4,7 +4,7 @@ module TagModerators
return if user.has_role?(:trusted) || user.suspended?
user.add_role(:trusted)
user.update(email_community_mod_newsletter: true)
user.notification_setting.update(email_community_mod_newsletter: true)
Rails.cache.delete("user-#{user.id}/has_trusted_role")
NotifyMailer.with(user: user).trusted_role_email.deliver_now
return unless community_mod_newsletter_enabled?

View file

@ -2,7 +2,9 @@ module TagModerators
class Remove
def self.call(user, tag)
user.remove_role(:tag_moderator, tag)
user.update(email_tag_mod_newsletter: false) if user.email_tag_mod_newsletter?
if user.notification_setting.email_tag_mod_newsletter?
user.notification_setting.update(email_tag_mod_newsletter: false)
end
Rails.cache.delete("user-#{user.id}/tag_moderators_list")
return unless tag_mod_newsletter_enabled?

View file

@ -19,7 +19,7 @@
<center style="margin-top:50px;">
<em>
<% if @user.experience_level == nil %>
<% if @user.setting.experience_level.nil? %>
<b>
You can now add your experience level to your account in order to receive more relevant content.
</b>

View file

@ -1,7 +1,12 @@
<div class="grid gap-6 m:grid-cols-2">
<div class="grid gap-6">
<% account.bg_color_hex = user_colors(account)[:bg] if account.bg_color_hex.blank? %>
<% account.text_color_hex = user_colors(account)[:text] if account.text_color_hex.blank? %>
<% if account.class.name == "User" %>
<% account.setting.brand_color1 = user_colors(account)[:bg] if account.setting && account.setting.brand_color1.blank? %>
<% account.setting.brand_color2 = user_colors(account)[:text] if account.setting && account.setting.brand_color2.blank? %>
<% else %>
<% account.bg_color_hex = user_colors(account)[:bg] if account.bg_color_hex.blank? %>
<% account.text_color_hex = user_colors(account)[:text] if account.text_color_hex.blank? %>
<% end %>
<div class="crayons-field">
<%= f.label :bg_color_hex, "Brand color 1", class: "crayons-field__label" %>
<p class="crayons-field__description">Used for backgrounds, borders etc.</p>
@ -15,7 +20,11 @@
</div>
<div class="crayons-field">
<% account.bg_color_hex = user_colors(account)[:text] if account.text_color_hex.blank? %>
<% if account.class.name == "User" %>
<% account.setting.brand_color1 = user_colors(account)[:text] if account.setting && account.setting.brand_color2.blank? %>
<% else %>
<% account.bg_color_hex = user_colors(account)[:text] if account.text_color_hex.blank? %>
<% end %>
<%= f.label :text_color_hex, "Brand color 2", class: "crayons-field__label" %>
<p class="crayons-field__description">Used for texts (usually put on <em>Brand color 1</em>).</p>
<div class="flex items-center">

View file

@ -19,7 +19,7 @@
<% if actor.class.name == "User" %>
<div class="user-metadata-details">
<ul class="user-metadata-details-inner">
<% if actor.email_public && context != "sidebar" %>
<% if actor.setting.display_email_on_profile && context != "sidebar" %>
<li>
<div class="key">
Email

View file

@ -1,6 +1,9 @@
<%= javascript_packs_with_chunks_tag "stickySaveFooter", defer: true %>
<%= form_for @user, html: { id: "ux-customization-form", class: "sticky-footer-form" } do |f| %>
<%= form_for @users_setting,
url: users_settings_path,
html: { id: "ux-customization-form", class: "sticky-footer-form" } do |f| %>
<%= f.hidden_field :user_id, value: @users_setting.user_id, id: nil %>
<%= f.hidden_field :tab, value: @tab, id: nil %>
<div class="crayons-card crayons-card--content-rows">
@ -11,7 +14,7 @@
<fieldset class="crayons-field">
<legend class="crayons-field__label">Site Theme</legend>
<div class="theme-selector-field grid gap-4 grid-cols-2 l:grid-cols-3">
<% %w[default night_theme minimal_light_theme pink_theme ten_x_hacker_theme].each do |theme| %>
<% Users::Setting.config_themes.keys.each do |theme| %>
<%= render partial: "theme_selector", locals: { f: f, theme: theme } %>
<% end %>
</div>
@ -20,7 +23,7 @@
<div class="crayons-field">
<%= f.label :config_font, "Base Reading Font", class: "crayons-field__label" %>
<div class="theme-selector-field grid gap-4 grid-cols-2 l:grid-cols-3">
<% User::FONTS.each do |font| %>
<% Users::Setting.config_fonts.keys.each do |font| %>
<%= render partial: "font_selector", locals: { f: f, font: font } %>
<% end %>
</div>
@ -29,7 +32,7 @@
<div class="crayons-field">
<%= f.label :config_navbar, "Site Navbar", class: "crayons-field__label" %>
<div class="theme-selector-field grid gap-4 grid-cols-2 l:grid-cols-3">
<% %w[default static].each do |navbar| %>
<% Users::Setting.config_navbars.keys.each do |navbar| %>
<%= render partial: "navbar_selector", locals: { f: f, navbar: navbar } %>
<% end %>
</div>
@ -43,7 +46,7 @@
<div class="crayons-field">
<%= f.label :editor_version, "Editor Version", class: "crayons-field__label" %>
<div class="theme-selector-field grid gap-4 grid-cols-2 l:grid-cols-3">
<% %w[v1 v2].each do |version| %>
<% Users::Setting.editor_versions.keys.each do |version| %>
<%= render partial: "editor_selector", locals: { f: f, version: version } %>
<% end %>
</div>

View file

@ -1,5 +1,5 @@
<label class="rich-selector crayons-field crayons-field--radio editor-selector--<%= version %>">
<%= f.radio_button :editor_version, version, checked: @user.editor_version == version, class: "crayons-radio" %>
<%= f.radio_button :editor_version, version, checked: @users_setting.editor_version == version, class: "crayons-radio" %>
<div class="crayons-field__label">
<% if version == "v1" %>
Basic markdown

View file

@ -1,5 +1,5 @@
<label>
<%= f.radio_button :experience_level, exp, checked: @user.experience_level == exp.to_i, class: "rich-selector-radio" %>
<%= f.radio_button :experience_level, exp, checked: @users_setting.experience_level == exp.to_i, class: "rich-selector-radio" %>
<div class="rich-selector rich-selector--experience align-center my-2 mx-1 fw-medium fs-l px-0 s:px-1">
<p><%= i + 1 %></p>
<p class="fs-xs m:fs-s fw-normal"><%= label[i] %></p>

View file

@ -4,7 +4,7 @@
<h2 class="crayons-subtitle-1">
Connect settings
</h2>
<%= form_for(@user, html: { id: nil, class: "grid gap-6" }) do |f| %>
<%= form_for(@users_setting, url: users_settings_path, html: { id: nil, class: "grid gap-6" }) do |f| %>
<div class="crayons-field">
<%= f.label :inbox_type, "Inbox privacy", class: "crayons-field__label" %>
<p class="crayons-field__description">Open your inbox to messages from people you don't follow or keep your inbox private to mutual follows.</p>

View file

@ -1,5 +1,5 @@
<label class="rich-selector crayons-field crayons-field--radio font-selector--<%= font.tr("_", "-") %>">
<%= f.radio_button :config_font, font, checked: @user.decorate.config_font_name == font, class: "crayons-radio" %>
<%= f.radio_button :config_font, font, checked: @users_setting.resolved_font_name == font, class: "crayons-radio" %>
<div class="crayons-field__label fw-normal">
<%= font.tr("_", " ") %>
</div>

View file

@ -1,5 +1,5 @@
<label class="rich-selector crayons-field crayons-field--radio navbar-selector--<%= navbar %>">
<%= f.radio_button :config_navbar, navbar, checked: @user.config_navbar == navbar, class: "crayons-radio" %>
<%= f.radio_button :config_navbar, navbar, checked: @users_setting.config_navbar == navbar, class: "crayons-radio" %>
<div class="crayons-field__label">
<% if navbar == "default" %>
Fixed to window

View file

@ -1,4 +1,4 @@
<%= form_for @user do |f| %>
<%= form_for @users_notification_setting, url: users_notification_settings_path do |f| %>
<div class="crayons-card crayons-card--content-rows">
<header>
<h2 class="crayons-subtitle-1">
@ -105,7 +105,6 @@
<% end %>
<div class="crayons-card crayons-card--content-rows">
<%= f.hidden_field :tab, value: @tab %>
<button type="submit" class="crayons-btn">Save</button>
</div>

View file

@ -17,6 +17,11 @@
<%= f.text_field "user[email]", class: "crayons-textfield", placeholder: "john.doe@example.com", value: @user.email %>
</div>
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box "users_setting[display_email_on_profile]", class: "crayons-checkbox", checked: @user.setting.display_email_on_profile %>
<%= f.label :display_email_on_profile, for: "users_setting[display_email_on_profile]", class: "crayons-field__label" %>
</div>
<div class="crayons-field">
<%= f.label :username, for: "user[username]", class: "crayons-field__label" %>
<%= f.text_field "user[username]", maxlength: 30, class: "crayons-textfield", placeholder: "johndoe", value: @user.username %>
@ -84,6 +89,7 @@
<% ProfileFieldGroup.non_empty_groups.each do |group| %>
<% next if group.name == "Basic" # TODO: @citizen428 Remove this after user settings work (email field) %>
<% next if group.name == "Links" # TODO: [@jacobherrington] Remove this when we drop social links %>
<% next if group.name == "Branding" # TODO: [@msarit @jacobherrington] May be further refactored later as part of Profiles work %>
<div class="crayons-card crayons-card--content-rows">
<h2><%= group.name %></h2>
<% if group.description.present? %>
@ -91,6 +97,7 @@
<% end %>
<% group.profile_fields.each do |field| %>
<% next if field.attribute_name == "display_email_on_profile" %>
<div class="crayons-field <%= "crayons-field--checkbox" if field.input_type == "check_box" %>">
<% if field["input_type"] == "check_box" %>
<%= f.public_send(field["input_type"],
@ -132,6 +139,44 @@
</div>
<% end %>
<% users_setting = @user.setting %>
<div class="crayons-card crayons-card--content-rows">
<h2>Branding</h2>
<div class="crayons-field ">
<label class="crayons-field__label" for="users_setting[brand_color1]">
Brand color 1
</label>
<div class="flex items-center w-100 m:w-50">
<%= f.public_send("text_field", "users_setting[brand_color1]",
value: users_setting.public_send("brand_color1"),
placeholder: "#000000",
class: "crayons-textfield js-color-field") %>
<%= f.public_send("color_field",
"users_setting[brand_color1]",
value: users_setting.public_send("brand_color1"),
class: "crayons-color-selector js-color-field ml-2") %>
</div>
<p class="crayons-field__description">Used for backgrounds, borders etc.</p>
</div>
<div class="crayons-field ">
<label class="crayons-field__label" for="users_setting[brand_color1]">
Brand color 2
</label>
<div class="flex items-center w-100 m:w-50">
<%= f.public_send("text_field", "users_setting[brand_color2]",
value: users_setting.public_send("brand_color2"),
placeholder: "#000000",
class: "crayons-textfield js-color-field") %>
<%= f.public_send("color_field",
"users_setting[brand_color2]",
value: users_setting.public_send("brand_color2"),
class: "crayons-color-selector js-color-field ml-2") %>
</div>
<p class="crayons-field__description">Used for texts (usually put on Brand color 1).</p>
</div>
</div>
<div class="save-footer crayons-card crayons-card--content-rows">
<button type="submit" class="crayons-btn">Save Profile Information</button>
</div>

View file

@ -3,13 +3,13 @@
<h2 class="crayons-subtitle-1">
Publishing to <%= community_name %> from RSS
</h2>
<% if @user.feed_url.present? %>
<%= form_for(@user) do |f| %>
<%= f.hidden_field :feed_url, value: @user.feed_url %>
<% if @users_setting.feed_url.present? %>
<%= form_for @users_setting, url: users_settings_path do |f| %>
<%= f.hidden_field :feed_url, value: @users_setting.feed_url %>
<%= f.hidden_field :tab, value: @tab %>
<div class="flex items-center">
<button type="submit" class="crayons-btn crayons-btn--secondary">Fetch feed now</button>
<p>Last Fetched: <time id="rss-fetch-time" datetime="<%= @user.feed_fetched_at.iso8601 %>"></time></p>
<p class="pl-2">Last Fetched: <time id="rss-fetch-time" datetime="<%= @users_setting.user.feed_fetched_at.iso8601 %>"></time></p>
</div>
<% end %>
<% end %>
@ -30,7 +30,7 @@
<p><em>By submitting your RSS Feed URL, you agree that you own and/or have permission to syndicate the associated content.</em></p>
</div>
<%= form_for @user, html: { class: "grid gap-6" } do |f| %>
<%= form_for @users_setting, url: users_settings_path, html: { class: "grid gap-6" } do |f| %>
<div class="crayons-field">
<%= f.label :feed_url, "RSS Feed URL", class: "crayons-field__label" %>
<%= f.url_field :feed_url, placeholder: "https://yoursite.com/feed", class: "crayons-textfield" %>
@ -51,7 +51,7 @@
</div>
<%= f.hidden_field :tab, value: @tab, id: nil %>
<% button_text = @user.feed_url.present? ? "Save Feed Settings" : "Submit Feed Settings" %>
<% button_text = @users_setting.feed_url.present? ? "Save Feed Settings" : "Submit Feed Settings" %>
<button class="crayons-btn w-max" type="submit"><%= button_text %></button>
<% end %>
</div>

View file

@ -1,5 +1,5 @@
<label class="rich-selector crayons-field crayons-field--radio p-4 theme-selector theme-selector--<%= theme.tr("_", "-") %>" title="<%= theme.titlecase %>">
<%= f.radio_button :config_theme, theme, checked: @user.config_theme == theme, class: "crayons-radio" %>
<%= f.radio_button :config_theme, theme, checked: @users_setting.config_theme == theme, class: "crayons-radio" %>
<div class="crayons-field__label">
<%= theme.titlecase %>

View file

@ -76,7 +76,7 @@
<%= local_date(@user.created_at) %>
</span>
<% if @user.email_public %>
<% if @user.setting.display_email_on_profile %>
<a href="mailto:<%= @user.email %>" class="profile-header__meta__item">
<%= inline_svg_tag("email.svg", class: "crayons-icon mr-2 shrink-0", aria: true, title: "Email address") %>
<%= @user.email %>
@ -114,11 +114,11 @@
</div>
<div class="crayons-modal__box__body">
<% if @user.inbox_guidelines.present? %>
<% if @user.setting.inbox_guidelines.present? %>
<p class="fw-bold mb-2">@<%= @user.username %>'s guidelines:</p>
<p class="mb-6"><%= @user.inbox_guidelines %></p>
<p class="mb-6"><%= @user.setting.inbox_guidelines %></p>
<% end %>
<form id="new-message-form" class="message-form mb-4" data-info='{"id":<%= @user.id %>,"className":"<%= @user.class.name %>","username":"<%= @user.username %>", "showChat":"<%= @user.inbox_type %>"}'>
<form id="new-message-form" class="message-form mb-4" data-info='{"id":<%= @user.id %>,"className":"<%= @user.class.name %>","username":"<%= @user.username %>", "showChat":"<%= @user.setting.inbox_type %>"}'>
<textarea id="new-message" rows="4" cols="70" placeholder="Enter your message here..." class="crayons-textfield"></textarea>
<button type="submit" class="submit-message crayons-btn">Send</button>
</form>

View file

@ -6,7 +6,7 @@ module Emails
def perform(user_id)
user = User.find_by(id: user_id)
return unless user&.email_digest_periodic? && user&.registered?
return unless user&.notification_setting&.email_digest_periodic? && user&.registered?
articles = EmailDigestArticleCollector.new(user).articles_to_send
return unless articles.any?

View file

@ -1,52 +0,0 @@
class MigrateUserNotificationSettingsWorker
include Sidekiq::Worker
SQL = <<~SQL.freeze
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
WHERE users.id = $1
)
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
def perform(user_id)
Users::Setting.connection.exec_insert SQL, nil, [user_id]
end
end

View file

@ -1,83 +0,0 @@
class MigrateUserSettingsWorker
include Sidekiq::Worker
SQL = <<~SQL.freeze
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
WHERE users.id = $1
)
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
def perform(user_id)
Users::Setting.connection.exec_insert SQL, nil, [user_id]
end
end

View file

@ -20,7 +20,7 @@ module Notifications
return false unless receiver
return true if receiver_klass == "Organization"
receiver.reaction_notifications
receiver.notification_setting.reaction_notifications
end
end
end

View file

@ -82,6 +82,11 @@ Rails.application.configure do
Bullet.add_whitelist(type: :unused_eager_loading, class_name: "Comment", association: :user)
# NOTE: @citizen428 Temporarily ignoring this while working out user - profile relationship
Bullet.add_whitelist(type: :n_plus_one_query, class_name: "User", association: :profile)
Bullet.add_whitelist(type: :n_plus_one_query, class_name: "User", association: :setting)
Bullet.add_whitelist(type: :n_plus_one_query, class_name: "User", association: :notification_setting)
# @mstruve: These occur during setting updates, not sure how since we are only dealing with single setting records
Bullet.add_whitelist(type: :n_plus_one_query, class_name: "Users::Setting", association: :user)
Bullet.add_whitelist(type: :n_plus_one_query, class_name: "Users::NotificationSetting", association: :user)
end
end
# rubocop:enable Metrics/BlockLength

View file

@ -136,6 +136,10 @@ Rails.application.routes.draw do
resources :devices, only: %i[create destroy]
end
end
namespace :users do
resource :settings, only: %i[update]
resource :notification_settings, only: %i[update]
end
resources :users, only: %i[update]
resources :reactions, only: %i[index create]
resources :response_templates, only: %i[index create edit update destroy]
@ -227,6 +231,8 @@ Rails.application.routes.draw do
post "/notification_subscriptions/:notifiable_type/:notifiable_id", to: "notification_subscriptions#upsert"
patch "/onboarding_update", to: "users#onboarding_update"
patch "/onboarding_checkbox_update", to: "users#onboarding_checkbox_update"
patch "/onboarding_notifications_checkbox_update",
to: "users/notification_settings#onboarding_notifications_checkbox_update"
get "email_subscriptions/unsubscribe"
post "/chat_channels/:id/moderate", to: "chat_channels#moderate"
post "/chat_channels/:id/open", to: "chat_channels#open"

View file

@ -75,8 +75,6 @@ users_in_random_order = seeder.create_if_none(User, num_users) do
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
twitter_username: Faker::Internet.username(specifier: name),
email_comment_notifications: false,
email_follower_notifications: false,
# Emails limited to 50 characters
email: Faker::Internet.email(name: name, separators: "+", domain: Faker::Internet.domain_word.first(20)),
confirmed_at: Time.current,
@ -142,8 +140,6 @@ seeder.create_if_doesnt_exist(User, "email", "admin@forem.local") do
summary: Faker::Lorem.paragraph_by_chars(number: 199, supplemental: false),
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
email_comment_notifications: false,
email_follower_notifications: false,
confirmed_at: Time.current,
password: "password",
password_confirmation: "password",

View file

@ -1,9 +0,0 @@
module DataUpdateScripts
class MigrateRelevantFieldsFromUsersToUsersSettings
def run
User.ids.each do |id|
MigrateUserSettingsWorker.perform_async id
end
end
end
end

View file

@ -1,9 +0,0 @@
module DataUpdateScripts
class MigrateRelevantFieldsFromUsersToUsersNotificationSettings
def run
User.ids.each do |id|
MigrateUserNotificationSettingsWorker.perform_async id
end
end
end
end

View file

@ -0,0 +1,12 @@
module DataUpdateScripts
class RemoveNotificationSettingMigrationScripts
SCRIPTS_TO_REMOVE = %w[
20210423155327_migrate_relevant_fields_from_users_to_users_settings
20210503174302_migrate_relevant_fields_from_users_to_users_notification_settings
].freeze
def run
DataUpdateScript.delete_by(file_name: SCRIPTS_TO_REMOVE)
end
end
end

View file

@ -2,7 +2,7 @@ require "rails_helper"
RSpec.describe UserDecorator, type: :decorator do
let(:saved_user) { create(:user) }
let(:user) { build(:user) }
let(:user) { create(:user) }
context "with serialization" do
it "serializes both the decorated object IDs and decorated methods" do
@ -54,120 +54,104 @@ RSpec.describe UserDecorator, type: :decorator do
describe "#darker_color" do
it "returns a darker version of the assigned color if colors are blank" do
saved_user.assign_attributes(bg_color_hex: "", text_color_hex: "")
saved_user.setting.update(brand_color1: "", brand_color2: "")
expect(saved_user.decorate.darker_color).to be_present
end
it "returns a darker version of the color if bg_color_hex is present" do
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
it "returns a darker version of the color if brand_color1 is present" do
saved_user.setting.update(brand_color1: "#dddddd", brand_color2: "#ffffff")
expect(saved_user.decorate.darker_color).to eq("#c2c2c2")
end
it "returns an adjusted darker version of the color" do
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
saved_user.setting.update(brand_color1: "#dddddd", brand_color2: "#ffffff")
expect(saved_user.decorate.darker_color(0.3)).to eq("#424242")
end
it "returns an adjusted lighter version of the color if adjustment is over 1.0" do
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
saved_user.setting.update(brand_color1: "#dddddd", brand_color2: "#ffffff")
expect(saved_user.decorate.darker_color(1.1)).to eq("#f3f3f3")
end
end
describe "#enriched_colors" do
it "returns assigned colors if bg_color_hex is blank" do
saved_user.assign_attributes(bg_color_hex: "")
it "returns assigned colors if brand_color1 is blank" do
saved_user.setting.update(brand_color1: "")
expect(saved_user.decorate.enriched_colors[:bg]).to be_present
expect(saved_user.decorate.enriched_colors[:text]).to be_present
end
it "returns assigned colors if text_color_hex is blank" do
saved_user.assign_attributes(text_color_hex: "")
it "returns assigned colors if brand_color2 is blank" do
saved_user.setting.update(brand_color2: "")
expect(saved_user.decorate.enriched_colors[:bg]).to be_present
expect(saved_user.decorate.enriched_colors[:text]).to be_present
end
it "returns bg_color_hex and assigned text_color_hex if text_color_hex is blank" do
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "")
it "returns brand_color1 and assigned brand_color2 if brand_color2 is blank" do
saved_user.setting.update(brand_color1: "#dddddd", brand_color2: "")
expect(saved_user.decorate.enriched_colors[:bg]).to be_present
expect(saved_user.decorate.enriched_colors[:text]).to be_present
end
it "returns text_color_hex and assigned bg_color_hex if bg_color_hex is blank" do
saved_user.assign_attributes(bg_color_hex: "", text_color_hex: "#ffffff")
it "returns brand_color2 and assigned brand_color1 if brand_color1 is blank" do
saved_user.setting.update(brand_color1: "", brand_color2: "#ffffff")
expect(saved_user.decorate.enriched_colors[:bg]).to be_present
expect(saved_user.decorate.enriched_colors[:text]).to be_present
end
it "returns bg_color_hex and text_color_hex if both are present" do
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "#fffff3")
it "returns brand_color1 and brand_color2 if both are present" do
saved_user.setting.update(brand_color1: "#dddddd", brand_color2: "#fffff3")
expect(saved_user.decorate.enriched_colors).to eq(bg: "#dddddd", text: "#fffff3")
end
end
describe "#config_font_name" do
it "replaces 'default' with font configured for the site in Settings::General" do
expect(user.config_font).to eq("default")
%w[sans_serif serif open_dyslexic].each do |font|
allow(Settings::UserExperience).to receive(:default_font).and_return(font)
expect(user.decorate.config_font_name).to eq(font)
end
end
it "doesn't replace the user's custom selected font" do
user_comic_sans = create(:user, config_font: "comic_sans")
allow(Settings::UserExperience).to receive(:default_font).and_return("open_dyslexic")
expect(user_comic_sans.decorate.config_font_name).to eq("comic_sans")
end
end
describe "#config_body_class" do
it "creates proper body class with defaults" do
expected_result = %W[
default sans-serif-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-header
trusted-status-#{user.trusted} #{user.setting.config_navbar}-header
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
end
it "creates proper body class with sans serif config" do
user.config_font = "sans_serif"
user.setting.config_font = "sans_serif"
expected_result = %W[
default sans-serif-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-header
trusted-status-#{user.trusted} #{user.setting.config_navbar}-header
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
end
it "creates proper body class with night theme" do
user.config_theme = "night_theme"
user.setting.config_theme = "night_theme"
expected_result = %W[
night-theme sans-serif-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-header
trusted-status-#{user.trusted} #{user.setting.config_navbar}-header
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
end
it "creates proper body class with pink theme" do
user.config_theme = "pink_theme"
user.setting.config_theme = "pink_theme"
expected_result = %W[
pink-theme sans-serif-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-header
trusted-status-#{user.trusted} #{user.setting.config_navbar}-header
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
end
it "creates proper body class with minimal light theme" do
user.config_theme = "minimal_light_theme"
user.setting.config_theme = "minimal_light_theme"
expected_result = %W[
minimal-light-theme sans-serif-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-header
trusted-status-#{user.trusted} #{user.setting.config_navbar}-header
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
end
it "works with static navbar" do
user.config_navbar = "static"
user.setting.config_navbar = "static"
expected_result = %W[
default sans-serif-article-body
trusted-status-#{user.trusted} static-header
@ -192,17 +176,17 @@ RSpec.describe UserDecorator, type: :decorator do
describe "#dark_theme?" do
it "determines dark theme if night theme" do
user.config_theme = "night_theme"
user.setting.config_theme = "night_theme"
expect(user.decorate.dark_theme?).to be(true)
end
it "determines dark theme if ten x hacker" do
user.config_theme = "ten_x_hacker_theme"
user.setting.config_theme = "ten_x_hacker_theme"
expect(user.decorate.dark_theme?).to be(true)
end
it "determines not dark theme if not one of the dark themes" do
user.config_theme = "default"
user.setting.config_theme = "default"
expect(user.decorate.dark_theme?).to be(false)
end
end

View file

@ -17,10 +17,8 @@ FactoryBot.define do
saw_onboarding { true }
checked_code_of_conduct { true }
checked_terms_and_conditions { true }
display_announcements { true }
registered_at { Time.current }
signup_cta_variant { "navbar_basic" }
email_digest_periodic { false }
trait :with_identity do
transient { identities { Authentication::Providers.available } }
@ -164,8 +162,10 @@ FactoryBot.define do
end
trait :with_newsletters do
email_newsletter { true }
email_digest_periodic { true }
after(:create) do |user|
Users::NotificationSetting.find_by(user_id: user.id)
.update_columns(email_newsletter: true, email_digest_periodic: true)
end
end
end
end

View file

@ -0,0 +1,19 @@
FactoryBot.define do
factory :users_notification_setting, class: "Users::NotificationSetting" do
email_badge_notifications { true }
email_comment_notifications { true }
email_community_mod_newsletter { false }
email_connect_messages { true }
email_digest_periodic { false }
email_follower_notifications { true }
email_membership_newsletter { false }
email_mention_notifications { true }
email_newsletter { false }
email_tag_mod_newsletter { false }
email_unread_notifications { true }
mobile_comment_notifications { true }
mod_roundrobin_notifications { true }
reaction_notifications { true }
welcome_notifications { true }
end
end

View file

@ -1,85 +0,0 @@
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, sidekiq: :inline do
let(:users_notification_setting) { Users::NotificationSetting.last }
before do
Users::NotificationSetting.destroy_all
User.destroy_all
end
# NOTE: an after_commit in User model creates a users_notification_settings record for users
# So in these specs, we destroy these users_notification_settings records
# so that he migration script creates them instead
context "when migrating data" do
it "sets the expected number of records" do
create_list(:user, 3)
Users::NotificationSetting.destroy_all
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
user = create(:user)
user.notification_setting.destroy
user.update_columns(email_newsletter: nil)
user.reload
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
user.notification_setting.destroy
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)
user.notification_setting.destroy
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

@ -1,133 +0,0 @@
require "rails_helper"
require Rails.root.join(
"lib/data_update_scripts/20210423155327_migrate_relevant_fields_from_users_to_users_settings.rb",
)
describe DataUpdateScripts::MigrateRelevantFieldsFromUsersToUsersSettings, sidekiq: :inline do
let(:users_setting) { Users::Setting.last }
before do
Users::Setting.destroy_all
Profile.destroy_all
User.destroy_all
end
# NOTE: an after_commit in User model creates a users_notification_settings record for users
# So in these specs, we destroy these users_notification_settings records
# so that he migration script creates them instead
context "when migrating data" do
it "sets the expected number of records" do
create_list(:user, 3)
Users::Setting.destroy_all
expect do
described_class.new.run
end.to change(Users::Setting, :count).from(0).to(3)
end
it "sets the correct User records" do
user = create(:user, config_font: "sans_serif")
user.setting.destroy
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
user = create(:user)
user.setting.destroy
user.update_columns(display_announcements: nil)
user.reload
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
user = create(:user, config_font: "monospace")
user.setting.destroy
described_class.new.run
expect(users_setting.monospace_font?).to be(true)
end
it "assigns the fallback value when the value passed does not have an enum defined" do
user = create(:user)
user.setting.destroy
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

View file

@ -0,0 +1,18 @@
require "rails_helper"
require Rails.root.join(
"lib/data_update_scripts/20210701134702_remove_notification_setting_migration_scripts.rb",
)
describe DataUpdateScripts::RemoveNotificationSettingMigrationScripts do
it "removes scripts correctly from the DB", :aggregate_failures do
described_class::SCRIPTS_TO_REMOVE.each do |script_name|
create(:data_update_script, file_name: script_name)
end
described_class.new.run
described_class::SCRIPTS_TO_REMOVE.each do |script_name|
expect(DataUpdateScript.exists?(file_name: script_name)).to be(false)
end
end
end

View file

@ -1,7 +1,7 @@
require "rails_helper"
RSpec.describe DigestMailer, type: :mailer do
let(:user) { build_stubbed(:user) }
let(:user) { create(:user) }
let(:article) { build_stubbed(:article) }
describe "#digest_email" do

View file

@ -82,7 +82,7 @@ RSpec.describe Follow, type: :model do
it "sends an email notification" do
allow(ForemInstance).to receive(:smtp_enabled?).and_return(true)
user_2.update_column(:email_follower_notifications, true)
user_2.notification_setting.update(email_follower_notifications: true)
expect do
Sidekiq::Testing.inline! do
described_class.create!(follower: user, followable: user_2)

View file

@ -144,7 +144,8 @@ RSpec.describe Message, type: :model do
it "does not send email if user has email_messages turned off" do
chat_channel.update_column(:channel_type, "direct")
user2.update_columns(updated_at: 1.day.ago, email_connect_messages: false)
user2.update_columns(updated_at: 1.day.ago)
user2.notification_setting.update_columns(email_connect_messages: false)
user2.chat_channel_memberships.last.update_column(:last_opened_at, 2.days.ago)
expect do

View file

@ -179,10 +179,7 @@ RSpec.describe User, type: :model do
it { is_expected.to allow_value(nil).for(:payment_pointer) }
it { is_expected.to allow_value("").for(:payment_pointer) }
it { is_expected.to validate_inclusion_of(:inbox_type).in_array(%w[open private]) }
it { is_expected.to validate_length_of(:email).is_at_most(50).allow_nil }
it { is_expected.to validate_length_of(:inbox_guidelines).is_at_most(250).allow_nil }
it { is_expected.to validate_length_of(:name).is_at_most(100).is_at_least(1) }
it { is_expected.to validate_length_of(:password).is_at_most(100).is_at_least(8) }
it { is_expected.to validate_length_of(:username).is_at_most(30).is_at_least(2) }
@ -192,9 +189,6 @@ RSpec.describe User, type: :model do
it { is_expected.to validate_presence_of(:blocked_by_count) }
it { is_expected.to validate_presence_of(:blocking_others_count) }
it { is_expected.to validate_presence_of(:comments_count) }
it { is_expected.to validate_presence_of(:config_font) }
it { is_expected.to validate_presence_of(:config_navbar) }
it { is_expected.to validate_presence_of(:config_theme) }
it { is_expected.to validate_presence_of(:credits_count) }
it { is_expected.to validate_presence_of(:following_orgs_count) }
it { is_expected.to validate_presence_of(:following_tags_count) }
@ -277,26 +271,6 @@ RSpec.describe User, type: :model do
expect(user.errors[:base].to_s).to include("could not be saved. Rate limit reached")
expect(limiter).to have_received(:track_limit_by_action).with(:user_update).twice
end
context "when validating feed_url", vcr: true do
it "is valid with no feed_url" do
user.feed_url = nil
expect(user).to be_valid
end
it "is not valid with an invalid feed_url", vcr: { cassette_name: "feeds_validate_url_invalid" } do
user.feed_url = "http://example.com"
expect(user).not_to be_valid
end
it "is valid with a valid feed_url", vcr: { cassette_name: "feeds_import_medium_vaidehi" } do
user.feed_url = "https://medium.com/feed/@vaidehijoshi"
expect(user).to be_valid
end
end
end
context "when callbacks are triggered before validation" do
@ -350,42 +324,6 @@ RSpec.describe User, type: :model do
end
end
describe "#config_theme" do
it "accepts valid theme" do
user.config_theme = "night theme"
expect(user).to be_valid
end
it "does not accept invalid theme" do
user.config_theme = "no night mode"
expect(user).not_to be_valid
end
end
describe "#config_font" do
it "accepts valid font" do
user.config_font = "sans serif"
expect(user).to be_valid
end
it "does not accept invalid font" do
user.config_font = "goobledigook"
expect(user).not_to be_valid
end
end
describe "#config_navbar" do
it "accepts valid navbar" do
user.config_navbar = "static"
expect(user).to be_valid
end
it "does not accept invalid navbar" do
user.config_navbar = "not valid navbar input"
expect(user).not_to be_valid
end
end
context "when the past value is relevant" do
let(:user) { create(:user) }
@ -663,50 +601,52 @@ RSpec.describe User, type: :model do
end
it "creates proper body class with defaults" do
classes = "default sans-serif-article-body trusted-status-#{user.trusted} #{user.config_navbar}-header"
classes = "default sans-serif-article-body trusted-status-#{user.trusted} #{user.setting.config_navbar}-header"
expect(user.decorate.config_body_class).to eq(classes)
end
it "determines dark theme if night theme" do
user.config_theme = "night_theme"
user.setting.config_theme = "night_theme"
expect(user.decorate.dark_theme?).to eq(true)
end
it "determines dark theme if ten x hacker" do
user.config_theme = "ten_x_hacker_theme"
user.setting.config_theme = "ten_x_hacker_theme"
expect(user.decorate.dark_theme?).to eq(true)
end
it "determines not dark theme if not one of the dark themes" do
user.config_theme = "default"
user.setting.config_theme = "default"
expect(user.decorate.dark_theme?).to eq(false)
end
it "creates proper body class with sans serif config" do
user.config_font = "sans_serif"
user.setting.config_font = "sans_serif"
classes = "default sans-serif-article-body trusted-status-#{user.trusted} #{user.config_navbar}-header"
classes = "default sans-serif-article-body trusted-status-#{user.trusted} #{user.setting.config_navbar}-header"
expect(user.decorate.config_body_class).to eq(classes)
end
it "creates proper body class with open dyslexic config" do
user.config_font = "open_dyslexic"
user.setting.config_font = "open_dyslexic"
classes = "default open-dyslexic-article-body trusted-status-#{user.trusted} #{user.config_navbar}-header"
classes = "default open-dyslexic-article-body trusted-status-#{user.trusted} #{user.setting.config_navbar}-header"
expect(user.decorate.config_body_class).to eq(classes)
end
it "creates proper body class with night theme" do
user.config_theme = "night_theme"
user.setting.config_theme = "night_theme"
classes = "night-theme sans-serif-article-body trusted-status-#{user.trusted} #{user.config_navbar}-header"
# rubocop:disable Layout/LineLength
classes = "night-theme sans-serif-article-body trusted-status-#{user.trusted} #{user.setting.config_navbar}-header"
# rubocop:enable Layout/LineLength
expect(user.decorate.config_body_class).to eq(classes)
end
it "creates proper body class with pink theme" do
user.config_theme = "pink_theme"
user.setting.config_theme = "pink_theme"
classes = "pink-theme sans-serif-article-body trusted-status-#{user.trusted} #{user.config_navbar}-header"
classes = "pink-theme sans-serif-article-body trusted-status-#{user.trusted} #{user.setting.config_navbar}-header"
expect(user.decorate.config_body_class).to eq(classes)
end
end
@ -788,12 +728,12 @@ RSpec.describe User, type: :model do
end
it "returns false if user opted out from follower notifications" do
user.assign_attributes(email_follower_notifications: false)
user.notification_setting.update(email_follower_notifications: false)
expect(user.receives_follower_email_notifications?).to be(false)
end
it "returns true if user opted in from follower notifications and has an email" do
user.assign_attributes(email_follower_notifications: true)
user.notification_setting.update(email_follower_notifications: true)
expect(user.receives_follower_email_notifications?).to be(true)
end
end
@ -896,14 +836,5 @@ RSpec.describe User, type: :model do
# Changes were also persisted in the users table
expect(user.reload.available_for).to eq "profile migrations"
end
it "propagates changes of mapped attributes to the profile model", :aggregate_failures do
expect do
user.update(bg_color_hex: "#abcdef")
end.to change { user.profile.reload.brand_color1 }.to("#abcdef")
# Changes were also persisted in the users table
expect(user.reload.bg_color_hex).to eq "#abcdef"
end
end
end

View file

@ -1,12 +1,12 @@
require "rails_helper"
# rubocop:disable Layout/LineLength
RSpec.describe Users::Setting, type: :model do
let(:user) { create(:user) }
let(:setting) { described_class.find_by(user_id: user.id) }
describe "validations" do
subject { setting }
let(:user) { create(:user) }
let(:setting) { user.setting }
it { is_expected.to validate_length_of(:inbox_guidelines).is_at_most(250).allow_nil }
it { is_expected.to define_enum_for(:inbox_type).with_values(private: 0, open: 1).with_suffix(:inbox) }
it { is_expected.to define_enum_for(:config_font).with_values(default: 0, comic_sans: 1, monospace: 2, open_dyslexic: 3, sans_serif: 4, serif: 5).with_suffix(:font) }
@ -48,6 +48,43 @@ RSpec.describe Users::Setting, type: :model do
expect { setting.config_navbar = 10 }.to raise_error(ArgumentError)
end
end
context "when validating feed_url", vcr: true do
it "is valid with no feed_url" do
setting.feed_url = nil
expect(setting).to be_valid
end
it "is not valid with an invalid feed_url", vcr: { cassette_name: "feeds_validate_url_invalid" } do
setting.feed_url = "http://example.com"
expect(setting).not_to be_valid
end
it "is valid with a valid feed_url", vcr: { cassette_name: "feeds_import_medium_vaidehi" } do
setting.feed_url = "https://medium.com/feed/@vaidehijoshi"
expect(setting).to be_valid
end
end
end
describe "#resolved_font_name" do
it "replaces 'default' with font configured for the site in Settings::General" do
expect(setting.config_font).to eq("default")
%w[sans_serif serif open_dyslexic].each do |font|
allow(Settings::UserExperience).to receive(:default_font).and_return(font)
expect(setting.resolved_font_name).to eq(font)
end
end
it "doesn't replace the user's custom selected font" do
user_comic_sans = create(:user)
user_comic_sans.setting.update(config_font: "comic_sans")
allow(Settings::UserExperience).to receive(:default_font).and_return("open_dyslexic")
expect(user_comic_sans.setting.resolved_font_name).to eq("comic_sans")
end
end
end
# rubocop:enable Layout/LineLength

View file

@ -2,7 +2,11 @@ require "rails_helper"
RSpec.describe "ChatChannels", type: :request do
let(:user) { create(:user) }
let(:user_open_inbox) { create(:user, inbox_type: "open") }
let(:user_open_inbox) do
u = create(:user)
u.setting.update(inbox_type: "open")
u
end
let(:user_closed_inbox) { create(:user) }
let(:test_subject) { create(:user) }
let(:chat_channel) { create(:chat_channel) }

View file

@ -1,7 +1,7 @@
require "rails_helper"
RSpec.describe "EmailSubscriptions", type: :request do
let(:user) { build(:user) }
let(:user) { create(:user) }
before do
allow(User).to receive(:find).and_return(user)
@ -24,7 +24,7 @@ RSpec.describe "EmailSubscriptions", type: :request do
it "does unsubscribe the user" do
get email_subscriptions_unsubscribe_url(ut: generate_token(user.id))
user.reload
expect(user.email_mention_notifications).to be(false)
expect(user.notification_setting.email_mention_notifications).to be(false)
end
it "handles error properly" do

View file

@ -30,7 +30,7 @@ RSpec.describe "IncomingWebhooks::MailchimpUnsubscribesController", type: :reque
expect do
post "/incoming_webhooks/mailchimp/#{secret}/unsubscribe", params: params
end.to change { user.reload.email_newsletter }.from(true).to(false)
end.to change { user.reload.notification_setting.email_newsletter }.from(true).to(false)
end
end
end

View file

@ -537,7 +537,7 @@ RSpec.describe "NotificationsIndex", type: :request do
before do
user.add_role(:trusted)
user.update(mod_roundrobin_notifications: false)
user.notification_setting.update(mod_roundrobin_notifications: false)
sign_in user
sidekiq_perform_enqueued_jobs do
Notification.send_moderation_notification(comment)

View file

@ -209,7 +209,7 @@ RSpec.describe "Reactions", type: :request do
context "when creating readinglist" do
before do
user.update_column(:experience_level, 8)
user.setting.update_column(:experience_level, 8)
sign_in user
post "/reactions", params: {
reactable_id: article.id,

View file

@ -0,0 +1,93 @@
require "rails_helper"
RSpec.describe "UserNotificationSettings", type: :request do
let(:user) { create(:user) }
before { sign_in user }
describe "PUT /update/:id" do
it "disables reaction notifications (in both users and notification_settings tables)" do
expect(user.notification_setting.reaction_notifications).to be(true)
expect do
put users_notification_settings_path(user.notification_setting.id),
params: { users_notification_setting: { tab: "notifications", reaction_notifications: 0 } }
end.to change { user.notification_setting.reload.reaction_notifications }.from(true).to(false)
end
it "enables community-success notifications" do
put users_notification_settings_path(user.notification_setting.id),
params: { users_notification_setting: { tab: "notifications", mod_roundrobin_notifications: 1 } }
expect(user.reload.subscribed_to_mod_roundrobin_notifications?).to be(true)
end
it "disables community-success notifications" do
put users_notification_settings_path(user.notification_setting.id),
params: { users_notification_setting: { tab: "notifications", mod_roundrobin_notifications: 0 } }
expect(user.reload.subscribed_to_mod_roundrobin_notifications?).to be(false)
end
it "can toggle welcome notifications" do
put users_notification_settings_path(user.notification_setting.id),
params: { users_notification_setting: { tab: "notifications", welcome_notifications: 0 } }
expect(user.reload.subscribed_to_welcome_notifications?).to be(false)
put users_notification_settings_path(user.notification_setting.id),
params: { users_notification_setting: { tab: "notifications", welcome_notifications: 1 } }
expect(user.reload.subscribed_to_welcome_notifications?).to be(true)
end
it "returns error message if settings can't be saved" do
put users_notification_settings_path(user.notification_setting.id),
params: { users_notification_setting: { tab: "notifications", email_digest_periodic: nil } }
expect(flash[:error]).not_to be_blank
end
end
describe "PATCH /onboarding_notifications_checkbox_update" do
before { sign_in user }
it "updates onboarding checkbox" do
user.update_column(:saw_onboarding, false)
expect do
patch onboarding_notifications_checkbox_update_path(format: :json),
params: { notifications: { tab: "notifications", email_newsletter: 1 } }
end.to change { user.notification_setting.reload.email_newsletter }.from(false).to(true)
expect(user.saw_onboarding).to be(true)
end
it "can toggle email_newsletter" do
expect do
patch onboarding_notifications_checkbox_update_path(format: :json),
params: { notifications: { tab: "notifications", email_newsletter: 1 } }
end.to change { user.notification_setting.reload.email_newsletter }.from(false).to(true)
expect do
patch onboarding_notifications_checkbox_update_path(format: :json),
params: { notifications: { tab: "notifications", email_newsletter: 0 } }
end.to change { user.notification_setting.reload.email_newsletter }.from(true).to(false)
end
it "can toggle email_digest_periodic" do
expect do
patch onboarding_notifications_checkbox_update_path(format: :json),
params: { notifications: { tab: "notifications", email_digest_periodic: 1 } }
end.to change { user.notification_setting.reload.email_digest_periodic }.from(false).to(true)
expect do
patch onboarding_notifications_checkbox_update_path(format: :json),
params: { notifications: { tab: "notifications", email_digest_periodic: 0 } }
end.to change { user.notification_setting.reload.email_digest_periodic }.from(true).to(false)
end
it "returns 422 status and errors if errors occur" do
patch onboarding_notifications_checkbox_update_path(format: :json),
params: { notifications: { tab: "notifications", email_digest_periodic: nil } }
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).not_to be_blank
end
end
end

View file

@ -238,44 +238,16 @@ RSpec.describe "UserSettings", type: :request do
expect(user.reload.profile_updated_at).to be > 2.minutes.ago
end
it "disables reaction notifications (in both users and notification_settings tables)" do
expect(user.notification_setting.reaction_notifications).to be(true)
expect do
put "/users/#{user.id}", params: { user: { tab: "notifications", reaction_notifications: 0 } }
end.to change { user.reload.reaction_notifications }.from(true).to(false)
expect(user.notification_setting.reload.reaction_notifications).to be(false)
end
it "enables community-success notifications" do
put "/users/#{user.id}", params: { user: { tab: "notifications", mod_roundrobin_notifications: 1 } }
expect(user.reload.subscribed_to_mod_roundrobin_notifications?).to be(true)
end
it "updates the users announcement display preferences (in both users and user_settings tables)" do
expect(user.setting.display_announcements).to be(true)
expect do
put "/users/#{user.id}", params: { user: { tab: "misc", display_announcements: 0 } }
end.to change { user.reload.display_announcements }.from(true).to(false)
put users_settings_path(user.setting.id), params: { users_setting: { display_announcements: 0 } }
end.to change { user.setting.reload.display_announcements }.from(true).to(false)
expect(user.setting.reload.display_announcements).to be(false)
end
it "disables community-success notifications" do
put "/users/#{user.id}", params: { user: { tab: "notifications", mod_roundrobin_notifications: 0 } }
expect(user.reload.subscribed_to_mod_roundrobin_notifications?).to be(false)
end
it "can toggle welcome notifications" do
put "/users/#{user.id}", params: { user: { tab: "notifications", welcome_notifications: 0 } }
expect(user.reload.subscribed_to_welcome_notifications?).to be(false)
put "/users/#{user.id}", params: { user: { tab: "notifications", welcome_notifications: 1 } }
expect(user.reload.subscribed_to_welcome_notifications?).to be(true)
end
it "updates username to too short username" do
put "/users/#{user.id}", params: { user: { tab: "profile", username: "h" } }
expect(response.body).to include("Username is too short")
@ -338,12 +310,16 @@ RSpec.describe "UserSettings", type: :request do
context "when requesting a fetch of the feed", vcr: { cassette_name: "feeds_import_medium_vaidehi" } do
let(:feed_url) { "https://medium.com/feed/@vaidehijoshi" }
let(:user) { create(:user, feed_url: feed_url) }
let(:user) do
u = create(:user)
u.setting.update(feed_url: feed_url)
u
end
it "invokes Feeds::ImportArticlesWorker" do
allow(Feeds::ImportArticlesWorker).to receive(:perform_async).with(user.id)
put user_path(user.id), params: { user: { feed_url: feed_url } }
put users_settings_path(user.setting.id), params: { users_setting: { feed_url: feed_url } }
expect(Feeds::ImportArticlesWorker).to have_received(:perform_async).with(user.id)
end

View file

@ -6,7 +6,6 @@ RSpec.describe "UserShow", type: :request do
:profile,
:with_DEV_info,
user: create(:user, :without_profile),
display_email_on_profile: true,
)
end
let(:user) { profile.user }
@ -19,6 +18,7 @@ RSpec.describe "UserShow", type: :request do
# rubocop:disable RSpec/ExampleLength
it "renders the proper JSON-LD for a user" do
user.setting.update(display_email_on_profile: true)
get user.path
text = Nokogiri::HTML(response.body).at('script[type="application/ld+json"]').text
response_json = JSON.parse(text)

View file

@ -10,7 +10,8 @@ RSpec.describe ArticleWithVideoCreationService, type: :service do
describe "#create!" do
it "works" do
Timecop.travel(3.weeks.ago)
user = create(:user, editor_version: "v1")
user = create(:user)
user.setting.update(editor_version: "v1")
Timecop.return
test = build_stubbed(:article, user: user, video: link).attributes.symbolize_keys
article = described_class.new(test, user).create!

View file

@ -89,7 +89,7 @@ RSpec.describe Articles::Builder, type: :service do
end
context "when tag" do
let(:user) { create(:user, editor_version: "v1") }
let(:user) { create(:user) }
let(:tag) { create(:tag) }
let(:correct_attributes) do
{
@ -100,6 +100,7 @@ RSpec.describe Articles::Builder, type: :service do
end
it "initializes an article with the correct attributes and does not need authorization" do
user.setting.update(editor_version: "v1")
subject, needs_authorization = described_class.call(user, tag, prefill)
expect(subject).to be_an_instance_of(Article)
@ -126,7 +127,7 @@ RSpec.describe Articles::Builder, type: :service do
end
context "when user_editor_v1" do
let(:user) { create(:user, editor_version: "v1") }
let(:user) { create(:user) }
let(:correct_attributes) do
body = "---\ntitle: \npublished: false\ndescription: \ntags: " \
"\n//cover_image: https://direct_url_to_image.jpg\n---\n\n"
@ -139,6 +140,7 @@ RSpec.describe Articles::Builder, type: :service do
end
it "initializes an article with the correct attributes and does not need authorization" do
user.setting.update(editor_version: "v1")
subject, needs_authorization = described_class.call(user, tag, prefill)
expect(subject).to be_an_instance_of(Article)

View file

@ -282,7 +282,11 @@ RSpec.describe Articles::Feeds::LargeForemExperimental, type: :service do
let(:article) { create(:article, experience_level_rating: 7) }
context "when user has a further experience level" do
let(:user) { create(:user, experience_level: 1) }
let(:user) { create(:user) }
before do
user.setting.update(experience_level: 1)
end
it "returns negative of (absolute value of the difference between article and user experience) divided by 2" do
expect(feed.score_experience_level(article)).to eq(-3)
@ -295,7 +299,11 @@ RSpec.describe Articles::Feeds::LargeForemExperimental, type: :service do
end
context "when user has a closer experience level" do
let(:user) { create(:user, experience_level: 9) }
let(:user) { create(:user) }
before do
user.setting.update(experience_level: 9)
end
it "returns negative of (absolute value of the difference between article and user experience) divided by 2" do
expect(feed.score_experience_level(article)).to eq(-1)
@ -303,7 +311,11 @@ RSpec.describe Articles::Feeds::LargeForemExperimental, type: :service do
end
context "when the user does not have an experience level set" do
let(:user) { create(:user, experience_level: nil) }
let(:user) { create(:user) }
before do
user.setting.update(experience_level: nil)
end
it "uses a value of 5 for user experience level" do
expect(feed.score_experience_level(article)).to eq(-1)

View file

@ -49,7 +49,7 @@ RSpec.describe Authentication::Authenticator, type: :service do
expect(user.password).to be_present
expect(user.signup_cta_variant).to be_nil
expect(user.saw_onboarding).to be(false)
expect(user.editor_version).to eq("v2")
expect(user.setting.editor_version).to eq("v2")
end
it "sets the correct sign up cta variant" do
@ -244,7 +244,7 @@ RSpec.describe Authentication::Authenticator, type: :service do
expect(user.password).to be_present
expect(user.signup_cta_variant).to be_nil
expect(user.saw_onboarding).to be(false)
expect(user.editor_version).to eq("v2")
expect(user.setting.editor_version).to eq("v2")
end
it "sets the correct sign up cta variant" do
@ -445,7 +445,7 @@ RSpec.describe Authentication::Authenticator, type: :service do
expect(user.password).to be_present
expect(user.signup_cta_variant).to be_nil
expect(user.saw_onboarding).to be(false)
expect(user.editor_version).to eq("v2")
expect(user.setting.editor_version).to eq("v2")
end
it "sets the correct sign up cta variant" do
@ -534,7 +534,7 @@ RSpec.describe Authentication::Authenticator, type: :service do
expect(user.password).to be_present
expect(user.signup_cta_variant).to be_nil
expect(user.saw_onboarding).to be(false)
expect(user.editor_version).to eq("v2")
expect(user.setting.editor_version).to eq("v2")
end
it "sets the correct sign up cta variant" do

View file

@ -31,7 +31,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
let(:user) { create(:user, :with_identity, identities: ["github"], created_at: 1.week.ago) }
it "does not send a notification to an unsubscribed user" do
user.update!(welcome_notifications: false)
user.notification_setting.update!(welcome_notifications: false)
expect do
sidekiq_perform_enqueued_jobs { described_class.call(user.id) }
end.to not_change(user.notifications, :count)

View file

@ -3,7 +3,8 @@ require "rails_helper"
RSpec.describe EmailDigest, type: :service do
describe "::send_digest_email" do
it "enqueues Emails::SendUserDigestWorker" do
user = create(:user, email_digest_periodic: true)
user = create(:user)
user.notification_setting.update(email_digest_periodic: true)
allow(Emails::SendUserDigestWorker).to receive(:perform_async)
described_class.send_periodic_digest_email
expect(Emails::SendUserDigestWorker).to have_received(:perform_async).with(user.id)
@ -11,7 +12,8 @@ RSpec.describe EmailDigest, type: :service do
it "performs job inline if community is DEV" do
allow(ForemInstance).to receive(:dev_to?).and_return(true)
user = create(:user, email_digest_periodic: true)
user = create(:user)
user.notification_setting.update(email_digest_periodic: true)
worker = Emails::SendUserDigestWorker.new
allow(worker).to receive(:perform)
allow(Emails::SendUserDigestWorker).to receive(:new).and_return(worker)

View file

@ -1,7 +1,11 @@
require "rails_helper"
RSpec.describe Feeds::AssembleArticleMarkdown, type: :service do
let(:user) { create(:user, feed_mark_canonical: true) }
let(:user) do
u = create(:user)
u.setting.update(feed_mark_canonical: true)
u
end
let(:feed_source_url) { "https://feed.source/url" }
let(:feed) { instance_double("Feedjira::Parser::RSS", url: "https://feed.source/") }
let(:title) { "A title" }

View file

@ -5,13 +5,14 @@ RSpec.describe Feeds::Import, type: :service, vcr: true do
let(:nonmedium_link) { "https://circleci.com/blog/feed.xml" }
let(:nonpermanent_link) { "https://medium.com/feed/@macsiri/" }
describe ".call" do
before do
[link, nonmedium_link, nonpermanent_link].each do |feed_url|
create(:user, feed_url: feed_url)
end
before do
[link, nonmedium_link, nonpermanent_link].each do |feed_url|
user = create(:user)
user.setting.update(feed_url: feed_url)
end
end
describe ".call" do
# TODO: We could probably improve these tests by parsing against the items in the feed rather than hardcoding
it "fetch only articles from a feed_url", vcr: { cassette_name: "feeds_import" } do
num_articles = described_class.call
@ -34,14 +35,14 @@ RSpec.describe Feeds::Import, type: :service, vcr: true do
it "parses correctly", vcr: { cassette_name: "feeds_import" } do
expect do
described_class.call
end.to change(User.find_by(feed_url: nonpermanent_link).articles, :count).by(1)
end.to change(Users::Setting.find_by(feed_url: nonpermanent_link).user.articles, :count).by(1)
end
it "sets feed_fetched_at to the current time", vcr: { cassette_name: "feeds_import" } do
Timecop.freeze(Time.current) do
described_class.call
user = User.find_by(feed_url: nonpermanent_link)
user = Users::Setting.find_by(feed_url: nonpermanent_link).user
feed_fetched_at = user.feed_fetched_at
expect(feed_fetched_at.to_i).to eq(Time.current.to_i)
end
@ -103,28 +104,25 @@ RSpec.describe Feeds::Import, type: :service, vcr: true do
context "with an explicit set of users", vcr: { cassette_name: "feeds_import" } do
# TODO: We could probably improve these tests by parsing against the items in the feed rather than hardcoding
it "accepts a subset of users" do
num_articles = described_class.call(users: User.with_feed.limit(1))
num_articles = described_class.call(users: User.where(id: Users::Setting.with_feed.select(:user_id)).limit(1))
expect(num_articles).to eq(10)
end
it "imports no articles if given users are without feed" do
create(:user, feed_url: nil)
user = create(:user)
user.setting.update(feed_url: nil)
expect(described_class.call(users: User.where(feed_url: nil))).to eq(0)
# rubocop:disable Layout/LineLength
expect(described_class.call(users: User.where(id: Users::Setting.where(feed_url: nil).select(:user_id)))).to eq(0)
# rubocop:enable Layout/LineLength
end
end
end
context "when refetching" do
before do
[link, nonmedium_link, nonpermanent_link].each do |feed_url|
create(:user, feed_url: feed_url)
end
end
it "does refetch same user over and over by default", vcr: { cassette_name: "feeds_import_multiple_times" } do
user = User.find_by(feed_url: nonpermanent_link)
user = Users::Setting.find_by(feed_url: nonpermanent_link).user
Timecop.freeze(Time.current) do
user.update_columns(feed_fetched_at: Time.current)
@ -175,7 +173,8 @@ RSpec.describe Feeds::Import, type: :service, vcr: true do
# checking its invocation is a shortcut to testing the functionality.
allow(Article).to receive(:find_by).and_call_original
create(:user, feed_url: nonpermanent_link, feed_referential_link: false)
user = create(:user)
user.setting.update(feed_url: nonpermanent_link, feed_referential_link: false)
described_class.call
@ -185,7 +184,8 @@ RSpec.describe Feeds::Import, type: :service, vcr: true do
describe "feeds parsing and regressions" do
it "parses https://medium.com/feed/@dvirsegal correctly", vcr: { cassette_name: "rss_reader_dvirsegal" } do
user = create(:user, feed_url: "https://medium.com/feed/@dvirsegal")
user = create(:user)
user.setting.update(feed_url: "https://medium.com/feed/@dvirsegal")
expect do
described_class.call(users: User.where(id: user.id))
@ -193,7 +193,8 @@ RSpec.describe Feeds::Import, type: :service, vcr: true do
end
it "converts/replaces <picture> tags to <img>", vcr: { cassette_name: "rss_reader_swimburger" } do
user = create(:user, feed_url: "https://swimburger.net/atom.xml")
user = create(:user)
user.setting.update(feed_url: "https://swimburger.net/atom.xml")
expect do
described_class.call(users: User.where(id: user.id))

View file

@ -35,7 +35,7 @@ RSpec.describe Mailchimp::Bot, type: :service do
ARTICLES: user.articles.size,
COMMENTS: user.comments.size,
ONBOARD_PK: user.onboarding_package_requested.to_s,
EXPERIENCE: user.experience_level || 666
EXPERIENCE: user.setting.experience_level || 666
}
}
}
@ -50,20 +50,21 @@ RSpec.describe Mailchimp::Bot, type: :service do
describe "#upsert_to_newsletter" do
it "sends proper information" do
user.notification_setting.update(email_newsletter: true)
described_class.new(user).upsert_to_newsletter
expect(my_gibbon_client).to have_received(:upsert).with(matcher)
end
it "unsubscribes properly" do
user.update(email_newsletter: false)
user.notification_setting.update(email_newsletter: false)
described_class.new(user).upsert_to_newsletter
expect(my_gibbon_client).to have_received(:upsert)
.with(hash_including(body: hash_including(status: "unsubscribed")))
end
it "subscribes properly" do
user.update(email_newsletter: false)
user.update(email_newsletter: true)
user.notification_setting.update(email_newsletter: false)
user.notification_setting.update(email_newsletter: true)
described_class.new(user).upsert_to_newsletter
expect(my_gibbon_client).to have_received(:upsert)
.with(hash_including(body: hash_including(status: "subscribed")))
@ -78,7 +79,7 @@ RSpec.describe Mailchimp::Bot, type: :service do
end
it "tries to resubscribe the user if the user has previously been subscribed" do
user.update(email_newsletter: false)
user.notification_setting.update(email_newsletter: false)
mailchimp_bot = described_class.new(user)
mc_error =
Gibbon::MailChimpError.new("Error", status_code: 400, title: "Member In Compliance State")
@ -101,7 +102,7 @@ RSpec.describe Mailchimp::Bot, type: :service do
end
it "sends proper information" do
user.update(email_community_mod_newsletter: true)
user.notification_setting.update(email_community_mod_newsletter: true)
user.add_role(:trusted)
Settings::General.mailchimp_community_moderators_id = "something"
described_class.new(user).manage_community_moderator_list
@ -124,7 +125,7 @@ RSpec.describe Mailchimp::Bot, type: :service do
end
it "sends proper information" do
user.update(email_tag_mod_newsletter: true)
user.notification_setting.update(email_tag_mod_newsletter: true)
user.add_role(:tag_moderator, tag)
described_class.new(user).manage_tag_moderator_list
expect(my_gibbon_client).to have_received(:upsert)

View file

@ -1,7 +1,7 @@
require "rails_helper"
RSpec.describe Notifications::Moderation::Send, type: :service do
let(:last_moderation_time) { Time.zone.now - Notifications::Moderation::MODERATORS_AVAILABILITY_DELAY - 3.hours }
let(:last_moderation_time) { Time.zone.now - Notifications::Moderation::MODERATORS_AVAILABILITY_DELAY - 1.week }
let(:dev_account) { create(:user) }
let(:user) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
@ -10,7 +10,8 @@ RSpec.describe Notifications::Moderation::Send, type: :service do
let(:moderator) { available_moderators.first }
before do
create(:user, :trusted, last_moderation_notification: last_moderation_time)
u = create(:user, :trusted, last_moderation_notification: last_moderation_time)
u.notification_setting.update(mod_roundrobin_notifications: true)
allow(User).to receive(:dev_account).and_return(dev_account)
# Creating a comment calls moderation job which itself call moderation service
Comment.skip_callback(:commit, :after, :send_to_moderator)

View file

@ -15,6 +15,6 @@ RSpec.describe TagModerators::AddTrustedRole, type: :service do
it "signs the user up for the community mods newsletter" do
expect do
described_class.call(user)
end.to change { user.reload.email_community_mod_newsletter }.from(false).to(true)
end.to change { user.reload.notification_setting.email_community_mod_newsletter }.from(false).to(true)
end
end

View file

@ -31,8 +31,6 @@ seeder.create_if_doesnt_exist(User, "email", "admin@forem.local") do
username: "Admin_McAdmin",
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
email_comment_notifications: false,
email_follower_notifications: false,
confirmed_at: Time.current,
password: "password",
password_confirmation: "password",
@ -40,6 +38,10 @@ seeder.create_if_doesnt_exist(User, "email", "admin@forem.local") do
checked_code_of_conduct: true,
checked_terms_and_conditions: true,
)
user.notification_setting.update(
email_comment_notifications: false,
email_follower_notifications: false,
)
user.profile.update({
summary: "Admin user summary",
@ -76,15 +78,13 @@ end
##############################################################################
seeder.create_if_doesnt_exist(User, "email", "change-password-user@forem.com") do
User.create!(
user = User.create!(
name: "Change Password User",
email: "change-password-user@forem.com",
username: "changepassworduser",
summary: Faker::Lorem.paragraph_by_chars(number: 199, supplemental: false),
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
email_comment_notifications: false,
email_follower_notifications: false,
confirmed_at: Time.current,
password: "password",
password_confirmation: "password",
@ -92,42 +92,48 @@ seeder.create_if_doesnt_exist(User, "email", "change-password-user@forem.com") d
checked_code_of_conduct: true,
checked_terms_and_conditions: true,
)
user.notification_setting.update(
email_comment_notifications: false,
email_follower_notifications: false,
)
user
end
##############################################################################
seeder.create_if_doesnt_exist(User, "email", "article-editor-v1-user@forem.com") do
User.create!(
user = User.create!(
name: "Article Editor v1 User",
email: "article-editor-v1-user@forem.local",
username: "article_editor_v1_user",
summary: Faker::Lorem.paragraph_by_chars(number: 199, supplemental: false),
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
email_comment_notifications: false,
email_follower_notifications: false,
confirmed_at: Time.current,
password: "password",
password_confirmation: "password",
saw_onboarding: true,
checked_code_of_conduct: true,
checked_terms_and_conditions: true,
editor_version: "v1",
)
user.setting.update(editor_version: "v1")
user.notification_setting.update(
email_comment_notifications: false,
email_follower_notifications: false,
)
user
end
##############################################################################
seeder.create_if_doesnt_exist(User, "email", "article-editor-v2-user@forem.com") do
User.create!(
user = User.create!(
name: "Article Editor v2 User",
email: "article-editor-v2-user@forem.local",
username: "article_editor_v2_user",
summary: Faker::Lorem.paragraph_by_chars(number: 199, supplemental: false),
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
email_comment_notifications: false,
email_follower_notifications: false,
confirmed_at: Time.current,
password: "password",
password_confirmation: "password",
@ -135,20 +141,23 @@ seeder.create_if_doesnt_exist(User, "email", "article-editor-v2-user@forem.com")
checked_code_of_conduct: true,
checked_terms_and_conditions: true,
)
user.notification_setting.update(
email_comment_notifications: false,
email_follower_notifications: false,
)
user
end
##############################################################################
chat_user_1 = seeder.create_if_doesnt_exist(User, "email", "chat-user-1@forem.local") do
User.create!(
user = User.create!(
name: "Chat user 1",
email: "chat-user-1@forem.local",
username: "chat_user_1",
summary: Faker::Lorem.paragraph_by_chars(number: 199, supplemental: false),
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
email_comment_notifications: false,
email_follower_notifications: false,
confirmed_at: Time.current,
password: "password",
password_confirmation: "password",
@ -156,20 +165,23 @@ chat_user_1 = seeder.create_if_doesnt_exist(User, "email", "chat-user-1@forem.lo
checked_code_of_conduct: true,
checked_terms_and_conditions: true,
)
user.notification_setting.update(
email_comment_notifications: false,
email_follower_notifications: false,
)
user
end
##############################################################################
chat_user_2 = seeder.create_if_doesnt_exist(User, "email", "chat-user-2@forem.local") do
User.create!(
user = User.create!(
name: "Chat user 2",
email: "chat-user-2@forem.local",
username: "chat_user_2",
summary: Faker::Lorem.paragraph_by_chars(number: 199, supplemental: false),
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
email_comment_notifications: false,
email_follower_notifications: false,
confirmed_at: Time.current,
password: "password",
password_confirmation: "password",
@ -177,6 +189,11 @@ chat_user_2 = seeder.create_if_doesnt_exist(User, "email", "chat-user-2@forem.lo
checked_code_of_conduct: true,
checked_terms_and_conditions: true,
)
user.notification_setting.update(
email_comment_notifications: false,
email_follower_notifications: false,
)
user
end
##############################################################################

View file

@ -3,7 +3,11 @@ require "rails_helper"
RSpec.describe "Creating an article with the editor", type: :system do
include_context "with runkit_tag"
let(:user) { create(:user, editor_version: "v1") }
let(:user) do
u = create(:user)
u.setting.update(editor_version: "v1")
u
end
let!(:template) { file_fixture("article_published.txt").read }
let!(:template_with_runkit_tag) do
file_fixture("article_with_runkit_tag.txt").read

View file

@ -89,7 +89,7 @@ RSpec.describe "User visits a homepage", type: :system do
context "when opting-out of announcements" do
before do
user.update!(display_announcements: false)
user.setting.update!(display_announcements: false)
create(:announcement_broadcast, active: true)
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
visit "/"

View file

@ -47,7 +47,7 @@ RSpec.describe "User edits their extensions", type: :system, js: true do
stub_request(:get, "https://medium.com/feed/alkdmksadksa")
.to_return(status: 200, body: "not an xml feed")
fill_in "user[feed_url]", with: "https://medium.com/feed/alkdmksadksa"
fill_in "users_setting[feed_url]", with: "https://medium.com/feed/alkdmksadksa"
click_on "Submit Feed Settings"
expect(page).to have_text("Feed url is not a valid RSS/Atom feed")

View file

@ -58,7 +58,7 @@ RSpec.describe "Using the editor", type: :system do
end
describe "Submitting an article with v1 editor", js: true do
before { user.update!(editor_version: "v1") }
before { user.setting.update!(editor_version: "v1") }
it "fill out form and submit", cloudinary: true do
fill_markdown_with(read_from_file(raw_text))

View file

@ -4,8 +4,8 @@ RSpec.describe "rendering locals in a partial", type: :view do
context "when comment is low-quality" do
it "renders the comment with low-quality marker" do
allow(Settings::General).to receive(:mascot_image_url).and_return("https://i.imgur.com/fKYKgo4.png")
comment = build_stubbed(:comment, processed_html: "hi", score: CommentDecorator::LOW_QUALITY_THRESHOLD - 100)
article = build_stubbed(:article)
comment = create(:comment, processed_html: "hi", score: CommentDecorator::LOW_QUALITY_THRESHOLD - 100)
article = create(:article)
render "comments/comment",
comment: comment,

View file

@ -2,7 +2,11 @@ require "rails_helper"
RSpec.describe Emails::SendUserDigestWorker, type: :worker do
let(:worker) { subject }
let(:user) { create(:user, email_digest_periodic: true) }
let(:user) do
u = create(:user)
u.notification_setting.update(email_digest_periodic: true)
u
end
let(:author) { create(:user) }
let(:mailer) { double }
let(:message_delivery) { double }
@ -31,7 +35,7 @@ RSpec.describe Emails::SendUserDigestWorker, type: :worker do
it "does not send email when user does not have email_digest_periodic" do
create_list(:article, 3, user_id: author.id, public_reactions_count: 20, score: 20)
user.update_column(:email_digest_periodic, false)
user.notification_setting.update_column(:email_digest_periodic, false)
worker.perform(user.id)
expect(DigestMailer).not_to have_received(:with)

Some files were not shown because too many files have changed in this diff Show more