* Add "Welcome" type to Broadcasts, allow dynamic "authoring" of Broadcasts This adds a new `type_of` to the Broadcast model: "Welcome". As we begin to create a new "welcome notification" workflow, we'll categorize them by making them all of the same type. This also adds a concept of a "welcoming user", which can be set via an ENV var. The WELCOMING_USER_ID will allow us to explicitly set (and change) which user in the database is the one that "sends" welcoming notification. In production, we plan for this to be dev.to/sloan. * Allow WelcomeNotificationWorker to accept + send any kind of broadcast This abstracts out the logic of deciding which broadcast to send from the woker into the calling method. This will help us send many different kinds of welcome notifications using one, resuable worker class. * Raise if a Notification can't be created * Add STAFF_USER_ID as an ENV var This is in preparation for moving away from SiteConfig.staff_user_id, and replacing it with an ENV var. We will need to set this in production first, and then make a separate PR to replace all instances of staff_user_id with the newly-set ENV var.
687 lines
22 KiB
Ruby
687 lines
22 KiB
Ruby
class User < ApplicationRecord
|
|
include CloudinaryHelper
|
|
|
|
attr_accessor(
|
|
:scholar_email, :new_note, :note_for_current_role, :user_status, :pro, :merge_user_id,
|
|
:add_credits, :remove_credits, :add_org_credits, :remove_org_credits, :ghostify
|
|
)
|
|
|
|
rolify
|
|
include AlgoliaSearch
|
|
include Storext.model
|
|
|
|
acts_as_followable
|
|
acts_as_follower
|
|
|
|
has_many :organization_memberships, dependent: :destroy
|
|
has_many :organizations, through: :organization_memberships
|
|
has_many :api_secrets, dependent: :destroy
|
|
has_many :articles, dependent: :destroy
|
|
has_many :badge_achievements, dependent: :destroy
|
|
has_many :badges, through: :badge_achievements
|
|
has_many :collections, dependent: :destroy
|
|
has_many :comments, dependent: :destroy
|
|
has_many :email_messages, class_name: "Ahoy::Message", dependent: :destroy
|
|
has_many :github_repos, dependent: :destroy
|
|
has_many :identities, dependent: :destroy
|
|
has_many :mentions, dependent: :destroy
|
|
has_many :messages, dependent: :destroy
|
|
has_many :notes, as: :noteable, inverse_of: :noteable
|
|
has_many :profile_pins, as: :profile, inverse_of: :profile, dependent: :delete_all
|
|
has_many :authored_notes, inverse_of: :author, class_name: "Note", foreign_key: :author_id, dependent: :delete_all
|
|
has_many :notifications, dependent: :destroy
|
|
has_many :reactions, dependent: :destroy
|
|
has_many :tweets, dependent: :destroy
|
|
has_many :chat_channel_memberships, dependent: :destroy
|
|
has_many :chat_channels, through: :chat_channel_memberships
|
|
has_many :notification_subscriptions, dependent: :destroy
|
|
|
|
has_many :offender_feedback_messages, class_name: "FeedbackMessage", inverse_of: :offender, foreign_key: :offender_id, dependent: :nullify
|
|
has_many :reporter_feedback_messages, class_name: "FeedbackMessage", inverse_of: :reporter, foreign_key: :reporter_id, dependent: :nullify
|
|
has_many :affected_feedback_messages, class_name: "FeedbackMessage", inverse_of: :affected, foreign_key: :affected_id, dependent: :nullify
|
|
|
|
has_many :rating_votes, dependent: :destroy
|
|
has_many :html_variants, dependent: :destroy
|
|
has_many :page_views, dependent: :destroy
|
|
has_many :credits, dependent: :destroy
|
|
has_many :classified_listings
|
|
has_many :poll_votes, dependent: :destroy
|
|
has_many :poll_skips, dependent: :destroy
|
|
has_many :backup_data, foreign_key: "instance_user_id", inverse_of: :instance_user, class_name: "BackupData", dependent: :delete_all
|
|
has_many :display_ad_events, dependent: :destroy
|
|
has_many :access_grants, class_name: "Doorkeeper::AccessGrant", foreign_key: :resource_owner_id, inverse_of: :resource_owner, dependent: :delete_all
|
|
has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id, inverse_of: :resource_owner, dependent: :delete_all
|
|
has_many :webhook_endpoints, class_name: "Webhook::Endpoint", foreign_key: :user_id, inverse_of: :user, dependent: :delete_all
|
|
has_many :blocker_blocks, class_name: "UserBlock", foreign_key: :blocker_id, inverse_of: :blocker, dependent: :delete_all
|
|
has_many :blocked_blocks, class_name: "UserBlock", foreign_key: :blocked_id, inverse_of: :blocked, dependent: :delete_all
|
|
has_one :pro_membership, dependent: :destroy
|
|
has_one :counters, class_name: "UserCounter", dependent: :destroy
|
|
has_many :created_podcasts, class_name: "Podcast", foreign_key: :creator_id, inverse_of: :creator, dependent: :nullify
|
|
|
|
mount_uploader :profile_image, ProfileImageUploader
|
|
|
|
devise :omniauthable, :registerable, :database_authenticatable, :confirmable, :rememberable
|
|
|
|
validates :email,
|
|
length: { maximum: 50 },
|
|
email: true,
|
|
allow_nil: true
|
|
validates :email, uniqueness: { allow_nil: true, case_sensitive: false }, if: :email_changed?
|
|
validates :name, length: { minimum: 1, maximum: 100 }
|
|
validates :username,
|
|
presence: true,
|
|
format: { with: /\A[a-zA-Z0-9_]+\Z/ },
|
|
length: { in: 2..30 },
|
|
exclusion: { in: ReservedWords.all, message: "username is reserved" }
|
|
validates :username, uniqueness: { case_sensitive: false }, if: :username_changed?
|
|
validates :twitter_username, uniqueness: { allow_nil: true }, if: :twitter_username_changed?
|
|
validates :github_username, uniqueness: { allow_nil: true }, if: :github_username_changed?
|
|
validates :experience_level, numericality: { less_than_or_equal_to: 10 }, allow_blank: true
|
|
validates :text_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
|
|
validates :bg_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
|
|
validates :website_url, :employer_url, :mastodon_url,
|
|
url: { allow_blank: true, no_local: true, schemes: %w[https http] }
|
|
validates :facebook_url,
|
|
format: /\A(http(s)?:\/\/)?(www.facebook.com|facebook.com)\/.*\Z/,
|
|
allow_blank: true
|
|
validates :stackoverflow_url,
|
|
allow_blank: true,
|
|
format:
|
|
/\A(http(s)?:\/\/)?(((www|pt|ru|es|ja).)?stackoverflow.com|(www.)?stackexchange.com)\/.*\Z/
|
|
validates :behance_url,
|
|
allow_blank: true,
|
|
format: /\A(http(s)?:\/\/)?(www.behance.net|behance.net)\/.*\Z/
|
|
validates :linkedin_url,
|
|
allow_blank: true,
|
|
format:
|
|
/\A(http(s)?:\/\/)?(www.linkedin.com|linkedin.com|[A-Za-z]{2}.linkedin.com)\/.*\Z/
|
|
validates :dribbble_url,
|
|
allow_blank: true,
|
|
format: /\A(http(s)?:\/\/)?(www.dribbble.com|dribbble.com)\/.*\Z/
|
|
validates :medium_url,
|
|
allow_blank: true,
|
|
format: /\A(http(s)?:\/\/)?(www.medium.com|medium.com)\/.*\Z/
|
|
validates :gitlab_url,
|
|
allow_blank: true,
|
|
format: /\A(http(s)?:\/\/)?(www.gitlab.com|gitlab.com)\/.*\Z/
|
|
validates :instagram_url,
|
|
allow_blank: true,
|
|
format: /\A(http(s)?:\/\/)?(?:www.)?instagram.com\/(?=.{1,30}\/?$)([a-zA-Z\d_]\.?)*[a-zA-Z\d_]+\/?\Z/
|
|
validates :twitch_url,
|
|
allow_blank: true,
|
|
format: /\A(http(s)?:\/\/)?(www.twitch.tv|twitch.tv)\/.*\Z/
|
|
validates :shirt_gender,
|
|
inclusion: { in: %w[unisex womens],
|
|
message: "%<value>s is not a valid shirt style" },
|
|
allow_blank: true
|
|
validates :shirt_size,
|
|
inclusion: { in: %w[xs s m l xl 2xl 3xl 4xl],
|
|
message: "%<value>s is not a valid size" },
|
|
allow_blank: true
|
|
validates :tabs_or_spaces,
|
|
inclusion: { in: %w[tabs spaces],
|
|
message: "%<value>s is not a valid answer" },
|
|
allow_blank: true
|
|
validates :editor_version,
|
|
inclusion: { in: %w[v1 v2],
|
|
message: "%<value>s must be either v1 or v2" }
|
|
|
|
validates :config_theme,
|
|
inclusion: { in: %w[default night_theme pink_theme minimal_light_theme ten_x_hacker_theme],
|
|
message: "%<value>s is not a valid theme" }
|
|
validates :config_font,
|
|
inclusion: { in: %w[default sans_serif monospace comic_sans open_dyslexic],
|
|
message: "%<value>s is not a valid font selection" }
|
|
validates :config_navbar,
|
|
inclusion: { in: %w[default static],
|
|
message: "%<value>s is not a valid navbar value" }
|
|
validates :shipping_country,
|
|
length: { in: 2..2 },
|
|
allow_blank: true
|
|
validates :website_url, :employer_name, :employer_url,
|
|
length: { maximum: 100 }
|
|
validates :employment_title, :education, :location,
|
|
length: { maximum: 100 }
|
|
validates :mostly_work_with, :currently_learning,
|
|
:currently_hacking_on, :available_for,
|
|
length: { maximum: 500 }
|
|
validates :inbox_type, inclusion: { in: %w[open private] }
|
|
validates :currently_streaming_on, inclusion: { in: %w[twitch] }, allow_nil: true
|
|
validates :feed_referential_link, inclusion: [true, false]
|
|
validate :conditionally_validate_summary
|
|
validate :validate_mastodon_url
|
|
validate :validate_feed_url, if: :feed_url_changed?
|
|
validate :non_banished_username, :username_changed?
|
|
validate :unique_including_orgs_and_podcasts, if: :username_changed?
|
|
|
|
scope :dev_account, -> { find_by(id: SiteConfig.staff_user_id) }
|
|
scope :welcoming_account, -> { find_by(id: ApplicationConfig["WELCOMING_USER_ID"]) }
|
|
|
|
scope :with_this_week_comments, lambda { |number|
|
|
includes(:counters).joins(:counters).where("(user_counters.data -> 'comments_these_7_days')::int >= ?", number)
|
|
}
|
|
scope :with_previous_week_comments, lambda { |number|
|
|
includes(:counters).joins(:counters).where("(user_counters.data -> 'comments_prior_7_days')::int >= ?", number)
|
|
}
|
|
scope :top_commenters, lambda { |number = 10|
|
|
includes(:counters).order(Arel.sql("user_counters.data -> 'comments_these_7_days' DESC")).limit(number)
|
|
}
|
|
|
|
after_create_commit :send_welcome_notification
|
|
after_save :bust_cache
|
|
after_save :subscribe_to_mailchimp_newsletter
|
|
after_save :conditionally_resave_articles
|
|
after_create_commit :estimate_default_language
|
|
before_create :set_default_language
|
|
before_validation :set_username
|
|
# make sure usernames are not empty, to be able to use the database unique index
|
|
before_validation :verify_twitter_username, :verify_github_username, :verify_email, :verify_twitch_username
|
|
before_validation :set_config_input
|
|
before_validation :downcase_email
|
|
before_validation :check_for_username_change
|
|
before_destroy :remove_from_algolia_index, prepend: true
|
|
before_destroy :destroy_empty_dm_channels, prepend: true
|
|
before_destroy :destroy_follows, prepend: true
|
|
before_destroy :unsubscribe_from_newsletters, prepend: true
|
|
|
|
algoliasearch per_environment: true, enqueue: :trigger_delayed_index do
|
|
attribute :name
|
|
add_index "searchables",
|
|
id: :index_id,
|
|
per_environment: true,
|
|
enqueue: true do
|
|
attribute :user do
|
|
{
|
|
username: user.username,
|
|
name: user.username,
|
|
profile_image_90: profile_image_90,
|
|
pro: user.pro?
|
|
}
|
|
end
|
|
attribute :title, :path, :tag_list, :main_image, :id,
|
|
:featured, :published, :published_at, :featured_number, :comments_count,
|
|
:reactions_count, :positive_reactions_count, :class_name, :user_name,
|
|
:user_username, :comments_blob, :body_text, :tag_keywords_for_search,
|
|
:search_score, :hotness_score
|
|
searchableAttributes ["unordered(title)",
|
|
"body_text",
|
|
"tag_list",
|
|
"tag_keywords_for_search",
|
|
"user_name",
|
|
"user_username",
|
|
"comments_blob"]
|
|
attributesForFaceting [:class_name]
|
|
customRanking ["desc(search_score)", "desc(hotness_score)"]
|
|
end
|
|
end
|
|
|
|
def estimated_default_language
|
|
language_settings["estimated_default_language"]
|
|
end
|
|
|
|
def self.trigger_delayed_index(record, remove)
|
|
return if remove
|
|
|
|
Search::IndexWorker.perform_async("User", record.id)
|
|
end
|
|
|
|
def tag_line
|
|
summary
|
|
end
|
|
|
|
def set_remember_fields
|
|
self.remember_token ||= self.class.remember_token if respond_to?(:remember_token)
|
|
self.remember_created_at ||= Time.now.utc
|
|
end
|
|
|
|
def calculate_score
|
|
score = (articles.where(featured: true).size * 100) + comments.sum(:score)
|
|
update_column(:score, score)
|
|
end
|
|
|
|
def path
|
|
"/" + username.to_s
|
|
end
|
|
|
|
def followed_articles
|
|
Article.tagged_with(cached_followed_tag_names, any: true).
|
|
union(Article.where(user_id: cached_following_users_ids)).
|
|
where(language: preferred_languages_array, published: true)
|
|
end
|
|
|
|
def cached_following_users_ids
|
|
Rails.cache.fetch(
|
|
"user-#{id}-#{last_followed_at}-#{following_users_count}/following_users_ids",
|
|
expires_in: 12.hours,
|
|
) do
|
|
Follow.where(follower_id: id, followable_type: "User").limit(150).pluck(:followable_id)
|
|
end
|
|
end
|
|
|
|
def cached_following_organizations_ids
|
|
Rails.cache.fetch(
|
|
"user-#{id}-#{last_followed_at}-#{following_orgs_count}/following_organizations_ids",
|
|
expires_in: 12.hours,
|
|
) do
|
|
Follow.where(follower_id: id, followable_type: "Organization").limit(150).pluck(:followable_id)
|
|
end
|
|
end
|
|
|
|
def cached_following_podcasts_ids
|
|
Rails.cache.fetch(
|
|
"user-#{id}-#{last_followed_at}/following_podcasts_ids",
|
|
expires_in: 12.hours,
|
|
) do
|
|
Follow.where(follower_id: id, followable_type: "Podcast").pluck(:followable_id)
|
|
end
|
|
end
|
|
|
|
# handles both old (prefer_language_*) and new (Array of language codes) formats
|
|
def preferred_languages_array
|
|
# return @prefer_languages_array if defined? @preferred_languages_array
|
|
return @preferred_languages_array if defined?(@preferred_languages_array)
|
|
|
|
if language_settings["preferred_languages"].present?
|
|
@preferred_languages_array = language_settings["preferred_languages"].to_a
|
|
else
|
|
languages = []
|
|
language_settings.each_key do |setting|
|
|
languages << setting.split("prefer_language_")[1] if language_settings[setting] && setting.include?("prefer_language_")
|
|
end
|
|
@preferred_languages_array = languages
|
|
end
|
|
@preferred_languages_array
|
|
end
|
|
|
|
def processed_website_url
|
|
website_url.to_s.strip if website_url.present?
|
|
end
|
|
|
|
def remember_me
|
|
true
|
|
end
|
|
|
|
def cached_followed_tag_names
|
|
cache_name = "user-#{id}-#{following_tags_count}-#{last_followed_at&.rfc3339}/followed_tag_names"
|
|
Rails.cache.fetch(cache_name, expires_in: 24.hours) do
|
|
Tag.where(
|
|
id: Follow.where(
|
|
follower_id: id,
|
|
followable_type: "ActsAsTaggableOn::Tag",
|
|
).pluck(:followable_id),
|
|
).pluck(:name)
|
|
end
|
|
end
|
|
|
|
# methods for Administrate field
|
|
def banned
|
|
has_role? :banned
|
|
end
|
|
|
|
def warned
|
|
has_role? :warned
|
|
end
|
|
|
|
def admin?
|
|
has_role?(:super_admin)
|
|
end
|
|
|
|
def any_admin?
|
|
has_role?(:super_admin) || has_role?(:admin)
|
|
end
|
|
|
|
def tech_admin?
|
|
has_role?(:tech_admin) || has_role?(:super_admin)
|
|
end
|
|
|
|
def pro?
|
|
Rails.cache.fetch("user-#{id}/has_pro_membership", expires_in: 200.hours) do
|
|
pro_membership&.active? || has_role?(:pro)
|
|
end
|
|
end
|
|
|
|
def trusted
|
|
Rails.cache.fetch("user-#{id}/has_trusted_role", expires_in: 200.hours) do
|
|
has_role? :trusted
|
|
end
|
|
end
|
|
|
|
def moderator_for_tags
|
|
Rails.cache.fetch("user-#{id}/tag_moderators_list", expires_in: 200.hours) do
|
|
tag_ids = roles.where(name: "tag_moderator").pluck(:resource_id)
|
|
Tag.where(id: tag_ids).pluck(:name)
|
|
end
|
|
end
|
|
|
|
def scholar
|
|
valid_pass = workshop_expiration.nil? || workshop_expiration > Time.current
|
|
has_role?(:workshop_pass) && valid_pass
|
|
end
|
|
|
|
def comment_banned
|
|
has_role? :comment_banned
|
|
end
|
|
|
|
def workshop_eligible?
|
|
has_any_role?(:workshop_pass)
|
|
end
|
|
|
|
def admin_organizations
|
|
org_ids = organization_memberships.where(type_of_user: "admin").pluck(:organization_id)
|
|
organizations.where(id: org_ids)
|
|
end
|
|
|
|
def member_organizations
|
|
org_ids = organization_memberships.where(type_of_user: %w[admin member]).pluck(:organization_id)
|
|
organizations.where(id: org_ids)
|
|
end
|
|
|
|
def org_member?(organization)
|
|
OrganizationMembership.exists?(user: user, organization: organization, type_of_user: %w[admin member])
|
|
end
|
|
|
|
def org_admin?(organization)
|
|
OrganizationMembership.exists?(user: user, organization: organization, type_of_user: "admin")
|
|
end
|
|
|
|
def block; end
|
|
|
|
def all_blocking
|
|
UserBlock.where(blocker_id: id)
|
|
end
|
|
|
|
def all_blocked_by
|
|
UserBlock.where(blocked_id: id)
|
|
end
|
|
|
|
def blocking?(blocked_id)
|
|
UserBlock.blocking?(id, blocked_id)
|
|
end
|
|
|
|
def blocked_by?(blocker_id)
|
|
UserBlock.blocking?(blocker_id, id)
|
|
end
|
|
|
|
def unique_including_orgs_and_podcasts
|
|
errors.add(:username, "is taken.") if Organization.find_by(slug: username) || Podcast.find_by(slug: username) || Page.find_by(slug: username)
|
|
end
|
|
|
|
def non_banished_username
|
|
errors.add(:username, "has been banished.") if BanishedUser.exists?(username: username)
|
|
end
|
|
|
|
def subscribe_to_mailchimp_newsletter
|
|
return unless email.present? && email.include?("@")
|
|
return if saved_changes["unconfirmed_email"] && saved_changes["confirmation_sent_at"]
|
|
|
|
Users::SubscribeToMailchimpNewsletterWorker.perform_async(id)
|
|
end
|
|
|
|
def a_sustaining_member?
|
|
monthly_dues.positive?
|
|
end
|
|
|
|
def resave_articles
|
|
articles.find_each do |article|
|
|
if article.path
|
|
CacheBuster.bust(article.path)
|
|
CacheBuster.bust("#{article.path}?i=i")
|
|
end
|
|
article.save
|
|
end
|
|
end
|
|
|
|
def settings_tab_list
|
|
%w[
|
|
Profile
|
|
UX
|
|
Integrations
|
|
Notifications
|
|
Publishing\ from\ RSS
|
|
Organization
|
|
Billing
|
|
Account
|
|
Misc
|
|
]
|
|
end
|
|
|
|
def profile_image_90
|
|
ProfileImage.new(self).get(width: 90)
|
|
end
|
|
|
|
def remove_from_algolia_index
|
|
remove_from_index!
|
|
Search::RemoveFromIndexWorker.perform_async("searchables_#{Rails.env}", index_id)
|
|
end
|
|
|
|
def unsubscribe_from_newsletters
|
|
MailchimpBot.new(self).unsubscribe_all_newsletters
|
|
end
|
|
|
|
def tag_moderator?
|
|
roles.where(name: "tag_moderator").any?
|
|
end
|
|
|
|
def currently_streaming?
|
|
currently_streaming_on.present?
|
|
end
|
|
|
|
def currently_streaming_on_twitch?
|
|
currently_streaming_on == "twitch"
|
|
end
|
|
|
|
def enough_credits?(num_credits_needed)
|
|
credits.unspent.size >= num_credits_needed
|
|
end
|
|
|
|
def receives_follower_email_notifications?
|
|
email.present? &&
|
|
email_follower_notifications
|
|
end
|
|
|
|
def title
|
|
name
|
|
end
|
|
|
|
private
|
|
|
|
def index_id
|
|
"users-#{id}"
|
|
end
|
|
|
|
def estimate_default_language
|
|
Users::EstimateDefaultLanguageWorker.perform_async(id)
|
|
end
|
|
|
|
def set_default_language
|
|
language_settings["preferred_languages"] ||= ["en"]
|
|
end
|
|
|
|
def send_welcome_notification
|
|
return unless (welcome_broadcast = Broadcast.find_by(title: "Welcome Notification"))
|
|
|
|
Notification.send_welcome_notification(id, welcome_broadcast.id)
|
|
end
|
|
|
|
def verify_twitter_username
|
|
self.twitter_username = nil if twitter_username == ""
|
|
end
|
|
|
|
def verify_github_username
|
|
self.github_username = nil if github_username == ""
|
|
end
|
|
|
|
def verify_email
|
|
self.email = nil if email == ""
|
|
end
|
|
|
|
def verify_twitch_username
|
|
self.twitch_username = nil if twitch_username == ""
|
|
end
|
|
|
|
def set_username
|
|
set_temp_username if username.blank?
|
|
self.username = username&.downcase
|
|
end
|
|
|
|
def set_temp_username
|
|
self.username = if temp_name_exists?
|
|
temp_username + "_" + rand(100).to_s
|
|
else
|
|
temp_username
|
|
end
|
|
end
|
|
|
|
def temp_name_exists?
|
|
User.find_by(username: temp_username) || Organization.find_by(slug: temp_username)
|
|
end
|
|
|
|
def temp_username
|
|
if twitter_username
|
|
twitter_username.downcase.gsub(/[^0-9a-z_]/i, "").delete(" ")
|
|
elsif github_username
|
|
github_username.downcase.gsub(/[^0-9a-z_]/i, "").delete(" ")
|
|
end
|
|
end
|
|
|
|
def downcase_email
|
|
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?
|
|
|
|
self.old_old_username = old_username
|
|
self.old_username = username_was
|
|
chat_channels.find_each do |channel|
|
|
channel.slug = channel.slug.gsub(username_was, username)
|
|
channel.save
|
|
end
|
|
articles.find_each do |article|
|
|
article.path = article.path.gsub(username_was, username)
|
|
article.save
|
|
end
|
|
end
|
|
|
|
def conditionally_resave_articles
|
|
Users::ResaveArticlesWorker.perform_async(id) if core_profile_details_changed? && !user.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? ||
|
|
saved_change_to_github_username? ||
|
|
saved_change_to_twitter_username?
|
|
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?
|
|
|
|
errors.add(:feed_url, "is not a valid rss feed") unless RssReader.new.valid_feed_url?(feed_url)
|
|
end
|
|
|
|
def validate_mastodon_url
|
|
return if mastodon_url.blank?
|
|
|
|
uri = URI.parse(mastodon_url)
|
|
return if uri.host&.in?(Constants::ALLOWED_MASTODON_INSTANCES)
|
|
|
|
errors.add(:mastodon_url, "is not an allowed Mastodon instance")
|
|
end
|
|
|
|
def tag_list
|
|
"" # Unused but necessary for search index
|
|
end
|
|
|
|
def main_image; end
|
|
|
|
def featured
|
|
true
|
|
end
|
|
|
|
def published
|
|
true
|
|
end
|
|
|
|
def published_at; end
|
|
|
|
def featured_number; end
|
|
|
|
def positive_reactions_count
|
|
reactions_count
|
|
end
|
|
|
|
def user
|
|
self
|
|
end
|
|
|
|
def class_name
|
|
self.class.name
|
|
end
|
|
|
|
def user_name
|
|
username
|
|
end
|
|
|
|
def user_username
|
|
username
|
|
end
|
|
|
|
def comments_blob
|
|
"" # Unused but necessary for search index
|
|
end
|
|
|
|
def body_text
|
|
"" # Unused but necessary for search index
|
|
end
|
|
|
|
def tag_keywords_for_search
|
|
employer_name.to_s + mostly_work_with.to_s + available_for.to_s
|
|
end
|
|
|
|
def hotness_score
|
|
search_score
|
|
end
|
|
|
|
def search_score
|
|
counts_score = (articles_count + comments_count + reactions_count + badge_achievements_count) * 10
|
|
score = (counts_score + tag_keywords_for_search.size) * reputation_modifier
|
|
score.to_i
|
|
end
|
|
|
|
def destroy_empty_dm_channels
|
|
return if chat_channels.empty? ||
|
|
chat_channels.where(channel_type: "direct").empty?
|
|
|
|
empty_dm_channels = chat_channels.where(channel_type: "direct").
|
|
select { |chat_channel| chat_channel.messages.empty? }
|
|
empty_dm_channels.destroy_all
|
|
end
|
|
|
|
def destroy_follows
|
|
follower_relationships = Follow.where(followable_id: id, followable_type: "User")
|
|
follower_relationships.destroy_all
|
|
follows.destroy_all
|
|
end
|
|
end
|