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:
rhymes 2019-05-28 23:32:53 +02:00 committed by Ben Halpern
parent 72d118cf48
commit 6a626e819d
30 changed files with 148 additions and 106 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"] })

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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!

View file

@ -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

View file

@ -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

View file

@ -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 %>

View file

@ -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" %>

View file

@ -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>

View file

@ -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 %>

View file

@ -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" %>

View file

@ -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>

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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" %>

View file

@ -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? %>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"