docbrown/app/models/listing.rb
Jeremy Friesen 61ccc29fed
Conditionally disabling Listings (Waiting on Product but technically Good to Go) (#16437)
* Conditionally disabling Listings

Prior to this commit, we were assuming everyone still had the Listing's
feature turned on.

With this commit, we move from the "implicitly on" nature of
`FeatureFlag.accessible?` to the "explicitly off" nature of
`FeatureFlag.enabled?`.  The key differentiator is that we are not
"pushing out a feature flag setting to other forems."  Listings now
become an "opt-in".

This requires a few things:

1) Ensure that DEV.to enables the `:listing_feature` before we deploy
   this branch.
2) Communicate this change to other Forem adopters and provide a
   timeline for when we'll deploy this change.
3) Documentation.  Given that we have this feature, but it will be
   disabled by default, we want to look into communicating what it is
   and how to enable it.

Further, I chose to do the least effort change on our specs.  Namely to
treat the specs as though someone fully enabled the listing feature.

This pull request is a "competetor" to #16423.

Closes forem/rfcs#291

Relates to #16420 and #16461.

* Favoring only setting feature for specific tests.

* Adjusting cypress to avoid listing check

* Removing redundant test based on feedback

* Fixing feature spec

* Fixing cypress test
2022-04-11 20:23:56 -04:00

142 lines
4.6 KiB
Ruby

class Listing < ApplicationRecord
# We used to use both "classified listing" and "listing" throughout the app.
# We standardized on the latter, but keeping the table name was easier.
self.table_name = "classified_listings"
self.ignored_columns = %w[contact_via_connect].freeze
include PgSearch::Model
attr_accessor :action
# NOTE: categories were hardcoded at first and the model was only added later.
# The foreign_key and inverse_of options are used because of legacy table names.
belongs_to :listing_category, inverse_of: :listings, foreign_key: :classified_listing_category_id
belongs_to :user
belongs_to :organization, optional: true
before_validation :modify_inputs
before_save :evaluate_markdown
before_create :create_slug
acts_as_taggable_on :tags
has_many :credits, as: :purchase, inverse_of: :purchase, dependent: :nullify
validates :organization_id, presence: true, unless: :user_id?
validates :title, presence: true, length: { maximum: 128 }
validates :body_markdown, presence: true, length: { maximum: 400 }
validates :location, length: { maximum: 32 }
validate :restrict_markdown_input
validate :validate_tags
pg_search_scope :search_listings,
against: %i[body_markdown cached_tag_list location slug title],
using: { tsearch: { prefix: true } }
scope :published, -> { where(published: true) }
# NOTE: we still need to use the old column name for the join query
scope :in_category, lambda { |slug|
joins(:listing_category).where("classified_listing_categories.slug" => slug)
}
delegate :cost, to: :listing_category
# As part of making listings "optional", this is the current place to go for the answer "Is the
# Listing feature enabled?" This approach will get us quite far, at least up until we flip this
# into a plugin (e.g. we won't be able to guarantee that we have the constant :Listing in the Ruby
# object space).
#
# @note As of <2022-01-28 Fri>, the assumption is that everyone will have this feature enabled.
# In part because marking this feature as disabled won't yet properly disable all aspects of
# the feature.
#
# @see https://github.com/forem/rfcs/issues/291 for discussion and rollout strategy
# @see FeatureFlag.accessible?
#
# @return [TrueClass] if the Listing is enabled for this Forem
# @return [FalseClass] if the Listing is disabled for this Forem
def self.feature_enabled?
FeatureFlag.enabled?(:listing_feature)
end
# Wrapping the column accessor names for consistency. Aliasing did not work.
def listing_category_id
classified_listing_category_id
end
def listing_category_id=(id)
self.classified_listing_category_id = id
end
def category
listing_category&.slug
end
def author
organization || user
end
def path
"/listings/#{category}/#{slug}"
end
def natural_expiration_date
(bumped_at || created_at) + 30.days
end
def publish
update(published: true)
end
def unpublish
update(published: false)
end
def bump
update(bumped_at: Time.current)
end
def clear_cache
Listings::BustCacheWorker.perform_async(id)
end
# First tries to purchase the listing with the org's credit. IF that doesn't
# work it tries to charge the user instead. The purchasers will be yielded
# to the provided block so it can be used for further processing.
def purchase(user)
purchaser = [organization, user].detect { |who| who&.enough_credits?(cost) }
return false unless purchaser
yield purchaser
true
end
private
def evaluate_markdown
self.processed_html = MarkdownProcessor::Parser.new(body_markdown).evaluate_listings_markdown
end
def modify_inputs
temp_tags = tag_list
self.tag_list = [] # overwrite any existing tag with those from the front matter
tag_list.add(temp_tags, parser: ActsAsTaggableOn::TagParser)
self.body_markdown = body_markdown.to_s.gsub(/\r\n/, "\n")
end
def restrict_markdown_input
markdown_string = body_markdown.to_s
if markdown_string.scan(/(?=\n)/).count > 12
errors.add(:body_markdown, I18n.t("models.listing.too_many_linebreaks"))
end
errors.add(:body_markdown, I18n.t("models.listing.image_not_allowed")) if markdown_string.include?("![")
errors.add(:body_markdown, I18n.t("models.listing.liquid_not_allowed")) if markdown_string.include?("{% ")
end
def validate_tags
errors.add(:tag_list, I18n.t("models.listing.too_many_tags")) if tag_list.length > 8
end
def create_slug
self.slug = "#{title.downcase.parameterize.delete('_')}-#{rand(100_000).to_s(26)}"
end
end