Add page for users to "manage" their posts after publication (distribution, etc.) (#2627)

* Set up basic /manage route

* Add basic article manage page

* Add tips and finalize permissions for /manage

* Add final test and add manage button to article show

* Update failing specs
This commit is contained in:
Ben Halpern 2019-05-01 09:00:09 -04:00 committed by GitHub
parent 8e6d193d25
commit 158f56bc9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 240 additions and 27 deletions

View file

@ -76,7 +76,9 @@ function addRelevantButtonsToArticle(user) {
document.getElementById('action-space').innerHTML =
'<a href="' +
articleContainer.dataset.path +
'/edit" rel="nofollow">EDIT <span class="post-word">POST</span></a>';
'/edit" rel="nofollow">EDIT</a><a href="' +
articleContainer.dataset.path +
'/manage" rel="nofollow">MANAGE</a>';
} else if (user.trusted) {
document.getElementById('action-space').innerHTML =
'<a href="' +

View file

@ -218,11 +218,12 @@ article {
display: inline-block;
background: $green;
color: $black;
padding: 2px 8px 3px;
padding: 2px 12px 3px;
border-radius: 3px;
line-height: 1.1em;
font-family: $helvetica-condensed;
font-stretch: condensed;
margin-right: 4px;
@media screen and (max-width: 376px) {
padding: 2px 3px 3px;
margin-left: 3px;

View file

@ -221,6 +221,9 @@
border-radius: 3px;
color: white;
}
.manage-header {
background: $bold-blue;
}
.single-article {
border: 1px solid $light-medium-gray;
@ -239,6 +242,72 @@
background: var(--theme-container-accent-background,
lighten($yellow, 20%));
}
&.thanks-for-suggesting-a-tweet {
background: lighten($green, 20%);
padding: 30px 0px;
text-align: center;
font-size: 1.4em;
font-weight: bold;
}
&.single-article-manage-form-wrapper {
form {
text-align: center;
width: 490px;
max-width: 90%;
margin: 10px auto;
textarea {
width: 100%;
border: 1px solid $light-medium-gray;
height: 100px;
border-radius: 3px;
}
input[type="submit"] {
background: #1da1f2; // twitter blue
color: white;
font-size: 19px;
padding: 8px 35px;
border-radius: 100px;
font-family: $helvetica-condensed;
margin: 18px auto;
}
.button-section {
display: inline-block;
border-radius: 12px;
padding: 3px;
&.button-section-newbie {
background: lighten($green, 12%);
}
&.button-section-intermediate {
background: lighten($purple, 5%);
}
&.button-section-senior {
background: lighten($bold-blue, 19%);
}
}
button {
color: white;
font-size: 15px;
font-weight: bold;
border: 2px solid white;
background: $black;
padding: 5px 0px;
width: 38px;
border-radius: 8px;
&.selected {
background: $green;
padding: 15px 0px;
width: 70px;
font-size: 20px;
}
}
p {
.scale-pod {
display: inline-block;
width: 160px;
}
}
}
}
.dashboard-collection-org-details {
.dashboard-top-pill {
@ -381,7 +450,7 @@
}
&.mute {
background-color: #ee7453;
background-color: $medium-gray;
color: $white;
}
@ -564,3 +633,29 @@
.header-card {
max-width: 100%;
}
.dashboard-manage-header {
background: darken($bold-blue, 25%);
color: white;
margin-top: 60px;
padding: 50px 0px 120px;
margin-bottom: -160px;
line-height: 1.3em;
li {
margin: 20px auto;
}
a {
color: white;
text-decoration: underline;
}
.dashboard-manage-header-inner-container {
width: 800px;
max-width: 96%;
margin: 10px auto;
}
.dashboard-manage-nav {
padding: 10px 0px;
font-size: 0.9em;
font-weight: bold;
}
}

View file

@ -1,7 +1,7 @@
class ArticlesController < ApplicationController
include ApplicationHelper
before_action :authenticate_user!, except: %i[feed new]
before_action :set_article, only: %i[edit update destroy]
before_action :set_article, only: %i[edit manage update destroy]
before_action :raise_banned, only: %i[new create update]
before_action :set_cache_control_headers, only: %i[feed]
after_action :verify_authorized
@ -80,6 +80,15 @@ class ArticlesController < ApplicationController
@organization = @user&.organization
end
def manage
@article = @article.decorate
authorize @article
@user = @article.user
@rating_vote = RatingVote.where(article_id: @article.id, user_id: @user.id).first
@buffer_updates = BufferUpdate.where(composer_user_id: @user.id)
@organization = @user&.organization
end
def preview
authorize Article
begin

View file

@ -2,8 +2,8 @@ class BufferUpdatesController < ApplicationController
after_action :verify_authorized
def create
authorize BufferUpdate
@article = Article.find(params[:buffer_update][:article_id])
authorize @article, policy_class: BufferUpdatePolicy
create_main_tweet
create_satellite_tweets
create_facebook_post

View file

@ -20,6 +20,6 @@ class RatingVote < ApplicationRecord
private
def permissions
errors.add(:user_id, "is not permitted to take this action.") unless user&.trusted
errors.add(:user_id, "is not permitted to take this action.") if !user&.trusted && user_id != article&.user_id
end
end

View file

@ -32,6 +32,10 @@ class ApplicationPolicy
update?
end
def manage?
update? && record.published
end
def destroy?
false
end

View file

@ -1,5 +1,11 @@
class BufferUpdatePolicy < ApplicationPolicy
def create?
user_is_trusted?
user_is_trusted? || user_is_author?
end
private
def user_is_author?
record.user_id == user.id
end
end

View file

@ -0,0 +1,67 @@
<% title "Manage #{@article.title}" %>
<div class="dashboard-manage-header">
<div class="dashboard-manage-header-inner-container">
<div class="dashboard-manage-nav">
<a href="/dashboard">Dashboard</a> 👉 Manage Your Post
</div>
<h2>Tools:</h2>
<p><strong>Suggest a Tweet:</strong> Help get your post in front of more developers by suggesting a tweet that <a href="https://twitter.com/thepracticaldev">@ThePracticalDev</a> editors can use.
<p><strong>Experience Level of Post:</strong> The best target dev experience level— a <em>gentle</em> indicator to help show the post to the people who will benefit the most.
<h2>Tips:</h2>
<ol>
<li>
Make your own tweet about the post, describing personally why you wrote it or why people might find it useful. <a href="https://twitter.com/thepracticaldev">@ThePracticalDev</a> may retweet you.
</li>
<li>
Share to link aggregators such as <a href="https://www.reddit.com/">Reddit</a>. Try to choose the most topic-specific subreddits, such as <a href="https://www.reddit.com/r/javascript">/r/javascript</a> instead of <a href="https://www.reddit.com/r/programming">/r/programming</a>. <em>Warning: jerks and trolls may be lurking.</em>
</li>
<li>
Share with your co-workers or local communities. Ask people to leave questions for you in the comments. It's a great way to spark additional discussion.
</li>
</ol>
</div>
</div>
<div class="dashboard-container">
<%= render "dashboards/dashboard_article", article: @article, org_admin: @user.org_admin, manage_view: true %>
<% if @buffer_updates.any? %>
<div class="single-article thanks-for-suggesting-a-tweet">
Tweet Suggestion Sent 🙏
</div>
<% else %>
<div class="single-article single-article-manage-form-wrapper">
<%= form_for(BufferUpdate.new) do |f| %>
<h2>Suggest a Tweet</h2>
<%= f.hidden_field :article_id, value: @article.id %>
<%= f.text_area :body_text, minlength: 10, maxlength: 220 %>
<%= f.submit "Share Tweet Suggestion" %>
<p>Suggestion can be a quote, summary, or bullet points from the post.</p>
<% end %>
</div>
<% end %>
<div class="single-article single-article-manage-form-wrapper">
<%= form_for(RatingVote.new) do |f| %>
<h2>Experience Level of Post</h2>
<%= f.hidden_field :article_id, value: @article.id %>
<%= f.hidden_field :group, value: "experience_level" %>
<span class="button-section button-section-newbie">
<button value="1" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 1.0 %>">1</button>
<button value="2" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 2.0 %>">2</button>
<button value="3" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 3.0 %>">3</button>
</span>
<span class="button-section button-section-intermediate">
<button value="4" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 4.0 %>">4</button>
<button value="5" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 5.0 %>">5</button>
<button value="6" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 6.0 %>">6</button>
<button value="7" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 7.0 %>">7</button>
</span>
<span class="button-section button-section-senior">
<button value="8" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 8.0 %>">8</button>
<button value="9" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 9.0 %>">9</button>
<button value="10" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 10.0 %>">10</button>
</span>
<p>Who is this post most relevant for?</p>
<p><span class="scale-pod">👈 Total Newbies</span><span class="scale-pod">Senior Devs 👉</span></p>
<% end %>
</div>
</div>

View file

@ -21,10 +21,14 @@
<a href="<%= article.current_state_path %>" class="pill cta yellow">DRAFT</a>
<% end %>
<a href="<%= article.path %>/edit" class="pill cta green">EDIT</a>
<a href="<%= article.path %>/delete_confirm" data-no-instant class="cta pill black">DELETE</a>
<%= form_for(article, url: "/article_mutes/#{article.id}", method: "patch", html: { class: "mute-form" }) do |f| %>
<%= f.hidden_field :receive_notifications, value: !article.receive_notifications %>
<%= f.submit article.receive_notifications ? "MUTE NOTIFICATIONS" : "NOTIFICATIONS MUTED", class: "cta pill #{article.receive_notifications ? 'mute' : 'unmute'}" %>
<% if manage_view %>
<%= form_for(article, url: "/article_mutes/#{article.id}", method: "patch", html: { class: "mute-form" }) do |f| %>
<%= f.hidden_field :receive_notifications, value: !article.receive_notifications %>
<%= f.submit article.receive_notifications ? "MUTE NOTIFICATIONS" : "NOTIFICATIONS MUTED", class: "cta pill #{article.receive_notifications ? 'mute' : 'unmute'}" %>
<% end %>
<a href="<%= article.path %>/delete_confirm" data-no-instant class="cta pill black">DELETE</a>
<% elsif article.published %>
<a href="<%= article.path %>/manage" class="cta pill black">MANAGE</a>
<% end %>
<% if article.published %>
<span id="pageviews-<%= article.id %>" class="cta pill dashboard-pageviews-indicator" data-analytics-pageviews data-article-id="<%= article.id %>">

View file

@ -17,7 +17,7 @@
<% if @user.org_admin && @user.organization && params[:which] == "organization" %>
<%= render "analytics" %>
<% @articles.each do |article| %>
<%= render "dashboard_article", article: article, org_admin: true %>
<%= render "dashboard_article", article: article, org_admin: true, manage_view: false %>
<% end %>
<% elsif @articles.any? %>
<%= render "analytics" %>
@ -31,7 +31,7 @@
<%= select_tag "dashhboard_sort", options_for_select(sort_options, params[:sort]) %>
</div>
<% @articles.each do |article| %>
<%= render "dashboard_article", article: article, org_admin: false %>
<%= render "dashboard_article", article: article, org_admin: false, manage_view: false %>
<% end %>
<% else %>
<div class="no-articles">

View file

@ -333,6 +333,7 @@ Rails.application.routes.draw do
get "/:username/:slug/:view" => "stories#show",
constraints: { view: /moderate/ }
get "/:username/:slug/mod" => "moderations#article"
get "/:username/:slug/manage" => "articles#manage"
get "/:username/:slug/edit" => "articles#edit"
get "/:username/:slug/delete_confirm" => "articles#delete_confirm"
get "/:username/:view" => "stories#index",

View file

@ -52,5 +52,12 @@ RSpec.describe RatingVote, type: :model do
rating = build(:rating_vote, article_id: article.id, user_id: nontrusted_user.id)
expect(rating).not_to be_valid
end
it "does allows author to make rating on own post" do
nontrusted_user = create(:user)
article = create(:article, user_id: nontrusted_user.id)
rating = build(:rating_vote, article_id: article.id, user_id: nontrusted_user.id)
expect(rating).to be_valid
end
end
end

View file

@ -3,7 +3,7 @@ require "rails_helper"
RSpec.describe ArticlePolicy do
subject { described_class.new(user, article) }
let(:article) { build(:article) }
let(:article) { create(:article) }
let(:valid_attributes) do
%i[title body_html body_markdown main_image published
description allow_small_edits allow_big_edits tag_list publish_under_org
@ -20,20 +20,20 @@ RSpec.describe ArticlePolicy do
let(:user) { create(:user) }
it { is_expected.to permit_actions(%i[new create preview]) }
it { is_expected.to forbid_actions(%i[update delete_confirm destroy]) }
it { is_expected.to forbid_actions(%i[update edit manage delete_confirm destroy]) }
context "with banned status" do
before { user.add_role :banned }
it { is_expected.to permit_actions(%i[new preview]) }
it { is_expected.to forbid_actions(%i[create update delete_confirm destroy]) }
it { is_expected.to forbid_actions(%i[create edit manage update delete_confirm destroy]) }
end
end
context "when user is the author" do
let(:user) { article.user }
it { is_expected.to permit_actions(%i[update new create delete_confirm destroy preview]) }
it { is_expected.to permit_actions(%i[update edit manage new create delete_confirm destroy preview]) }
it { is_expected.to permit_mass_assignment_of(valid_attributes) }
context "with banned status" do
@ -46,6 +46,6 @@ RSpec.describe ArticlePolicy do
context "when user is a super_admin" do
let(:user) { build(:user, :super_admin) }
it { is_expected.to permit_actions(%i[update new create delete_confirm destroy preview]) }
it { is_expected.to permit_actions(%i[update new edit manage create delete_confirm destroy preview]) }
end
end

View file

@ -1,19 +1,13 @@
require "rails_helper"
RSpec.describe BufferUpdatePolicy do
subject { described_class.new(user, block) }
subject { described_class.new(user, article) }
let(:block) { build(:block) }
let(:article) { build(:article) }
context "when user is trusted" do
let(:user) { build(:user, :trusted) }
it { is_expected.to permit_actions(%i[create]) }
end
context "when user is not trusted" do
let(:user) { build(:user) }
it { is_expected.to forbid_actions(%i[create]) }
end
end

View file

@ -73,4 +73,19 @@ RSpec.describe "Articles", type: :request do
end
end
end
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
end
end

View file

@ -4,6 +4,7 @@ RSpec.describe "BufferUpdates", type: :request do
let(:user) { create(:user) }
let(:mod_user) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:mod_article) { create(:article, user_id: mod_user.id) }
let(:comment) { create(:comment, user_id: user.id, commentable_id: article.id) }
context "when trusted user is logged in" do
@ -58,9 +59,15 @@ RSpec.describe "BufferUpdates", type: :request do
expect do
post "/buffer_updates",
params:
{ buffer_update: { body_text: "This is the text!!!!", tag_id: "javascript", article_id: article.id } }
{ buffer_update: { body_text: "This is the text!!!!", tag_id: "javascript", article_id: mod_article.id } }
end.to raise_error(Pundit::NotAuthorizedError)
end
it "accepts buffer update from author of article" do
post "/buffer_updates",
params:
{ buffer_update: { body_text: "This is the text!!!!", tag_id: "javascript", article_id: article.id } }
expect(BufferUpdate.first.body_text).to include(article.path)
end
end
# it "updates last buffered at" do

View file

@ -6,6 +6,7 @@ RSpec.describe "Deleting Article", type: :system do
it "author of article deletes own article" do
sign_in article.user
visit "/dashboard"
click_on "MANAGE"
click_on "DELETE"
click_on "DELETE" # This is for confirming deletion
expect(page).to have_text("Write your first post now")