Switch decorators to Rails builtin decorations - take 2 (#6125) [deploy]
* Revert "Revert "[deploy] Switch decorators from Draper to Rails builtin decorations (#6040)" (#6122) [deploy]"
This reverts commit d438349550.
* Enable serialization for decorators
* Refactor and test default home feed serialization
* Improve spec
* Applied @vaidehijoshi suggestions
* Applied @citizen428 suggestions
* Make CodeClimate happy
* Cleanup stories_index_spec.rb
* Refactor UserDecorator#cached_followed_tags to loop less
This commit is contained in:
parent
aee50e9ea0
commit
bc385ae6d5
32 changed files with 778 additions and 152 deletions
1
Gemfile
1
Gemfile
|
|
@ -32,7 +32,6 @@ 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,10 +39,6 @@ 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)
|
||||
|
|
@ -233,12 +229,6 @@ 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.11.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 0.4, >= 0.4.7)
|
||||
|
|
@ -887,7 +877,6 @@ 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)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ module Api
|
|||
end
|
||||
|
||||
def create
|
||||
@article = Articles::Creator.call(@user, article_params)
|
||||
@article = Articles::Creator.call(@user, article_params).decorate
|
||||
|
||||
if @article.persisted?
|
||||
render "show", status: :created, location: @article.url
|
||||
else
|
||||
|
|
|
|||
|
|
@ -88,14 +88,16 @@ class ArticlesController < ApplicationController
|
|||
|
||||
def edit
|
||||
authorize @article
|
||||
|
||||
@version = @article.has_frontmatter? ? "v1" : "v2"
|
||||
@user = @article.user
|
||||
@organizations = @user&.organizations
|
||||
end
|
||||
|
||||
def manage
|
||||
@article = @article.decorate
|
||||
authorize @article
|
||||
|
||||
@article = @article.decorate
|
||||
@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)
|
||||
|
|
@ -136,13 +138,12 @@ class ArticlesController < ApplicationController
|
|||
def create
|
||||
authorize Article
|
||||
|
||||
@user = current_user
|
||||
@article = Articles::Creator.call(@user, article_params_json)
|
||||
article = Articles::Creator.call(current_user, article_params_json)
|
||||
|
||||
render json: if @article.persisted?
|
||||
@article.to_json(only: [:id], methods: [:current_state_path])
|
||||
render json: if article.persisted?
|
||||
{ id: article.id, current_state_path: article.decorate.current_state_path }.to_json
|
||||
else
|
||||
@article.errors.to_json
|
||||
article.errors.to_json
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
class StoriesController < ApplicationController
|
||||
DEFAULT_HOME_FEED_ATTRIBUTES_FOR_SERIALIZATION = {
|
||||
only: %i[
|
||||
title path id user_id comments_count positive_reactions_count organization_id
|
||||
reading_time video_thumbnail_url video video_duration_in_minutes language
|
||||
experience_level_rating experience_level_rating_distribution cached_user cached_organization
|
||||
],
|
||||
methods: %i[
|
||||
readable_publish_date cached_tag_list_array flare_tag class_name
|
||||
cloudinary_video_url video_duration_in_minutes published_at_int published_timestamp
|
||||
]
|
||||
}.freeze
|
||||
|
||||
before_action :authenticate_user!, except: %i[index search show]
|
||||
before_action :set_cache_control_headers, only: %i[index search show]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,20 @@
|
|||
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
|
||||
class ApplicationDecorator
|
||||
include ActiveModel::Serialization
|
||||
include ActiveModel::Serializers::JSON
|
||||
|
||||
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
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
class ArticleDecorator < ApplicationDecorator
|
||||
delegate_all
|
||||
|
||||
def current_state_path
|
||||
published ? "/#{username}/#{slug}" : "/#{username}/#{slug}?preview=#{password}"
|
||||
end
|
||||
|
|
@ -26,13 +24,13 @@ class ArticleDecorator < ApplicationDecorator
|
|||
end
|
||||
|
||||
def title_length_classification
|
||||
if article.title.size > 105
|
||||
if title.size > 105
|
||||
"longest"
|
||||
elsif article.title.size > 80
|
||||
elsif title.size > 80
|
||||
"longer"
|
||||
elsif article.title.size > 60
|
||||
elsif title.size > 60
|
||||
"long"
|
||||
elsif article.title.size > 22
|
||||
elsif title.size > 22
|
||||
"medium"
|
||||
else
|
||||
"short"
|
||||
|
|
@ -40,12 +38,15 @@ class ArticleDecorator < ApplicationDecorator
|
|||
end
|
||||
|
||||
def internal_utm_params(place = "additional_box")
|
||||
org_slug = organization&.slug
|
||||
|
||||
campaign = if boosted_additional_articles
|
||||
"#{organization&.slug}_boosted"
|
||||
"#{org_slug}_boosted"
|
||||
else
|
||||
"regular"
|
||||
end
|
||||
"?utm_source=#{place}&utm_medium=internal&utm_campaign=#{campaign}&booster_org=#{organization&.slug}"
|
||||
|
||||
"?utm_source=#{place}&utm_medium=internal&utm_campaign=#{campaign}&booster_org=#{org_slug}"
|
||||
end
|
||||
|
||||
def published_at_int
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
class CommentDecorator < ApplicationDecorator
|
||||
delegate_all
|
||||
|
||||
LOW_QUALITY_THRESHOLD = -75
|
||||
|
||||
def low_quality
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
class NotificationDecorator < Draper::Decorator
|
||||
delegate_all
|
||||
|
||||
def mocked_object(type)
|
||||
struct = Struct.new(:name, :id) do
|
||||
def class
|
||||
second_struct = Struct.new(:name)
|
||||
second_struct.new(name)
|
||||
end
|
||||
class NotificationDecorator < ApplicationDecorator
|
||||
NOTIFIABLE_STUB = Struct.new(:name, :id) do
|
||||
def class
|
||||
Struct.new(:name).new(name)
|
||||
end
|
||||
struct.new(json_data[type]["class"]["name"], json_data[type]["id"])
|
||||
end.freeze
|
||||
|
||||
# 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"])
|
||||
end
|
||||
|
||||
# returns the type of a milestone notification action,
|
||||
# eg. "Milestone::Reaction::64"
|
||||
def milestone_type
|
||||
action.split("::")[1]
|
||||
return "" if action.blank?
|
||||
|
||||
action.split("::").second
|
||||
end
|
||||
|
||||
# returns the count of a milestone notification action,
|
||||
# eg. "Milestone::Reaction::64"
|
||||
def milestone_count
|
||||
action.split("::")[2]
|
||||
return "" if action.blank?
|
||||
|
||||
action.split("::").third
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
class OrganizationDecorator < ApplicationDecorator
|
||||
delegate_all
|
||||
|
||||
def darker_color(adjustment = 0.88)
|
||||
HexComparer.new([enriched_colors[:bg], enriched_colors[:text]]).brightness(adjustment)
|
||||
end
|
||||
|
|
@ -13,8 +11,8 @@ class OrganizationDecorator < ApplicationDecorator
|
|||
}
|
||||
else
|
||||
{
|
||||
bg: bg_color_hex || assigned_color[:bg],
|
||||
text: text_color_hex || assigned_color[:text]
|
||||
bg: bg_color_hex,
|
||||
text: text_color_hex.presence || assigned_color[:text]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
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 || "").split(", ")
|
||||
tag_list
|
||||
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")
|
||||
|
|
@ -20,6 +21,8 @@ class PodcastEpisodeDecorator < ApplicationDecorator
|
|||
end
|
||||
|
||||
def published_timestamp
|
||||
published_at&.utc&.iso8601
|
||||
return "" unless published_at
|
||||
|
||||
published_at.utc.iso8601
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
class ReactionDecorator < ApplicationDecorator; end
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
class SponsorshipDecorator < ApplicationDecorator
|
||||
delegate_all
|
||||
|
||||
def level_color_hex
|
||||
def level_background_color
|
||||
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]
|
||||
|
||||
hexes[level].to_s
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,15 +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_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_query.detect { |f| f[0] == t.id }
|
||||
t.points = follow_query_item[1]
|
||||
follows = Follow.where(follower_id: id, followable_type: "ActsAsTaggableOn::Tag").pluck(:followable_id, :points)
|
||||
follows_map = follows.to_h
|
||||
|
||||
tags = Tag.where(id: follows_map.keys).order(hotness_score: :desc)
|
||||
tags.each do |tag|
|
||||
tag.points = follows_map[tag.id]
|
||||
end
|
||||
tags
|
||||
end
|
||||
|
|
@ -44,20 +43,21 @@ class UserDecorator < ApplicationDecorator
|
|||
}
|
||||
else
|
||||
{
|
||||
bg: bg_color_hex || assigned_color[:bg],
|
||||
text: text_color_hex || assigned_color[:text]
|
||||
bg: bg_color_hex,
|
||||
text: text_color_hex
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def config_body_class
|
||||
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
|
||||
body_class = [
|
||||
config_theme.tr("_", "-"),
|
||||
"#{config_font.tr('_', '-')}-article-body",
|
||||
"pro-status-#{pro?}",
|
||||
"trusted-status-#{trusted}",
|
||||
"#{config_navbar.tr('_', '-')}-navbar-config",
|
||||
]
|
||||
body_class.join(" ")
|
||||
end
|
||||
|
||||
def dark_theme?
|
||||
|
|
@ -95,11 +95,9 @@ class UserDecorator < ApplicationDecorator
|
|||
colors[id % 10]
|
||||
end
|
||||
|
||||
# returns true if the user has been suspended and has no content
|
||||
def fully_banished?
|
||||
# User suspended and has no content
|
||||
articles_count.zero? &&
|
||||
comments_count.zero? &&
|
||||
banned
|
||||
articles_count.zero? && comments_count.zero? && banned
|
||||
end
|
||||
|
||||
def stackbit_integration?
|
||||
|
|
|
|||
4
app/errors/uninferrable_decorator_error.rb
Normal file
4
app/errors/uninferrable_decorator_error.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# raised when an object or collection tries to decorate itself,
|
||||
# without having an inferrable decorator
|
||||
class UninferrableDecoratorError < NameError
|
||||
end
|
||||
|
|
@ -20,4 +20,31 @@ 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,10 +98,6 @@ 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,6 +14,7 @@ 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?
|
||||
|
|
@ -21,7 +22,7 @@ module Articles
|
|||
dispatch_event(article)
|
||||
end
|
||||
|
||||
article.decorate
|
||||
article
|
||||
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_color_hex %>">
|
||||
<div class="sidebar-sponsorship-level" style="background:<%= sponsorship.decorate.level_background_color %>">
|
||||
<%= sponsorship.level.capitalize %> Community Sponsor ❤️
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,8 @@
|
|||
<% cache("fetched-home-articles-object-#{ApplicationConfig['HEROKU_SLUG_COMMIT']}", expires_in: 3.minutes) do %>
|
||||
<div id="followed-podcasts" data-episodes="<%= @podcast_episodes.to_json(include: { podcast: { only: %i[slug title id], methods: %i[image_90] } }) %>">
|
||||
</div>
|
||||
<div id="home-articles-object" data-articles="
|
||||
<%= @stories.to_json(
|
||||
only: %i[title path id user_id comments_count positive_reactions_count organization_id
|
||||
reading_time video_thumbnail_url video video_duration_in_minutes language
|
||||
experience_level_rating experience_level_rating_distribution cached_user cached_organization],
|
||||
methods: %i[readable_publish_date cached_tag_list_array flare_tag class_name
|
||||
cloudinary_video_url video_duration_in_minutes published_at_int
|
||||
published_timestamp],
|
||||
) %>">
|
||||
<div id="home-articles-object"
|
||||
data-articles="<%= @stories.to_json(controller.class.const_get(:DEFAULT_HOME_FEED_ATTRIBUTES_FOR_SERIALIZATION)) %>">
|
||||
<% 3.times do %>
|
||||
<div class="single-article single-article-small-pic">
|
||||
<div class="small-pic">
|
||||
|
|
|
|||
40
spec/decorators/application_decorator_spec.rb
Normal file
40
spec/decorators/application_decorator_spec.rb
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe ApplicationDecorator, type: :decorator do
|
||||
describe "#object" do
|
||||
it "exposes the decorated object" do
|
||||
obj = Object.new
|
||||
expect(described_class.new(obj).object).to be(obj)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#decorated?" do
|
||||
it "returns true" do
|
||||
obj = Object.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,7 +6,37 @@ RSpec.describe ArticleDecorator, type: :decorator do
|
|||
article.decorate
|
||||
end
|
||||
|
||||
let(:article) { build_stubbed(:article) }
|
||||
let(:article) { build(:article) }
|
||||
let(:published_article) { create_article(published: true) }
|
||||
let(:organization) { build(:organization) }
|
||||
|
||||
context "with serialization" do
|
||||
it "serializes both the decorated object IDs and decorated methods" do
|
||||
article = published_article
|
||||
expected_result = { "id" => article.id, "published_at_int" => article.published_at_int }
|
||||
expect(article.as_json(only: [:id], methods: [:published_at_int])).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "serializes collections of decorated objects" do
|
||||
article = published_article
|
||||
decorated_collection = Article.published.decorate
|
||||
expected_result = [{ "id" => article.id, "published_at_int" => article.published_at_int }]
|
||||
expect(decorated_collection.as_json(only: [:id], methods: [:published_at_int])).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#current_state_path" do
|
||||
it "returns the path /:username/:slug when published" do
|
||||
article = published_article
|
||||
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
|
||||
|
||||
describe "#processed_canonical_url" do
|
||||
it "strips canonical_url" do
|
||||
|
|
@ -21,6 +51,37 @@ 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
|
||||
|
|
@ -36,6 +97,58 @@ 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"
|
||||
|
|
|
|||
|
|
@ -1,26 +1,42 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe CommentDecorator, type: :decorator do
|
||||
describe "#low_quality" do
|
||||
let(:threshold) { CommentDecorator::LOW_QUALITY_THRESHOLD }
|
||||
context "with serialization" do
|
||||
let_it_be_readonly(:comment) { create(:comment).decorate }
|
||||
|
||||
it "returns true if the comment is low quality" do
|
||||
comment = build_stubbed(:comment, score: threshold - 1).decorate
|
||||
expect(comment.low_quality).to be(true)
|
||||
it "serializes both the decorated object IDs and decorated methods" do
|
||||
expected_result = { "id" => comment.id, "published_timestamp" => comment.published_timestamp }
|
||||
expect(comment.as_json(only: [:id], methods: [:published_timestamp])).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "returns false if the comment score is on threshold quality" do
|
||||
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 = build_stubbed(:comment, score: threshold + 1).decorate
|
||||
expect(comment.low_quality).to be(false)
|
||||
it "serializes collections of decorated objects" do
|
||||
decorated_collection = Comment.decorate
|
||||
expected_result = [{ "id" => comment.id, "published_timestamp" => comment.published_timestamp }]
|
||||
expect(decorated_collection.as_json(only: [:id], methods: [:published_timestamp])).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe "published_timestamp" 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)
|
||||
end
|
||||
|
||||
it "returns false if the comment score is on threshold quality" do
|
||||
comment.score = threshold
|
||||
expect(comment.decorate.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)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#published_timestamp" do
|
||||
it "returns empty string if the comment is new" do
|
||||
expect(Comment.new.decorate.published_timestamp).to eq("")
|
||||
end
|
||||
|
|
|
|||
76
spec/decorators/notification_decorator_spec.rb
Normal file
76
spec/decorators/notification_decorator_spec.rb
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe NotificationDecorator, type: :decorator do
|
||||
let(:notification) { build(:notification) }
|
||||
|
||||
context "with serialization" do
|
||||
let_it_be_readonly(:notification) { create(:notification).decorate }
|
||||
|
||||
it "serializes both the decorated object IDs and decorated methods" do
|
||||
expected_result = { "id" => notification.id, "milestone_type" => notification.milestone_type }
|
||||
expect(notification.as_json(only: [:id], methods: [:milestone_type])).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "serializes collections of decorated objects" do
|
||||
decorated_collection = Notification.decorate
|
||||
expected_result = [{ "id" => notification.id, "milestone_type" => notification.milestone_type }]
|
||||
expect(decorated_collection.as_json(only: [:id], methods: [:milestone_type])).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#mocked_object" do
|
||||
let(:comment) { create(:comment, commentable: create(:article, organization: create(:organization))) }
|
||||
|
||||
it "returns empty struct if the notification is new" do
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
71
spec/decorators/organization_decorator_spec.rb
Normal file
71
spec/decorators/organization_decorator_spec.rb
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe OrganizationDecorator, type: :decorator do
|
||||
context "with serialization" do
|
||||
let_it_be_readonly(:organization) { create(:organization).decorate }
|
||||
|
||||
it "serializes both the decorated object IDs and decorated methods" do
|
||||
expected_result = { "id" => organization.id, "fully_banished?" => organization.fully_banished? }
|
||||
expect(organization.as_json(only: [:id], methods: [:fully_banished?])).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "serializes collections of decorated objects" do
|
||||
decorated_collection = Organization.decorate
|
||||
expected_result = [{ "id" => organization.id, "fully_banished?" => organization.fully_banished? }]
|
||||
expect(decorated_collection.as_json(only: [:id], methods: [:fully_banished?])).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
|
@ -1,10 +1,80 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe PodcastEpisodeDecorator, type: :decorator do
|
||||
context "with serialization" do
|
||||
let(:podcast_episode) { create(:podcast_episode).decorate }
|
||||
|
||||
it "serializes both the decorated object IDs and decorated methods" do
|
||||
expected_result = {
|
||||
"id" => podcast_episode.id, "comments_to_show_count" => podcast_episode.comments_to_show_count
|
||||
}
|
||||
expect(podcast_episode.as_json(only: [:id], methods: [:comments_to_show_count])).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "serializes collections of decorated objects" do
|
||||
podcast_episode # for the side effect
|
||||
|
||||
decorated_collection = PodcastEpisode.decorate
|
||||
expected_result = [
|
||||
{ "id" => podcast_episode.id, "comments_to_show_count" => podcast_episode.comments_to_show_count },
|
||||
]
|
||||
expect(decorated_collection.as_json(only: [:id], methods: [:comments_to_show_count])).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#comments_to_show_count" do
|
||||
it "returns 25 if does not have a discuss tag" do
|
||||
pe = build_stubbed(:podcast_episode).decorate
|
||||
expect(pe.comments_to_show_count).to eq(25)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
33
spec/decorators/sponsorship_decorator_spec.rb
Normal file
33
spec/decorators/sponsorship_decorator_spec.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe SponsorshipDecorator, type: :decorator do
|
||||
context "with serialization" do
|
||||
let_it_be_readonly(:sponsorship) { create(:sponsorship).decorate }
|
||||
|
||||
it "serializes both the decorated object IDs and decorated methods" do
|
||||
expected_result = { "id" => sponsorship.id, "level_background_color" => sponsorship.level_background_color }
|
||||
expect(sponsorship.as_json(only: [:id], methods: [:level_background_color])).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "serializes collections of decorated objects" do
|
||||
decorated_collection = Sponsorship.decorate
|
||||
expected_result = [{ "id" => sponsorship.id, "level_background_color" => sponsorship.level_background_color }]
|
||||
expect(decorated_collection.as_json(only: [:id], methods: [:level_background_color])).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
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,48 +1,107 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UserDecorator, type: :decorator do
|
||||
let(:user) { build_stubbed(:user) }
|
||||
let_it_be_changeable(:saved_user) { create(:user) }
|
||||
let(:user) { build(:user) }
|
||||
|
||||
context "with serialization" do
|
||||
it "serializes both the decorated object IDs and decorated methods" do
|
||||
user = saved_user.decorate
|
||||
expected_result = { "id" => user.id, "dark_theme?" => user.dark_theme? }
|
||||
expect(user.as_json(only: [:id], methods: [:dark_theme?])).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "serializes collections of decorated objects" do
|
||||
user = saved_user.decorate
|
||||
decorated_collection = User.decorate
|
||||
expected_result = [{ "id" => user.id, "dark_theme?" => user.dark_theme? }]
|
||||
expect(decorated_collection.as_json(only: [:id], methods: [:dark_theme?])).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
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(user.decorate.cached_followed_tags.size).to eq(0)
|
||||
expect(saved_user.decorate.cached_followed_tags.size).to eq(0)
|
||||
end
|
||||
|
||||
it "returns array of tags if user follows them" do
|
||||
user.follow(tag1)
|
||||
user.follow(tag2)
|
||||
user.follow(tag3)
|
||||
expect(user.decorate.cached_followed_tags.size).to eq(3)
|
||||
saved_user.follow(tag1)
|
||||
saved_user.follow(tag2)
|
||||
saved_user.follow(tag3)
|
||||
expect(saved_user.decorate.cached_followed_tags.size).to eq(3)
|
||||
end
|
||||
|
||||
it "returns tag object with name" do
|
||||
user.follow(tag1)
|
||||
expect(user.decorate.cached_followed_tags.first.name).to eq(tag1.name)
|
||||
saved_user.follow(tag1)
|
||||
expect(saved_user.decorate.cached_followed_tags.first.name).to eq(tag1.name)
|
||||
end
|
||||
|
||||
it "returns follow points for tag" do
|
||||
user.follow(tag1)
|
||||
expect(user.decorate.cached_followed_tags.first.points).to eq(1.0)
|
||||
saved_user.follow(tag1)
|
||||
expect(saved_user.decorate.cached_followed_tags.first.points).to eq(1.0)
|
||||
end
|
||||
|
||||
it "returns adjusted points for tag" do
|
||||
follow = user.follow(tag1)
|
||||
follow = saved_user.follow(tag1)
|
||||
follow.update(points: 0.1)
|
||||
expect(user.decorate.cached_followed_tags.first.points).to eq(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
|
||||
end
|
||||
|
||||
it "returns not fully banished if in good standing" do
|
||||
expect(user.decorate.fully_banished?).to eq(false)
|
||||
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")
|
||||
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)
|
||||
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")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -141,4 +200,26 @@ 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,4 +7,35 @@ 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,6 +2,7 @@ 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
|
||||
|
|
@ -48,6 +49,15 @@ 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
|
||||
|
|
|
|||
|
|
@ -76,33 +76,38 @@ RSpec.describe "StoriesIndex", type: :request do
|
|||
end
|
||||
|
||||
context "with campaign hero" do
|
||||
let!(:hero_html) { create(:html_variant, group: "campaign", name: "hero", html: Faker::Book.title, published: true, approved: true) }
|
||||
let!(:hero_html) do
|
||||
create(:html_variant, group: "campaign", name: "hero", html: Faker::Book.title, published: true, approved: true)
|
||||
end
|
||||
|
||||
it "displays hero html when it exists and is set in config" do
|
||||
SiteConfig.campaign_hero_html_variant_name = "hero"
|
||||
get "/"
|
||||
expect(response.body).to include(CGI.escapeHTML(hero_html.html))
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't display when campaign_hero_html_variant_name is not set" do
|
||||
it "doesn't display when campaign_hero_html_variant_name is not set" do
|
||||
SiteConfig.campaign_hero_html_variant_name = ""
|
||||
get "/"
|
||||
expect(response.body).not_to include(CGI.escapeHTML(hero_html.html))
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't display when hero html is not approved" do
|
||||
it "doesn't display when hero html is not approved" do
|
||||
SiteConfig.campaign_hero_html_variant_name = "hero"
|
||||
hero_html.update_column(:approved, false)
|
||||
get "/"
|
||||
expect(response.body).not_to include(CGI.escapeHTML(hero_html.html))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with campaign_sidebar" do
|
||||
context "with campaign_sidebar" do
|
||||
before do
|
||||
SiteConfig.campaign_featured_tags = "shecoded,theycoded"
|
||||
create(:article, approved: true, body_markdown: "---\ntitle: Super-sheep#{rand(1000)}\npublished: true\ntags: heyheyhey,shecoded\n---\n\nHello")
|
||||
create(:article, approved: false, body_markdown: "---\ntitle: Unapproved-post#{rand(1000)}\npublished: true\ntags: heyheyhey,shecoded\n---\n\nHello")
|
||||
|
||||
a_body = "---\ntitle: Super-sheep#{rand(1000)}\npublished: true\ntags: heyheyhey,shecoded\n---\n\nHello"
|
||||
create(:article, approved: true, body_markdown: a_body)
|
||||
u_body = "---\ntitle: Unapproved-post#{rand(1000)}\npublished: true\ntags: heyheyhey,shecoded\n---\n\nHello"
|
||||
create(:article, approved: false, body_markdown: u_body)
|
||||
end
|
||||
|
||||
it "doesn't display posts with the campaign tags when sidebar is disabled" do
|
||||
|
|
@ -123,6 +128,31 @@ RSpec.describe "StoriesIndex", type: :request do
|
|||
expect(response.body).not_to include(CGI.escapeHTML("Super-puper"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "when authenticated" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "contains the stories correctly serialized" do
|
||||
# we control titles to avoid escaping errors with apostrophes and such
|
||||
article = create(:article, featured: true)
|
||||
article.update_columns(title: "abc")
|
||||
|
||||
articles = create_list(:article, 2)
|
||||
articles.each { |a| a.update_columns(title: "abc") }
|
||||
|
||||
get root_path
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
stories = controller.instance_variable_get(:@stories) # cheating a bit ;)
|
||||
expected_result = ArticleDecorator.decorate_collection(stories).
|
||||
to_json(controller.class.const_get(:DEFAULT_HOME_FEED_ATTRIBUTES_FOR_SERIALIZATION))
|
||||
expect(response.body).to include(ERB::Util.html_escape(expected_result))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET query page" do
|
||||
|
|
@ -143,6 +173,19 @@ RSpec.describe "StoriesIndex", type: :request do
|
|||
|
||||
describe "GET tag index" do
|
||||
let(:tag) { create(:tag) }
|
||||
let(:org) { create(:organization) }
|
||||
|
||||
def create_live_sponsor(org, tag)
|
||||
create(
|
||||
:sponsorship,
|
||||
level: :tag,
|
||||
blurb_html: "<p>Oh Yeah!!!</p>",
|
||||
status: "live",
|
||||
organization: org,
|
||||
sponsorable: tag,
|
||||
expires_at: 30.days.from_now,
|
||||
)
|
||||
end
|
||||
|
||||
it "renders page with proper header" do
|
||||
get "/t/#{tag.name}"
|
||||
|
|
@ -167,8 +210,9 @@ RSpec.describe "StoriesIndex", type: :request do
|
|||
end
|
||||
|
||||
it "does not render sponsor if not live" do
|
||||
org = create(:organization)
|
||||
sponsorship = create(:sponsorship, level: :tag, tagline: "Oh Yeah!!!", status: "pending", organization: org, sponsorable: tag)
|
||||
sponsorship = create(
|
||||
:sponsorship, level: :tag, tagline: "Oh Yeah!!!", status: "pending", organization: org, sponsorable: tag
|
||||
)
|
||||
|
||||
get "/t/#{tag.name}"
|
||||
expect(response.body).not_to include("is sponsored by")
|
||||
|
|
@ -176,8 +220,7 @@ RSpec.describe "StoriesIndex", type: :request do
|
|||
end
|
||||
|
||||
it "renders live sponsor" do
|
||||
org = create(:organization)
|
||||
sponsorship = create(:sponsorship, level: :tag, blurb_html: "<p>Oh Yeah!!!</p>", status: "live", organization: org, sponsorable: tag, expires_at: 30.days.from_now)
|
||||
sponsorship = create_live_sponsor(org, tag)
|
||||
get "/t/#{tag.name}"
|
||||
expect(response.body).to include("is sponsored by")
|
||||
expect(response.body).to include(sponsorship.blurb_html)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
end.to change(Article, :count).by(1)
|
||||
end
|
||||
|
||||
it "returns an article" do
|
||||
it "returns a non decorated, persisted article" do
|
||||
article = described_class.call(user, valid_attributes)
|
||||
expect(article).to be_kind_of(Article)
|
||||
|
||||
expect(article.decorated?).to be(false)
|
||||
expect(article).to be_persisted
|
||||
end
|
||||
|
||||
|
|
@ -35,7 +36,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.object)
|
||||
expect(event_dispatcher).to have_received(:call).with("article_created", article)
|
||||
end
|
||||
|
||||
it "doesn't call an event dispatcher when an article is unpublished" do
|
||||
|
|
@ -43,7 +44,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.object)
|
||||
expect(event_dispatcher).not_to have_received(:call).with("article_created", article)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -60,9 +61,10 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
end.not_to change(Article, :count)
|
||||
end
|
||||
|
||||
it "returns an unsaved article" do
|
||||
it "returns a non decorated, non persisted article" do
|
||||
article = described_class.call(user, invalid_attributes)
|
||||
expect(article).to be_kind_of(Article)
|
||||
|
||||
expect(article.decorated?).to be(false)
|
||||
expect(article).not_to be_persisted
|
||||
expect(article.errors.size).to eq(1)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue