FEATURE: flag dispositions normalization

All flags should end up in one of the three dispositions
  - Agree
  - Disagree
  - Defer

In the administration area, the *active* flags section displays 4 buttons
  - Agree (hide post + send PM)
  - Disagree
  - Defer
  - Delete

Clicking "Delete" will open a modal that offer to
  - Delete Post & Defer Flags
  - Delete Post & Agree with Flags
  - Delete Spammer (if available)

When the flag has a list associated, the list will now display 1
response and 1 reply and a "show more..." link if there are more in the
conversation. Replying to the conversation will NOT give a disposition.
Moderators must click the buttons that does that.

If someone clicks one buttons, this will add a default moderator message
from that moderator saying what happened.

The *old* flags section now displays the proper dispositions and is
super duper fast (no more N+9999 queries).

FIX: the old list includes deleted topics
FIX: the lists now properly display the topic states (deleted, closed,
archived, hidden, PM)
FIX: flagging a topic that you've already flagged the first post
This commit is contained in:
Régis Hanol
2014-07-28 19:17:37 +02:00
parent 717f57c968
commit bddffa7f9a
50 changed files with 886 additions and 558 deletions

View File

@@ -1,105 +1,139 @@
module FlagQuery
def self.flagged_posts_report(current_user, filter, offset = 0, per_page = 25)
def self.flagged_posts_report(current_user, filter, offset=0, per_page=25)
actions = flagged_post_actions(filter)
guardian = Guardian.new(current_user)
if !guardian.is_admin?
actions = actions.joins(:post => :topic)
.where('category_id in (?)', guardian.allowed_category_ids)
actions = actions.where('category_id in (?)', guardian.allowed_category_ids)
end
post_ids = actions
.limit(per_page)
.offset(offset)
.group(:post_id)
.order('min(post_actions.created_at) DESC')
.pluck(:post_id).uniq
post_ids = actions.limit(per_page)
.offset(offset)
.group(:post_id)
.order('min(post_actions.created_at) DESC')
.pluck(:post_id)
.uniq
return nil if post_ids.blank?
actions = actions
.order('post_actions.created_at DESC')
.includes({:related_post => :topic})
posts = SqlBuilder.new("SELECT p.id, t.title, p.cooked, p.user_id,
p.topic_id, p.post_number, p.hidden, t.visible topic_visible,
p.deleted_at, t.deleted_at topic_deleted_at
FROM posts p
JOIN topics t ON t.id = p.topic_id
WHERE p.id in (:post_ids)").map_exec(OpenStruct, post_ids: post_ids)
posts = SqlBuilder.new("
SELECT p.id,
p.cooked,
p.user_id,
p.topic_id,
p.post_number,
p.hidden,
p.deleted_at
FROM posts p
WHERE p.id in (:post_ids)").map_exec(OpenStruct, post_ids: post_ids)
post_lookup = {}
users = Set.new
user_ids = Set.new
topic_ids = Set.new
posts.each do |p|
users << p.user_id
user_ids << p.user_id
topic_ids << p.topic_id
p.excerpt = Post.excerpt(p.cooked)
p.topic_slug = Slug.for(p.title)
p.delete_field(:cooked)
post_lookup[p.id] = p
end
# maintain order
posts = post_ids.map{|id| post_lookup[id]}
post_actions = actions.where(:post_id => post_ids)
post_actions = actions.order('post_actions.created_at DESC')
.includes(related_post: { topic: { posts: :user }})
.where(post_id: post_ids)
post_actions.each do |pa|
post = post_lookup[pa.post_id]
post.post_actions ||= []
action = pa.attributes
# TODO: add serializer so we can skip this
action = {
id: pa.id,
post_id: pa.post_id,
user_id: pa.user_id,
post_action_type_id: pa.post_action_type_id,
created_at: pa.created_at,
disposed_by_id: pa.disposed_by_id,
disposed_at: pa.disposed_at,
disposition: pa.disposition,
related_post_id: pa.related_post_id,
targets_topic: pa.targets_topic,
staff_took_action: pa.staff_took_action
}
action[:name_key] = PostActionType.types.key(pa.post_action_type_id)
if (pa.related_post && pa.related_post.topic)
action.merge!(topic_id: pa.related_post.topic_id,
slug: pa.related_post.topic.slug,
permalink: pa.related_post.topic.url)
if pa.related_post && pa.related_post.topic
conversation = {}
related_topic = pa.related_post.topic
if response = related_topic.posts[0]
conversation[:response] = {
excerpt: excerpt(response.cooked),
user_id: response.user_id
}
user_ids << response.user_id
if reply = related_topic.posts[1]
conversation[:reply] = {
excerpt: excerpt(reply.cooked),
user_id: reply.user_id
}
user_ids << reply.user_id
conversation[:has_more] = related_topic.posts_count > 2
end
end
action.merge!(permalink: related_topic.relative_url, conversation: conversation)
end
post.post_actions << action
users << pa.user_id
users << pa.deleted_by_id if pa.deleted_by_id
user_ids << pa.user_id
user_ids << pa.disposed_by_id if pa.disposed_by_id
end
# TODO add serializer so we can skip this
# maintain order
posts = post_ids.map { |id| post_lookup[id] }
# TODO: add serializer so we can skip this
posts.map!(&:marshal_dump)
[posts, User.where(id: users.to_a).to_a]
[
posts,
Topic.with_deleted.where(id: topic_ids.to_a).to_a,
User.includes(:user_stat).where(id: user_ids.to_a).to_a
]
end
protected
def self.flagged_post_ids(filter, offset, limit)
<<SQL
def self.flagged_post_actions(filter)
post_actions = PostAction.flags
.joins("INNER JOIN posts ON posts.id = post_actions.post_id")
.joins("INNER JOIN topics ON topics.id = posts.topic_id")
SELECT p.id from posts p
JOIN topics t ON t.id = p.topic_id
WHERE p.id IN (
SELECT post_id from post_actions
WHERE
)
/*offset*/
/*limit*/
if filter == "old"
post_actions.with_deleted
.where("post_actions.deleted_at IS NOT NULL OR
post_actions.defered_at IS NOT NULL OR
post_actions.agreed_at IS NOT NULL")
else
post_actions.active
.where("posts.deleted_at" => nil)
.where("topics.deleted_at" => nil)
end
SQL
end
def self.flagged_post_actions(filter)
post_actions = PostAction
.where(post_action_type_id: PostActionType.notify_flag_type_ids)
.joins(:post => :topic)
if filter == 'old'
post_actions
.with_deleted
.where('post_actions.deleted_at IS NOT NULL OR
defer = true OR
topics.deleted_at IS NOT NULL OR
posts.deleted_at IS NOT NULL')
else
post_actions
.where('defer IS NULL OR
defer = false')
.where('posts.deleted_at IS NULL AND
topics.deleted_at IS NULL')
end
end
private
def self.excerpt(cooked)
excerpt = Post.excerpt(cooked, 200)
# remove the first link if it's the first node
fragment = Nokogiri::HTML.fragment(excerpt)
if fragment.children.first == fragment.css("a:first").first
fragment.children.first.remove
end
fragment.to_html.strip
end
end

View File

@@ -29,7 +29,7 @@ module PostGuardian
end
end
def can_clear_flags?(post)
def can_defer_flags?(post)
is_staff? && post
end
@@ -54,7 +54,11 @@ module PostGuardian
end
def can_delete_all_posts?(user)
is_staff? && user && !user.admin? && (user.first_post.nil? || user.first_post.created_at >= SiteSetting.delete_user_max_post_age.days.ago) && user.post_count <= SiteSetting.delete_all_posts_max.to_i
is_staff? &&
user &&
!user.admin? &&
(user.first_post_created_at.nil? || user.first_post_created_at >= SiteSetting.delete_user_max_post_age.days.ago) &&
user.post_count <= SiteSetting.delete_all_posts_max.to_i
end
# Creating Method

View File

@@ -35,12 +35,11 @@ module UserGuardian
end
def can_delete_user?(user)
return false if user.nil?
return false if user.admin?
return false if user.nil? || user.admin?
if is_me?(user)
user.post_count <= 1
else
is_staff? && (user.first_post.nil? || user.first_post.created_at > SiteSetting.delete_user_max_post_age.to_i.days.ago)
is_staff? && (user.first_post_created_at.nil? || user.first_post_created_at > SiteSetting.delete_user_max_post_age.to_i.days.ago)
end
end

View File

@@ -61,7 +61,6 @@ class PostCreator
save_post
extract_links
store_unique_post_key
consider_clearing_flags
track_topic
update_topic_stats
update_user_counts
@@ -147,21 +146,6 @@ class PostCreator
end
end
def clear_possible_flags(topic)
# at this point we know the topic is a PM and has been replied to ... check if we need to clear any flags
#
first_post = Post.select(:id).where(topic_id: topic.id).find_by("post_number = 1")
post_action = nil
if first_post
post_action = PostAction.find_by(related_post_id: first_post.id, deleted_at: nil, post_action_type_id: PostActionType.types[:notify_moderators])
end
if post_action
post_action.remove_act!(@user)
end
end
private
def setup_topic
@@ -233,20 +217,23 @@ class PostCreator
@post.store_unique_post_key
end
def consider_clearing_flags
return if @opts[:import_mode]
return unless @topic.private_message? && @post.post_number > 1 && @topic.user_id != @post.user_id
clear_possible_flags(@topic)
end
def update_user_counts
@user.create_user_stat if @user.user_stat.nil?
if @user.user_stat.first_post_created_at.nil?
@user.user_stat.first_post_created_at = @post.created_at
end
@user.user_stat.post_count += 1
@user.user_stat.topic_count += 1 if @post.post_number == 1
# We don't count replies to your own topics
if !@opts[:import_mode] && @user.id != @topic.user_id
@user.user_stat.update_topic_reply_count
@user.user_stat.save!
end
@user.user_stat.save!
@user.last_posted_at = @post.created_at
@user.save!
end

View File

@@ -62,7 +62,8 @@ class PostDestroyer
feature_users_in_the_topic
Topic.reset_highest(@post.topic_id)
end
trash_post_actions
trash_public_post_actions
agree_with_flags
trash_user_actions
@post.update_flagged_posts_count
remove_associated_replies
@@ -130,15 +131,18 @@ class PostDestroyer
Jobs.enqueue(:feature_topic_users, topic_id: @post.topic_id, except_post_id: @post.id)
end
def trash_post_actions
@post.post_actions.each do |pa|
pa.trash!(@user)
end
def trash_public_post_actions
public_post_actions = PostAction.publics.where(post_id: @post.id)
public_post_actions.each { |pa| pa.trash!(@user) }
f = PostActionType.types.map{|k,v| ["#{k}_count", 0]}
f = PostActionType.public_types.map { |k,v| ["#{k}_count", 0] }
Post.with_deleted.where(id: @post.id).update_all(Hash[*f.flatten])
end
def agree_with_flags
PostAction.agree_flags!(@post, @user, delete_post: true)
end
def trash_user_actions
UserAction.where(target_post_id: @post.id).each do |ua|
row = {

View File

@@ -29,9 +29,7 @@ class PostJobsEnqueuer
end
def after_post_create
if @post.post_number > 1
TopicTrackingState.publish_unread(@post)
end
TopicTrackingState.publish_unread(@post) if @post.post_number > 1
Jobs.enqueue_in(
SiteSetting.email_time_window_mins.minutes,

View File

@@ -105,7 +105,7 @@ class PostRevisor
@post.hidden_at = nil
@post.topic.update_attributes(visible: true)
PostAction.clear_flags!(@post, -1)
PostAction.clear_flags!(@post, Discourse.system_user)
end
@post.extract_quoted_post_numbers