docbrown/app/models/organization.rb
Jeremy Friesen d462abb5b5
Ensuring article's cache updates on org image update (#17052)
There are three major things occurring in this pull request:

1. Renaming `Article#update_cached_user` to `Article#set_cached_entities`.
2. Reducing an organization's direct knowledge of which of the org's
   attributes an article caches.
3. Removing duplicate calls to update the article associated with the
   organization.

For renaming to `Article#set_cached_entities`, the prior method implied
we were updating the persistence layer.  However, we were not making any
save nor update calls.  This rename should clarify intention.

For reducing knowledge, the comments for
`Article::ATTRIBUTES_CACHED_FOR_RELATED_ENTITY` should explain the details.

And last, removing the duplicate calls; we had three methods that were
attempting to build and update the `Article#cached_organization`'s
value.

Closes forem/forem#17041
2022-04-01 10:54:55 -04:00

148 lines
4.8 KiB
Ruby

class Organization < ApplicationRecord
include CloudinaryHelper
include Images::Profile.for(:profile_image_url)
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/
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 :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: :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: :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 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) }
articles.each(&:save)
end
def bust_cache
Organizations::BustCacheWorker.perform_async(id, slug)
end
end