Revert "[deploy] Switch decorators from Draper to Rails builtin decorations (#6040)" (#6122) [deploy]
This reverts commit cc53b55a4a.
This commit is contained in:
parent
b126326728
commit
d438349550
29 changed files with 118 additions and 582 deletions
1
Gemfile
1
Gemfile
|
|
@ -32,6 +32,7 @@ gem "ddtrace", "~> 0.32.0" # ddtrace is Datadog’s tracing client for Ruby.
|
|||
gem "devise", "~> 4.7" # Flexible authentication solution for Rails
|
||||
gem "dogstatsd-ruby", "~> 4.7" # A client for DogStatsD, an extension of the StatsD metric server for Datadog
|
||||
gem "doorkeeper", "~> 5.3" # Oauth 2 provider
|
||||
gem "draper", "~> 4.0" # Draper adds an object-oriented layer of presentation logic to your Rails apps
|
||||
gem "dry-struct", "~> 1.2" # Typed structs and value objects
|
||||
gem "elasticsearch", "~> 7.4" # Powers DEVs core search functionality
|
||||
gem "email_validator", "~> 2.0" # Email validator for Rails and ActiveModel
|
||||
|
|
|
|||
11
Gemfile.lock
11
Gemfile.lock
|
|
@ -39,6 +39,10 @@ GEM
|
|||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
activemodel-serializers-xml (1.0.2)
|
||||
activemodel (> 5.x)
|
||||
activesupport (> 5.x)
|
||||
builder (~> 3.1)
|
||||
activerecord (5.2.4.1)
|
||||
activemodel (= 5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
|
|
@ -229,6 +233,12 @@ GEM
|
|||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.3.1)
|
||||
railties (>= 5)
|
||||
draper (4.0.0)
|
||||
actionpack (>= 5.0)
|
||||
activemodel (>= 5.0)
|
||||
activemodel-serializers-xml (>= 1.0)
|
||||
activesupport (>= 5.0)
|
||||
request_store (>= 1.0)
|
||||
dry-configurable (0.9.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 0.4, >= 0.4.7)
|
||||
|
|
@ -874,6 +884,7 @@ DEPENDENCIES
|
|||
devise (~> 4.7)
|
||||
dogstatsd-ruby (~> 4.7)
|
||||
doorkeeper (~> 5.3)
|
||||
draper (~> 4.0)
|
||||
dry-struct (~> 1.2)
|
||||
elasticsearch (~> 7.4)
|
||||
email_validator (~> 2.0)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ module Api
|
|||
end
|
||||
|
||||
def create
|
||||
@article = Articles::Creator.call(@user, article_params).decorate
|
||||
|
||||
@article = Articles::Creator.call(@user, article_params)
|
||||
if @article.persisted?
|
||||
render "show", status: :created, location: @article.url
|
||||
else
|
||||
|
|
|
|||
|
|
@ -88,16 +88,14 @@ class ArticlesController < ApplicationController
|
|||
|
||||
def edit
|
||||
authorize @article
|
||||
|
||||
@version = @article.has_frontmatter? ? "v1" : "v2"
|
||||
@user = @article.user
|
||||
@organizations = @user&.organizations
|
||||
end
|
||||
|
||||
def manage
|
||||
authorize @article
|
||||
|
||||
@article = @article.decorate
|
||||
authorize @article
|
||||
@user = @article.user
|
||||
@rating_vote = RatingVote.where(article_id: @article.id, user_id: @user.id).first
|
||||
@buffer_updates = BufferUpdate.where(composer_user_id: @user.id, article_id: @article.id)
|
||||
|
|
@ -138,12 +136,13 @@ class ArticlesController < ApplicationController
|
|||
def create
|
||||
authorize Article
|
||||
|
||||
article = Articles::Creator.call(current_user, article_params_json)
|
||||
@user = current_user
|
||||
@article = Articles::Creator.call(@user, article_params_json)
|
||||
|
||||
render json: if article.persisted?
|
||||
{ id: article.id, current_state_path: article.decorate.current_state_path }.to_json
|
||||
render json: if @article.persisted?
|
||||
@article.to_json(only: [:id], methods: [:current_state_path])
|
||||
else
|
||||
article.errors.to_json
|
||||
@article.errors.to_json
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
class ApplicationDecorator
|
||||
delegate_missing_to :@object
|
||||
|
||||
attr_reader :object
|
||||
|
||||
def initialize(object)
|
||||
@object = object
|
||||
end
|
||||
|
||||
def decorated?
|
||||
true
|
||||
end
|
||||
|
||||
def self.decorate_collection(objects)
|
||||
objects.map(&:decorate)
|
||||
end
|
||||
class ApplicationDecorator < Draper::Decorator
|
||||
# Define methods for all decorated objects.
|
||||
# Helpers are accessed through `helpers` (aka `h`). For example:
|
||||
#
|
||||
# def percent_amount
|
||||
# h.number_to_percentage object.amount, precision: 2
|
||||
# end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
class ArticleDecorator < ApplicationDecorator
|
||||
delegate_all
|
||||
|
||||
def current_state_path
|
||||
published ? "/#{username}/#{slug}" : "/#{username}/#{slug}?preview=#{password}"
|
||||
end
|
||||
|
|
@ -24,13 +26,13 @@ class ArticleDecorator < ApplicationDecorator
|
|||
end
|
||||
|
||||
def title_length_classification
|
||||
if title.size > 105
|
||||
if article.title.size > 105
|
||||
"longest"
|
||||
elsif title.size > 80
|
||||
elsif article.title.size > 80
|
||||
"longer"
|
||||
elsif title.size > 60
|
||||
elsif article.title.size > 60
|
||||
"long"
|
||||
elsif title.size > 22
|
||||
elsif article.title.size > 22
|
||||
"medium"
|
||||
else
|
||||
"short"
|
||||
|
|
@ -38,15 +40,12 @@ class ArticleDecorator < ApplicationDecorator
|
|||
end
|
||||
|
||||
def internal_utm_params(place = "additional_box")
|
||||
org_slug = organization&.slug
|
||||
|
||||
campaign = if boosted_additional_articles
|
||||
"#{org_slug}_boosted"
|
||||
"#{organization&.slug}_boosted"
|
||||
else
|
||||
"regular"
|
||||
end
|
||||
|
||||
"?utm_source=#{place}&utm_medium=internal&utm_campaign=#{campaign}&booster_org=#{org_slug}"
|
||||
"?utm_source=#{place}&utm_medium=internal&utm_campaign=#{campaign}&booster_org=#{organization&.slug}"
|
||||
end
|
||||
|
||||
def published_at_int
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
class CommentDecorator < ApplicationDecorator
|
||||
delegate_all
|
||||
|
||||
LOW_QUALITY_THRESHOLD = -75
|
||||
|
||||
def low_quality
|
||||
|
|
|
|||
|
|
@ -1,30 +1,21 @@
|
|||
class NotificationDecorator < ApplicationDecorator
|
||||
NOTIFIABLE_STUB = Struct.new(:name, :id) do
|
||||
def class
|
||||
Struct.new(:name).new(name)
|
||||
end
|
||||
end.freeze
|
||||
class NotificationDecorator < Draper::Decorator
|
||||
delegate_all
|
||||
|
||||
# returns a stub notifiable object with name and id
|
||||
def mocked_object(type)
|
||||
return NOTIFIABLE_STUB.new("", nil) if json_data.blank?
|
||||
|
||||
NOTIFIABLE_STUB.new(json_data[type]["class"]["name"], json_data[type]["id"])
|
||||
struct = Struct.new(:name, :id) do
|
||||
def class
|
||||
second_struct = Struct.new(:name)
|
||||
second_struct.new(name)
|
||||
end
|
||||
end
|
||||
struct.new(json_data[type]["class"]["name"], json_data[type]["id"])
|
||||
end
|
||||
|
||||
# returns the type of a milestone notification action,
|
||||
# eg. "Milestone::Reaction::64"
|
||||
def milestone_type
|
||||
return "" if action.blank?
|
||||
|
||||
action.split("::").second
|
||||
action.split("::")[1]
|
||||
end
|
||||
|
||||
# returns the count of a milestone notification action,
|
||||
# eg. "Milestone::Reaction::64"
|
||||
def milestone_count
|
||||
return "" if action.blank?
|
||||
|
||||
action.split("::").third
|
||||
action.split("::")[2]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
class OrganizationDecorator < ApplicationDecorator
|
||||
delegate_all
|
||||
|
||||
def darker_color(adjustment = 0.88)
|
||||
HexComparer.new([enriched_colors[:bg], enriched_colors[:text]]).brightness(adjustment)
|
||||
end
|
||||
|
|
@ -11,8 +13,8 @@ class OrganizationDecorator < ApplicationDecorator
|
|||
}
|
||||
else
|
||||
{
|
||||
bg: bg_color_hex,
|
||||
text: text_color_hex.presence || assigned_color[:text]
|
||||
bg: bg_color_hex || assigned_color[:bg],
|
||||
text: text_color_hex || assigned_color[:text]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
class PodcastEpisodeDecorator < ApplicationDecorator
|
||||
delegate_all
|
||||
|
||||
def comments_to_show_count
|
||||
cached_tag_list_array.include?("discuss") ? 75 : 25
|
||||
end
|
||||
|
||||
# this method exists because podcast episodes are "commentables"
|
||||
# and in some parts of the code we assume they have this method,
|
||||
# but podcast episodes don't have a cached_tag_list like articles do
|
||||
def cached_tag_list_array
|
||||
tag_list
|
||||
(tag_list || "").split(", ")
|
||||
end
|
||||
|
||||
def readable_publish_date
|
||||
return "" unless published_at
|
||||
return unless published_at
|
||||
|
||||
if published_at.year == Time.current.year
|
||||
published_at.strftime("%b %e")
|
||||
|
|
@ -21,8 +20,6 @@ class PodcastEpisodeDecorator < ApplicationDecorator
|
|||
end
|
||||
|
||||
def published_timestamp
|
||||
return "" unless published_at
|
||||
|
||||
published_at.utc.iso8601
|
||||
published_at&.utc&.iso8601
|
||||
end
|
||||
end
|
||||
|
|
|
|||
1
app/decorators/reaction_decorator.rb
Normal file
1
app/decorators/reaction_decorator.rb
Normal file
|
|
@ -0,0 +1 @@
|
|||
class ReactionDecorator < ApplicationDecorator; end
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
class SponsorshipDecorator < ApplicationDecorator
|
||||
def level_background_color
|
||||
delegate_all
|
||||
|
||||
def level_color_hex
|
||||
hexes = {
|
||||
"gold" => "linear-gradient(to right, #faf0e6 8%, #faf3e6 18%, #fcf6eb 33%);",
|
||||
"silver" => "linear-gradient(to right, #e3e3e3 8%, #f0eded 18%, #e8e8e8 33%);",
|
||||
"bronze" => "linear-gradient(to right, #ebe2d3 8%, #f5eee1 18%, #ede6d8 33%);"
|
||||
}
|
||||
|
||||
hexes[level].to_s
|
||||
hexes[level]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ class UserDecorator < ApplicationDecorator
|
|||
},
|
||||
].freeze
|
||||
|
||||
delegate_all
|
||||
|
||||
def cached_followed_tags
|
||||
Rails.cache.fetch("user-#{id}-#{updated_at}/followed_tags_11-30", expires_in: 20.hours) do
|
||||
follows = Follow.where(follower_id: id, followable_type: "ActsAsTaggableOn::Tag").pluck(:followable_id, :points)
|
||||
tags = Tag.where(id: follows.map { |f| f[0] }).order("hotness_score DESC")
|
||||
follows_query = Follow.where(follower_id: id, followable_type: "ActsAsTaggableOn::Tag").pluck(:followable_id, :points)
|
||||
tags = Tag.where(id: follows_query.map { |f| f[0] }).order("hotness_score DESC")
|
||||
tags.each do |t|
|
||||
follow_query_item = follows.detect { |f| f[0] == t.id }
|
||||
follow_query_item = follows_query.detect { |f| f[0] == t.id }
|
||||
t.points = follow_query_item[1]
|
||||
end
|
||||
tags
|
||||
|
|
@ -42,21 +44,20 @@ class UserDecorator < ApplicationDecorator
|
|||
}
|
||||
else
|
||||
{
|
||||
bg: bg_color_hex,
|
||||
text: text_color_hex
|
||||
bg: bg_color_hex || assigned_color[:bg],
|
||||
text: text_color_hex || assigned_color[:text]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def config_body_class
|
||||
body_class = [
|
||||
config_theme.tr("_", "-"),
|
||||
"#{config_font.tr('_', '-')}-article-body",
|
||||
"pro-status-#{pro?}",
|
||||
"trusted-status-#{trusted}",
|
||||
"#{config_navbar.tr('_', '-')}-navbar-config",
|
||||
]
|
||||
body_class.join(" ")
|
||||
body_class = ""
|
||||
body_class += config_theme.tr("_", "-")
|
||||
body_class += " #{config_font.tr('_', '-')}-article-body"
|
||||
body_class += " pro-status-#{pro?}"
|
||||
body_class += " trusted-status-#{trusted}"
|
||||
body_class += " #{config_navbar.tr('_', '-')}-navbar-config"
|
||||
body_class
|
||||
end
|
||||
|
||||
def dark_theme?
|
||||
|
|
@ -94,9 +95,11 @@ class UserDecorator < ApplicationDecorator
|
|||
colors[id % 10]
|
||||
end
|
||||
|
||||
# returns true if the user has been suspended and has no content
|
||||
def fully_banished?
|
||||
articles_count.zero? && comments_count.zero? && banned
|
||||
# User suspended and has no content
|
||||
articles_count.zero? &&
|
||||
comments_count.zero? &&
|
||||
banned
|
||||
end
|
||||
|
||||
def stackbit_integration?
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
# raised when an object or collection tries to decorate itself,
|
||||
# without having an inferrable decorator
|
||||
class UninferrableDecoratorError < NameError
|
||||
end
|
||||
|
|
@ -20,31 +20,4 @@ class ApplicationRecord < ActiveRecord::Base
|
|||
result.clear # PG::Result is manually managed in memory, we need to release its resources
|
||||
count
|
||||
end
|
||||
|
||||
# Decorate object with appropriate decorator
|
||||
def decorate
|
||||
self.class.decorator_class.new(self)
|
||||
end
|
||||
|
||||
def decorated?
|
||||
false
|
||||
end
|
||||
|
||||
# Decorate collection with appropriate decorator
|
||||
def self.decorate
|
||||
decorator_class.decorate_collection(all)
|
||||
end
|
||||
|
||||
# Infers the decorator class to be used by (e.g. `User` maps to `UserDecorator`).
|
||||
# adapted from https://github.com/drapergem/draper/blob/157eb955072a941e6455e0121fca09a989fcbc21/lib/draper/decoratable.rb#L71
|
||||
def self.decorator_class(called_on = self)
|
||||
prefix = respond_to?(:model_name) ? model_name : name
|
||||
decorator_name = "#{prefix}Decorator"
|
||||
decorator_name_constant = decorator_name.safe_constantize
|
||||
return decorator_name_constant unless decorator_name_constant.nil?
|
||||
|
||||
return superclass.decorator_class(called_on) if superclass.respond_to?(:decorator_class)
|
||||
|
||||
raise UninferrableDecoratorError, "Could not infer a decorator for #{called_on.class.name}."
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ class PodcastEpisode < ApplicationRecord
|
|||
ActionView::Base.full_sanitizer.sanitize(processed_html)
|
||||
end
|
||||
|
||||
def published_at_date_slashes
|
||||
published_at&.to_date&.strftime("%m/%d/%Y")
|
||||
end
|
||||
|
||||
def user
|
||||
podcast
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ module Articles
|
|||
raise if RateLimitChecker.new(user).limit_by_action("published_article_creation")
|
||||
|
||||
article = save_article
|
||||
|
||||
if article.persisted?
|
||||
NotificationSubscription.create(user: user, notifiable_id: article.id, notifiable_type: "Article", config: "all_comments")
|
||||
Notification.send_to_followers(article, "Published") if article.published?
|
||||
|
|
@ -22,7 +21,7 @@ module Articles
|
|||
dispatch_event(article)
|
||||
end
|
||||
|
||||
article
|
||||
article.decorate
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="sidebar-bg" id="sidebar-bg-right"></div>
|
||||
<div class="side-bar sidebar-additional showing" id="sidebar-additional">
|
||||
<% @organization.sponsorships.live.find_each do |sponsorship| %>
|
||||
<div class="sidebar-sponsorship-level" style="background:<%= sponsorship.decorate.level_background_color %>">
|
||||
<div class="sidebar-sponsorship-level" style="background:<%= sponsorship.decorate.level_color_hex %>">
|
||||
<%= sponsorship.level.capitalize %> Community Sponsor ❤️
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe ApplicationDecorator, type: :decorator do
|
||||
describe "#object" do
|
||||
it "exposes the decorated object" do
|
||||
obj = Class.new
|
||||
expect(described_class.new(obj).object).to be(obj)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#decorated?" do
|
||||
it "returns true" do
|
||||
obj = Class.new
|
||||
expect(described_class.new(obj).decorated?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
# as ApplicationDecorator is an abstract class, some tests also use an actual decorator
|
||||
describe ".decorate_collection" do
|
||||
before do
|
||||
create(:sponsorship, level: :gold)
|
||||
end
|
||||
|
||||
it "receives an ActiveRecord relation and returns an array of decorated records" do
|
||||
relation = Sponsorship.gold
|
||||
|
||||
decorated_collection = described_class.decorate_collection(relation)
|
||||
expect(decorated_collection.map(&:class)).to eq([SponsorshipDecorator])
|
||||
expect(decorated_collection.map(&:object)).to eq(relation.to_a)
|
||||
end
|
||||
|
||||
it "receives an array and returns an array of decorated records" do
|
||||
relation = Sponsorship.gold
|
||||
|
||||
decorated_collection = described_class.decorate_collection(relation.to_a)
|
||||
expect(decorated_collection.map(&:class)).to eq([SponsorshipDecorator])
|
||||
expect(decorated_collection.map(&:object)).to eq(relation.to_a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,21 +6,7 @@ RSpec.describe ArticleDecorator, type: :decorator do
|
|||
article.decorate
|
||||
end
|
||||
|
||||
let(:article) { build(:article) }
|
||||
let(:organization) { build(:organization) }
|
||||
|
||||
describe "#current_state_path" do
|
||||
it "returns the path /:username/:slug when published" do
|
||||
article = create_article(published: true)
|
||||
expect(article.current_state_path).to eq("/#{article.username}/#{article.slug}")
|
||||
end
|
||||
|
||||
it "returns the path /:username/:slug?:password when draft" do
|
||||
article = create_article(published: false)
|
||||
expected_result = "/#{article.username}/#{article.slug}?preview=#{article.password}"
|
||||
expect(article.current_state_path).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
let(:article) { build_stubbed(:article) }
|
||||
|
||||
describe "#processed_canonical_url" do
|
||||
it "strips canonical_url" do
|
||||
|
|
@ -35,37 +21,6 @@ RSpec.describe ArticleDecorator, type: :decorator do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#comments_to_show_count" do
|
||||
it "returns 25 if does not have a discuss tag" do
|
||||
article.cached_tag_list = ""
|
||||
expect(article.decorate.comments_to_show_count).to eq(25)
|
||||
end
|
||||
|
||||
it "returns 75 if it does have a discuss tag" do
|
||||
article.cached_tag_list = "discuss, python"
|
||||
expect(article.decorate.comments_to_show_count).to eq(75)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#cached_tag_list_array" do
|
||||
it "returns no tags if the cached tag list is empty" do
|
||||
article.cached_tag_list = ""
|
||||
expect(article.decorate.cached_tag_list_array).to be_empty
|
||||
end
|
||||
|
||||
it "returns cached tag list as an array" do
|
||||
article.cached_tag_list = "discuss, python"
|
||||
expect(article.decorate.cached_tag_list_array).to eq(%w[discuss python])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#url" do
|
||||
it "returns the article url" do
|
||||
expected_url = "https://#{ApplicationConfig['APP_DOMAIN']}#{article.path}"
|
||||
expect(article.decorate.url).to eq(expected_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#title_length_classification" do
|
||||
it "returns article title length classifications" do
|
||||
article.title = "0" * 106
|
||||
|
|
@ -81,58 +36,6 @@ RSpec.describe ArticleDecorator, type: :decorator do
|
|||
end
|
||||
end
|
||||
|
||||
describe "internal_utm_params" do
|
||||
it "returns utm params for a boosted article" do
|
||||
article.boosted_additional_articles = true
|
||||
|
||||
params = ["utm_medium=internal", "utm_campaign=_boosted", "booster_org="]
|
||||
expected_result = "?utm_source=additional_box&#{params.join('&')}"
|
||||
expect(article.decorate.internal_utm_params).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "returns utm params for a boosted article belonging to an organization" do
|
||||
article.boosted_additional_articles = true
|
||||
article.organization = organization
|
||||
|
||||
slug = organization.slug
|
||||
params = ["utm_medium=internal", "utm_campaign=#{slug}_boosted", "booster_org=#{slug}"]
|
||||
expected_result = "?utm_source=additional_box&#{params.join('&')}"
|
||||
expect(article.decorate.internal_utm_params).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "returns utm params for a regular article" do
|
||||
article.boosted_additional_articles = false
|
||||
|
||||
params = ["utm_medium=internal", "utm_campaign=regular", "booster_org="]
|
||||
expected_result = "?utm_source=additional_box&#{params.join('&')}"
|
||||
expect(article.decorate.internal_utm_params).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "returns utm params for a regular article belonging to an organization" do
|
||||
article.boosted_additional_articles = false
|
||||
article.organization = organization
|
||||
|
||||
slug = organization.slug
|
||||
params = ["utm_medium=internal", "utm_campaign=regular", "booster_org=#{slug}"]
|
||||
expected_result = "?utm_source=additional_box&#{params.join('&')}"
|
||||
expect(article.decorate.internal_utm_params).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "returns utm params for an article in a different place" do
|
||||
article.boosted_additional_articles = false
|
||||
|
||||
params = ["utm_medium=internal", "utm_campaign=regular", "booster_org="]
|
||||
expected_result = "?utm_source=homepage&#{params.join('&')}"
|
||||
expect(article.decorate.internal_utm_params("homepage")).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#published_at_int" do
|
||||
it "returns the publication date as an integer" do
|
||||
expect(article.decorate.published_at_int).to eq(article.published_at.to_i)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#description_and_tags" do
|
||||
it "creates proper description when it is not present and body is present and short, and tags are present" do
|
||||
body_markdown = "---\ntitle: Title\npublished: false\ndescription:\ntags: heytag\n---\n\nHey this is the article"
|
||||
|
|
|
|||
|
|
@ -3,21 +3,20 @@ require "rails_helper"
|
|||
RSpec.describe CommentDecorator, type: :decorator do
|
||||
describe "#low_quality" do
|
||||
let(:threshold) { CommentDecorator::LOW_QUALITY_THRESHOLD }
|
||||
let(:comment) { build(:comment) }
|
||||
|
||||
it "returns true if the comment is low quality" do
|
||||
comment.score = threshold - 1
|
||||
expect(comment.decorate.low_quality).to be(true)
|
||||
comment = build_stubbed(:comment, score: threshold - 1).decorate
|
||||
expect(comment.low_quality).to be(true)
|
||||
end
|
||||
|
||||
it "returns false if the comment score is on threshold quality" do
|
||||
comment.score = threshold
|
||||
expect(comment.decorate.low_quality).to be(false)
|
||||
comment = build_stubbed(:comment, score: threshold).decorate
|
||||
expect(comment.low_quality).to be(false)
|
||||
end
|
||||
|
||||
it "returns false if the comment is a good quality" do
|
||||
comment.score = threshold + 1
|
||||
expect(comment.decorate.low_quality).to be(false)
|
||||
comment = build_stubbed(:comment, score: threshold + 1).decorate
|
||||
expect(comment.low_quality).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe NotificationDecorator, type: :decorator do
|
||||
describe "#mocked_object" do
|
||||
let(:comment) { create(:comment, commentable: create(:article, organization: create(:organization))) }
|
||||
|
||||
it "returns empty struct if the notification is new" do
|
||||
notification = build(:notification)
|
||||
result = notification.decorate.mocked_object("user")
|
||||
|
||||
expect(result.name).to be_empty
|
||||
expect(result.id).to be_nil
|
||||
end
|
||||
|
||||
it "returns empty struct class and its name if the notification is new" do
|
||||
notification = build(:notification)
|
||||
result = notification.decorate.mocked_object("user")
|
||||
|
||||
expect(result.class).to be_a(Struct)
|
||||
expect(result.class.name).to be_empty
|
||||
end
|
||||
|
||||
it "returns class name and id for the reactable in a struct" do
|
||||
notification = Notification.send_new_comment_notifications_without_delay(comment)
|
||||
|
||||
result = notification.decorate.mocked_object("user")
|
||||
expect(result.name).to eq("User")
|
||||
expect(result.id).to eq(comment.user.id)
|
||||
end
|
||||
|
||||
it "returns struct class and its name" do
|
||||
notification = Notification.send_new_comment_notifications_without_delay(comment)
|
||||
result = notification.decorate.mocked_object("user")
|
||||
|
||||
expect(result.class).to be_a(Struct)
|
||||
expect(result.class.name).to eq("User")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#milestone_type" do
|
||||
it "returns empty string if there is no action" do
|
||||
notification = build(:notification)
|
||||
expect(notification.decorate.milestone_type).to be_empty
|
||||
end
|
||||
|
||||
it "returns the type of the milestone action" do
|
||||
notification = build(:notification, action: "Milestone::Reaction::64")
|
||||
expect(notification.decorate.milestone_type).to eq("Reaction")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#milestone_count" do
|
||||
it "returns empty string if there is no action" do
|
||||
notification = build(:notification)
|
||||
expect(notification.decorate.milestone_count).to be_empty
|
||||
end
|
||||
|
||||
it "returns the count of the milestone action" do
|
||||
notification = build(:notification, action: "Milestone::Reaction::64")
|
||||
expect(notification.decorate.milestone_count).to eq("64")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe OrganizationDecorator, type: :decorator do
|
||||
describe "#darker_color" do
|
||||
it "returns a darker version of the assigned color if colors are blank" do
|
||||
organization = build(:organization, bg_color_hex: "", text_color_hex: "")
|
||||
expect(organization.decorate.darker_color).to eq("#090909")
|
||||
end
|
||||
|
||||
it "returns a darker version of the color if bg_color_hex is present" do
|
||||
organization = build(:organization, bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
|
||||
expect(organization.decorate.darker_color).to eq("#c2c2c2")
|
||||
end
|
||||
|
||||
it "returns an adjusted darker version of the color" do
|
||||
organization = build(:organization, bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
|
||||
expect(organization.decorate.darker_color(0.3)).to eq("#424242")
|
||||
end
|
||||
|
||||
it "returns an adjusted lighter version of the color if adjustment is over 1.0" do
|
||||
organization = build(:organization, bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
|
||||
expect(organization.decorate.darker_color(1.1)).to eq("#f3f3f3")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#enriched_colors" do
|
||||
it "returns the assigned colors if bg_color_hex is blank" do
|
||||
organization = build(:organization, bg_color_hex: "")
|
||||
expect(organization.decorate.enriched_colors).to eq(bg: "#0a0a0a", text: "#ffffff")
|
||||
end
|
||||
|
||||
it "returns bg_color_hex and assigned text_color_hex if text_color_hex is blank" do
|
||||
organization = build(:organization, bg_color_hex: "#dddddd", text_color_hex: "")
|
||||
expect(organization.decorate.enriched_colors).to eq(bg: "#dddddd", text: "#ffffff")
|
||||
end
|
||||
|
||||
it "returns bg_color_hex and text_color_hex" do
|
||||
organization = build(:organization, bg_color_hex: "#dddddd", text_color_hex: "#fffff3")
|
||||
expect(organization.decorate.enriched_colors).to eq(bg: "#dddddd", text: "#fffff3")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#assigned_color" do
|
||||
it "returns the default assigned colors" do
|
||||
organization = build(:organization)
|
||||
expect(organization.decorate.assigned_color).to eq(bg: "#0a0a0a", text: "#ffffff")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#fully_banished?" do
|
||||
it "returns false" do
|
||||
organization = build(:organization)
|
||||
expect(organization.decorate.fully_banished?).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,57 +3,8 @@ require "rails_helper"
|
|||
RSpec.describe PodcastEpisodeDecorator, type: :decorator do
|
||||
describe "#comments_to_show_count" do
|
||||
it "returns 25 if does not have a discuss tag" do
|
||||
pe = build(:podcast_episode)
|
||||
expect(pe.decorate.comments_to_show_count).to eq(25)
|
||||
end
|
||||
|
||||
it "returns 75 if it does have a discuss tag" do
|
||||
pe = build(:podcast_episode, tag_list: ["discuss"])
|
||||
expect(pe.decorate.comments_to_show_count).to eq(75)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#cached_tag_list_array" do
|
||||
it "returns no tags if the tag list is empty" do
|
||||
pe = build(:podcast_episode, tag_list: [])
|
||||
expect(pe.decorate.cached_tag_list_array).to be_empty
|
||||
end
|
||||
|
||||
it "returns tag list" do
|
||||
pe = build(:podcast_episode, tag_list: ["discuss"])
|
||||
expect(pe.decorate.cached_tag_list_array).to eq(pe.tag_list)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#readable_publish_date" do
|
||||
it "returns empty string if the episode does not have a published_at" do
|
||||
pe = build(:podcast_episode, published_at: nil)
|
||||
expect(pe.decorate.readable_publish_date).to be_empty
|
||||
end
|
||||
|
||||
it "returns the correct date for a same year publication" do
|
||||
published_at = Time.current
|
||||
pe = build(:podcast_episode, published_at: published_at)
|
||||
expect(pe.decorate.readable_publish_date).to eq(published_at.strftime("%b %e"))
|
||||
end
|
||||
|
||||
it "returns the correct date for a publication within a different year" do
|
||||
published_at = 2.years.ago
|
||||
pe = build(:podcast_episode, published_at: published_at)
|
||||
expect(pe.decorate.readable_publish_date).to eq(published_at.strftime("%b %e '%y"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "#published_timestamp" do
|
||||
it "returns empty string if the episode does not have a published_at" do
|
||||
pe = build(:podcast_episode, published_at: nil)
|
||||
expect(pe.decorate.published_timestamp).to be_empty
|
||||
end
|
||||
|
||||
it "returns the correct date for a published episode" do
|
||||
published_at = Time.current
|
||||
pe = build(:podcast_episode, published_at: published_at)
|
||||
expect(pe.decorate.published_timestamp).to eq(published_at.utc.iso8601)
|
||||
pe = build_stubbed(:podcast_episode).decorate
|
||||
expect(pe.comments_to_show_count).to eq(25)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe SponsorshipDecorator, type: :decorator do
|
||||
describe "#level_background_color" do
|
||||
let(:sponsorship) { build(:sponsorship) }
|
||||
|
||||
it "returns the correct hex for gold" do
|
||||
sponsorship.level = "gold"
|
||||
expected_result = "linear-gradient(to right, #faf0e6 8%, #faf3e6 18%, #fcf6eb 33%);"
|
||||
expect(sponsorship.decorate.level_background_color).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "returns empty string for unsupported level" do
|
||||
sponsorship.level = "media"
|
||||
expect(sponsorship.decorate.level_background_color).to eq("")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,92 +1,48 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UserDecorator, type: :decorator do
|
||||
let_it_be_changeable(:saved_user) { create(:user) }
|
||||
let(:user) { build(:user) }
|
||||
let(:user) { build_stubbed(:user) }
|
||||
|
||||
describe "#cached_followed_tags" do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let(:tag1) { create(:tag) }
|
||||
let(:tag2) { create(:tag) }
|
||||
let(:tag3) { create(:tag) }
|
||||
|
||||
it "returns empty if no tags followed" do
|
||||
expect(saved_user.decorate.cached_followed_tags.size).to eq(0)
|
||||
expect(user.decorate.cached_followed_tags.size).to eq(0)
|
||||
end
|
||||
|
||||
it "returns array of tags if user follows them" do
|
||||
saved_user.follow(tag1)
|
||||
saved_user.follow(tag2)
|
||||
saved_user.follow(tag3)
|
||||
expect(saved_user.decorate.cached_followed_tags.size).to eq(3)
|
||||
user.follow(tag1)
|
||||
user.follow(tag2)
|
||||
user.follow(tag3)
|
||||
expect(user.decorate.cached_followed_tags.size).to eq(3)
|
||||
end
|
||||
|
||||
it "returns tag object with name" do
|
||||
saved_user.follow(tag1)
|
||||
expect(saved_user.decorate.cached_followed_tags.first.name).to eq(tag1.name)
|
||||
user.follow(tag1)
|
||||
expect(user.decorate.cached_followed_tags.first.name).to eq(tag1.name)
|
||||
end
|
||||
|
||||
it "returns follow points for tag" do
|
||||
saved_user.follow(tag1)
|
||||
expect(saved_user.decorate.cached_followed_tags.first.points).to eq(1.0)
|
||||
user.follow(tag1)
|
||||
expect(user.decorate.cached_followed_tags.first.points).to eq(1.0)
|
||||
end
|
||||
|
||||
it "returns adjusted points for tag" do
|
||||
follow = saved_user.follow(tag1)
|
||||
follow = user.follow(tag1)
|
||||
follow.update(points: 0.1)
|
||||
expect(saved_user.decorate.cached_followed_tags.first.points).to eq(0.1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#darker_color" do
|
||||
it "returns a darker version of the assigned color if colors are blank" do
|
||||
saved_user.assign_attributes(bg_color_hex: "", text_color_hex: "")
|
||||
expect(saved_user.decorate.darker_color).to be_present
|
||||
expect(user.decorate.cached_followed_tags.first.points).to eq(0.1)
|
||||
end
|
||||
|
||||
it "returns a darker version of the color if bg_color_hex is present" do
|
||||
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
|
||||
expect(saved_user.decorate.darker_color).to eq("#c2c2c2")
|
||||
it "returns not fully banished if in good standing" do
|
||||
expect(user.decorate.fully_banished?).to eq(false)
|
||||
end
|
||||
|
||||
it "returns an adjusted darker version of the color" do
|
||||
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
|
||||
expect(saved_user.decorate.darker_color(0.3)).to eq("#424242")
|
||||
end
|
||||
|
||||
it "returns an adjusted lighter version of the color if adjustment is over 1.0" do
|
||||
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "#ffffff")
|
||||
expect(saved_user.decorate.darker_color(1.1)).to eq("#f3f3f3")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#enriched_colors" do
|
||||
it "returns assigned colors if bg_color_hex is blank" do
|
||||
saved_user.assign_attributes(bg_color_hex: "")
|
||||
expect(saved_user.decorate.enriched_colors[:bg]).to be_present
|
||||
expect(saved_user.decorate.enriched_colors[:text]).to be_present
|
||||
end
|
||||
|
||||
it "returns assigned colors if text_color_hex is blank" do
|
||||
saved_user.assign_attributes(text_color_hex: "")
|
||||
expect(saved_user.decorate.enriched_colors[:bg]).to be_present
|
||||
expect(saved_user.decorate.enriched_colors[:text]).to be_present
|
||||
end
|
||||
|
||||
it "returns bg_color_hex and assigned text_color_hex if text_color_hex is blank" do
|
||||
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "")
|
||||
expect(saved_user.decorate.enriched_colors[:bg]).to be_present
|
||||
expect(saved_user.decorate.enriched_colors[:text]).to be_present
|
||||
end
|
||||
|
||||
it "returns text_color_hex and assigned bg_color_hex if bg_color_hex is blank" do
|
||||
saved_user.assign_attributes(bg_color_hex: "", text_color_hex: "#ffffff")
|
||||
expect(saved_user.decorate.enriched_colors[:bg]).to be_present
|
||||
expect(saved_user.decorate.enriched_colors[:text]).to be_present
|
||||
end
|
||||
|
||||
it "returns bg_color_hex and text_color_hex if both are present" do
|
||||
saved_user.assign_attributes(bg_color_hex: "#dddddd", text_color_hex: "#fffff3")
|
||||
expect(saved_user.decorate.enriched_colors).to eq(bg: "#dddddd", text: "#fffff3")
|
||||
it "returns fully banished if user has been banished" do
|
||||
Moderator::BanishUser.call(admin: user, user: user)
|
||||
expect(user.decorate.fully_banished?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -185,26 +141,4 @@ RSpec.describe UserDecorator, type: :decorator do
|
|||
expect(user.decorate.dark_theme?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#fully_banished?" do
|
||||
it "returns not fully banished if in good standing" do
|
||||
expect(user.decorate.fully_banished?).to eq(false)
|
||||
end
|
||||
|
||||
it "returns fully banished if user has been banished" do
|
||||
Moderator::BanishUser.call(admin: user, user: user)
|
||||
expect(user.decorate.fully_banished?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#stackbit_integration?" do
|
||||
it "returns false by default" do
|
||||
expect(user.decorate.stackbit_integration?).to be(false)
|
||||
end
|
||||
|
||||
it "returns true if the user has access tokens" do
|
||||
user.access_tokens.build
|
||||
expect(user.decorate.stackbit_integration?).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,35 +7,4 @@ RSpec.describe ApplicationRecord, type: :model do
|
|||
expect { User.estimated_count }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe "#decorate" do
|
||||
it "decorates an object that has a decorator" do
|
||||
sponsorship = build(:sponsorship)
|
||||
expect(sponsorship.decorate).to be_a(SponsorshipDecorator)
|
||||
end
|
||||
|
||||
it "raises an error if an object has no decorator" do
|
||||
badge = build(:badge)
|
||||
expect { badge.decorate }.to raise_error(UninferrableDecoratorError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#decorated?" do
|
||||
it "returns false" do
|
||||
sponsorship = build(:sponsorship)
|
||||
expect(sponsorship.decorated?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".decorate" do
|
||||
before do
|
||||
create(:sponsorship, level: :gold)
|
||||
end
|
||||
|
||||
it "decorates a relation" do
|
||||
decorated_collection = Sponsorship.gold.decorate
|
||||
expect(decorated_collection.size).to eq(Sponsorship.gold.size)
|
||||
expect(decorated_collection.first).to be_a(SponsorshipDecorator)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe "ArticlesCreate", type: :request do
|
||||
let(:user) { create(:user, :org_member) }
|
||||
let(:template) { file_fixture("article_published.txt").read }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
|
|
@ -49,15 +48,6 @@ RSpec.describe "ArticlesCreate", type: :request do
|
|||
expect(Collection.last.slug).to eq("helloyo")
|
||||
end
|
||||
|
||||
it "returns the ID and the current_state_path of the article" do
|
||||
post "/articles", params: { article: { body_markdown: template } }
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
article = Article.last
|
||||
expect(response.parsed_body["id"]).to eq(article.id)
|
||||
expect(response.parsed_body["current_state_path"]).to eq(article.current_state_path)
|
||||
end
|
||||
|
||||
context "when scheduling jobs" do
|
||||
let(:url) { Faker::Internet.url(scheme: "https") }
|
||||
let(:article_params) do
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
end.to change(Article, :count).by(1)
|
||||
end
|
||||
|
||||
it "returns a non decorated, persisted, article" do
|
||||
it "returns an article" do
|
||||
article = described_class.call(user, valid_attributes)
|
||||
|
||||
expect(article.decorated?).to be(false)
|
||||
expect(article).to be_kind_of(Article)
|
||||
expect(article).to be_persisted
|
||||
end
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
event_dispatcher = double
|
||||
allow(event_dispatcher).to receive(:call)
|
||||
article = described_class.call(user, valid_attributes, event_dispatcher)
|
||||
expect(event_dispatcher).to have_received(:call).with("article_created", article)
|
||||
expect(event_dispatcher).to have_received(:call).with("article_created", article.object)
|
||||
end
|
||||
|
||||
it "doesn't call an event dispatcher when an article is unpublished" do
|
||||
|
|
@ -44,7 +43,7 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
event_dispatcher = double
|
||||
allow(event_dispatcher).to receive(:call)
|
||||
article = described_class.call(user, attributes, event_dispatcher)
|
||||
expect(event_dispatcher).not_to have_received(:call).with("article_created", article)
|
||||
expect(event_dispatcher).not_to have_received(:call).with("article_created", article.object)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -61,10 +60,9 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
end.not_to change(Article, :count)
|
||||
end
|
||||
|
||||
it "returns a non decorated, non persisted, article" do
|
||||
it "returns an unsaved article" do
|
||||
article = described_class.call(user, invalid_attributes)
|
||||
|
||||
expect(article.decorated?).to be(false)
|
||||
expect(article).to be_kind_of(Article)
|
||||
expect(article).not_to be_persisted
|
||||
expect(article.errors.size).to eq(1)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue