* Extracting cross-model uniqueness validator Prior to this commit, we had somewhat duplicated logic across four models. In adding this validator we're consolidating the logic and tidying up each of the other models; letting the validator know which models are part of the "uniqueness gang." * Adding conditional test for validator * Bump for travis * Apply suggestions from code review Co-authored-by: Michael Kohl <citizen428@forem.com> Co-authored-by: Michael Kohl <citizen428@forem.com>
153 lines
5.4 KiB
Ruby
153 lines
5.4 KiB
Ruby
class Organization < ApplicationRecord
|
|
include CloudinaryHelper
|
|
|
|
COLOR_HEX_REGEXP = /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
|
|
INTEGER_REGEXP = /\A\d+\z/
|
|
SLUG_REGEXP = /\A[a-zA-Z0-9\-_]+\z/
|
|
MESSAGES = {
|
|
integer_only: "Integer only. No sign allowed.",
|
|
reserved_word: "%<value>s is a reserved word. Contact site admins for help registering your organization."
|
|
}.freeze
|
|
|
|
acts_as_followable
|
|
|
|
before_validation :downcase_slug
|
|
before_validation :check_for_slug_change
|
|
before_validation :evaluate_markdown
|
|
|
|
# TODO: [@rhymes] revisit this callback and `update_articles_cached_organization`
|
|
# when we remove Elasticsearch
|
|
before_save :update_articles
|
|
before_save :remove_at_from_usernames
|
|
before_save :generate_secret
|
|
|
|
after_save :bust_cache
|
|
|
|
# This callback will eventually invoke Article.update_cached_user to update the organization.name
|
|
# only when it has been changed, thus invoking the trigger on Article.reading_list_document
|
|
after_update_commit :update_articles_cached_organization
|
|
after_destroy_commit :bust_cache
|
|
|
|
has_many :articles, dependent: :nullify
|
|
has_many :collections, dependent: :nullify
|
|
has_many :credits, dependent: :restrict_with_error
|
|
has_many :display_ads, dependent: :destroy
|
|
has_many :listings, dependent: :destroy
|
|
has_many :notifications, dependent: :delete_all
|
|
has_many :organization_memberships, dependent: :delete_all
|
|
has_many :profile_pins, as: :profile, inverse_of: :profile, dependent: :destroy
|
|
has_many :sponsorships, dependent: :destroy
|
|
has_many :unspent_credits, -> { where spent: false }, class_name: "Credit", inverse_of: :organization
|
|
has_many :users, through: :organization_memberships
|
|
|
|
validates :articles_count, presence: true
|
|
validates :bg_color_hex, format: COLOR_HEX_REGEXP, allow_blank: true
|
|
validates :company_size, format: { with: INTEGER_REGEXP, message: MESSAGES[:integer_only], allow_blank: true }
|
|
validates :company_size, length: { maximum: 7 }, allow_nil: true
|
|
validates :credits_count, presence: true
|
|
validates :cta_body_markdown, length: { maximum: 256 }
|
|
validates :cta_button_text, length: { maximum: 20 }
|
|
validates :cta_button_url, length: { maximum: 150 }, url: { allow_blank: true, no_local: true }
|
|
validates :github_username, length: { maximum: 50 }
|
|
validates :location, :email, length: { maximum: 64 }
|
|
validates :name, :summary, :url, :profile_image, presence: true
|
|
validates :name, length: { maximum: 50 }
|
|
validates :proof, length: { maximum: 1500 }
|
|
validates :secret, length: { is: 100 }, allow_nil: true
|
|
validates :secret, uniqueness: true
|
|
validates :slug, exclusion: { in: ReservedWords.all, message: MESSAGES[:reserved_word] }
|
|
validates :slug, format: { with: SLUG_REGEXP }, length: { in: 2..18 }
|
|
validates :slug, presence: true, uniqueness: { case_sensitive: false }
|
|
validates :spent_credits_count, presence: true
|
|
validates :summary, length: { maximum: 250 }
|
|
validates :tag_line, length: { maximum: 60 }
|
|
validates :tech_stack, :story, length: { maximum: 640 }
|
|
validates :text_color_hex, format: COLOR_HEX_REGEXP, allow_blank: true
|
|
validates :twitter_username, length: { maximum: 15 }
|
|
validates :unspent_credits_count, presence: true
|
|
validates :url, length: { maximum: 200 }, url: { allow_blank: true, no_local: true }
|
|
|
|
validates :slug, unique_cross_model_slug: true, if: :slug_changed?
|
|
|
|
mount_uploader :profile_image, ProfileImageUploader
|
|
mount_uploader :nav_image, ProfileImageUploader
|
|
mount_uploader :dark_nav_image, ProfileImageUploader
|
|
|
|
alias_attribute :username, :slug
|
|
alias_attribute :old_username, :old_slug
|
|
alias_attribute :old_old_username, :old_old_slug
|
|
alias_attribute :website_url, :url
|
|
|
|
def check_for_slug_change
|
|
return unless slug_changed?
|
|
|
|
self.old_old_slug = old_slug
|
|
self.old_slug = slug_was
|
|
Organizations::UpdateOrganizationArticlesPathsWorker.perform_async(id, slug_was, slug)
|
|
end
|
|
|
|
def path
|
|
"/#{slug}"
|
|
end
|
|
|
|
def generate_secret
|
|
self.secret = generated_random_secret if secret.blank?
|
|
end
|
|
|
|
def generated_random_secret
|
|
SecureRandom.hex(50)
|
|
end
|
|
|
|
def approved_and_filled_out_cta?
|
|
cta_processed_html?
|
|
end
|
|
|
|
def profile_image_90
|
|
Images::Profile.call(profile_image_url, length: 90)
|
|
end
|
|
|
|
def enough_credits?(num_credits_needed)
|
|
credits.unspent.size >= num_credits_needed
|
|
end
|
|
|
|
def destroyable?
|
|
organization_memberships.count == 1 && articles.count.zero? && credits.count.zero?
|
|
end
|
|
|
|
# NOTE: We use Organization and User objects interchangeably. Since the former
|
|
# don't have profiles we return self instead.
|
|
def profile
|
|
self
|
|
end
|
|
|
|
private
|
|
|
|
def evaluate_markdown
|
|
self.cta_processed_html = MarkdownProcessor::Parser.new(cta_body_markdown).evaluate_limited_markdown
|
|
end
|
|
|
|
def remove_at_from_usernames
|
|
self.twitter_username = twitter_username.delete("@") if twitter_username
|
|
self.github_username = github_username.delete("@") if github_username
|
|
end
|
|
|
|
def downcase_slug
|
|
self.slug = slug&.downcase
|
|
end
|
|
|
|
def update_articles
|
|
return unless saved_change_to_slug || saved_change_to_name || saved_change_to_profile_image
|
|
|
|
articles.update(cached_organization: Articles::CachedEntity.from_object(self))
|
|
end
|
|
|
|
def update_articles_cached_organization
|
|
return unless saved_change_to_attribute?(:name)
|
|
|
|
articles.update(cached_organization: Articles::CachedEntity.from_object(self))
|
|
end
|
|
|
|
def bust_cache
|
|
Organizations::BustCacheWorker.perform_async(id, slug)
|
|
end
|
|
end
|