docbrown/app/models/user.rb
Jacob Herrington 099b188530 Rename uncommunicative variables (#3890)
* Clarify uncommunicative variables in labor classes

Single letter variable names are largely a question of preference, in
some cases I think that convention mitigates the opaque nature of single
letter variable names (e.g., e for error, i for index, etc).

However, in some cases they can be unclear and there isn't much reason
to use single letter variables unless for some reason character length is
really important.

In this case, I would prefer clarity in variable names over brevity of
code so I've used Reek to identify short variable names and I'm changing
them.

It's pretty boring, but hopefully incremental code love changes like
this one add up and improve readability and accessibility for those
interested in reading this codebase.

* Remove commented code from 2 years ago

* Clarify uncommunicative naming in services classes

There is some context for this change in 6b81880f

* Clarify uncommunicative naming in models

There is some context for this change in 6b81880f

* Clarify uncommunicative naming in liquid_tags

There is some context for this change in 6b81880f
2019-09-02 13:05:07 -04:00

636 lines
19 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"
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
has_many :authored_notes, as: :author, inverse_of: :author, class_name: "Note"
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 :push_notification_subscriptions, dependent: :destroy
has_many :feedback_messages
has_many :rating_votes
has_many :html_variants, dependent: :destroy
has_many :page_views
has_many :credits
has_many :classified_listings
has_many :poll_votes
has_many :poll_skips
has_many :backup_data, foreign_key: "instance_user_id", inverse_of: :instance_user, class_name: "BackupData"
has_many :display_ad_events
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
mount_uploader :profile_image, ProfileImageUploader
devise :omniauthable, :rememberable,
:registerable, :database_authenticatable, :confirmable
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} 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} is not a valid size" },
allow_blank: true
validates :tabs_or_spaces,
inclusion: { in: %w[tabs spaces],
message: "%{value} is not a valid answer" },
allow_blank: true
validates :editor_version,
inclusion: { in: %w[v1 v2],
message: "%{value} must be either v1 or v2" }
validates :config_theme,
inclusion: { in: %w[default night_theme pink_theme minimal_light_theme],
message: "%{value} is not a valid theme" }
validates :config_font,
inclusion: { in: %w[default sans_serif comic_sans],
message: "%{value} must be either default or sans serif" }
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 :unique_including_orgs_and_podcasts, if: :username_changed?
scope :dev_account, -> { find_by(id: ApplicationConfig["DEVTO_USER_ID"]) }
after_create :send_welcome_notification
after_save :bust_cache
after_save :subscribe_to_mailchimp_newsletter
after_save :conditionally_resave_articles
after_create :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
before_destroy :destroy_empty_dm_channels
before_destroy :destroy_follows
before_destroy :unsubscribe_from_newsletters
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 }
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
AlgoliaSearch::AlgoliaJob.perform_later(record, "index!")
end
def tag_line
summary
end
def index_id
"users-#{id}"
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 estimate_default_language!
Users::EstimateDefaultLanguageJob.perform_later(id)
end
def estimate_default_language_without_delay!
Users::EstimateDefaultLanguageJob.perform_now(id)
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: cached_preferred_langs, published: true)
end
def cached_following_users_ids
Rails.cache.fetch(
"user-#{id}-#{updated_at}-#{following_users_count}/following_users_ids",
expires_in: 120.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}-#{updated_at}-#{following_orgs_count}/following_organizations_ids",
expires_in: 120.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}-#{updated_at}-#{last_followed_at}/following_podcasts_ids",
expires_in: 120.hours,
) do
Follow.where(follower_id: id, followable_type: "Podcast").pluck(:followable_id)
end
end
def cached_preferred_langs
Rails.cache.fetch("user-#{id}-#{updated_at}/cached_preferred_langs", expires_in: 24.hours) do
preferred_languages_array
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}-#{updated_at}/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?
has_role?(:pro)
end
def trusted
Rails.cache.fetch("user-#{id}/has_trusted_role", expires_in: 200.hours) do
has_role? :trusted
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 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 subscribe_to_mailchimp_newsletter_without_delay
return unless email.present? && email.include?("@")
return if saved_changes["unconfirmed_email"] && saved_changes["confirmation_sent_at"]
Users::SubscribeToMailchimpNewsletterJob.perform_now(id)
end
def subscribe_to_mailchimp_newsletter
return unless email.present? && email.include?("@")
return if saved_changes["unconfirmed_email"] && saved_changes["confirmation_sent_at"]
Users::SubscribeToMailchimpNewsletterJob.perform_later(id)
end
def a_sustaining_member?
monthly_dues.positive?
end
def resave_articles
cache_buster = CacheBuster.new
articles.find_each do |article|
cache_buster.bust(article.path) if article.path
cache_buster.bust(article.path + "?i=i") if article.path
article.save
end
end
def settings_tab_list
%w[
Profile
Integrations
Notifications
Publishing\ from\ RSS
Organization
Billing
Account
Misc
]
end
def profile_image_90
ProfileImage.new(self).get(90)
end
def remove_from_algolia_index
remove_from_index!
index = Algolia::Index.new("searchables_#{Rails.env}")
index.delay.delete_object("users-#{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
private
def set_default_language
language_settings["preferred_languages"] ||= ["en"]
end
def send_welcome_notification
Notification.send_welcome_notification(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(" ", "_")
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
delay.resave_articles if core_profile_details_changed? && !user.banned
end
def bust_cache
Users::BustCacheJob.perform_later(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 title
name
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