diff --git a/app/assets/images/emoji/emoji-one-sloth.png b/app/assets/images/emoji/emoji-one-sloth.png new file mode 100644 index 000000000..55b75d6de Binary files /dev/null and b/app/assets/images/emoji/emoji-one-sloth.png differ diff --git a/app/assets/javascripts/initializePage.js.erb b/app/assets/javascripts/initializePage.js.erb index b8708e26c..c80cd47da 100644 --- a/app/assets/javascripts/initializePage.js.erb +++ b/app/assets/javascripts/initializePage.js.erb @@ -12,8 +12,9 @@ function callInitalizers(){ if (document.getElementsByTagName('body')[0].getAttribute('data-loaded') == "true") { clearInterval(waitingForDataLoad); if (document.getElementsByTagName('body')[0].getAttribute('data-user-status') == "logged-in") { - initializeAllChatButtons(); initializeBaseUserData(); + initializeAllChatButtons(); + initializeAllTagEditButtons(); } initializeAllFollowButts(); initializeReadingListIcons(); diff --git a/app/assets/javascripts/initializers/initializeAllTagEditButtons.js b/app/assets/javascripts/initializers/initializeAllTagEditButtons.js new file mode 100644 index 000000000..ddb27f812 --- /dev/null +++ b/app/assets/javascripts/initializers/initializeAllTagEditButtons.js @@ -0,0 +1,12 @@ +function initializeAllTagEditButtons() { + var tagEditButton = document.getElementById('tag-edit-button'); + var user = userData(); + if ( + user && + tagEditButton && + user.moderator_for_tags.indexOf(tagEditButton.dataset.tag) > -1 + ) { + tagEditButton.style.display = 'inline-block'; + document.getElementById('tag-mod-button').style.display = 'inline-block'; + } +} diff --git a/app/assets/stylesheets/scaffolds.scss b/app/assets/stylesheets/scaffolds.scss index 92a99bebb..d5f0a6c17 100644 --- a/app/assets/stylesheets/scaffolds.scss +++ b/app/assets/stylesheets/scaffolds.scss @@ -214,12 +214,12 @@ body.comic-sans-article-body { } } -.pro-visible-block { +.pro-visible-block, .trusted-visible-block { display: none !important; } -body.pro-status-true .pro-visible-block { - display: block !important;; +body.pro-status-true .pro-visible-block, body.trusted-status-true .trusted-visible-block { + display: block !important; } .delete-confirm{ diff --git a/app/assets/stylesheets/sidebar-data.scss b/app/assets/stylesheets/sidebar-data.scss index 201a457d6..29ae460e8 100644 --- a/app/assets/stylesheets/sidebar-data.scss +++ b/app/assets/stylesheets/sidebar-data.scss @@ -13,4 +13,11 @@ $dark-medium-gray ); font-family: $monospace; + .mod-action-button { + padding: 2px 9px; + font-size: 1.2em; + display: block; + margin-top: 5px; + border-radius: 3px; + } } \ No newline at end of file diff --git a/app/controllers/async_info_controller.rb b/app/controllers/async_info_controller.rb index 4414b5189..41f40d598 100644 --- a/app/controllers/async_info_controller.rb +++ b/app/controllers/async_info_controller.rb @@ -49,6 +49,7 @@ class AsyncInfoController < ApplicationController number_of_comments: @user.comments.count, display_sponsors: @user.display_sponsors, trusted: @user.trusted, + moderator_for_tags: @user.moderator_for_tags, experience_level: @user.experience_level, preferred_languages_array: @user.preferred_languages_array, config_body_class: @user.config_body_class, diff --git a/app/controllers/moderations_controller.rb b/app/controllers/moderations_controller.rb index 3316c1cb7..d5ece75eb 100644 --- a/app/controllers/moderations_controller.rb +++ b/app/controllers/moderations_controller.rb @@ -8,7 +8,7 @@ class ModerationsController < ApplicationController @articles = Article.published. where("rating_votes_count < 3"). where("score > -5"). - order("hotness_score DESC").limit(100) + order("hotness_score DESC").limit(50) @articles = @articles.cached_tagged_with(params[:tag]) if params[:tag].present? @rating_votes = RatingVote.where(article: @articles, user: current_user) diff --git a/app/decorators/user_decorator.rb b/app/decorators/user_decorator.rb index 6c16a37a4..aff0e385e 100644 --- a/app/decorators/user_decorator.rb +++ b/app/decorators/user_decorator.rb @@ -34,7 +34,7 @@ class UserDecorator < ApplicationDecorator def config_body_class body_class = "" body_class += config_theme.tr("_", "-") - body_class = body_class + " " + config_font.tr("_", "-") + "-article-body" + " pro-status-#{pro?}" + body_class = body_class + " " + config_font.tr("_", "-") + "-article-body" + " pro-status-#{pro?} trusted-status-#{trusted}" body_class end diff --git a/app/labor/assign_tag_moderator.rb b/app/labor/assign_tag_moderator.rb index 3b8ada559..6b6ea503e 100644 --- a/app/labor/assign_tag_moderator.rb +++ b/app/labor/assign_tag_moderator.rb @@ -17,6 +17,7 @@ module AssignTagModerator def self.add_tag_mod_role(user, tag) user.update(email_tag_mod_newsletter: true) if user.email_tag_mod_newsletter == false user.add_role(:tag_moderator, tag) + Rails.cache.delete("user-#{user.id}/tag_moderators_list") MailchimpBot.new(user).manage_tag_moderator_list end @@ -34,6 +35,7 @@ module AssignTagModerator def self.remove_tag_moderator(user, tag) user.remove_role(:tag_moderator, tag) user.update(email_tag_mod_newsletter: false) if user.email_tag_mod_newsletter == true + Rails.cache.delete("user-#{user.id}/tag_moderators_list") MailchimpBot.new(user).manage_tag_moderator_list end end diff --git a/app/models/reaction.rb b/app/models/reaction.rb index 3b44567b9..396bd0be4 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -58,9 +58,10 @@ class Reaction < ApplicationRecord end def cached_any_reactions_for?(reactable, user, category) - cache_name = "any_reactions_for-#{reactable.class.name}-#{reactable.id}-#{user.updated_at}-#{category}" + class_name = reactable.class.name == "ArticleDecorator" ? "Article" : reactable.class.name + cache_name = "any_reactions_for-#{class_name}-#{reactable.id}-#{user.updated_at}-#{category}" Rails.cache.fetch(cache_name, expires_in: 24.hours) do - Reaction.where(reactable_id: reactable.id, reactable_type: reactable.class.name, user: user, category: category).any? + Reaction.where(reactable_id: reactable.id, reactable_type: class_name, user: user, category: category).any? end end end diff --git a/app/models/user.rb b/app/models/user.rb index 8ed5d58e5..9f4e5a0b3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -336,6 +336,13 @@ class User < ApplicationRecord end end + def moderator_for_tags + Rails.cache.fetch("user-#{id}/tag_moderators_list", expires_in: 200.hours) do + tag_ids = roles.where(name: "tag_moderator").pluck(:resource_id) + Tag.where(id: tag_ids).pluck(:name) + end + end + def scholar valid_pass = workshop_expiration.nil? || workshop_expiration > Time.current has_role?(:workshop_pass) && valid_pass diff --git a/app/views/articles/_sidebar_nav.html.erb b/app/views/articles/_sidebar_nav.html.erb index b68072a05..c3711ea9a 100644 --- a/app/views/articles/_sidebar_nav.html.erb +++ b/app/views/articles/_sidebar_nav.html.erb @@ -23,6 +23,9 @@ " alt="reading list icon" /> Reading List + + " alt="moderation icon" /> Moderation + " alt="history icon" /> History diff --git a/app/views/articles/tags/_sidebar.html.erb b/app/views/articles/tags/_sidebar.html.erb index 95e3143d5..95e4efb99 100644 --- a/app/views/articles/tags/_sidebar.html.erb +++ b/app/views/articles/tags/_sidebar.html.erb @@ -65,6 +65,18 @@
<%= pluralize Article.published.cached_tagged_with(@tag).size, "Post" %> Published
+ <% if user_signed_in? %> + + + <% end %> diff --git a/app/views/moderations/_reactions.html.erb b/app/views/moderations/_reactions.html.erb new file mode 100644 index 000000000..63618b3e5 --- /dev/null +++ b/app/views/moderations/_reactions.html.erb @@ -0,0 +1,13 @@ +

Add a reaction

+ + + diff --git a/app/views/moderations/index.html.erb b/app/views/moderations/index.html.erb index d8e9c2265..44b11fee0 100644 --- a/app/views/moderations/index.html.erb +++ b/app/views/moderations/index.html.erb @@ -1,3 +1,5 @@ +<% title "Moderate" %> + <% if current_user&.trusted %>



-

Mod Convenience Dashboard

-

Add "Experience Level" ratings

-
More mod tools will be added as they are created.
+

Mod Dashboard (beta)

+ <% if current_user.moderator_for_tags.any? %> + ">all recent + <% current_user.moderator_for_tags.each do |tag| %> + | ">#<%= tag %> + <% end %> + <% end %>
<% @articles.each do |article| %> @@ -80,7 +111,8 @@ <%= HTML_Truncator.truncate( article.processed_html, 200, - ellipsis: '... Read Entire Post').html_safe %> + ellipsis: '... Read Entire Post').html_safe + %>
<% if article.last_buffered.nil? %> @@ -88,17 +120,13 @@ <%= 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.text_area :body_text, maxlength: 220, placeholder: "Short summary, bullet points, etc." %> <%= f.submit "Share Tweet Suggestion" %> <% 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:

+

Suggest experience level

<% 10.times do |i| %> <%= form_for(RatingVote.new, html: { style: "display: inline-block" }) do |f| %> <%= f.hidden_field :article_id, value: article.id %> @@ -108,7 +136,8 @@ <% end %> <% end %>
-

Full Moderation

+ <%= render "moderations/reactions", moderatable: article %> +

Full Moderation for Post





@@ -153,4 +182,36 @@ }); } } + setTimeout(function () { + var butts = document.getElementsByClassName('reaction-button'); + for (var i = 0; i < butts.length; i++) { + var butt = butts[i]; + butt.onclick = function (event) { + event.preventDefault(); + var thisButt = this; + thisButt.classList.add('reacted'); + + function successCb(response) { + if (response.result === 'create') { + thisButt.classList.add('reacted'); + } else { + thisButt.classList.remove('reacted'); + } + } + + var formData = new FormData(); + formData.append('reactable_type', thisButt.dataset.reactableType); + formData.append('category', thisButt.dataset.category); + formData.append('reactable_id', thisButt.dataset.reactableId); + + getCsrfToken() + .then(sendFetch('reaction-creation', formData)) + .then(function (response) { + if (response.status === 200) { + response.json().then(successCb); + } + }); + }; + } + }, 200) diff --git a/config/routes.rb b/config/routes.rb index d2c241967..472708f44 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -285,7 +285,7 @@ Rails.application.routes.draw do get "/stories/warm_comments/:username/:slug" => "stories#warm_comments" get "/freestickers" => "giveaways#new" get "/shop", to: redirect("https://shop.dev.to/") - get "/mod" => "moderations#index" + get "/mod" => "moderations#index", as: :mod post "/fallback_activity_recorder" => "ga_events#create" diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6960680db..f8c524902 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -481,6 +481,24 @@ RSpec.describe User, type: :model do expect(user.all_follows.size).to eq(2) end + describe "#moderator_for_tags" do + let(:tag1) { create(:tag) } + let(:tag2) { create(:tag) } + let(:tag3) { create(:tag) } + + it "lists tags user moderates" do + user.add_role(:tag_moderator, tag1) + user.add_role(:tag_moderator, tag2) + expect(user.moderator_for_tags).to include(tag1.name) + expect(user.moderator_for_tags).to include(tag2.name) + expect(user.moderator_for_tags).not_to include(tag3.name) + end + + it "returns empty array if no tags moderated" do + expect(user.moderator_for_tags).to eq([]) + end + end + describe "#followed_articles" do let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -534,32 +552,37 @@ RSpec.describe User, type: :model do end it "creates proper body class with defaults" do - expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?}") + expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted}") end it "creates proper body class with sans serif config" do user.config_font = "sans_serif" - expect(user.decorate.config_body_class).to eq("default sans-serif-article-body pro-status-#{user.pro?}") + expect(user.decorate.config_body_class).to eq("default sans-serif-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted}") end it "creates proper body class with night theme" do user.config_theme = "night_theme" - expect(user.decorate.config_body_class).to eq("night-theme default-article-body pro-status-#{user.pro?}") + expect(user.decorate.config_body_class).to eq("night-theme default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted}") end it "creates proper body class with pink theme" do user.config_theme = "pink_theme" - expect(user.decorate.config_body_class).to eq("pink-theme default-article-body pro-status-#{user.pro?}") + expect(user.decorate.config_body_class).to eq("pink-theme default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted}") end it "creates proper body class with minimal light theme" do user.config_theme = "minimal_light_theme" - expect(user.decorate.config_body_class).to eq("minimal-light-theme default-article-body pro-status-#{user.pro?}") + expect(user.decorate.config_body_class).to eq("minimal-light-theme default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted}") end it "creates proper body class with pro user" do user.add_role(:pro) - expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?}") + expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted}") + end + + it "creates proper body class with trusted user" do + user.add_role(:trusted) + expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted}") end it "inserts into mailchimp" do diff --git a/spec/requests/moderations_spec.rb b/spec/requests/moderations_spec.rb index c936d04f8..ee6dac6d1 100644 --- a/spec/requests/moderations_spec.rb +++ b/spec/requests/moderations_spec.rb @@ -60,7 +60,7 @@ RSpec.describe "Moderations", type: :request do it "grants access to /mod index with articles" do create(:article, published: true) get "/mod" - expect(response.body).to include("Experience Level Target") + expect(response.body).to include("Suggest experience level") end end end