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:
parent
8e6d193d25
commit
158f56bc9d
18 changed files with 240 additions and 27 deletions
|
|
@ -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="' +
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ class ApplicationPolicy
|
|||
update?
|
||||
end
|
||||
|
||||
def manage?
|
||||
update? && record.published
|
||||
end
|
||||
|
||||
def destroy?
|
||||
false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
67
app/views/articles/manage.html.erb
Normal file
67
app/views/articles/manage.html.erb
Normal 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>
|
||||
|
|
@ -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 %>">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue