docbrown/app/models/organization.rb
Joshua Wehner a20cf3e02b
Unique cross-model-slug check is likely misconfigured (#19263)
* Page unique_cross_model_slug seems misconfigured

* Update validator test

* More verbose invalid attribute message

* Try dynamic model/attribute registration

* Rubocop

* Use the cross-model-slug existence check

* Register with the dynamic cross-model-slug checker

* Cleanup

* Add missing keys to fr

* Update format regex

* Adjust validation: always presence, unique if changed

* Tests can create reserved-word pages when they need to

* Slugs can be mixed case?

* Case-sensitive is better, actually

* Refactor to use CrossModel check

* Refactor, rename for clarity

* Refactor, avoid mocking oneself

* Refactor, injectable everything

* Add reservedword check to extracted exists? checker

* Move to concerns

* Without dynamic registration

* extend when needed

* Cleanup comments, remove registration references

---------

Co-authored-by: Goran <gorang.pub@gmail.com>
2023-04-07 17:04:45 +02:00

145 lines
4.6 KiB
Ruby

class Organization < ApplicationRecord
include CloudinaryHelper
include Images::Profile.for(:profile_image_url)
extend UniqueAcrossModels
COLOR_HEX_REGEXP = /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
INTEGER_REGEXP = /\A\d+\z/
acts_as_followable
before_validation :downcase_slug
before_validation :check_for_slug_change
before_validation :evaluate_markdown
before_save :remove_at_from_usernames
before_save :generate_secret
after_save :bust_cache
after_update_commit :conditionally_update_articles
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 :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: :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 :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 }
unique_across_models :slug, length: { in: 2..30 }
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 self.integer_only
I18n.t("models.organization.integer_only")
end
def self.reserved_word
I18n.t("models.organization.reserved_word")
end
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
profile_image_url_for(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 conditionally_update_articles
return unless Article::ATTRIBUTES_CACHED_FOR_RELATED_ENTITY.detect { |attr| saved_change_to_attribute?(attr) }
article_ids = articles.ids.map { |id| [id] }
Organizations::SaveArticleWorker.perform_bulk(article_ids)
end
def bust_cache
Organizations::BustCacheWorker.perform_async(id, slug)
end
end