diff --git a/app/assets/javascripts/review_queues.coffee b/app/assets/javascripts/review_queues.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/review_queues.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/review_queues.scss b/app/assets/stylesheets/review_queues.scss new file mode 100644 index 000000000..48fe787b3 --- /dev/null +++ b/app/assets/stylesheets/review_queues.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the ReviewQueues controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/review_queues_controller.rb b/app/controllers/review_queues_controller.rb new file mode 100644 index 000000000..8463a0d8f --- /dev/null +++ b/app/controllers/review_queues_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class ReviewQueuesController < ApplicationController + before_action :set_queue, except: [:index] + before_action :verify_permissions, except: [:index] + + def index + @queues = ReviewQueue.all.includes(:items) + end + + def queue; end + + def next_item + unreviewed = ReviewItem.unreviewed_by(@queue, current_user) + if unreviewed.empty? + render plain: "You've reviewed all available items!" + else + item = unreviewed.first + render "#{item.reviewable_type.underscore.pluralize}/_review_item.html.erb", locals: { queue: @queue, item: item }, layout: nil + end + end + + def submit + render json: { status: 'invalid' }, status: 400 unless @queue.responses.map { |r| r[1] }.include? params[:response] + @item = ReviewItem.find params[:item_id] + + ReviewResult.create user: current_user, result: params[:response], item: @item + + @item.reviewable.custom_review_action(@queue, @item, current_user, params[:response]) if @item.reviewable.respond_to? :custom_review_action + if @item.reviewable.respond_to?(:should_dq?) && @item.reviewable.should_dq?(@queue) + @item.update(completed: true) + end + + render json: { status: 'ok' } + end + + private + + def set_queue + @queue = ReviewQueue[params[:name]] + end + + def verify_permissions + return if user_signed_in? && current_user.has_role?(@queue.privileges) + not_found + end +end diff --git a/app/helpers/review_queues_helper.rb b/app/helpers/review_queues_helper.rb new file mode 100644 index 000000000..270856fe5 --- /dev/null +++ b/app/helpers/review_queues_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module ReviewQueuesHelper +end diff --git a/app/javascript/review.js b/app/javascript/review.js index ce913a670..4e95d4b40 100644 --- a/app/javascript/review.js +++ b/app/javascript/review.js @@ -1,3 +1,5 @@ +import { route } from './util'; + $(() => { $(document).on('ajax:success', 'a.feedback-button[data-remote]', e => { if (!$(e.target).hasClass('on-post')) { @@ -6,3 +8,20 @@ $(() => { } }); }); + +route(/\/review\/\w+/i, async () => { + const loadNextPost = async () => { + const response = await fetch(location.pathname + '/next', { + credentials: 'include' + }); + const html = await response.text(); + $('.review-item-container').html(html); + }; + + loadNextPost(); + + $(document).on('ajax:success', '.review-submit-link', () => { + $('.review-item-container').text('Loading...'); + loadNextPost(); + }); +}); diff --git a/app/models/feedback.rb b/app/models/feedback.rb index 33ee7dc6e..35701e757 100644 --- a/app/models/feedback.rb +++ b/app/models/feedback.rb @@ -13,7 +13,6 @@ class Feedback < ApplicationRecord belongs_to :user belongs_to :invalidated_by, class_name: 'User', foreign_key: 'invalidated_by' belongs_to :api_key - has_one :review, class_name: 'ReviewResult', required: false, dependent: :destroy before_save :check_for_user_assoc before_save :check_for_dupe_feedback diff --git a/app/models/post.rb b/app/models/post.rb index 279d9a544..bf6ce1d74 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -18,7 +18,7 @@ class Post < ApplicationRecord has_many :flag_logs, dependent: :destroy has_many :flags, dependent: :destroy has_and_belongs_to_many :spam_domains - has_many :reviews, class_name: 'ReviewResult', dependent: :destroy + has_one :review_item, as: :reviewable has_many :comments, class_name: 'PostComment', dependent: :destroy scope(:includes_for_post_row, -> do @@ -39,6 +39,14 @@ class Post < ApplicationRecord scope(:undeleted, -> { where(deleted_at: nil) }) + after_commit do + if review_item.present? && should_dq?(ReviewQueue['posts']) + review_item.update(completed: true) + elsif !review_item.present? + ReviewItem.create(reviewable: self, queue: ReviewQueue['posts'], completed: false) + end + end + after_commit :parse_domains, on: :create after_create do @@ -319,4 +327,17 @@ def parse_domains domain.posts << self unless domain.posts.include? self end end + + def custom_review_action(_queue, _item, user, response) + feedbacks.create(user: user, feedback_type: response) + end + + def should_dq?(queue) + case queue.name + when 'posts' + feedbacks.count >= 2 + else + false + end + end end diff --git a/app/models/review_item.rb b/app/models/review_item.rb index e7e9c5d33..899c16fe8 100644 --- a/app/models/review_item.rb +++ b/app/models/review_item.rb @@ -2,7 +2,17 @@ class ReviewItem < ApplicationRecord belongs_to :user - belongs_to :review_queue + belongs_to :queue, class_name: 'ReviewQueue', foreign_key: 'review_queue_id' belongs_to :reviewable, polymorphic: true has_many :results, class_name: 'ReviewResult' + + validates :reviewable_type, inclusion: { in: ['Post'] } + + scope(:active, -> { where(completed: false) }) + scope(:completed, -> { where(completed: true) }) + + def self.unreviewed_by(queue, user) + joins("LEFT JOIN review_results rr ON rr.review_item_id = review_items.id AND rr.user_id = #{user.id}").where(review_items: { queue: queue }, + rr: { id: nil }) + end end diff --git a/app/models/review_queue.rb b/app/models/review_queue.rb index a4b8408ec..710a20ddc 100644 --- a/app/models/review_queue.rb +++ b/app/models/review_queue.rb @@ -4,7 +4,14 @@ class ReviewQueue < ApplicationRecord has_many :items, class_name: 'ReviewItem' has_many :results, class_name: 'ReviewResult', through: :items + serialize :responses, JSON + def self.[](key) find_by name: key end + + def should_dq?(item) + item.reviewable.should_dq?(self) if item.reviewable.respond_to? :should_dq? + false + end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 969cdfe8d..fc2f8fc74 100755 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -49,8 +49,8 @@ <%= nav_link GraphsController %> <%= nav_link SearchController %> <% if user_signed_in? %> - <%= nav_link ReviewController do %> - <% review_items = Post.unreviewed.count %> + <%= nav_link ReviewQueuesController, label: 'review' do %> + <% review_items = ReviewItem.active.count %> <%# badge hides itself if it doesn’t have contents. %> <% unless review_items == 0 || !current_user.has_role?(:reviewer) %><%= review_items %><% end %> <% end %> diff --git a/app/views/posts/_review_item.html.erb b/app/views/posts/_review_item.html.erb new file mode 100644 index 000000000..efbbc5efe --- /dev/null +++ b/app/views/posts/_review_item.html.erb @@ -0,0 +1,4 @@ +
<%= q.description %>
+<%= @queue.description %>
+ +