From 1552df98d2bc038008f7888ad87653bbce5ee9c3 Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Thu, 4 Apr 2019 17:01:58 -0400 Subject: [PATCH] Add ability for trusted users to suggest social copy for posts (#2306) * Add ability for trusted users to suggest social copy for posts * Fix schema * Fix tests * Fix test --- app/controllers/buffer_updates_controller.rb | 50 ++++++++ .../internal/articles_controller.rb | 1 + .../internal/buffer_updates_controller.rb | 5 + app/controllers/moderations_controller.rb | 2 +- app/models/buffer_update.rb | 14 +++ app/policies/application_policy.rb | 4 + app/policies/buffer_update_policy.rb | 5 + .../articles/_individual_article.html.erb | 5 +- app/views/internal/articles/index.html.erb | 28 +++++ app/views/moderations/index.html.erb | 113 +++++++++++++++--- app/views/moderations/mod.html.erb | 64 ++++++---- config/routes.rb | 4 +- ...2224426_add_user_info_to_buffer_updates.rb | 7 ++ db/schema.rb | 6 +- spec/policies/buffer_update_policy_spec.rb | 19 +++ spec/requests/buffer_updates_spec.rb | 60 ++++++++++ spec/requests/internal_buffer_updates_spec.rb | 76 +++++++----- 17 files changed, 389 insertions(+), 74 deletions(-) create mode 100644 app/controllers/buffer_updates_controller.rb create mode 100644 app/policies/buffer_update_policy.rb create mode 100644 db/migrate/20190402224426_add_user_info_to_buffer_updates.rb create mode 100644 spec/policies/buffer_update_policy_spec.rb create mode 100644 spec/requests/buffer_updates_spec.rb diff --git a/app/controllers/buffer_updates_controller.rb b/app/controllers/buffer_updates_controller.rb new file mode 100644 index 000000000..fb1a7b94a --- /dev/null +++ b/app/controllers/buffer_updates_controller.rb @@ -0,0 +1,50 @@ +class BufferUpdatesController < ApplicationController + after_action :verify_authorized + + def create + authorize BufferUpdate + @article = Article.find(params[:buffer_update][:article_id]) + create_main_tweet + create_satellite_tweets + @article.update(last_buffered: Time.current) + redirect_back(fallback_location: "/mod") + end + + def create_main_tweet + BufferUpdate.create( + article_id: @article.id, + composer_user_id: current_user.id, + body_text: params[:buffer_update][:body_text], + social_service_name: "twitter", + buffer_profile_id_code: ApplicationConfig["BUFFER_TWITTER_ID"], + status: "pending", + ) + end + + def create_satellite_tweets + tags_names = @article.decorate.cached_tag_list_array + tags_names.each do |name| + tag = Tag.find_by(name: name) + if tag&.buffer_profile_id_code.present? + BufferUpdate.create( + article_id: @article.id, + composer_user_id: current_user.id, + body_text: params[:buffer_update][:body_text], + social_service_name: "twitter", + buffer_profile_id_code: tag.buffer_profile_id_code, + tag_id: params[:buffer_update][:tag_id], + status: "pending", + ) + end + end + end + + def modified_body_text + @user = @article.user + if @user.twitter_username.present? + params[:buffer_update][:body_text] + "\n{ author: @#{@user.twitter_username} } #DEVCommunity" + else + params[:buffer_update][:body_text] + end + end +end diff --git a/app/controllers/internal/articles_controller.rb b/app/controllers/internal/articles_controller.rb index db85b0e06..3c71c7bdd 100644 --- a/app/controllers/internal/articles_controller.rb +++ b/app/controllers/internal/articles_controller.rb @@ -2,6 +2,7 @@ class Internal::ArticlesController < Internal::ApplicationController layout "internal" def index + @pending_buffer_updates = BufferUpdate.where(status: "pending").includes(:article) case params[:state] when /not\-buffered/ diff --git a/app/controllers/internal/buffer_updates_controller.rb b/app/controllers/internal/buffer_updates_controller.rb index db4fe3017..6bdfe73f1 100644 --- a/app/controllers/internal/buffer_updates_controller.rb +++ b/app/controllers/internal/buffer_updates_controller.rb @@ -14,4 +14,9 @@ class Internal::BufferUpdatesController < Internal::ApplicationController render body: nil end end + + def update + BufferUpdate.upbuff!(params[:id], current_user.id, params[:body_text], params[:status]) + render body: nil + end end diff --git a/app/controllers/moderations_controller.rb b/app/controllers/moderations_controller.rb index 0e154c49d..6f900283d 100644 --- a/app/controllers/moderations_controller.rb +++ b/app/controllers/moderations_controller.rb @@ -9,7 +9,7 @@ class ModerationsController < ApplicationController includes(:rating_votes). where("rating_votes_count < 3"). where("score > -5"). - order("hotness_score DESC").limit(50) + order("hotness_score DESC").limit(100) if params[:tag].present? @articles = @articles. cached_tagged_with(params[:tag]) diff --git a/app/models/buffer_update.rb b/app/models/buffer_update.rb index f910e4058..1b27302f6 100644 --- a/app/models/buffer_update.rb +++ b/app/models/buffer_update.rb @@ -2,6 +2,7 @@ class BufferUpdate < ApplicationRecord belongs_to :article validate :validate_body_text_recent_uniqueness + validates :status, inclusion: { in: %w[pending sent_direct confirmed dismissed] } def self.buff!(article_id, text, buffer_profile_id_code, social_service_name = "twitter", tag_id = nil) buffer_response = send_to_buffer(text, buffer_profile_id_code) @@ -12,9 +13,20 @@ class BufferUpdate < ApplicationRecord buffer_profile_id_code: buffer_profile_id_code, social_service_name: social_service_name, buffer_response: buffer_response, + status: "sent_direct", ) end + def self.upbuff!(buffer_update_id, admin_id, body_text, status) + buffer_update = BufferUpdate.find(buffer_update_id) + if status == "confirmed" + buffer_response = send_to_buffer(buffer_update.body_text, buffer_update.buffer_profile_id_code) + buffer_update.update!(buffer_response: buffer_response, status: status, approver_user_id: admin_id, body_text: body_text) + else + buffer_update.update!(status: status, approver_user_id: admin_id) + end + end + def self.send_to_buffer(text, buffer_profile_id_code) client = Buffer::Client.new(ApplicationConfig["BUFFER_ACCESS_TOKEN"]) client.create_update( @@ -31,6 +43,8 @@ class BufferUpdate < ApplicationRecord private def validate_body_text_recent_uniqueness + return if persisted? + if BufferUpdate.where(body_text: body_text, article_id: article_id, tag_id: tag_id, social_service_name: social_service_name). where("created_at > ?", 2.minutes.ago).any? errors.add(:body_text, "\"#{body_text}\" has already been submitted very recently") diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 5d5797b1e..57da0fe95 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -66,4 +66,8 @@ class ApplicationPolicy def user_is_banned? user.banned end + + def user_is_trusted? + user.has_role?(:trusted) + end end diff --git a/app/policies/buffer_update_policy.rb b/app/policies/buffer_update_policy.rb new file mode 100644 index 000000000..b5a09a2ae --- /dev/null +++ b/app/policies/buffer_update_policy.rb @@ -0,0 +1,5 @@ +class BufferUpdatePolicy < ApplicationPolicy + def create? + user_is_trusted? + end +end diff --git a/app/views/internal/articles/_individual_article.html.erb b/app/views/internal/articles/_individual_article.html.erb index c29ce42a4..1262687bb 100644 --- a/app/views/internal/articles/_individual_article.html.erb +++ b/app/views/internal/articles/_individual_article.html.erb @@ -129,7 +129,10 @@

Twitter MAIN

+ <% if !article.user.twitter_username.blank? %> + { author: @<%= article.user.twitter_username %> } #DEVCommunity + <% end %> + <% end %> <% if (article.decorate.cached_tag_list_array & Tag.bufferized_tags).any? %> diff --git a/app/views/internal/articles/index.html.erb b/app/views/internal/articles/index.html.erb index 83c4780fc..76f13c74c 100644 --- a/app/views/internal/articles/index.html.erb +++ b/app/views/internal/articles/index.html.erb @@ -44,6 +44,34 @@ <% end %> +<% @pending_buffer_updates.each do |buffer_update| %> +
+

<%= buffer_update.article.title %>

+

Score: <%= buffer_update.article.score %>

+
+ <%= HTML_Truncator.truncate(buffer_update.article.processed_html, + 50, ellipsis: '... Read Entire Post').html_safe %> +
+ <%= Tag.find_by(id: buffer_update.tag_id)&.name || "@ThePracticalDev (main)" %>: +
+ + + + + +
+ +
+
+ + + + + +
+
+<% end %> + <% if @featured_articles && @featured_articles.any? %>

Manually Featured Articles

<% @featured_articles.each do |article| %> diff --git a/app/views/moderations/index.html.erb b/app/views/moderations/index.html.erb index f60831918..db56aaa65 100644 --- a/app/views/moderations/index.html.erb +++ b/app/views/moderations/index.html.erb @@ -12,6 +12,46 @@ .level-rating-button.selected { background: #9bebff; } + .tag-mod-form { + border-top: 1px solid rgba(255, 255, 255, 0.5); + margin: 40px auto; + } + + .tag-mod-form ul { + text-align: left; + font-size: 15px; + } + + .tag-mod-form form { + width: 500px; + margin: auto; + max-width: 90%; + } + + .tag-mod-form form input { + width: 100%; + padding: 5px; + font-size: 20px; + margin-bottom: 5px; + border: 1px solid #888; + } + + .tag-mod-form form input[type="submit"] { + background: #0045ff; + color: white; + font-weight: bold; + padding: 10px; + margin: auto; + display: block; + } + + .tag-mod-form form textarea { + width: 100%; + padding: 5px; + font-size: 17px; + height: 100px; + border: 1px solid #888; + }



@@ -23,8 +63,8 @@
<% @articles.each do |article| %> <% @rating_vote = article.rating_votes.where(user_id: current_user.id).first %> - <% unless @rating_vote %> -
+ <% if !@rating_vote || article.last_buffered.nil? %> +

<%= article.title %>

<% cache("main-article-tags-#{article.cached_tag_list}", expires_in: 30.hours) do %> @@ -40,23 +80,33 @@ 200, ellipsis: '... Read Entire Post').html_safe %>
-

Moderate

-

Experience Level Target:

- <%= form_for(RatingVote.new) do |f| %> - <%= f.hidden_field :article_id, value: article.id %> - <%= f.hidden_field :group, value: "experience_level" %> - - - - - - - - - - + <% if article.last_buffered.nil? %> +
+ <%= form_for(BufferUpdate.new) do |f| %> +

Suggest a Tweet

+ <%= f.hidden_field :article_id, value: article.id %> + <%= f.text_area :body_text, maxlength: 220 %> + <%= f.submit %> + <% end %> +
+

+

Text will be used in the body of a tweet linking to this post. +

Tweet suggestion can be a TLDR of the post, an interesting quote from the post, or bullet points from topics covered in the post. +

<% end %> -

+
+

Experience Level Target:

+ <% 10.times do |i| %> + <%= form_for(RatingVote.new, html: { style: "display: inline-block" }) do |f| %> + <%= f.hidden_field :article_id, value: article.id %> + <%= f.hidden_field :group, value: "experience_level" %> + <%= f.hidden_field :rating, value: i + 1 %> + + <% end %> + <% end %> +
+

Full Moderation

+



<% end %> @@ -75,3 +125,30 @@
<% end %> + + + diff --git a/app/views/moderations/mod.html.erb b/app/views/moderations/mod.html.erb index 3a4993568..8da2ce25a 100644 --- a/app/views/moderations/mod.html.erb +++ b/app/views/moderations/mod.html.erb @@ -106,7 +106,7 @@

The DEV team will be notified about vomits and take appropriate action, but leaving a level-headed comment addressing the behavior is welcome if you feel up for it.

<% end %> - <% is_mod = (@moderatable.tag_list.select { |t| current_user.has_role?(:tag_moderator, Tag.find_by_name(t)) }).any? if @moderatable.class.name == "Article" %> + <% is_mod = (@moderatable.tag_list.select { |t| current_user.has_role?(:tag_moderator, Tag.find_by(name: t)) }).any? if @moderatable.class.name == "Article" %> <% if @moderatable.class.name == "Article" && (current_user.has_role?(:super_admin) || is_mod) %>
<%= form_for(TagAdjustment.new) do |f| %> @@ -137,30 +137,46 @@
<% end %> <% if @moderatable.class.name == "Article" %> - <% @rating_vote = RatingVote.where(article_id: @moderatable.id, user_id: current_user.id).first %> - <%= form_for(RatingVote.new) do |f| %> -

Experience Level of Post

- <%= f.hidden_field :article_id, value: @moderatable.id %> - <%= f.hidden_field :group, value: "experience_level" %> - - - - - - - - - - + <% if @moderatable.last_buffered.blank? %> +
+ <%= form_for(BufferUpdate.new) do |f| %> +

Suggest a Tweet

+ <%= f.hidden_field :article_id, value: @moderatable.id %> + <%= f.text_area :body_text, maxlength: 220 %> + <%= f.submit %> + <% end %> +

+

Text will be used in the body of a tweet linking to this post. +

Tweet suggestion can be a TLDR of the post, an interesting quote from the post, or bullet points from topics covered in the post. +

+
<% end %> -

- All rating votes are private. -

Provide a rating between 1 and 10 based on which audience would find this post most valuable. -

1: for brand new developers, 3: for junior developers, 10: for very advanced developers, etc. -

This will provide - approximate and gentle algorithmic adjustments to help deliver relevant content. -

It will be used as one of many indicators. No one rating will have an overly dramatic affect. -

+
+ <% @rating_vote = RatingVote.where(article_id: @moderatable.id, user_id: current_user.id).first %> + <%= form_for(RatingVote.new) do |f| %> +

Experience Level of Post

+ <%= f.hidden_field :article_id, value: @moderatable.id %> + <%= f.hidden_field :group, value: "experience_level" %> + + + + + + + + + + + <% end %> +

+ All rating votes are private. +

Provide a rating between 1 and 10 based on which audience would find this post most valuable. +

1: for brand new developers, 3: for junior developers, 10: for very advanced developers, etc. +

This will provide + approximate and gentle algorithmic adjustments to help deliver relevant content. +

It will be used as one of many indicators. No one rating will have an overly dramatic affect. +

+
<% end %> diff --git a/config/routes.rb b/config/routes.rb index 7881b9093..62e59fdb0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,7 +42,7 @@ Rails.application.routes.draw do end resources :events resources :dogfood, only: [:index] - resources :buffer_updates, only: [:create] + resources :buffer_updates, only: %i[create update] resources :articles, only: %i[index update] do get "rss_articles", on: :collection end @@ -137,6 +137,8 @@ Rails.application.routes.draw do resources :tag_adjustments, only: [:create] resources :rating_votes, only: [:create] resources :page_views, only: %i[create update] + resources :buffer_updates, only: [:create] + get "/notifications/:filter" => "notifications#index" get "/notifications/:filter/:org_id" => "notifications#index" diff --git a/db/migrate/20190402224426_add_user_info_to_buffer_updates.rb b/db/migrate/20190402224426_add_user_info_to_buffer_updates.rb new file mode 100644 index 000000000..d022ac038 --- /dev/null +++ b/db/migrate/20190402224426_add_user_info_to_buffer_updates.rb @@ -0,0 +1,7 @@ +class AddUserInfoToBufferUpdates < ActiveRecord::Migration[5.1] + def change + add_column :buffer_updates, :composer_user_id, :integer + add_column :buffer_updates, :approver_user_id, :integer + add_column :buffer_updates, :status, :string, default: "pending" + end +end diff --git a/db/schema.rb b/db/schema.rb index a391323c8..125158b58 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_04_01_213605) do +ActiveRecord::Schema.define(version: 2019_04_02_224426) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "plpgsql" @@ -192,13 +192,16 @@ ActiveRecord::Schema.define(version: 2019_04_01_213605) do end create_table "buffer_updates", force: :cascade do |t| + t.integer "approver_user_id" t.integer "article_id", null: false t.text "body_text" t.string "buffer_id_code" t.string "buffer_profile_id_code" t.text "buffer_response", default: "--- {}\n" + t.integer "composer_user_id" t.datetime "created_at", null: false t.string "social_service_name" + t.string "status", default: "pending" t.integer "tag_id" t.datetime "updated_at", null: false end @@ -484,7 +487,6 @@ ActiveRecord::Schema.define(version: 2019_04_01_213605) do t.index ["json_data"], name: "index_notifications_on_json_data", using: :gin t.index ["notifiable_id"], name: "index_notifications_on_notifiable_id" t.index ["notifiable_type"], name: "index_notifications_on_notifiable_type" - t.index ["user_id", "organization_id", "notifiable_id", "notifiable_type", "action"], name: "index_notifications_on_user_organization_notifiable_and_action", unique: true t.index ["user_id"], name: "index_notifications_on_user_id" end diff --git a/spec/policies/buffer_update_policy_spec.rb b/spec/policies/buffer_update_policy_spec.rb new file mode 100644 index 000000000..3da642bbd --- /dev/null +++ b/spec/policies/buffer_update_policy_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +RSpec.describe BufferUpdatePolicy do + subject { described_class.new(user, block) } + + let(:block) { build(:block) } + + 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 diff --git a/spec/requests/buffer_updates_spec.rb b/spec/requests/buffer_updates_spec.rb new file mode 100644 index 000000000..17d5cd079 --- /dev/null +++ b/spec/requests/buffer_updates_spec.rb @@ -0,0 +1,60 @@ +require "rails_helper" + +RSpec.describe "BufferUpdates", type: :request do + let(:user) { create(:user) } + let(:mod_user) { create(:user) } + let(:article) { create(:article, user_id: user.id) } + let(:comment) { create(:comment, user_id: user.id, commentable_id: article.id) } + + context "when trusted user is logged in" do + before do + sign_in mod_user + mod_user.add_role(:trusted) + end + + it "creates buffer update for tweet if tweet params are passed" do + post "/buffer_updates", + params: + { buffer_update: { body_text: "This is the text!!!!", tag_id: "javascript", article_id: article.id } } + expect(BufferUpdate.all.size).to eq(1) + expect(BufferUpdate.last.body_text).to eq("This is the text!!!!") + expect(BufferUpdate.last.status).to eq("pending") + end + end + + context "when non-trusted user is logged in" do + before do + sign_in user + mod_user.add_role(:trusted) + end + + it "rejects buffer update for non-trusted user" do + expect do + post "/buffer_updates", + params: + { buffer_update: { body_text: "This is the text!!!!", tag_id: "javascript", article_id: article.id } } + end.to raise_error(Pundit::NotAuthorizedError) + end + end + + # it "updates last buffered at" do + # post "/internal/buffer_updates", + # params: + # { social_channel: "main_twitter", article_id: article.id, tweet: "Hello this is a test" } + # expect(article.reload.last_buffered).not_to eq(nil) + # end + + # it "updates last buffered at with satellite buffer" do + # post "/internal/buffer_updates", + # params: + # { social_channel: "satellite_twitter", article_id: article.id, tweet: "Hello this is a test" } + # expect(article.reload.last_buffered).not_to eq(nil) + # end + + # it "updates last facebook buffered at" do + # post "/internal/buffer_updates", + # params: + # { social_channel: "facebook", article_id: article.id, tweet: "Hello this is a test" } + # expect(article.reload.facebook_last_buffered).not_to eq(nil) + # end +end diff --git a/spec/requests/internal_buffer_updates_spec.rb b/spec/requests/internal_buffer_updates_spec.rb index e6bf2edc2..444a38fb8 100644 --- a/spec/requests/internal_buffer_updates_spec.rb +++ b/spec/requests/internal_buffer_updates_spec.rb @@ -10,35 +10,57 @@ RSpec.describe "InternalBufferUpdates", type: :request do user.add_role(:super_admin) end - it "creates buffer update for tweet if tweet params are passed" do - post "/internal/buffer_updates", - params: - { social_channel: "main_twitter", article_id: article.id, tweet: "Hello this is a test" } - expect(BufferUpdate.all.size).to eq(1) - post "/internal/buffer_updates", - params: { social_channel: "main_twitter", article_id: article.id, tweet: "Hello this is a test!" } - expect(BufferUpdate.all.size).to eq(2) - expect(BufferUpdate.last.article_id).to eq(article.id) + describe "POST /internal/buffer_updates" do + it "creates buffer update for tweet if tweet params are passed" do + post "/internal/buffer_updates", + params: + { social_channel: "main_twitter", article_id: article.id, tweet: "Hello this is a test" } + expect(BufferUpdate.all.size).to eq(1) + post "/internal/buffer_updates", + params: { social_channel: "main_twitter", article_id: article.id, tweet: "Hello this is a test!" } + expect(BufferUpdate.all.size).to eq(2) + expect(BufferUpdate.last.article_id).to eq(article.id) + end + + it "updates last buffered at" do + post "/internal/buffer_updates", + params: + { social_channel: "main_twitter", article_id: article.id, tweet: "Hello this is a test" } + expect(article.reload.last_buffered).not_to eq(nil) + end + + it "updates last buffered at with satellite buffer" do + post "/internal/buffer_updates", + params: + { social_channel: "satellite_twitter", article_id: article.id, tweet: "Hello this is a test" } + expect(article.reload.last_buffered).not_to eq(nil) + end + + it "updates last facebook buffered at" do + post "/internal/buffer_updates", + params: + { social_channel: "facebook", article_id: article.id, tweet: "Hello this is a test" } + expect(article.reload.facebook_last_buffered).not_to eq(nil) + end end - it "updates last buffered at" do - post "/internal/buffer_updates", - params: - { social_channel: "main_twitter", article_id: article.id, tweet: "Hello this is a test" } - expect(article.reload.last_buffered).not_to eq(nil) - end + describe "PUT /internal/buffer_updates" do + let(:buffer_update) do + BufferUpdate.create(article_id: article.id, + composer_user_id: user.id, + body_text: "This is text - #{rand(100)}", + social_service_name: "twitter", + tag_id: "ruby", + status: "pending") + end - it "updates last buffered at with satellite buffer" do - post "/internal/buffer_updates", - params: - { social_channel: "satellite_twitter", article_id: article.id, tweet: "Hello this is a test" } - expect(article.reload.last_buffered).not_to eq(nil) - end - - it "updates last facebook buffered at" do - post "/internal/buffer_updates", - params: - { social_channel: "facebook", article_id: article.id, tweet: "Hello this is a test" } - expect(article.reload.facebook_last_buffered).not_to eq(nil) + it "sends to buffer" do + put "/internal/buffer_updates/#{buffer_update.id}", params: { + status: "confirmed", body_text: "test" + } + expect(buffer_update.reload.buffer_response).not_to eq(nil) + expect(buffer_update.reload.status).to eq("confirmed") + expect(buffer_update.reload.approver_user_id).to eq(user.id) + end end end