Refactor: fix issues raised by bullet (#2965)
* Preload organization, not user * Preload users only when needed * Pre-load podcasts for podcast episodes in API * Avoid eager loading error by loading rating votes separately * Preload associations for moderation * Preload user comments in trees * Preload organization for non org dashboard and cleanup queries * Optimize ArticleSuggester to only load N articles at need * Remove eager loading and pass variables to partials for easier debug * Reorganize tags validation code and ignore actsastaggableon eager loading issues * Remove unused eager loading and bring up comments relation * Preload podcasts when loading podcast episodes * Fix views specs * Make sure ArticleSuggester never returns duplicates * Remove commented code * Re-trigger build * Move suggested articles back to view to respect fragment caching
This commit is contained in:
parent
72d118cf48
commit
6a626e819d
30 changed files with 148 additions and 106 deletions
|
|
@ -13,8 +13,8 @@ module Api
|
|||
respond_to :json
|
||||
|
||||
def index
|
||||
@commentable = Article.find(params[:a_id])
|
||||
@commentable_type = "Article"
|
||||
article = Article.find(params[:a_id])
|
||||
@comments = Comment.rooted_on(article, "Article").order(score: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ module Api
|
|||
page(@page).
|
||||
per(30)
|
||||
else
|
||||
@podcast_episodes = PodcastEpisode.order("published_at desc").page(@page).per(30)
|
||||
@podcast_episodes = PodcastEpisode.
|
||||
includes(:podcast).
|
||||
order("published_at desc").page(@page).per(30)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -73,9 +73,8 @@ class ArticlesController < ApplicationController
|
|||
|
||||
def edit
|
||||
authorize @article
|
||||
@user = @article.user
|
||||
@organization = @article.user&.organization
|
||||
@version = @article.has_frontmatter? ? "v1" : "v2"
|
||||
@organization = @user&.organization
|
||||
end
|
||||
|
||||
def manage
|
||||
|
|
@ -203,7 +202,7 @@ class ArticlesController < ApplicationController
|
|||
def set_article
|
||||
owner = User.find_by(username: params[:username]) || Organization.find_by(slug: params[:username])
|
||||
found_article = if params[:slug]
|
||||
owner.articles.includes(:user).find_by(slug: params[:slug])
|
||||
owner.articles.find_by(slug: params[:slug])
|
||||
else
|
||||
Article.includes(:user).find(params[:id])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ class CommentsController < ApplicationController
|
|||
@article = @commentable
|
||||
not_found unless @commentable.published
|
||||
end
|
||||
|
||||
@commentable_type = @commentable.class.name
|
||||
@root_comment = Comment.find(params[:id_code].to_i(26)) if params[:id_code].present?
|
||||
|
||||
set_surrogate_key_header "comments-for-#{@commentable.id}-#{@commentable_type}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,9 +6,17 @@ class DashboardsController < ApplicationController
|
|||
def show
|
||||
fetch_and_authorize_user
|
||||
|
||||
target = @user
|
||||
target = @user.organization if @user&.organization && @user&.org_admin && params[:which] == "organization"
|
||||
@articles = target.articles.sorting(params[:sort]).decorate
|
||||
@org_dashboard = @user&.organization && @user&.org_admin && params[:which] == "organization"
|
||||
|
||||
if @org_dashboard
|
||||
articles = @user.organization.articles
|
||||
@org_members = @user.organization.users.pluck(:name, :id)
|
||||
else
|
||||
articles = @user.articles.includes(:organization)
|
||||
end
|
||||
|
||||
@articles = articles.sorting(params[:sort]).decorate
|
||||
|
||||
# Updates analytics in background if appropriate:
|
||||
ArticleAnalyticsFetcher.new.delay.update_analytics(current_user.id) if @articles && ApplicationConfig["GA_FETCH_RATE"] < 50 # Rate limit concerned, sometimes we throttle down.
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ class ModerationsController < ApplicationController
|
|||
return unless current_user&.trusted
|
||||
|
||||
@articles = Article.published.
|
||||
includes(:rating_votes).
|
||||
where("rating_votes_count < 3").
|
||||
where("score > -5").
|
||||
order("hotness_score DESC").limit(100)
|
||||
|
||||
@articles = @articles.cached_tagged_with(params[:tag]) if params[:tag].present?
|
||||
|
||||
@rating_votes = RatingVote.where(article: @articles.pluck(:id), user: current_user)
|
||||
@articles = @articles.decorate
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class PodcastEpisodesController < ApplicationController
|
|||
def index
|
||||
@podcast_index = true
|
||||
@podcasts = Podcast.order("title asc")
|
||||
@podcast_episodes = PodcastEpisode.order("published_at desc").first(20)
|
||||
@podcast_episodes = PodcastEpisode.includes(:podcast).order("published_at desc").first(20)
|
||||
if params[:q].blank?
|
||||
set_surrogate_key_header("podcast_episodes_all " + params[:q].to_s,
|
||||
@podcast_episodes.map { |e| e["record_key"] })
|
||||
|
|
|
|||
|
|
@ -1,33 +1,48 @@
|
|||
class ArticleSuggester
|
||||
attr_accessor :article
|
||||
def initialize(article)
|
||||
@article = article
|
||||
end
|
||||
|
||||
def articles(num = 4)
|
||||
def articles(max: 4)
|
||||
if article.tag_list.any?
|
||||
(suggestions_by_tag + other_suggestions(num)).flatten.first(num).to_a
|
||||
# avoid loading more data if we don't need to
|
||||
tagged_suggestions = suggestions_by_tag(max: max)
|
||||
return tagged_suggestions if tagged_suggestions.size == max
|
||||
|
||||
# if there are not enough tagged articles, load other suggestions
|
||||
# ignoring tagged articles that might be relevant twice, hence avoiding duplicates
|
||||
num_remaining_needed = max - tagged_suggestions.size
|
||||
other_articles = other_suggestions(
|
||||
max: num_remaining_needed,
|
||||
ids_to_ignore: tagged_suggestions.pluck(:id),
|
||||
)
|
||||
tagged_suggestions.union(other_articles)
|
||||
else
|
||||
other_suggestions(num).to_a
|
||||
other_suggestions(max: max)
|
||||
end
|
||||
end
|
||||
|
||||
def other_suggestions(num = 4)
|
||||
private
|
||||
|
||||
attr_reader :article
|
||||
|
||||
def other_suggestions(max: 4, ids_to_ignore: [])
|
||||
ids_to_ignore << article.id
|
||||
Article.published.where(featured: true).
|
||||
where.not(id: article.id).
|
||||
where.not(id: ids_to_ignore).
|
||||
order("hotness_score DESC").
|
||||
includes(:user).
|
||||
offset(rand(0..offsets[1])).
|
||||
first(num)
|
||||
first(max)
|
||||
end
|
||||
|
||||
def suggestions_by_tag
|
||||
def suggestions_by_tag(max: 4)
|
||||
Article.published.tagged_with(article.tag_list, any: true).
|
||||
where.not(id: article.id).
|
||||
order("hotness_score DESC").
|
||||
includes(:user).
|
||||
offset(rand(0..offsets[0])).
|
||||
first(4)
|
||||
first(max)
|
||||
end
|
||||
|
||||
def offsets
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class CacheBuster
|
|||
end
|
||||
bust("#{commentable.path}/comments/")
|
||||
bust(commentable.path.to_s)
|
||||
commentable.comments.find_each do |c|
|
||||
commentable.comments.includes(:user).find_each do |c|
|
||||
bust(c.path)
|
||||
bust(c.path + "?i=i")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -62,8 +62,6 @@ class Article < ApplicationRecord
|
|||
after_save :detect_human_language
|
||||
before_save :update_cached_user
|
||||
after_update :update_notifications, if: proc { |article| article.notifications.any? && !article.saved_changes.empty? }
|
||||
# after_save :send_to_moderator
|
||||
# turned off for now
|
||||
before_destroy :before_destroy_actions
|
||||
|
||||
serialize :ids_for_suggested_articles
|
||||
|
|
@ -466,11 +464,9 @@ class Article < ApplicationRecord
|
|||
self.title = front_matter["title"] if front_matter["title"].present?
|
||||
if front_matter["tags"].present?
|
||||
ActsAsTaggableOn::Taggable::Cache.included(Article)
|
||||
self.tag_list = []
|
||||
self.tag_list = [] # overwrite any existing tag with those from the front matter
|
||||
tag_list.add(front_matter["tags"], parser: ActsAsTaggableOn::TagParser)
|
||||
TagAdjustment.where(article_id: id, adjustment_type: "removal", status: "committed").pluck(:tag_name).each do |name|
|
||||
tag_list.remove(name, parser: ActsAsTaggableOn::TagParser)
|
||||
end
|
||||
remove_tag_adjustments_from_tag_list
|
||||
end
|
||||
self.published = front_matter["published"] if %w[true false].include?(front_matter["published"].to_s)
|
||||
self.published_at = parsed_date(front_matter["date"]) if published
|
||||
|
|
@ -494,17 +490,22 @@ class Article < ApplicationRecord
|
|||
|
||||
def validate_tag
|
||||
# remove adjusted tags
|
||||
TagAdjustment.where(article_id: id, adjustment_type: "removal", status: "committed").pluck(:tag_name).each do |name|
|
||||
tag_list.remove(name, parser: ActsAsTaggableOn::TagParser)
|
||||
self.tag_list = tag_list
|
||||
end
|
||||
return errors.add(:tag_list, "exceed the maximum of 4 tags") if tag_list.length > 4
|
||||
remove_tag_adjustments_from_tag_list
|
||||
|
||||
# check there are not too many tags
|
||||
return errors.add(:tag_list, "exceed the maximum of 4 tags") if tag_list.size > 4
|
||||
|
||||
# check tags names aren't too long
|
||||
tag_list.each do |tag|
|
||||
errors.add(:tag, "\"#{tag}\" is too long (maximum is 20 characters)") if tag.length > 20
|
||||
end
|
||||
end
|
||||
|
||||
def remove_tag_adjustments_from_tag_list
|
||||
tags_to_remove = TagAdjustment.where(article_id: id, adjustment_type: "removal", status: "committed").pluck(:tag_name)
|
||||
tag_list.remove(tags_to_remove, parser: ActsAsTaggableOn::TagParser) if tags_to_remove
|
||||
end
|
||||
|
||||
def validate_video
|
||||
return errors.add(:published, "cannot be set to true if video is still processing") if published && video_state == "PROGRESSING"
|
||||
return errors.add(:video, "cannot be added member without permission") if video.present? && user.created_at > 2.weeks.ago
|
||||
|
|
|
|||
|
|
@ -122,12 +122,10 @@ class Comment < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.rooted_on(commentable_id, commentable_type)
|
||||
includes(:user, :commentable).
|
||||
includes(:user).
|
||||
select(:id, :user_id, :commentable_type, :commentable_id,
|
||||
:deleted, :created_at, :processed_html, :ancestry, :updated_at, :score).
|
||||
where(commentable_id: commentable_id,
|
||||
ancestry: nil,
|
||||
commentable_type: commentable_type)
|
||||
where(commentable_id: commentable_id, ancestry: nil, commentable_type: commentable_type)
|
||||
end
|
||||
|
||||
def self.tree_for(commentable, limit = 0)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ArticleApiIndexService
|
|||
|
||||
if (user = User.find_by(username: username))
|
||||
user.articles.published.
|
||||
includes(:user).
|
||||
includes(:organization).
|
||||
order("published_at DESC").
|
||||
page(page).
|
||||
per(num)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,7 @@ module Articles
|
|||
end
|
||||
|
||||
def call
|
||||
article = if user.has_role?(:super_admin)
|
||||
Article.includes(:user).find(article_id)
|
||||
else
|
||||
user.articles.find(article_id)
|
||||
end
|
||||
article = load_article
|
||||
|
||||
# the client can change the series the article belongs to
|
||||
if article_params.key?(:series)
|
||||
|
|
@ -51,5 +47,10 @@ module Articles
|
|||
|
||||
attr_reader :user, :article_id
|
||||
attr_accessor :article_params
|
||||
|
||||
def load_article
|
||||
relation = user.has_role?(:super_admin) ? Article.includes(:user) : user.articles
|
||||
relation.find(article_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ module Moderator
|
|||
def reassign_articles
|
||||
return unless user.articles.any?
|
||||
|
||||
user.articles.find_each do |article|
|
||||
# preload associations that are going to be used during indexing
|
||||
user.articles.preload(:taggings, :organization).find_each do |article|
|
||||
path = "/#{@ghost.username}/#{article.slug}"
|
||||
article.update_columns(user_id: @ghost.id, path: path)
|
||||
article.index!
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ module Moderator
|
|||
|
||||
user.articles.find_each do |article|
|
||||
article.reactions.delete_all
|
||||
article.comments.find_each do |comment|
|
||||
article.comments.includes(:user).find_each do |comment|
|
||||
comment.reactions.delete_all
|
||||
CacheBuster.new.bust_comment(comment.commentable, comment.user.username)
|
||||
comment.delete
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
json.array! Comment.rooted_on(@commentable.id, @commentable_type).order("score DESC") do |comment|
|
||||
json.array! @comments do |comment|
|
||||
json.type_of "comment"
|
||||
|
||||
json.id_code comment.id_code_generated
|
||||
json.body_html comment.processed_html
|
||||
json.user do
|
||||
|
|
@ -11,7 +12,8 @@ json.array! Comment.rooted_on(@commentable.id, @commentable_type).order("score D
|
|||
json.profile_image ProfileImage.new(comment.user).get(640)
|
||||
json.profile_image_90 ProfileImage.new(comment.user).get(90)
|
||||
end
|
||||
json.children comment.children.order("score DESC").each do |children_comment|
|
||||
|
||||
json.children comment.children.order(score: :desc).each do |children_comment|
|
||||
json.id_code children_comment.id_code_generated
|
||||
json.body_html children_comment.processed_html
|
||||
json.user do
|
||||
|
|
@ -23,7 +25,8 @@ json.array! Comment.rooted_on(@commentable.id, @commentable_type).order("score D
|
|||
json.profile_image ProfileImage.new(children_comment.user).get(640)
|
||||
json.profile_image_90 ProfileImage.new(children_comment.user).get(90)
|
||||
end
|
||||
json.children children_comment.children.order("score DESC").each do |grandchild_comment|
|
||||
|
||||
json.children children_comment.children.order(score: :desc).each do |grandchild_comment|
|
||||
json.id_code grandchild_comment.id_code_generated
|
||||
json.body_html grandchild_comment.processed_html
|
||||
json.user do
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<% if articles %>
|
||||
<div class="more-articles container">
|
||||
<% ArticleSuggester.new(@article).articles(4).each do |article| %>
|
||||
<% articles.each do |article| %>
|
||||
<a href="<%= article.path %>" data-preload-image="<%= cloud_cover_url(article.main_image) %>">
|
||||
<div class="single-other-article">
|
||||
<div class="picture">
|
||||
|
|
@ -25,3 +26,4 @@
|
|||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
<div class="articleformcontainer">
|
||||
<div class="articleform"
|
||||
id="article-form"
|
||||
data-article="<%= @article.to_json(only: %i[id title cached_tag_list published body_markdown main_image organization_id canonical_url], methods: %i[series all_series]) %>"
|
||||
data-organization="<%= @organization&.to_json(only: %i[name bg_color_hex text_color_hex], methods: [:profile_image_90]) %>"
|
||||
data-version="<%= @version%>">
|
||||
<form class="articleform__form articleform__form--<%= @version %>">
|
||||
<% if @organization %>
|
||||
data-article="<%= article.to_json(only: %i[id title cached_tag_list published body_markdown main_image organization_id canonical_url], methods: %i[series all_series]) %>"
|
||||
data-organization="<%= organization&.to_json(only: %i[name bg_color_hex text_color_hex], methods: [:profile_image_90]) %>"
|
||||
data-version="<%= version %>">
|
||||
<form class="articleform__form articleform__form--<%= version %>">
|
||||
<% if organization %>
|
||||
<div class="articleform__orgsettings">
|
||||
<img src="<%= @organization.profile_image_90 %>" style="opacity: <%= @article.organization ? "1.0" : "0.7" %>" alt="<%= @organization.name %> profile image">
|
||||
<%= @organization.name %>
|
||||
<button class="<%= @article.organization ? "yes" : "no" %>">
|
||||
<%= @article.organization ? "✅ YES" : "◻️ NO" %>
|
||||
<img src="<%= organization.profile_image_90 %>" style="opacity: <%= article.organization ? "1.0" : "0.7" %>" alt="<%= organization.name %> profile image">
|
||||
<%= organization.name %>
|
||||
<button class="<%= article.organization ? "yes" : "no" %>">
|
||||
<%= article.organization ? "✅ YES" : "◻️ NO" %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @version == "v2" %>
|
||||
<% if @article.main_image.present? %>
|
||||
<div class="articleform__mainimage"><img src="<%= @article.main_image %>"></div>
|
||||
<% if version == "v2" %>
|
||||
<% if article.main_image.present? %>
|
||||
<div class="articleform__mainimage"><img src="<%= article.main_image %>"></div>
|
||||
<% end %>
|
||||
<input class="articleform__title articleform__titlepreview" type="text" value="<%= @article.title %>" placeholder="Title" />
|
||||
<input class="articleform__title articleform__titlepreview" type="text" value="<%= article.title %>" placeholder="Title" />
|
||||
<div class="articleform__detailfields">
|
||||
<div class="articleform__tagswrapper">
|
||||
<textarea type="text" class="articleform__tags" placeholder="tags"><%= @article.cached_tag_list %></textarea>
|
||||
<textarea type="text" class="articleform__tags" placeholder="tags"><%= article.cached_tag_list %></textarea>
|
||||
</div>
|
||||
<button class="articleform__detailsButton articleform__detailsButton--image"></button>
|
||||
<button class="articleform__detailsButton articleform__detailsButton--moreconfig"></button>
|
||||
</div>
|
||||
<% end %>
|
||||
<textarea class="articleform__body" placeholder="Body Markdown" name="body_markdown"><%= @article.body_markdown %></textarea>
|
||||
<textarea class="articleform__body" placeholder="Body Markdown" name="body_markdown"><%= article.body_markdown %></textarea>
|
||||
<div>
|
||||
<button class="articleform__detailsButton articleform__detailsButton--image articleform__detailsButton--bottom"></button>
|
||||
<% if @version == "v2" %>
|
||||
<% if version == "v2" %>
|
||||
<button class="articleform__detailsButton articleform__detailsButton--moreconfig articleform__detailsButton--bottom"></button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<div style="display:none">
|
||||
<% if @version == "v2" %>
|
||||
<% if version == "v2" %>
|
||||
<%= render "pages/editor_guide_text", version: "2" %>
|
||||
<% else %>
|
||||
<%= render "pages/editor_guide_text", version: "1" %>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<div itemscope itemtype="http://schema.org/VideoObject" class="video-player-header">
|
||||
<% if meta_tags %>
|
||||
<meta itemprop="uploadDate" content="<%= @article.published_at %>" />
|
||||
<meta itemprop="name" content="<%= @article.title %>" />
|
||||
<meta itemprop="description" content="<%= @article.description %>" />
|
||||
<meta itemprop="uploadDate" content="<%= article.published_at %>" />
|
||||
<meta itemprop="name" content="<%= article.title %>" />
|
||||
<meta itemprop="description" content="<%= article.description %>" />
|
||||
<meta itemprop="thumbnailUrl" content="<%= cloudinary(article.video_thumbnail_url, 880) %>" />
|
||||
<meta itemprop="contentUrl" content="<%= article.video_source_url %>" />
|
||||
<% minutes, seconds = @article.video_duration_in_minutes.split(":") %>
|
||||
<% minutes, seconds = article.video_duration_in_minutes.split(":") %>
|
||||
<meta itemprop="duration" content="<%= format("PT%<minutes>sM%<seconds>sS", minutes: minutes, seconds: seconds) %>" />
|
||||
<% end %>
|
||||
<script src="//content.jwplatform.com/libraries/b1zWy2iv.js" async></script>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
<% if @article.video.present? %>
|
||||
<div class="article-form-video-preview">
|
||||
|
||||
<% if @article.video_state == "PROGRESSING" %>
|
||||
<h1 style="color:#062144;margin-top:1.1em">⏳ Video Transcoding In Progress ⏳</h1>
|
||||
<img src="https://media.giphy.com/media/tXL4FHPSnVJ0A/giphy.gif" style="border-radius:12px;height:317px" alt="Bored kid incessantly tapping his fingers">
|
||||
|
|
@ -19,16 +18,14 @@
|
|||
<br /><br />
|
||||
<%= render "articles/video_player", meta_tags: false, article: @article %>
|
||||
<div class="article-form-video-image-url">
|
||||
|
||||
<%= form_for(@article) do |f| %>
|
||||
Preview Image URL: <%= f.text_field :video_thumbnail_url, placeholder: "New Thumbnail URL" %>
|
||||
<%= f.submit "Submit Change", class: "cta" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= javascript_pack_tag "articleForm", defer: true %>
|
||||
<%= render "articles/v2_form" %>
|
||||
<%= render "articles/v2_form", article: @article, organization: @organization, version: @version %>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<% if user_signed_in? %>
|
||||
<%= javascript_pack_tag "articleForm", defer: true %>
|
||||
<%= render "articles/v2_form" %>
|
||||
<%= render "articles/v2_form", article: @article, organization: @organization, version: @version %>
|
||||
<% else %>
|
||||
<% @new_article_not_logged_in = true %>
|
||||
<%= render "devise/registrations/registration_form" %>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<%= javascript_pack_tag "clipboardCopy", defer: true %>
|
||||
|
||||
<% cache("content-related-optional-scripts-#{@article.id}-#{@article.updated_at}", :expires_in => 30.hours) do %>
|
||||
<% cache("content-related-optional-scripts-#{@article.id}-#{@article.updated_at}", expires_in: 30.hours) do %>
|
||||
<% if @article.processed_html.include? "ltag_gist-liquid-tag" %>
|
||||
<%= javascript_include_tag "https://cdnjs.cloudflare.com/ajax/libs/postscribe/2.0.8/postscribe.min.js", defer: true %>
|
||||
<script defer>
|
||||
|
|
@ -196,6 +196,7 @@
|
|||
<%= render "articles/org_branding", organization: @organization %>
|
||||
<%= render "articles/full_comment_area" %>
|
||||
</div>
|
||||
|
||||
<% cache("article-bottom-content-#{@article.id}", expires_in: 18.hours) do %>
|
||||
<% @classic_article = Suggester::Articles::Classic.new(@article, not_ids: [@article.id]).get.first %>
|
||||
<% if @classic_article %>
|
||||
|
|
@ -206,7 +207,8 @@
|
|||
follow_cue: "Follow <a href='#{@classic_article.user.path}'>@#{@classic_article.user.username}</a> to see more of their posts in your feed." %>
|
||||
<% end %>
|
||||
<div id="additional-content-area" data-article-id="<%= @article.id %>,<%= @classic_article&.id %>"></div>
|
||||
<%= render "articles/bottom_content" %>
|
||||
<% suggested_articles = ArticleSuggester.new(@article).articles(max: 4) %>
|
||||
<%= render "articles/bottom_content", articles: suggested_articles %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
<% if @root_comment.present? %>
|
||||
<div class="root-comment">
|
||||
<% cache ["comment_root-view-root_#{user_signed_in?}", @root_comment] do %>
|
||||
<%= tree_for(@root_comment, @root_comment.subtree.arrange[@root_comment], @commentable) %>
|
||||
<%= tree_for(@root_comment, @root_comment.subtree.includes(:user).arrange[@root_comment], @commentable) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
<% if org_admin %>
|
||||
<%= form_for(article) do |f| %>
|
||||
<input type="hidden" name="destination" value="/dashboard" />
|
||||
AUTHOR: <%= f.select(:user_id, options_for_select(@user.organization.users.map { |x| [x.name, x.id] }, article.user_id)) %>
|
||||
AUTHOR: <%= f.select(:user_id, options_for_select(@org_members, article.user_id)) %>
|
||||
<%= f.submit "SUBMIT AUTHOR CHANGE", class: "cta pill black" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<% end %>
|
||||
</h1>
|
||||
<% end %>
|
||||
<% if @user.org_admin && @user.organization && params[:which] == "organization" %>
|
||||
<% if @org_dashboard %>
|
||||
<%= render "analytics" %>
|
||||
<% @articles.each do |article| %>
|
||||
<%= render "dashboard_article", article: article, org_admin: true, manage_view: false %>
|
||||
|
|
@ -27,7 +27,6 @@
|
|||
Upload a Video 🎥
|
||||
</a>
|
||||
<% end %>
|
||||
|
||||
<%= select_tag "dashhboard_sort", options_for_select(sort_options, params[:sort]) %>
|
||||
<% if @articles.any? {|article| article.archived } %>
|
||||
<%= link_to "Show archived", "javascript:;", id: "toggleArchivedLink" %>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
</center>
|
||||
<div class="container">
|
||||
<% @articles.each do |article| %>
|
||||
<% @rating_vote = article.rating_votes.where(user_id: current_user.id).first %>
|
||||
<% @rating_vote = @rating_votes.where(article: article).first %>
|
||||
<% if !@rating_vote || article.last_buffered.nil? %>
|
||||
<div style="border-bottom: 20px solid black; margin-bottom: 15px;">
|
||||
<center>
|
||||
|
|
@ -76,8 +76,10 @@
|
|||
<% end %>
|
||||
</center>
|
||||
<div class="body">
|
||||
<%= HTML_Truncator.truncate(article.processed_html,
|
||||
200, ellipsis: '<a class="comment-read-more" href="' + article.path + '">... Read Entire Post</a>').html_safe %>
|
||||
<%= HTML_Truncator.truncate(
|
||||
article.processed_html,
|
||||
200,
|
||||
ellipsis: '<a class="comment-read-more" href="' + article.path + '">... Read Entire Post</a>').html_safe %>
|
||||
</div>
|
||||
<center>
|
||||
<% if article.last_buffered.nil? %>
|
||||
|
|
|
|||
|
|
@ -63,5 +63,8 @@ Rails.application.configure do
|
|||
config.after_initialize do
|
||||
Bullet.enable = ENV["BULLET"]
|
||||
Bullet.raise = ENV["BULLET"]
|
||||
Bullet.add_whitelist(type: :unused_eager_loading, class_name: "ApiSecret", association: :user)
|
||||
# acts-as-taggable-on has super weird eager loading problems: <https://github.com/mbleigh/acts-as-taggable-on/issues/91>
|
||||
Bullet.add_whitelist(type: :n_plus_one_query, class_name: "ActsAsTaggableOn::Tagging", association: :tag)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,25 +3,27 @@ require "rails_helper"
|
|||
RSpec.describe ArticleSuggester do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:article, user_id: user.id, featured: true)
|
||||
create(:article, user_id: user.id, featured: true)
|
||||
create(:article, user_id: user.id, featured: true)
|
||||
create(:article, user_id: user.id, featured: true)
|
||||
it "returns proper number of articles with post with the same tags" do
|
||||
create_list(:article, 4, user: user, featured: true, tags: ["discuss"])
|
||||
article = create(:article, user: user, featured: true, tags: ["discuss"])
|
||||
expect(described_class.new(article).articles.size).to eq(4)
|
||||
end
|
||||
|
||||
it "returns proper number of articles with post with tags" do
|
||||
article = create(:article, user_id: user.id, featured: true)
|
||||
it "returns proper number of articles with post with different tags" do
|
||||
create_list(:article, 2, user: user, featured: true, tags: ["discuss"])
|
||||
create_list(:article, 2, user: user, featured: true, tags: ["javascript"])
|
||||
article = create(:article, user: user, featured: true, tags: ["discuss"])
|
||||
expect(described_class.new(article).articles.size).to eq(4)
|
||||
end
|
||||
|
||||
it "returns proper number of articles with post without tags" do
|
||||
article = create(:article, user_id: user.id, featured: true)
|
||||
article.tag_list = ""
|
||||
create_list(:article, 5, user: user, tags: [], with_tags: false, featured: true)
|
||||
article = create(:article, user: user, featured: true, tag_list: "")
|
||||
expect(described_class.new(article).articles.size).to eq(4)
|
||||
end
|
||||
|
||||
it "returns proper number articles if number is passed" do
|
||||
expect(described_class.new(Article.last).articles(2).size).to eq(2)
|
||||
it "returns the number of articles requested" do
|
||||
articles = create_list(:article, 3, user: user, featured: true)
|
||||
expect(described_class.new(articles.first).articles(max: 2).size).to eq(2)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -112,20 +112,26 @@ RSpec.describe "Articles", type: :request do
|
|||
describe "GET /:path/edit" do
|
||||
before { sign_in user }
|
||||
|
||||
it "returns a new article" do
|
||||
article = create(:article, user_id: user.id)
|
||||
get "#{article.path}/manage"
|
||||
expect(response.body).to include("Manage Your Post")
|
||||
end
|
||||
it "returns unauthorized if user not author" do
|
||||
second_user = create(:user)
|
||||
article = create(:article, user_id: second_user.id)
|
||||
expect { get "#{article.path}/manage" }.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
it "shows v1 if article has frontmatter" do
|
||||
article = create(:article, user_id: user.id)
|
||||
get "#{article.path}/edit"
|
||||
expect(response.body).to include("articleform__form--v1")
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /:path/manage" do
|
||||
before { sign_in user }
|
||||
|
||||
it "returns a new article" do
|
||||
article = create(:article, user_id: user.id)
|
||||
get "#{article.path}/manage"
|
||||
expect(response.body).to include("Manage Your Post")
|
||||
end
|
||||
|
||||
it "returns unauthorized if user not author" do
|
||||
second_user = create(:user)
|
||||
article = create(:article, user_id: second_user.id)
|
||||
expect { get "#{article.path}/manage" }.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,9 +52,11 @@ RSpec.describe "Moderations", type: :request do
|
|||
end
|
||||
|
||||
it "grants access to /mod index" do
|
||||
create(:rating_vote, article: article, user: user)
|
||||
get "/mod"
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it "grants access to /mod index with articles" do
|
||||
create(:article, published: true)
|
||||
get "/mod"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue