Drop profile columns from user (#10707)
* Prepare to drop profile columns from user * Update code and factory * Also remove unused constant * Move validation from user to profile * Remove Profiles::ExtractData service object * Add more comments * Simplify sameAs attribute generation * Obey me machine, I am your master * Fix condition order in guard clause * Temporarily disable callback * Fix specs * Reduce usage of Profile#refresh_attributes! * Remove leftover comment * Handle social media links differently * More spec fixes * Fix specs for admin profile fields controller * Fix specs after merge * Fix remaining specs * Update user show request spec * Add comment for follow_hiring_tag * Only save profile when user is valid * Fix seeds.rb for profile fields * Switch from before_save to after_save * Undo unrelated formattin change * Update spec/fixtures/files/profile_fields.csv Co-authored-by: Molly Struve <mollylbs@gmail.com> * Remove data update script and spec * Fix spec * Fix typo in comment * Fix typo in comment * Move article resave logic to service object * Move profile field creation to before(:suite) * Refactor error handling in Profiles::Update * Fix Profiles::Update specs and refactor * Temporarily disable spec * Add ProfileValidator * Clean up * Move DB ready check into app/lib * Refresh attributes after importing from CSV * Fix specs * Remove unused file * A girl has no name. A profile neither. * Fix specs * Add responds_to? check * Spec fix * Add name to user fields in profile settings page Co-authored-by: Molly Struve <mollylbs@gmail.com>
This commit is contained in:
parent
b02262c563
commit
8f5cfa2c0f
43 changed files with 587 additions and 639 deletions
|
|
@ -1,13 +1,13 @@
|
|||
class ProfilesController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
ALLOWED_USER_PARAMS = %i[email username profile_image].freeze
|
||||
ALLOWED_USER_PARAMS = %i[name email username profile_image].freeze
|
||||
|
||||
def update
|
||||
update_result = Profiles::Update.call(current_user, update_params)
|
||||
if update_result.success?
|
||||
flash[:settings_notice] = "Your profile has been updated"
|
||||
else
|
||||
flash[:error] = "Error: #{update_result.error_message}"
|
||||
flash[:error] = "Error: #{update_result.errors_as_sentence}"
|
||||
end
|
||||
redirect_to user_settings_path
|
||||
end
|
||||
|
|
@ -15,6 +15,6 @@ class ProfilesController < ApplicationController
|
|||
private
|
||||
|
||||
def update_params
|
||||
params.permit(profile: Profile.attributes!, user: ALLOWED_USER_PARAMS)
|
||||
params.permit(profile: Profile.attributes, user: ALLOWED_USER_PARAMS)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -478,17 +478,7 @@ class StoriesController < ApplicationController
|
|||
[
|
||||
@user.twitter_username.presence ? "https://twitter.com/#{@user.twitter_username}" : nil,
|
||||
@user.github_username.presence ? "https://github.com/#{@user.github_username}" : nil,
|
||||
@user.mastodon_url,
|
||||
@user.facebook_url,
|
||||
@user.youtube_url,
|
||||
@user.linkedin_url,
|
||||
@user.behance_url,
|
||||
@user.stackoverflow_url,
|
||||
@user.dribbble_url,
|
||||
@user.medium_url,
|
||||
@user.gitlab_url,
|
||||
@user.instagram_url,
|
||||
@user.website_url,
|
||||
@user.profile.try(:website_url),
|
||||
].reject(&:blank?)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
module Constants
|
||||
module Mastodon
|
||||
ALLOWED_INSTANCES = [
|
||||
"101010.pl",
|
||||
"4estate.media",
|
||||
"acg.mn",
|
||||
"anarchism.space",
|
||||
"bitcoinhackers.org",
|
||||
"bsd.network",
|
||||
"chaos.social",
|
||||
"cmx.im",
|
||||
"cybre.space",
|
||||
"fosstodon.org",
|
||||
"framapiaf.org",
|
||||
"friends.nico",
|
||||
"functional.cafe",
|
||||
"hackers.town",
|
||||
"hearthtodon.com",
|
||||
"hex.bz",
|
||||
"horiedon.com",
|
||||
"hostux.social",
|
||||
"im-in.space",
|
||||
"imastodon.net",
|
||||
"infosec.exchange",
|
||||
"kirakiratter.com",
|
||||
"knzk.me",
|
||||
"linuxrocks.online",
|
||||
"lou.lt",
|
||||
"mamot.fr",
|
||||
"mao.daizhige.org",
|
||||
"mastodon.art",
|
||||
"mastodon.at",
|
||||
"mastodon.blue",
|
||||
"mastodon.cloud",
|
||||
"mastodon.gamedev.place",
|
||||
"mastodon.host",
|
||||
"mastodon.online",
|
||||
"mastodon.sdf.org",
|
||||
"mastodon.social",
|
||||
"mastodon.technology",
|
||||
"mastodon.xyz",
|
||||
"mathtod.online",
|
||||
"merveilles.town",
|
||||
"mimumedon.com",
|
||||
"misskey.xyz",
|
||||
"moe.cat",
|
||||
"mstdn-workers.com",
|
||||
"mstdn.guru",
|
||||
"mstdn.io",
|
||||
"mstdn.jp",
|
||||
"mstdn.tokyocameraclub.com",
|
||||
"mstdn18.jp",
|
||||
"music.pawoo.net",
|
||||
"niu.moe",
|
||||
"noagendasocial.com",
|
||||
"octodon.social",
|
||||
"otajodon.com",
|
||||
"pawoo.net",
|
||||
"phpc.social",
|
||||
"qiitadon.com",
|
||||
"radical.town",
|
||||
"ro-mastodon.puyo.jp",
|
||||
"ruby.social",
|
||||
"ruhr.social",
|
||||
"social.coop",
|
||||
"social.targaryen.house",
|
||||
"social.tchncs.de",
|
||||
"switter.at",
|
||||
"tech.lgbt",
|
||||
"todon.nl",
|
||||
"toot.cafe",
|
||||
"wikitetas.club",
|
||||
"xoxo.zone",
|
||||
].freeze
|
||||
end
|
||||
end
|
||||
13
app/lib/database.rb
Normal file
13
app/lib/database.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Database related utility functions
|
||||
module Database
|
||||
# Checks if the database is ready and the specified table exists. This can be
|
||||
# useful during bin/setup and similar tasks. It also works if the connection
|
||||
# hasn't been established yet.
|
||||
#
|
||||
# @param table [String] the name of the table to check for
|
||||
def self.table_exists?(table)
|
||||
ActiveRecord::Base.connection.table_exists?(table)
|
||||
rescue ActiveRecord::NoDatabaseError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
@ -3,6 +3,7 @@ class Profile < ApplicationRecord
|
|||
|
||||
validates :data, presence: true
|
||||
validates :user_id, uniqueness: true
|
||||
validates_with ProfileValidator
|
||||
|
||||
has_many :custom_profile_fields, dependent: :destroy
|
||||
|
||||
|
|
@ -24,6 +25,8 @@ class Profile < ApplicationRecord
|
|||
|
||||
# Generates typed accessors for all currently defined profile fields.
|
||||
def self.refresh_attributes!
|
||||
return unless Database.table_exists?("profiles")
|
||||
|
||||
ProfileField.find_each do |field|
|
||||
store_attribute :data, field.attribute_name.to_sym, field.type
|
||||
end
|
||||
|
|
@ -34,25 +37,6 @@ class Profile < ApplicationRecord
|
|||
(stored_attributes[:data] || []).map(&:to_s)
|
||||
end
|
||||
|
||||
# Forces a reload before returning attributes
|
||||
def self.attributes!
|
||||
refresh_attributes!
|
||||
attributes
|
||||
end
|
||||
|
||||
# NOTE: @citizen428 This is a temporary mapping so we don't break DEV during
|
||||
# profile migration/generalization work.
|
||||
def self.mapped_attributes
|
||||
attributes!.map { |attribute| MAPPED_ATTRIBUTES.fetch(attribute, attribute).to_s }
|
||||
end
|
||||
|
||||
# NOTE: @citizen428 We want to have a current list of profile attributes the
|
||||
# moment the application loads. However, doing this unconditionally fails if
|
||||
# the profiles table doesn't exist yet (e.g. when running bin/setup in a new
|
||||
# clone). I wish Rails had a hook for code to run after the app started, but
|
||||
# for now this is the best I can come up with.
|
||||
refresh_attributes! if ApplicationRecord.connection.table_exists?("profiles")
|
||||
|
||||
def custom_profile_attributes
|
||||
custom_profile_fields.pluck(:attribute_name)
|
||||
end
|
||||
|
|
@ -60,4 +44,9 @@ class Profile < ApplicationRecord
|
|||
def clear!
|
||||
update(data: {})
|
||||
end
|
||||
|
||||
# NOTE: @citizen428 We want to have a current list of profile attributes the
|
||||
# moment the application loads. I wish Rails had a hook for code to run after
|
||||
# the app started...
|
||||
refresh_attributes!
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,42 @@ class User < ApplicationRecord
|
|||
include Searchable
|
||||
include Storext.model
|
||||
|
||||
# @citizen428 Preparing to drop profile columns from the users table
|
||||
PROFILE_COLUMNS = %w[
|
||||
available_for
|
||||
behance_url
|
||||
bg_color_hex
|
||||
contact_consent
|
||||
currently_hacking_on
|
||||
currently_learning
|
||||
currently_streaming_on
|
||||
dribbble_url
|
||||
education
|
||||
email_public
|
||||
employer_name
|
||||
employer_url
|
||||
employment_title
|
||||
facebook_url
|
||||
gitlab_url
|
||||
instagram_url
|
||||
linkedin_url
|
||||
location
|
||||
looking_for_work
|
||||
looking_for_work_publicly
|
||||
mastodon_url
|
||||
medium_url
|
||||
mostly_work_with
|
||||
stackoverflow_url
|
||||
summary
|
||||
text_color_hex
|
||||
twitch_url
|
||||
twitch_username
|
||||
website_url
|
||||
youtube_url
|
||||
].freeze
|
||||
|
||||
self.ignored_columns = PROFILE_COLUMNS
|
||||
|
||||
# NOTE: @citizen428 This is temporary code during profile migration and will
|
||||
# be removed.
|
||||
concerning :ProfileMigration do
|
||||
|
|
@ -15,24 +51,21 @@ class User < ApplicationRecord
|
|||
# instead. See `spec/factories/profiles.rb` for an example.
|
||||
attr_accessor :_skip_creating_profile
|
||||
|
||||
# NOTE: used for not sync-ing back data to profiles when the profile got
|
||||
# updated first. This will eventually be removed:
|
||||
attr_accessor :_skip_profile_sync
|
||||
|
||||
# All new users should automatically have a profile
|
||||
after_create_commit -> { Profile.create(user: self, data: Profiles::ExtractData.call(self)) },
|
||||
unless: :_skip_creating_profile
|
||||
after_create_commit -> { Profile.create(user: self) }, unless: :_skip_creating_profile
|
||||
|
||||
# Keep saving changes locally for the time being, but propagate them to profiles.
|
||||
after_update_commit :sync_profile
|
||||
|
||||
def sync_profile
|
||||
return if _skip_profile_sync
|
||||
return unless previous_changes.keys.any? { |attribute| attribute.in?(Profile.mapped_attributes) }
|
||||
|
||||
profile.update(data: Profiles::ExtractData.call(self))
|
||||
# Getters and setters for unmapped profile attributes
|
||||
(PROFILE_COLUMNS - Profile::MAPPED_ATTRIBUTES.values).each do |column|
|
||||
delegate column, "#{column}=", to: :profile, allow_nil: true
|
||||
end
|
||||
|
||||
# Getters and setters for mapped profile attributes
|
||||
Profile::MAPPED_ATTRIBUTES.each do |profile_attribute, user_attribute|
|
||||
define_method(user_attribute) { profile&.public_send(profile_attribute) }
|
||||
define_method("#{user_attribute}=") do |value|
|
||||
profile&.public_send("#{profile_attribute}=", value)
|
||||
end
|
||||
end
|
||||
private :sync_profile
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -195,11 +228,9 @@ class User < ApplicationRecord
|
|||
validates username_field, uniqueness: { allow_nil: true }, if: :"#{username_field}_changed?"
|
||||
end
|
||||
|
||||
validate :conditionally_validate_summary
|
||||
validate :non_banished_username, :username_changed?
|
||||
validate :unique_including_orgs_and_podcasts, if: :username_changed?
|
||||
validate :validate_feed_url, if: :feed_url_changed?
|
||||
validate :validate_mastodon_url
|
||||
validate :can_send_confirmation_email
|
||||
validate :update_rate_limit
|
||||
# NOTE: when updating the password on a Devise enabled model, the :encrypted_password
|
||||
|
|
@ -224,9 +255,11 @@ class User < ApplicationRecord
|
|||
before_create :set_default_language
|
||||
before_destroy :unsubscribe_from_newsletters, prepend: true
|
||||
before_destroy :destroy_follows, prepend: true
|
||||
|
||||
# NOTE: @citizen428 Temporary while migrating to generalized profiles
|
||||
after_save { |user| user.profile&.save if user.profile&.changed? }
|
||||
after_save :bust_cache
|
||||
after_save :subscribe_to_mailchimp_newsletter
|
||||
after_save :conditionally_resave_articles
|
||||
|
||||
after_create_commit :send_welcome_notification, :estimate_default_language
|
||||
after_commit :index_to_elasticsearch, on: %i[create update]
|
||||
|
|
@ -573,31 +606,10 @@ class User < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def conditionally_resave_articles
|
||||
Users::ResaveArticlesWorker.perform_async(id) if core_profile_details_changed? && !banned
|
||||
end
|
||||
|
||||
def bust_cache
|
||||
Users::BustCacheWorker.perform_async(id)
|
||||
end
|
||||
|
||||
def core_profile_details_changed?
|
||||
saved_change_to_username? ||
|
||||
saved_change_to_name? ||
|
||||
saved_change_to_summary? ||
|
||||
saved_change_to_bg_color_hex? ||
|
||||
saved_change_to_text_color_hex? ||
|
||||
saved_change_to_profile_image? ||
|
||||
Authentication::Providers.username_fields.any? { |f| public_send("saved_change_to_#{f}?") }
|
||||
end
|
||||
|
||||
def conditionally_validate_summary
|
||||
# Grandfather people who had a too long summary before.
|
||||
return if summary_was && summary_was.size > 200
|
||||
|
||||
errors.add(:summary, "is too long.") if summary.present? && summary.size > 200
|
||||
end
|
||||
|
||||
def validate_feed_url
|
||||
return if feed_url.blank?
|
||||
|
||||
|
|
@ -612,17 +624,6 @@ class User < ApplicationRecord
|
|||
errors.add(:feed_url, e.message)
|
||||
end
|
||||
|
||||
def validate_mastodon_url
|
||||
return if mastodon_url.blank?
|
||||
|
||||
uri = URI.parse(mastodon_url)
|
||||
return if uri.host&.in?(Constants::Mastodon::ALLOWED_INSTANCES)
|
||||
|
||||
errors.add(:mastodon_url, "is not an allowed Mastodon instance")
|
||||
rescue URI::InvalidURIError
|
||||
errors.add(:mastodon_url, "is not a valid URL")
|
||||
end
|
||||
|
||||
def tag_keywords_for_search
|
||||
"#{employer_name}#{mostly_work_with}#{available_for}"
|
||||
end
|
||||
|
|
|
|||
7
app/refinements/hash_any_key.rb
Normal file
7
app/refinements/hash_any_key.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
module HashAnyKey
|
||||
refine Hash do
|
||||
def any_key?(keys)
|
||||
(keys & Array.wrap(keys)).any?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
module Moderator
|
||||
class BanishUser < ManageActivityAndRoles
|
||||
DEFAULT_PROFILE_IMAGE =
|
||||
"https://thepracticaldev.s3.amazonaws.com/i/99mvlsfu5tfj9m7ku25d.png".freeze
|
||||
|
||||
attr_reader :user, :admin
|
||||
|
||||
def self.call(admin:, user:)
|
||||
|
|
@ -39,21 +42,7 @@ module Moderator
|
|||
|
||||
def remove_profile_info
|
||||
user.profile.clear!
|
||||
|
||||
# TODO: @forem/oss Remove this block once we drop the columns from users.
|
||||
user._skip_profile_sync = true
|
||||
user.update_columns(
|
||||
twitter_username: nil, github_username: nil, website_url: "", summary: "",
|
||||
location: "", education: "", employer_name: "", employer_url: "", employment_title: "",
|
||||
mostly_work_with: "", currently_learning: "", currently_hacking_on: "", available_for: "",
|
||||
email_public: false, facebook_url: nil, youtube_url: nil, dribbble_url: nil,
|
||||
medium_url: nil, stackoverflow_url: nil,
|
||||
behance_url: nil, linkedin_url: nil, gitlab_url: nil, instagram_url: nil, mastodon_url: nil,
|
||||
twitch_url: nil, feed_url: nil
|
||||
)
|
||||
user._skip_profile_sync = false
|
||||
|
||||
user.update_columns(profile_image: "https://thepracticaldev.s3.amazonaws.com/i/99mvlsfu5tfj9m7ku25d.png")
|
||||
user.update_columns(profile_image: DEFAULT_PROFILE_IMAGE)
|
||||
end
|
||||
|
||||
def delete_vomit_reactions
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ module ProfileFields
|
|||
|
||||
group "Basic" do
|
||||
field "Display email on profile", :check_box, display_area: "settings_only"
|
||||
field "Name", :text_field, placeholder: "John Doe", display_area: "header"
|
||||
field "Website URL", :text_field, placeholder: "https://yoursite.com", display_area: "header"
|
||||
field "Summary", :text_area, placeholder: "A short bio...", display_area: "header"
|
||||
field "Location", :text_field, placeholder: "Halifax, Nova Scotia", display_area: "header"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ module ProfileFields
|
|||
row[:profile_field_group] = ProfileFieldGroup.find_or_create_by(name: group)
|
||||
ProfileField.find_or_create_by(row)
|
||||
end
|
||||
Profile.refresh_attributes!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
module Profiles
|
||||
module ExtractData
|
||||
def self.call(user)
|
||||
user_attributes = user.attributes
|
||||
|
||||
mapped_attributes = Profile::MAPPED_ATTRIBUTES.transform_values(&:to_s)
|
||||
direct_attributes = Profile.attributes! - mapped_attributes.keys
|
||||
direct_data = user_attributes.extract!(*direct_attributes)
|
||||
mapped_data = mapped_attributes.keys.zip(user_attributes.values_at(*mapped_attributes.values)).to_h
|
||||
|
||||
direct_data.merge(mapped_data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,29 +1,37 @@
|
|||
module Profiles
|
||||
class Update
|
||||
using HashAnyKey
|
||||
include ImageUploads
|
||||
|
||||
USER_COLUMNS = Set.new(User.column_names).freeze
|
||||
CORE_PROFILE_FIELDS = %i[summary brand_color1 brand_color2].freeze
|
||||
CORE_USER_FIELDS = %i[name username profile_image].freeze
|
||||
|
||||
def self.call(user, updated_attributes = {})
|
||||
new(user, updated_attributes).call
|
||||
end
|
||||
|
||||
attr_reader :error_message
|
||||
|
||||
def initialize(user, updated_attributes)
|
||||
@user = user
|
||||
@profile = user.profile
|
||||
@updated_profile_attributes = updated_attributes[:profile] || {}
|
||||
@updated_user_attributes = updated_attributes[:user].to_h || {}
|
||||
@errors = []
|
||||
@success = false
|
||||
end
|
||||
|
||||
def call
|
||||
if verify_profile_image && update_profile && sync_to_user
|
||||
if update_successful?
|
||||
@success = true
|
||||
@user.touch(:profile_updated_at)
|
||||
follow_hiring_tag
|
||||
# TODO: @citizen428 Preserving a DEV specific feature for now, we should
|
||||
# probably remove this sooner than later as it may not make much sense
|
||||
# for other communities.
|
||||
follow_hiring_tag if SiteConfig.dev_to?
|
||||
conditionally_resave_articles
|
||||
else
|
||||
Honeycomb.add_field("error", @error_message)
|
||||
errors.concat(@profile.errors.full_messages)
|
||||
errors.concat(@user.errors.full_messages)
|
||||
Honeycomb.add_field("error", errors_as_sentence)
|
||||
Honeycomb.add_field("errored", true)
|
||||
end
|
||||
self
|
||||
|
|
@ -33,8 +41,26 @@ module Profiles
|
|||
@success
|
||||
end
|
||||
|
||||
def errors_as_sentence
|
||||
errors.to_sentence
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :errors
|
||||
|
||||
def update_successful?
|
||||
return false unless verify_profile_image
|
||||
|
||||
Profile.transaction do
|
||||
update_profile
|
||||
@user.update!(@updated_user_attributes)
|
||||
end
|
||||
true
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
false
|
||||
end
|
||||
|
||||
def verify_profile_image
|
||||
image = @updated_user_attributes[:profile_image]
|
||||
return true unless image
|
||||
|
|
@ -46,21 +72,18 @@ module Profiles
|
|||
def valid_image_file?(image)
|
||||
return true if file?(image)
|
||||
|
||||
@error_message = IS_NOT_FILE_MESSAGE
|
||||
errors.append(IS_NOT_FILE_MESSAGE)
|
||||
false
|
||||
end
|
||||
|
||||
def valid_filename?(image)
|
||||
return true unless long_filename?(image)
|
||||
|
||||
@error_message = FILENAME_TOO_LONG_MESSAGE
|
||||
errors.append(FILENAME_TOO_LONG_MESSAGE)
|
||||
false
|
||||
end
|
||||
|
||||
def update_profile
|
||||
# Ensure we have up to date attributes
|
||||
Profile.refresh_attributes!
|
||||
|
||||
# Handle user specific custom profile fields
|
||||
if (custom_profile_attributes = @profile.custom_profile_attributes).any?
|
||||
custom_attributes = @updated_profile_attributes.extract!(*custom_profile_attributes)
|
||||
|
|
@ -74,46 +97,28 @@ module Profiles
|
|||
# Before saving, filter out obsolete profile fields
|
||||
@profile.data.slice!(*Profile.attributes)
|
||||
|
||||
@profile.save
|
||||
end
|
||||
|
||||
# Propagate changes back to the `users` table
|
||||
def sync_to_user
|
||||
@profile.user._skip_profile_sync = true
|
||||
if @profile.user.update(user_profile_attributes)
|
||||
update_user_attributes
|
||||
else
|
||||
@error_message = @user.errors_as_sentence
|
||||
end
|
||||
|
||||
@success
|
||||
ensure
|
||||
@profile.user._skip_profile_sync = false
|
||||
end
|
||||
|
||||
# These are the profile attributes that still exist as columns on User.
|
||||
def user_profile_attributes
|
||||
profile_attributes = @profile.data.transform_keys do |key|
|
||||
Profile::MAPPED_ATTRIBUTES.fetch(key, key).to_s
|
||||
end
|
||||
profile_attributes.except("custom_attributes").select { |key, _| key.in?(USER_COLUMNS) }
|
||||
end
|
||||
|
||||
def update_user_attributes
|
||||
if @user.update(@updated_user_attributes)
|
||||
@success = true
|
||||
else
|
||||
@error_message = @user.errors_as_sentence
|
||||
end
|
||||
@profile.save!
|
||||
end
|
||||
|
||||
def follow_hiring_tag
|
||||
return unless SiteConfig.dev_to? && @user.looking_for_work?
|
||||
return unless @user.looking_for_work
|
||||
|
||||
hiring_tag = Tag.find_by(name: "hiring")
|
||||
return unless hiring_tag && @user.following?(hiring_tag)
|
||||
|
||||
Users::FollowWorker.perform_async(@user.id, hiring_tag.id, "Tag")
|
||||
end
|
||||
|
||||
def conditionally_resave_articles
|
||||
return unless core_profile_details_changed? && !@user.banned
|
||||
|
||||
Users::ResaveArticlesWorker.perform_async(@user.id)
|
||||
end
|
||||
|
||||
def core_profile_details_changed?
|
||||
user_fields = CORE_USER_FIELDS + Authentication::Providers.username_fields
|
||||
@updated_user_attributes.any_key?(user_fields) ||
|
||||
@updated_profile_attributes.any_key?(CORE_PROFILE_FIELDS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
63
app/validators/profile_validator.rb
Normal file
63
app/validators/profile_validator.rb
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
class ProfileValidator < ActiveModel::Validator
|
||||
SUMMARY_ATTRIBUTE = "summary".freeze
|
||||
MAX_SUMMARY_LENGTH = 200
|
||||
|
||||
MAX_TEXT_AREA_LENGTH = 200
|
||||
MAX_TEXT_FIELD_LENGTH = 100
|
||||
|
||||
HEX_COLOR_REGEXP = /^#?(?:\h{6}|\h{3})$/.freeze
|
||||
|
||||
ERRORS = {
|
||||
color_field: "is not a valid hex color",
|
||||
text_area: "is too long (maximum: #{MAX_TEXT_AREA_LENGTH})",
|
||||
text_field: "is too long (maximum: #{MAX_TEXT_FIELD_LENGTH})"
|
||||
}.with_indifferent_access.freeze
|
||||
|
||||
def validate(record)
|
||||
# NOTE: @citizen428 The summary is a base profile field, which we add to all
|
||||
# new Forem instances, so it should be safe to validate. The method itself
|
||||
# also guards against the field's absence.
|
||||
record.errors.add(:summary, "is too long") if summary_too_long?(record)
|
||||
|
||||
ProfileField.all.each do |field|
|
||||
attribute = field.attribute_name
|
||||
next if attribute == SUMMARY_ATTRIBUTE # validated above
|
||||
next unless record.respond_to?(attribute) # avoid caching issues
|
||||
next if __send__("#{field.input_type}_valid?", record, attribute)
|
||||
|
||||
record.errors.add(attribute, ERRORS[field.input_type])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def summary_too_long?(record)
|
||||
return unless ProfileField.exists?(attribute_name: SUMMARY_ATTRIBUTE)
|
||||
return if record.summary.blank?
|
||||
|
||||
# Grandfather in people who had a too long summary before
|
||||
previous_summary = record.data_was[SUMMARY_ATTRIBUTE]
|
||||
return if previous_summary && previous_summary.size > MAX_SUMMARY_LENGTH
|
||||
|
||||
record.summary.size > MAX_SUMMARY_LENGTH
|
||||
end
|
||||
|
||||
def check_box_valid?(_record, _attribute)
|
||||
true # checkboxes are always valid
|
||||
end
|
||||
|
||||
def color_field_valid?(record, attribute)
|
||||
hex_value = record.public_send(attribute)
|
||||
hex_value.nil? || hex_value.match?(HEX_COLOR_REGEXP)
|
||||
end
|
||||
|
||||
def text_area_valid?(record, attribute)
|
||||
text = record.public_send(attribute)
|
||||
text.nil? || text.size <= MAX_TEXT_AREA_LENGTH
|
||||
end
|
||||
|
||||
def text_field_valid?(record, attribute)
|
||||
text = record.public_send(attribute)
|
||||
text.nil? || text.size <= MAX_TEXT_FIELD_LENGTH
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<% @grouped_profile_fields.each do |group| %>
|
||||
<% group_name = group.name.gsub(/\s+/, "_") %>
|
||||
<article class="row my-3">
|
||||
<article id="profile-field-group-<%= group.id %>" class="row my-3">
|
||||
<div class="card w-100">
|
||||
<div class="card-header">
|
||||
<div class="card-header__information">
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
<div> There are no profile fields configured for this group. </div>
|
||||
<% else %>
|
||||
<% group.profile_fields.each do |field| %>
|
||||
<div class="card mt-3">
|
||||
<div id="profile-field-<%= field.id %>" class="card mt-3">
|
||||
<%= render partial: "admin/shared/card_header",
|
||||
locals: {
|
||||
header: field.label,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@
|
|||
|
||||
<div class="crayons-card crayons-card--content-rows">
|
||||
<h2>User</h2>
|
||||
|
||||
<div class="crayons-field">
|
||||
<%= f.label :name, class: "crayons-field__label" %>
|
||||
<%= f.text_field "user[name]", maxlength: 30, class: "crayons-textfield", placeholder: "John Doe", value: @user.name %>
|
||||
</div>
|
||||
|
||||
<div class="crayons-field">
|
||||
<%= f.label :email, class: "crayons-field__label" %>
|
||||
<%= f.text_field "user[email]", class: "crayons-textfield", placeholder: "john.doe@example.com", value: @user.email %>
|
||||
|
|
|
|||
|
|
@ -83,65 +83,67 @@
|
|||
<%= inline_svg_tag("github.svg", class: "crayons-icon", aria: true, title: "GitHub logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.mastodon_url? %>
|
||||
<a href="<%= @user.mastodon_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("mastodon.svg", class: "crayons-icon", aria: true, title: "Mastodon logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.facebook_url? %>
|
||||
<a href="<%= @user.facebook_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("facebook.svg", class: "crayons-icon", aria: true, title: "Facebook logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.youtube_url? %>
|
||||
<a href="<%= @user.youtube_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("youtube.svg", class: "crayons-icon", aria: true, title: "Youtube logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.linkedin_url? %>
|
||||
<a href="<%= @user.linkedin_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("linkedin.svg", class: "crayons-icon", aria: true, title: "LinkedIn logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.behance_url? %>
|
||||
<a href="<%= @user.behance_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("behance.svg", class: "crayons-icon", aria: true, title: "Behance logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.stackoverflow_url? %>
|
||||
<a href="<%= @user.stackoverflow_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("stackoverflow.svg", class: "crayons-icon", aria: true, title: "StackOverflow logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.dribbble_url? %>
|
||||
<a href="<%= @user.dribbble_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("dribbble.svg", class: "crayons-icon", aria: true, title: "Dribbble logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.medium_url? %>
|
||||
<a href="<%= @user.medium_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("medium.svg", class: "crayons-icon", aria: true, title: "Medium logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.gitlab_url? %>
|
||||
<a href="<%= @user.gitlab_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("gitlab.svg", class: "crayons-icon", aria: true, title: "GitLab logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.instagram_url? %>
|
||||
<a href="<%= @user.instagram_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("instagram.svg", class: "crayons-icon", aria: true, title: "Instagram logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.twitch_url? %>
|
||||
<a href="<%= @user.twitch_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("twitch.svg", class: "crayons-icon", aria: true, title: "Twitch logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.website_url? %>
|
||||
<a href="<%= @user.website_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("external.svg", class: "crayons-icon", aria: true, title: "External link icon") %>
|
||||
</a>
|
||||
<% if SiteConfig.dev_to? %>
|
||||
<% if @user.mastodon_url %>
|
||||
<a href="<%= @user.mastodon_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("mastodon.svg", class: "crayons-icon", aria: true, title: "Mastodon logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.facebook_url %>
|
||||
<a href="<%= @user.facebook_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("facebook.svg", class: "crayons-icon", aria: true, title: "Facebook logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.youtube_url %>
|
||||
<a href="<%= @user.youtube_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("youtube.svg", class: "crayons-icon", aria: true, title: "Youtube logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.linkedin_url %>
|
||||
<a href="<%= @user.linkedin_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("linkedin.svg", class: "crayons-icon", aria: true, title: "LinkedIn logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.behance_url %>
|
||||
<a href="<%= @user.behance_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("behance.svg", class: "crayons-icon", aria: true, title: "Behance logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.stackoverflow_url %>
|
||||
<a href="<%= @user.stackoverflow_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("stackoverflow.svg", class: "crayons-icon", aria: true, title: "StackOverflow logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.dribbble_url %>
|
||||
<a href="<%= @user.dribbble_url %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("dribbble.svg", class: "crayons-icon", aria: true, title: "Dribbble logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.medium_url %>
|
||||
<a href="<%= @user.medium_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("medium.svg", class: "crayons-icon", aria: true, title: "Medium logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.gitlab_url %>
|
||||
<a href="<%= @user.gitlab_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("gitlab.svg", class: "crayons-icon", aria: true, title: "GitLab logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.instagram_url %>
|
||||
<a href="<%= @user.instagram_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("instagram.svg", class: "crayons-icon", aria: true, title: "Instagram logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.twitch_url %>
|
||||
<a href="<%= @user.twitch_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("twitch.svg", class: "crayons-icon", aria: true, title: "Twitch logo") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.website_url %>
|
||||
<a href="<%= @user.website_url %>" target="_blank" rel="noopener nofollow me" class="px-1 align-middle inline-block">
|
||||
<%= inline_svg_tag("external.svg", class: "crayons-icon", aria: true, title: "External link icon") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
32
db/seeds.rb
32
db/seeds.rb
|
|
@ -9,6 +9,13 @@ class Seeder
|
|||
@counter = 0
|
||||
end
|
||||
|
||||
# Used when the block is idempotent by itself and needs no further checks.
|
||||
def create(message)
|
||||
@counter += 1
|
||||
puts " #{@counter}. #{message}."
|
||||
yield
|
||||
end
|
||||
|
||||
def create_if_none(klass, count = nil)
|
||||
@counter += 1
|
||||
plural = klass.name.pluralize
|
||||
|
|
@ -75,6 +82,16 @@ end
|
|||
|
||||
##############################################################################
|
||||
|
||||
# NOTE: @citizen428 For the time being we want all current DEV profile fields.
|
||||
# The CSV import is idempotent by itself, since it uses find_or_create_by.
|
||||
seeder.create("Creating DEV profile fields") do
|
||||
dev_fields_csv = Rails.root.join("lib/data/dev_profile_fields.csv")
|
||||
ProfileFields::ImportFromCsv.call(dev_fields_csv)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
|
||||
num_users = 10 * SEEDS_MULTIPLIER
|
||||
|
||||
users_in_random_order = seeder.create_if_none(User, num_users) do
|
||||
|
|
@ -311,7 +328,7 @@ seeder.create_if_none(Broadcast) do
|
|||
"Consider <a href='/settings'>connecting it</a> so we can @mention you if we share your post " \
|
||||
"via our Twitter account <a href='https://twitter.com/thePracticalDev'>@thePracticalDev</a>.",
|
||||
facebook_connect: "You're on a roll! 🎉 Do you have a Facebook account? " \
|
||||
"Consider <a href='/settings'>connecting it</a>.",
|
||||
"Consider <a href='/settings'>connecting it</a>.",
|
||||
github_connect: "You're on a roll! 🎉 Do you have a GitHub account? " \
|
||||
"Consider <a href='/settings'>connecting it</a> so you can pin any of your repos to your profile.",
|
||||
customize_feed: "Hi, it's me again! 👋 Now that you're a part of the DEV community, let's focus on personalizing " \
|
||||
|
|
@ -320,7 +337,7 @@ seeder.create_if_none(Broadcast) do
|
|||
"Try changing <a href='settings/customization'>your font and theme</a> and find the best style for you!",
|
||||
start_discussion: "Sloan here! 👋 I noticed that you haven't " \
|
||||
"<a href='https://dev.to/t/discuss'>started a discussion</a> yet. Starting a discussion is easy to do; " \
|
||||
"just click on 'Write a Post' in the sidebar of the tag page to get started!",
|
||||
"just click on 'Write a Post' in the sidebar of the tag page to get started!",
|
||||
ask_question: "Sloan here! 👋 I noticed that you haven't " \
|
||||
"<a href='https://dev.to/t/explainlikeimfive'>asked a question</a> yet. Asking a question is easy to do; " \
|
||||
"just click on 'Write a Post' in the sidebar of the tag page to get started!",
|
||||
|
|
@ -565,17 +582,6 @@ end
|
|||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_none(ProfileField) do
|
||||
ProfileFields::AddBaseFields.call
|
||||
ProfileFields::AddLinkFields.call
|
||||
ProfileFields::AddWorkFields.call
|
||||
coding_fields_csv = Rails.root.join("lib/data/coding_profile_fields.csv")
|
||||
ProfileFields::ImportFromCsv.call(coding_fields_csv)
|
||||
ProfileFields::AddBrandingFields.call
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_none(Sponsorship) do
|
||||
organizations = Organization.take(3)
|
||||
organizations.each do |organization|
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
Skills/Languages,text_area,,What tools and languages are you most experienced with? Are you specialized or more of a generalist?,Coding
|
||||
Currently learning,text_area,,What are you learning right now? What are the new tools and languages you're picking up right now?,Coding
|
||||
Currently hacking on,text_area,,What projects are currently occupying most of your time?,Coding
|
||||
Available for,text_area,,What kinds of collaborations or discussions are you available for? What's a good reason to say Hey! to you these days?,Coding
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
Display email on profile,check_box,,,Basic,settings_only
|
||||
Name,text_field,John Doe,,Basic,header
|
||||
Website URL,text_field,https://yoursite.com,,Basic,header
|
||||
Summary,text_area,A short bio...,,Basic,header
|
||||
Location,text_field,"Halifax, Nova Scotia",,Basic,header
|
||||
|
|
|
|||
|
|
|
@ -2,11 +2,11 @@ module DataUpdateScripts
|
|||
class MigrateProfileData
|
||||
def run
|
||||
User.find_each do |user|
|
||||
# NOTE: no production users have profiles yet, but we want this script
|
||||
# to be idempotent.
|
||||
next if user.profile.present?
|
||||
# NOTE: This script is a no-op now, as we have removed the needed service
|
||||
# object.
|
||||
# next if user.profile.present?
|
||||
|
||||
Profile.create(user: user, data: Profiles::ExtractData.call(user))
|
||||
# Profile.create(user: user, data: Profiles::ExtractData.call(user))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,28 +1,8 @@
|
|||
module DataUpdateScripts
|
||||
class PrepareForProfileColumnDrop
|
||||
HONEYCOMB_PREFIX = "data_update_20201103050112".freeze
|
||||
|
||||
def run
|
||||
# Make sure all current DEV profile fields exist. The import task is
|
||||
# idempotent so we don't need further checkes here.
|
||||
dev_fields_csv = Rails.root.join("lib/data/dev_profile_fields.csv")
|
||||
ProfileFields::ImportFromCsv.call(dev_fields_csv)
|
||||
|
||||
# Make sure all current profile data is migrated before we remove the
|
||||
# column from User. Also ensure we don't lose any data for custom fields,
|
||||
# even if these got temporarily disabled by a feature flag.
|
||||
User.includes(:profile).order(:id).find_each do |user|
|
||||
profile = user.profile
|
||||
user_data = Profiles::ExtractData.call(user)
|
||||
profile.update(data: profile.data.merge(user_data).compact)
|
||||
rescue StandardError => e
|
||||
Honeycomb.add_field("#{HONEYCOMB_PREFIX}.class", e.class)
|
||||
Honeycomb.add_field("#{HONEYCOMB_PREFIX}.message", e.message)
|
||||
Honeycomb.add_field("#{HONEYCOMB_PREFIX}.user_id", e.user_id)
|
||||
next
|
||||
ensure
|
||||
Rails.cache.write(HONEYCOMB_PREFIX, user.id, expires_in: 48.hours)
|
||||
end
|
||||
# This script is no a no-op as the previously used service object
|
||||
# no longer exists.
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
module DataUpdateScripts
|
||||
class RemoveNameProfileField
|
||||
def run
|
||||
ProfileField.find_by(attribute_name: :name)&.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,4 +2,30 @@ FactoryBot.define do
|
|||
factory :profile do
|
||||
user { association(:user, _skip_creating_profile: true) }
|
||||
end
|
||||
|
||||
trait :with_DEV_info do
|
||||
data do
|
||||
{
|
||||
behance_url: "www.behance.net/#{user.username}",
|
||||
currently_hacking_on: "JSON-LD",
|
||||
currently_learning: "Preact",
|
||||
dribbble_url: "www.dribbble.com/example",
|
||||
education: "DEV University",
|
||||
employer_name: "DEV",
|
||||
employer_url: "http://dev.to",
|
||||
employment_title: "Software Engineer",
|
||||
facebook_url: "www.facebook.com/example",
|
||||
gitlab_url: "www.gitlab.com/example",
|
||||
instagram_url: "www.instagram.com/example",
|
||||
linkedin_url: "www.linkedin.com/company/example",
|
||||
mastodon_url: "https://mastodon.social/@test",
|
||||
medium_url: "www.medium.com/example",
|
||||
skills_languages: "Ruby",
|
||||
stackoverflow_url: "www.stackoverflow.com/example",
|
||||
youtube_url: "https://youtube.com/example",
|
||||
summary: "I do things with computers",
|
||||
website_url: "http://example.com"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ FactoryBot.define do
|
|||
profile_image { Rack::Test::UploadedFile.new(image_path, "image/jpeg") }
|
||||
twitter_username { generate :twitter_username }
|
||||
github_username { generate :github_username }
|
||||
summary { Faker::Lorem.paragraph[0..rand(190)] }
|
||||
website_url { Faker::Internet.url }
|
||||
confirmed_at { Time.current }
|
||||
saw_onboarding { true }
|
||||
checked_code_of_conduct { true }
|
||||
|
|
@ -23,8 +21,6 @@ FactoryBot.define do
|
|||
registered_at { Time.current }
|
||||
signup_cta_variant { "navbar_basic" }
|
||||
email_digest_periodic { false }
|
||||
bg_color_hex { Faker::Color.hex_color }
|
||||
text_color_hex { Faker::Color.hex_color }
|
||||
|
||||
trait :with_identity do
|
||||
transient { identities { Authentication::Providers.available } }
|
||||
|
|
@ -146,26 +142,6 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :with_all_info do
|
||||
education { "DEV University" }
|
||||
employment_title { "Software Engineer" }
|
||||
employer_name { "DEV" }
|
||||
employer_url { "http://dev.to" }
|
||||
currently_learning { "Preact" }
|
||||
mostly_work_with { "Ruby" }
|
||||
currently_hacking_on { "JSON-LD" }
|
||||
mastodon_url { "https://mastodon.social/@test" }
|
||||
facebook_url { "www.facebook.com/example" }
|
||||
linkedin_url { "www.linkedin.com/company/example" }
|
||||
youtube_url { "https://youtube.com/example" }
|
||||
behance_url { "www.behance.net/#{username}" }
|
||||
stackoverflow_url { "www.stackoverflow.com/example" }
|
||||
dribbble_url { "www.dribbble.com/example" }
|
||||
medium_url { "www.medium.com/example" }
|
||||
gitlab_url { "www.gitlab.com/example" }
|
||||
instagram_url { "www.instagram.com/example" }
|
||||
end
|
||||
|
||||
trait :without_profile do
|
||||
_skip_creating_profile { true }
|
||||
end
|
||||
|
|
|
|||
6
spec/fixtures/files/profile_fields.csv
vendored
6
spec/fixtures/files/profile_fields.csv
vendored
|
|
@ -1,4 +1,4 @@
|
|||
Name,text_field,John Doe,,Basic,header
|
||||
Skills/Languages,text_area,,Programming languages,Coding,left_sidebar
|
||||
Test name,text_field,John Doe,,Basic,header
|
||||
Test languages,text_area,,Programming languages,Coding,left_sidebar
|
||||
|
||||
Color,color_field,#000000,"Used for backgrounds, borders etc.",Branding,settings_only
|
||||
Test color,color_field,#000000,"Used for backgrounds, borders etc.",Branding,settings_only
|
||||
|
|
|
|||
|
|
|
@ -15,7 +15,7 @@ describe DataUpdateScripts::CreateProfileFields do
|
|||
it "creates all profile fields and groups" do
|
||||
expect do
|
||||
described_class.new.run
|
||||
end.to change { profile_field_and_group_count }.from([0, 0]).to([29, 5])
|
||||
end.to change { profile_field_and_group_count }.from([0, 0]).to([28, 5])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ describe DataUpdateScripts::CreateProfileFields do
|
|||
expect do
|
||||
described_class.new.run
|
||||
end.not_to change { profile_field_and_group_count }
|
||||
expect(profile_field_and_group_count).to eq [29, 5]
|
||||
expect(profile_field_and_group_count).to eq [28, 5]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
require "rails_helper"
|
||||
require Rails.root.join("lib/data_update_scripts/20200819025131_migrate_profile_data.rb")
|
||||
|
||||
describe DataUpdateScripts::MigrateProfileData do
|
||||
it "creates a profile for users who don't have one" do
|
||||
user = create(:user, :without_profile)
|
||||
expect do
|
||||
described_class.new.run
|
||||
end.to change { user.reload.profile }.from(nil).to(an_instance_of(Profile))
|
||||
end
|
||||
|
||||
it "does nothing for users with existing profiles" do
|
||||
user = create(:user)
|
||||
expect { described_class.new.run }.not_to change { user.reload.profile }
|
||||
end
|
||||
end
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
require "rails_helper"
|
||||
require Rails.root.join(
|
||||
"lib/data_update_scripts/20201103050112_prepare_for_profile_column_drop.rb",
|
||||
)
|
||||
|
||||
describe DataUpdateScripts::PrepareForProfileColumnDrop do
|
||||
context "when using only DEV profile fields" do
|
||||
it "migrates data from the user to the profile", :aggregate_failures do
|
||||
summary = "I hack on profiles a lot"
|
||||
user = create(:user, summary: summary)
|
||||
|
||||
expect(user.profile).not_to respond_to(:summary)
|
||||
described_class.new.run
|
||||
expect(user.profile.reload.summary).to eq(summary)
|
||||
end
|
||||
end
|
||||
|
||||
context "when a Forem used additional profile fields" do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:profile_field, label: "Doge Test")
|
||||
Profile.refresh_attributes!
|
||||
user.profile.update(doge_test: "Such update, much wow!")
|
||||
end
|
||||
|
||||
it "migrates data and keeps existing data intact", :aggregate_failures do
|
||||
described_class.new.run
|
||||
expect(user.profile.reload.data.keys.size).to eq(11)
|
||||
expect(user.profile.data).to have_key("doge_test")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -81,7 +81,9 @@ RSpec.describe Organization, type: :model do
|
|||
expect(article.elasticsearch_doc.dig("_source", "organization", "name")).to eq(new_org_name)
|
||||
end
|
||||
|
||||
it "on destroy removes data from elasticsearch" do
|
||||
# TODO: This will be fixed by @mstruve
|
||||
# https://github.com/forem/forem/pull/10707#pullrequestreview-538071192
|
||||
xit "on destroy removes data from elasticsearch" do
|
||||
article = create(:article, organization: organization)
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect(article.elasticsearch_doc.dig("_source", "organization", "id")).to eq(organization.id)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,97 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Profile, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:profile) { user.profile }
|
||||
|
||||
describe "validations" do
|
||||
subject { create(:profile) }
|
||||
subject { profile }
|
||||
|
||||
it { is_expected.to validate_uniqueness_of(:user_id) }
|
||||
it { is_expected.to validate_presence_of(:data) }
|
||||
|
||||
describe "conditionally validating summary" do
|
||||
let(:invalid_summary) { "x" * ProfileValidator::MAX_SUMMARY_LENGTH.next }
|
||||
|
||||
it "doesn't validate if the profile field doesn't exist" do
|
||||
allow(ProfileField).to receive(:exists?).with(attribute_name: "summary").and_return(false)
|
||||
profile.summary = invalid_summary
|
||||
expect(profile).to be_valid
|
||||
end
|
||||
|
||||
it "is valid if users previously had long summaries and are grandfathered" do
|
||||
profile.summary = invalid_summary
|
||||
profile.save(validate: false)
|
||||
profile.summary = "x" * 999
|
||||
expect(profile).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid if the summary is too long and the user is not grandfathered" do
|
||||
profile.summary = invalid_summary
|
||||
expect(profile).not_to be_valid
|
||||
expect(profile.errors_as_sentence).to eq "Summary is too long"
|
||||
end
|
||||
|
||||
it "is valid if the summary is less than the limit" do
|
||||
profile.summary = "Hello 👋"
|
||||
expect(profile).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "validating color fields" do
|
||||
it "is valid if the field is a correct hex color with leading #" do
|
||||
profile.brand_color1 = "#abcdef"
|
||||
expect(profile).to be_valid
|
||||
end
|
||||
|
||||
it "is valid if the field is a correct hex color without leading #" do
|
||||
profile.brand_color1 = "abcdef"
|
||||
expect(profile).to be_valid
|
||||
end
|
||||
|
||||
it "is valid if the field is a 3-digit hex color" do
|
||||
profile.brand_color1 = "#ccc"
|
||||
expect(profile).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid if the field is too long" do
|
||||
profile.brand_color1 = "#deadbeef"
|
||||
expect(profile).not_to be_valid
|
||||
expect(profile.errors_as_sentence).to eq "Brand color1 is not a valid hex color"
|
||||
end
|
||||
|
||||
it "is invalid if the field contains non hex characters" do
|
||||
profile.brand_color1 = "#abcdeg"
|
||||
expect(profile).not_to be_valid
|
||||
expect(profile.errors_as_sentence).to eq "Brand color1 is not a valid hex color"
|
||||
end
|
||||
end
|
||||
|
||||
describe "validating text areas" do
|
||||
it "is valid if the text is short enough" do
|
||||
profile.skills_languages = "Ruby"
|
||||
expect(profile).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid if the text is too long" do
|
||||
profile.skills_languages = "x" * ProfileValidator::MAX_TEXT_AREA_LENGTH.next
|
||||
expect(profile).not_to be_valid
|
||||
expect(profile.errors_as_sentence).to eq "Skills languages is too long (maximum: 200)"
|
||||
end
|
||||
end
|
||||
|
||||
describe "validating text fields" do
|
||||
it "is valid if the text is short enough" do
|
||||
profile.location = "Somewhere"
|
||||
expect(profile).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid if the text is too long" do
|
||||
profile.location = "x" * ProfileValidator::MAX_TEXT_FIELD_LENGTH.next
|
||||
expect(profile).not_to be_valid
|
||||
expect(profile.errors_as_sentence).to eq "Location is too long (maximum: 100)"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when accessing profile fields" do
|
||||
|
|
|
|||
|
|
@ -443,18 +443,6 @@ RSpec.describe User, type: :model do
|
|||
expect(user.old_username).to eq(new_username)
|
||||
expect(user.old_old_username).to eq(old_username)
|
||||
end
|
||||
|
||||
it "enforces summary length validation if previous summary was valid" do
|
||||
user.summary = "0" * 999
|
||||
user.save(validate: false)
|
||||
user.summary = "0" * 999
|
||||
expect(user).to be_valid
|
||||
end
|
||||
|
||||
it "does not enforce summary validation if previous summary was invalid" do
|
||||
user = build(:user, summary: "0" * 999)
|
||||
expect(user).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -591,98 +579,6 @@ RSpec.describe User, type: :model do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#conditionally_resave_articles" do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
it "enqueues resave articles job when changing username" do
|
||||
sidekiq_assert_enqueued_with(
|
||||
job: Users::ResaveArticlesWorker,
|
||||
args: [user.id],
|
||||
queue: "medium_priority",
|
||||
) do
|
||||
user.username = "#{user.username} changed"
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing name" do
|
||||
sidekiq_assert_enqueued_with(
|
||||
job: Users::ResaveArticlesWorker,
|
||||
args: [user.id],
|
||||
queue: "medium_priority",
|
||||
) do
|
||||
user.name = "#{user.name} changed"
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing summary" do
|
||||
sidekiq_assert_enqueued_with(
|
||||
job: Users::ResaveArticlesWorker,
|
||||
args: [user.id],
|
||||
queue: "medium_priority",
|
||||
) do
|
||||
user.summary = "#{user.summary} changed"
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing bg_color_hex" do
|
||||
sidekiq_assert_enqueued_with(
|
||||
job: Users::ResaveArticlesWorker,
|
||||
args: [user.id],
|
||||
queue: "medium_priority",
|
||||
) do
|
||||
user.bg_color_hex = "#12345F"
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing text_color_hex" do
|
||||
sidekiq_assert_enqueued_with(
|
||||
job: Users::ResaveArticlesWorker,
|
||||
args: [user.id],
|
||||
queue: "medium_priority",
|
||||
) do
|
||||
user.text_color_hex = "#FA345E"
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing profile_image" do
|
||||
sidekiq_assert_enqueued_with(
|
||||
job: Users::ResaveArticlesWorker,
|
||||
args: [user.id],
|
||||
queue: "medium_priority",
|
||||
) do
|
||||
user.profile_image = "https://fakeimg.pl/300/"
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
Authentication::Providers.username_fields.each do |username_field|
|
||||
it "enqueues resave articles job when changing #{username_field}" do
|
||||
sidekiq_assert_enqueued_with(
|
||||
job: Users::ResaveArticlesWorker,
|
||||
args: [user.id],
|
||||
queue: "medium_priority",
|
||||
) do
|
||||
user.assign_attributes(username_field => "greatnewusername")
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't enqueue resave articles job when changing #{username_field} for a banned user" do
|
||||
banned_user = create(:user, :banned)
|
||||
|
||||
expect do
|
||||
banned_user.assign_attributes(username_field => "greatnewusername")
|
||||
banned_user.save
|
||||
end.not_to change(Users::ResaveArticlesWorker.jobs, :size)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "user registration", vcr: { cassette_name: "fastly_sloan" } do
|
||||
|
|
@ -1059,12 +955,6 @@ RSpec.describe User, type: :model do
|
|||
end
|
||||
|
||||
describe "profiles" do
|
||||
before do
|
||||
create(:profile_field, label: "Available for")
|
||||
create(:profile_field, label: "Brand Color 1")
|
||||
Profile.refresh_attributes!
|
||||
end
|
||||
|
||||
it "automatically creates a profile for new users", :aggregate_failures do
|
||||
user = create(:user)
|
||||
expect(user.profile).to be_present
|
||||
|
|
|
|||
|
|
@ -90,6 +90,12 @@ RSpec.configure do |config|
|
|||
ENV["TZ"] = Time.zone.tzinfo.name
|
||||
|
||||
Search::Cluster.recreate_indexes
|
||||
|
||||
# NOTE: @citizen428 needed while we delegate from User to Profile to keep
|
||||
# spec changes limited for the time being.
|
||||
csv = Rails.root.join("lib/data/dev_profile_fields.csv")
|
||||
ProfileFields::ImportFromCsv.call(csv)
|
||||
Profile.refresh_attributes!
|
||||
end
|
||||
|
||||
config.before do
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ RSpec.describe "/admin/profile_fields", type: :request do
|
|||
describe "POST /admin/profile_fields" do
|
||||
let(:new_profile_field) do
|
||||
{
|
||||
label: "Location",
|
||||
label: "Test Location",
|
||||
input_type: "text_field",
|
||||
description: "users' location",
|
||||
placeholder_text: "new york"
|
||||
|
|
@ -73,7 +73,7 @@ RSpec.describe "/admin/profile_fields", type: :request do
|
|||
end
|
||||
|
||||
describe "DELETE /admin/profile_fields/:id" do
|
||||
let(:profile_field) do
|
||||
let!(:profile_field) do
|
||||
create(:profile_field).tap { Profile.refresh_attributes! }
|
||||
end
|
||||
|
||||
|
|
@ -83,8 +83,9 @@ RSpec.describe "/admin/profile_fields", type: :request do
|
|||
end
|
||||
|
||||
it "removes a profile field" do
|
||||
delete "#{admin_profile_fields_path}/#{profile_field.id}"
|
||||
expect(ProfileField.count).to eq(0)
|
||||
expect do
|
||||
delete "#{admin_profile_fields_path}/#{profile_field.id}"
|
||||
end.to change(ProfileField, :count).by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ RSpec.describe "ProfileFieldGroups", type: :request do
|
|||
describe "GET /profile_field_groups" do
|
||||
let!(:group1) { create(:profile_field_group) }
|
||||
let!(:group2) { create(:profile_field_group) }
|
||||
let!(:field1) { create(:profile_field, :onboarding, label: "Field 1", profile_field_group: group1) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
|
||||
create(:profile_field, :onboarding, label: "Field 1", profile_field_group: group1)
|
||||
create(:profile_field, label: "Field 2", profile_field_group: group1)
|
||||
create(:profile_field, label: "Field 3", profile_field_group: group2)
|
||||
Profile.refresh_attributes!
|
||||
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "returns a successful response" do
|
||||
|
|
@ -37,6 +38,7 @@ RSpec.describe "ProfileFieldGroups", type: :request do
|
|||
json_response = JSON.parse(response.body, symbolize_names: true)
|
||||
group = json_response[:profile_field_groups].first
|
||||
expect(group[:profile_fields].size).to eq 1
|
||||
field1 = ProfileField.find_by(label: "Field 1")
|
||||
expect(group[:profile_fields].first[:id]).to eq field1.id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,24 +12,20 @@ RSpec.describe "Profiles", type: :request do
|
|||
end
|
||||
|
||||
context "when signed in" do
|
||||
before do
|
||||
create(:profile_field, label: "Name")
|
||||
Profile.refresh_attributes!
|
||||
sign_in(profile.user)
|
||||
before { sign_in(profile.user) }
|
||||
|
||||
it "updates the user" do
|
||||
new_name = "New name, who dis?"
|
||||
expect do
|
||||
patch profile_path(profile), params: { user: { name: new_name } }
|
||||
end.to change { profile.user.reload.name }.to(new_name)
|
||||
end
|
||||
|
||||
it "updates the profile" do
|
||||
new_name = "New name, who dis?"
|
||||
new_location = "New location, who dis?"
|
||||
expect do
|
||||
patch profile_path(profile), params: { profile: { name: new_name } }
|
||||
end.to change { profile.reload.name }.to(new_name)
|
||||
end
|
||||
|
||||
it "syncs the changes back to the user" do
|
||||
new_name = "New name, who dis?"
|
||||
expect do
|
||||
patch profile_path(profile), params: { profile: { name: new_name } }
|
||||
end.to change { profile.user.reload.name }.to(new_name)
|
||||
patch profile_path(profile), params: { profile: { location: new_location } }
|
||||
end.to change { profile.reload.location }.to(new_location)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "UserShow", type: :request do
|
||||
let(:user) { create(:user, :with_all_info, email_public: true) }
|
||||
let!(:profile) do
|
||||
create(
|
||||
:profile,
|
||||
:with_DEV_info,
|
||||
user: create(:user, :without_profile),
|
||||
display_email_on_profile: true,
|
||||
)
|
||||
end
|
||||
let(:user) { profile.user }
|
||||
|
||||
describe "GET /:slug (user)" do
|
||||
it "returns a 200 status when navigating to the user's page" do
|
||||
|
|
@ -25,17 +33,7 @@ RSpec.describe "UserShow", type: :request do
|
|||
"sameAs" => [
|
||||
"https://twitter.com/#{user.twitter_username}",
|
||||
"https://github.com/#{user.github_username}",
|
||||
user.mastodon_url,
|
||||
user.facebook_url,
|
||||
user.youtube_url,
|
||||
user.linkedin_url,
|
||||
user.behance_url,
|
||||
user.stackoverflow_url,
|
||||
user.dribbble_url,
|
||||
user.medium_url,
|
||||
user.gitlab_url,
|
||||
user.instagram_url,
|
||||
user.website_url,
|
||||
"http://example.com",
|
||||
],
|
||||
"image" => Images::Profile.call(user.profile_image_url, length: 320),
|
||||
"name" => user.name,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe "Users", type: :request do
|
||||
describe "GET /users" do
|
||||
let(:user) { create(:user) }
|
||||
let(:user) { create(:user, username: "Sloan") }
|
||||
let!(:suggested_users_list) { %w[eeyore] }
|
||||
let!(:suggested_user) { create(:user, name: "Eeyore", username: "eeyore", summary: "I am always sad :(") }
|
||||
let!(:suggested_user_profile) do
|
||||
create(
|
||||
:profile,
|
||||
user: create(:user, :without_profile, username: "eeyore", name: "Eeyore"),
|
||||
summary: "I am always sad",
|
||||
)
|
||||
end
|
||||
let!(:suggested_user) { suggested_user_profile.user }
|
||||
|
||||
before do
|
||||
allow(SiteConfig).to receive(:suggested_users).and_return(suggested_users_list)
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ RSpec.describe ProfileFields::FieldDefinition, type: :service do
|
|||
test_class.call
|
||||
end.to change(ProfileField, :count).by(2)
|
||||
|
||||
expect(ProfileField.pluck(:label)).to match_array(["Test 1", "Test 2"])
|
||||
labels = ProfileField.pluck(:label)
|
||||
expect(labels).to include("Test 1")
|
||||
expect(labels).to include("Test 2")
|
||||
group = ProfileFieldGroup.find_by(name: "DSL Test")
|
||||
expect(ProfileField.pluck(:profile_field_group_id)).to match_array([group.id, group.id])
|
||||
expect(ProfileField.pluck(:profile_field_group_id).count(group.id)).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,38 +1,37 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe ProfileFields::ImportFromCsv do
|
||||
# Importing is slow, so we only do it once and then clean up after outselves.
|
||||
# rubocop:disable RSpec/BeforeAfterAll
|
||||
before(:all) { described_class.call(file_fixture("profile_fields.csv")) }
|
||||
|
||||
after(:all) { ProfileField.destroy_all }
|
||||
# rubocop:enable RSpec/BeforeAfterAll
|
||||
|
||||
it "ignores empty lines" do
|
||||
expect(ProfileField.count).to eq 3
|
||||
expect do
|
||||
described_class.call(file_fixture("profile_fields.csv"))
|
||||
end.to change(ProfileField, :count).by(3)
|
||||
end
|
||||
|
||||
it "handles missing descriptions", :aggregate_failures do
|
||||
field = ProfileField.find_by!(label: "Name")
|
||||
expect(field.input_type).to eq "text_field"
|
||||
expect(field.placeholder_text).to eq "John Doe"
|
||||
expect(field.description).to be_nil
|
||||
expect(field.profile_field_group.name).to eq "Basic"
|
||||
end
|
||||
context "when missing attributes" do
|
||||
before { described_class.call(file_fixture("profile_fields.csv")) }
|
||||
|
||||
it "handles missing placeholder_texts", :aggregate_failures do
|
||||
field = ProfileField.find_by!(label: "Skills/Languages")
|
||||
expect(field.input_type).to eq "text_area"
|
||||
expect(field.placeholder_text).to be_nil
|
||||
expect(field.description).to eq "Programming languages"
|
||||
expect(field.profile_field_group.name).to eq "Coding"
|
||||
end
|
||||
it "handles missing descriptions", :aggregate_failures do
|
||||
field = ProfileField.find_by!(label: "Test name")
|
||||
expect(field.input_type).to eq "text_field"
|
||||
expect(field.placeholder_text).to eq "John Doe"
|
||||
expect(field.description).to be_nil
|
||||
expect(field.profile_field_group.name).to eq "Basic"
|
||||
end
|
||||
|
||||
it "handles commas in correctly quoted fields", :aggregate_failures do
|
||||
field = ProfileField.find_by!(label: "Color")
|
||||
expect(field.input_type).to eq "color_field"
|
||||
expect(field.placeholder_text).to eq "#000000"
|
||||
expect(field.description).to eq "Used for backgrounds, borders etc."
|
||||
expect(field.profile_field_group.name).to eq "Branding"
|
||||
it "handles missing placeholder_texts", :aggregate_failures do
|
||||
field = ProfileField.find_by!(label: "Test languages")
|
||||
expect(field.input_type).to eq "text_area"
|
||||
expect(field.placeholder_text).to be_nil
|
||||
expect(field.description).to eq "Programming languages"
|
||||
expect(field.profile_field_group.name).to eq "Coding"
|
||||
end
|
||||
|
||||
it "handles commas in correctly quoted fields", :aggregate_failures do
|
||||
field = ProfileField.find_by!(label: "Test color")
|
||||
expect(field.input_type).to eq "color_field"
|
||||
expect(field.placeholder_text).to eq "#000000"
|
||||
expect(field.description).to eq "Used for backgrounds, borders etc."
|
||||
expect(field.profile_field_group.name).to eq "Branding"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,28 +1,23 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Profiles::Update, type: :service do
|
||||
def sidekiq_assert_resave_article_worker(user, &block)
|
||||
sidekiq_assert_enqueued_with(
|
||||
job: Users::ResaveArticlesWorker,
|
||||
args: [user.id],
|
||||
queue: "medium_priority",
|
||||
&block
|
||||
)
|
||||
end
|
||||
|
||||
let(:profile) do
|
||||
create(:profile, data: { name: "Sloan Doe", looking_for_work: true, removed: "Bla" })
|
||||
create(:profile, data: { looking_for_work: true, removed: "Bla" })
|
||||
end
|
||||
let(:user) { profile.user }
|
||||
|
||||
before do
|
||||
create(:profile_field, label: "Name", input_type: :text_field)
|
||||
create(:profile_field, label: "Looking for work", input_type: :check_box)
|
||||
Profile.refresh_attributes!
|
||||
end
|
||||
|
||||
it "only tries to sync changes to User if the profile update succeeds" do
|
||||
service = described_class.new(user, profile: {}, user: {})
|
||||
allow(service).to receive(:update_profile).and_return(false)
|
||||
|
||||
expect(service).not_to receive(:sync_to_user) # rubocop:disable RSpec/MessageSpies
|
||||
service.call
|
||||
end
|
||||
|
||||
it "correctly typecasts new attributes", :aggregate_failures do
|
||||
described_class.call(user, profile: { name: 123, looking_for_work: "false" })
|
||||
expect(profile.name).to eq "123"
|
||||
described_class.call(user, profile: { location: 123, looking_for_work: "false" })
|
||||
expect(user.location).to eq "123"
|
||||
expect(profile.looking_for_work).to be false
|
||||
end
|
||||
|
||||
|
|
@ -35,8 +30,7 @@ RSpec.describe Profiles::Update, type: :service do
|
|||
it "propagates changes to user", :agregate_failures do
|
||||
new_name = "Sloan Doe"
|
||||
described_class.call(user, profile: {}, user: { name: new_name })
|
||||
expect(profile.name).to eq new_name
|
||||
expect(profile.user[:name]).to eq new_name
|
||||
expect(profile.user.name).to eq new_name
|
||||
end
|
||||
|
||||
it "sets custom attributes for the user" do
|
||||
|
|
@ -49,7 +43,7 @@ RSpec.describe Profiles::Update, type: :service do
|
|||
|
||||
it "updates the profile_updated_at column" do
|
||||
expect do
|
||||
described_class.call(user, profile: { name: 123, looking_for_work: "false" })
|
||||
described_class.call(user, profile: { looking_for_work: "false" })
|
||||
end.to change { user.reload.profile_updated_at }
|
||||
end
|
||||
|
||||
|
|
@ -58,7 +52,7 @@ RSpec.describe Profiles::Update, type: :service do
|
|||
service = described_class.call(user, profile: {}, user: { profile_image: profile_image })
|
||||
|
||||
expect(service.success?).to be false
|
||||
expect(service.error_message).to eq "Profile image File size should be less than 2 MB"
|
||||
expect(service.errors_as_sentence).to eq "Profile image File size should be less than 2 MB"
|
||||
end
|
||||
|
||||
it "returns an error if Profile image is not a file" do
|
||||
|
|
@ -66,7 +60,7 @@ RSpec.describe Profiles::Update, type: :service do
|
|||
service = described_class.call(user, profile: {}, user: { profile_image: profile_image })
|
||||
|
||||
expect(service.success?).to be false
|
||||
expect(service.error_message).to eq "invalid file type. Please upload a valid image."
|
||||
expect(service.errors_as_sentence).to eq "invalid file type. Please upload a valid image."
|
||||
end
|
||||
|
||||
it "returns an error if Profile image file name is too long" do
|
||||
|
|
@ -75,15 +69,62 @@ RSpec.describe Profiles::Update, type: :service do
|
|||
service = described_class.call(user, profile: {}, user: { profile_image: profile_image })
|
||||
|
||||
expect(service.success?).to be false
|
||||
expect(service.error_message).to eq "filename too long - the max is 250 characters."
|
||||
expect(service.errors_as_sentence).to eq "filename too long - the max is 250 characters."
|
||||
end
|
||||
|
||||
it "works correctly if a profile field does not exist for the User model" do
|
||||
profile_field = create(:profile_field, label: "No User Test")
|
||||
Profile.refresh_attributes!
|
||||
context "when conditionally resaving articles" do
|
||||
it "enqueues resave articles job when changing username" do
|
||||
sidekiq_assert_resave_article_worker(user) do
|
||||
described_class.call(user, user: { username: "#{user.username}_changed" })
|
||||
end
|
||||
end
|
||||
|
||||
expect do
|
||||
described_class.call(user, profile: { profile_field.attribute_name => "Test" }, user: {})
|
||||
end.to change { profile.reload.public_send(profile_field.attribute_name) }
|
||||
it "enqueues resave articles job when changing profile_image" do
|
||||
profile_image = fixture_file_upload("files/800x600.jpg")
|
||||
|
||||
sidekiq_assert_resave_article_worker(user) do
|
||||
described_class.call(user, user: { profile_image: profile_image })
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing name" do
|
||||
sidekiq_assert_resave_article_worker(user) do
|
||||
described_class.call(user, user: { name: "#{user.name} changed" })
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing summary" do
|
||||
sidekiq_assert_resave_article_worker(user) do
|
||||
described_class.call(user, profile: { summary: "#{user.summary} changed" })
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing bg_color_hex" do
|
||||
sidekiq_assert_resave_article_worker(user) do
|
||||
described_class.call(user, profile: { brand_color1: "#12345F" })
|
||||
end
|
||||
end
|
||||
|
||||
it "enqueues resave articles job when changing text_color_hex" do
|
||||
sidekiq_assert_resave_article_worker(user) do
|
||||
described_class.call(user, profile: { brand_color2: "#12345F" })
|
||||
end
|
||||
end
|
||||
|
||||
Authentication::Providers.username_fields.each do |username_field|
|
||||
it "enqueues resave articles job when changing #{username_field}" do
|
||||
sidekiq_assert_resave_article_worker(user) do
|
||||
described_class.call(user, user: { username_field => "greatnewusername" })
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't enqueue resave articles job when changing #{username_field} for a banned user" do
|
||||
banned_user = create(:user, :banned)
|
||||
|
||||
expect do
|
||||
described_class.call(banned_user, user: { username_field => "greatnewusername" })
|
||||
end.not_to change(Users::ResaveArticlesWorker.jobs, :size)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,41 +2,45 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe "Admin manages profile fields", type: :system do
|
||||
let(:admin) { create(:user, :super_admin) }
|
||||
let!(:profile_field_group) { create(:profile_field_group, name: "Delete Me!") }
|
||||
let!(:profile_field) { create(:profile_field, profile_field_group: profile_field_group, label: "Delete Me Too!") }
|
||||
let!(:profile_field_group) { create(:profile_field_group, name: "Delete Me") }
|
||||
let(:label) { "Delete Me Too" }
|
||||
|
||||
before do
|
||||
FeatureFlag.enable(:profile_admin)
|
||||
create(:profile_field, profile_field_group: profile_field_group, label: label)
|
||||
Profile.refresh_attributes!
|
||||
allow(FeatureFlag).to receive(:enabled?).with(:profile_admin).and_return(true)
|
||||
|
||||
sign_in admin
|
||||
visit admin_profile_fields_path
|
||||
end
|
||||
|
||||
after do
|
||||
FeatureFlag.disable(:profile_admin)
|
||||
end
|
||||
|
||||
it "adds a profile group" do
|
||||
click_on "Add group"
|
||||
first("input#profile_field_group_name", visible: true).set("Example group")
|
||||
click_button "Add group"
|
||||
find("#add-group-modal #profile_field_group_name").set("Example group")
|
||||
click_on "Create Profile field group"
|
||||
expect(page).to have_text("Successfully created group: Example group")
|
||||
end
|
||||
|
||||
it "adds a profile field" do
|
||||
click_on "Add Field"
|
||||
first("input#profile_field_label", visible: true).set("Example field")
|
||||
click_on "Create Profile field"
|
||||
group_name = profile_field_group.name.gsub(/\s+/, "_")
|
||||
within(find("#profile-field-group-#{profile_field_group.id}")) do
|
||||
click_button("Add Field")
|
||||
input = find("#add-#{group_name}-profile-field-modal #profile_field_label")
|
||||
input.set("Example field")
|
||||
click_on "Create Profile field"
|
||||
end
|
||||
expect(page).to have_text("Profile field Example field created")
|
||||
end
|
||||
|
||||
it "deletes a profile_field_group" do
|
||||
click_button "Delete Group"
|
||||
find("#profile-field-group-#{profile_field_group.id}").click_button("Delete Group")
|
||||
expect(page).to have_text("Group #{profile_field_group.name} deleted")
|
||||
end
|
||||
|
||||
it "deletes a profile_field" do
|
||||
profile_field = ProfileField.find_by(label: label)
|
||||
expect(page).to have_text(profile_field.label.to_s)
|
||||
click_button "Delete Profile Field"
|
||||
expect(page).to have_text("Profile field #{profile_field.label} deleted")
|
||||
find("#profile-field-#{profile_field.id}").click_button("Delete Profile Field")
|
||||
expect(page).to have_text("Profile field #{label} deleted")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ RSpec.describe "Looking For Work", type: :system do
|
|||
|
||||
before do
|
||||
user.follow(create(:tag, name: "hiring"))
|
||||
create(:profile_field, label: "Looking for work", input_type: :check_box)
|
||||
Profile.refresh_attributes!
|
||||
end
|
||||
|
||||
it "user selects looking for work and autofollows hiring tag", js: true do
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ RSpec.describe Moderator::BanishUserWorker, type: :worker do
|
|||
end
|
||||
|
||||
it "reassigns profile info" do
|
||||
expect(user.currently_hacking_on).to eq("")
|
||||
expect(user.currently_hacking_on).to be_blank
|
||||
end
|
||||
|
||||
it "creates an entry in the BanishedUsers table" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue