discourse/app/models/user_action.rb

373 lines
12 KiB
Ruby
Raw Normal View History

2013-02-06 03:16:51 +08:00
class UserAction < ActiveRecord::Base
belongs_to :user
belongs_to :target_post, class_name: "Post"
belongs_to :target_topic, class_name: "Topic"
2013-02-07 23:45:24 +08:00
2013-02-06 03:16:51 +08:00
validates_presence_of :action_type
validates_presence_of :user_id
LIKE = 1
2013-02-07 23:45:24 +08:00
WAS_LIKED = 2
2013-02-06 03:16:51 +08:00
BOOKMARK = 3
NEW_TOPIC = 4
2013-05-01 08:52:31 +08:00
REPLY = 5
2013-02-06 03:16:51 +08:00
RESPONSE= 6
MENTION = 7
QUOTE = 9
EDIT = 11
NEW_PRIVATE_MESSAGE = 12
GOT_PRIVATE_MESSAGE = 13
2015-04-22 02:36:46 +08:00
PENDING = 14
2013-02-06 03:16:51 +08:00
ORDER = Hash[*[
GOT_PRIVATE_MESSAGE,
2013-05-18 02:11:33 +08:00
NEW_PRIVATE_MESSAGE,
2015-04-22 02:36:46 +08:00
PENDING,
2013-02-06 03:16:51 +08:00
NEW_TOPIC,
2013-05-01 08:52:31 +08:00
REPLY,
2013-02-06 03:16:51 +08:00
RESPONSE,
LIKE,
WAS_LIKED,
MENTION,
QUOTE,
2013-07-09 16:15:43 +08:00
BOOKMARK,
2013-02-06 03:16:51 +08:00
EDIT
].each_with_index.to_a.flatten]
# note, this is temporary until we upgrade to rails 4
# in rails 4 types are mapped correctly so you dont end up
# having strings where you would expect bools
class UserActionRow < OpenStruct
include ActiveModel::SerializerSupport
def as_json(options = nil)
@table.as_json(options)
end
end
def self.last_action_in_topic(user_id, topic_id)
UserAction.where(user_id: user_id,
target_topic_id: topic_id,
action_type: [RESPONSE, MENTION, QUOTE]).order('created_at DESC').pluck(:target_post_id).first
end
2013-02-06 03:16:51 +08:00
def self.stats(user_id, guardian)
2013-02-26 00:42:20 +08:00
# Sam: I tried this in AR and it got complex
builder = UserAction.sql_builder <<SQL
2013-02-06 03:16:51 +08:00
SELECT action_type, COUNT(*) count
FROM user_actions a
2015-04-22 02:36:46 +08:00
LEFT JOIN topics t ON t.id = a.target_topic_id
LEFT JOIN posts p on p.id = a.target_post_id
2015-04-22 02:36:46 +08:00
LEFT JOIN posts p2 on p2.topic_id = a.target_topic_id and p2.post_number = 1
LEFT JOIN categories c ON c.id = t.category_id
/*where*/
GROUP BY action_type
SQL
builder.where('a.user_id = :user_id', user_id: user_id)
apply_common_filters(builder, user_id, guardian)
results = builder.exec.to_a
results.sort! { |a,b| ORDER[a.action_type] <=> ORDER[b.action_type] }
2013-02-06 03:16:51 +08:00
results
end
def self.private_messages_stats(user_id, guardian)
return unless guardian.can_see_private_messages?(user_id)
# list the stats for: all/mine/unread (topic-based)
2015-09-10 15:07:20 +08:00
sql = <<SQL
SELECT COUNT(*) "all",
SUM(CASE WHEN t.user_id = :user_id THEN 1 ELSE 0 END) mine,
SUM(CASE WHEN tu.last_read_post_number IS NULL OR tu.last_read_post_number < t.highest_post_number THEN 1 ELSE 0 END) unread
FROM topics t
LEFT JOIN topic_users tu ON t.id = tu.topic_id AND tu.user_id = :user_id
WHERE t.deleted_at IS NULL AND
t.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = :user_id) AND
t.archetype = 'private_message'
SQL
all,mine,unread = exec_sql(sql, user_id: user_id).values[0].map(&:to_i)
{ all: all, mine: mine, unread: unread }
end
2013-02-06 03:16:51 +08:00
def self.stream_item(action_id, guardian)
stream(action_id: action_id, guardian: guardian).first
2013-02-06 03:16:51 +08:00
end
2015-04-22 02:36:46 +08:00
def self.stream_queued(opts=nil)
opts ||= {}
offset = opts[:offset] || 0
limit = opts[:limit] || 60
2015-04-22 02:36:46 +08:00
builder = SqlBuilder.new <<-SQL
SELECT
a.id,
t.title, a.action_type, a.created_at, t.id topic_id,
u.username, u.name, u.id AS user_id,
qp.raw,
t.category_id
FROM user_actions as a
JOIN queued_posts AS qp ON qp.id = a.queued_post_id
LEFT OUTER JOIN topics t on t.id = qp.topic_id
JOIN users u on u.id = a.user_id
LEFT JOIN categories c on c.id = t.category_id
/*where*/
/*order_by*/
/*offset*/
/*limit*/
SQL
builder
.where('a.user_id = :user_id', user_id: opts[:user_id].to_i)
.where('action_type = :pending', pending: UserAction::PENDING)
.order_by("a.created_at desc")
.offset(offset.to_i)
.limit(limit.to_i)
.map_exec(UserActionRow)
end
def self.stream(opts=nil)
opts ||= {}
2013-02-06 03:16:51 +08:00
action_types = opts[:action_types]
2015-04-22 02:36:46 +08:00
user_id = opts[:user_id]
action_id = opts[:action_id]
2013-02-06 03:16:51 +08:00
guardian = opts[:guardian]
ignore_private_messages = opts[:ignore_private_messages]
2015-04-22 02:36:46 +08:00
offset = opts[:offset] || 0
limit = opts[:limit] || 60
2013-02-06 03:16:51 +08:00
2013-02-26 00:42:20 +08:00
# The weird thing is that target_post_id can be null, so it makes everything
# ever so more complex. Should we allow this, not sure.
2015-04-22 02:36:46 +08:00
builder = SqlBuilder.new <<-SQL
SELECT
a.id,
t.title, a.action_type, a.created_at, t.id topic_id,
t.closed AS topic_closed, t.archived AS topic_archived,
2015-04-22 02:36:46 +08:00
a.user_id AS target_user_id, au.name AS target_name, au.username AS target_username,
coalesce(p.post_number, 1) post_number, p.id as post_id,
p.reply_to_post_number,
pu.username, pu.name, pu.id user_id,
pu.uploaded_avatar_id,
u.username acting_username, u.name acting_name, u.id acting_user_id,
u.uploaded_avatar_id acting_uploaded_avatar_id,
coalesce(p.cooked, p2.cooked) cooked,
CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted,
p.hidden,
p.post_type,
p.action_code,
2015-04-22 02:36:46 +08:00
p.edit_reason,
t.category_id
FROM user_actions as a
JOIN topics t on t.id = a.target_topic_id
LEFT JOIN posts p on p.id = a.target_post_id
JOIN posts p2 on p2.topic_id = a.target_topic_id and p2.post_number = 1
JOIN users u on u.id = a.acting_user_id
JOIN users pu on pu.id = COALESCE(p.user_id, t.user_id)
JOIN users au on au.id = a.user_id
LEFT JOIN categories c on c.id = t.category_id
/*where*/
/*order_by*/
/*offset*/
/*limit*/
SQL
2013-02-06 03:16:51 +08:00
apply_common_filters(builder, user_id, guardian, ignore_private_messages)
2013-02-06 03:16:51 +08:00
if action_id
builder.where("a.id = :id", id: action_id.to_i)
2013-02-07 23:45:24 +08:00
else
2013-02-06 03:16:51 +08:00
builder.where("a.user_id = :user_id", user_id: user_id.to_i)
2013-02-07 23:45:24 +08:00
builder.where("a.action_type in (:action_types)", action_types: action_types) if action_types && action_types.length > 0
builder
.order_by("a.created_at desc")
.offset(offset.to_i)
.limit(limit.to_i)
2013-02-06 03:16:51 +08:00
end
builder.map_exec(UserActionRow)
2013-02-06 03:16:51 +08:00
end
def self.log_action!(hash)
2015-04-22 02:36:46 +08:00
required_parameters = [:action_type, :user_id, :acting_user_id]
if hash[:action_type] == UserAction::PENDING
required_parameters << :queued_post_id
else
required_parameters << :target_post_id
required_parameters << :target_topic_id
end
require_parameters(hash, *required_parameters)
2013-02-06 03:16:51 +08:00
transaction(requires_new: true) do
2013-02-07 23:45:24 +08:00
begin
2013-08-16 15:04:30 +08:00
# TODO there are conditions when this is called and user_id was already rolled back and is invalid.
# protect against dupes, for some reason this is failing in some cases
action = self.find_by(hash.select { |k, _| required_parameters.include?(k) })
return action if action
action = self.new(hash)
2013-02-06 03:16:51 +08:00
if hash[:created_at]
2013-02-07 23:45:24 +08:00
action.created_at = hash[:created_at]
2013-02-06 03:16:51 +08:00
end
action.save!
user_id = hash[:user_id]
2013-05-28 07:13:53 +08:00
update_like_count(user_id, hash[:action_type], 1)
topic = Topic.includes(:category).find_by(id: hash[:target_topic_id])
# move into Topic perhaps
group_ids = nil
if topic && topic.category && topic.category.read_restricted
2013-05-28 07:13:53 +08:00
group_ids = topic.category.groups.pluck("groups.id")
end
if action.user
MessageBus.publish("/users/#{action.user.username.downcase}", action.id, user_ids: [user_id], group_ids: group_ids)
end
action
2013-02-06 03:16:51 +08:00
rescue ActiveRecord::RecordNotUnique
# can happen, don't care already logged
raise ActiveRecord::Rollback
end
end
end
def self.remove_action!(hash)
require_parameters(hash, :action_type, :user_id, :acting_user_id, :target_topic_id, :target_post_id)
if action = UserAction.find_by(hash.except(:created_at))
2013-02-07 23:45:24 +08:00
action.destroy
MessageBus.publish("/user/#{hash[:user_id]}", {user_action_id: action.id, remove: true})
2013-02-06 03:16:51 +08:00
end
2013-05-28 07:13:53 +08:00
update_like_count(hash[:user_id], hash[:action_type], -1)
end
def self.synchronize_target_topic_ids(post_ids = nil)
# nuke all dupes, using magic
builder = SqlBuilder.new <<SQL
DELETE FROM user_actions USING user_actions ua2
/*where*/
SQL
builder.where <<SQL
user_actions.action_type = ua2.action_type AND
user_actions.user_id = ua2.user_id AND
user_actions.acting_user_id = ua2.acting_user_id AND
user_actions.target_post_id = ua2.target_post_id AND
user_actions.target_post_id > 0 AND
user_actions.id > ua2.id
SQL
if post_ids
builder.where("user_actions.target_post_id in (:post_ids)", post_ids: post_ids)
end
builder.exec
builder = SqlBuilder.new("UPDATE user_actions
SET target_topic_id = (select topic_id from posts where posts.id = target_post_id)
/*where*/")
builder.where("target_topic_id <> (select topic_id from posts where posts.id = target_post_id)")
if post_ids
builder.where("target_post_id in (:post_ids)", post_ids: post_ids)
end
builder.exec
end
def self.ensure_consistency!
self.synchronize_target_topic_ids
end
2013-05-28 07:13:53 +08:00
def self.update_like_count(user_id, action_type, delta)
if action_type == LIKE
UserStat.where(user_id: user_id).update_all("likes_given = likes_given + #{delta.to_i}")
elsif action_type == WAS_LIKED
UserStat.where(user_id: user_id).update_all("likes_received = likes_received + #{delta.to_i}")
end
2013-02-06 03:16:51 +08:00
end
def self.apply_common_filters(builder,user_id,guardian,ignore_private_messages=false)
# We never return deleted topics in activity
builder.where("t.deleted_at is null")
# We will return deleted posts though if the user can see it
unless guardian.can_see_deleted_posts?
builder.where("p.deleted_at is null and p2.deleted_at is null")
current_user_id = -2
current_user_id = guardian.user.id if guardian.user
builder.where("NOT COALESCE(p.hidden, false) OR p.user_id = :current_user_id", current_user_id: current_user_id )
end
unless (guardian.user && guardian.user.id == user_id) || guardian.is_staff?
builder.where("a.action_type not in (#{BOOKMARK})")
builder.where("t.visible")
end
2015-04-22 02:36:46 +08:00
unless guardian.can_see_notifications?(User.where(id: user_id).first)
builder.where('a.action_type <> :pending', pending: UserAction::PENDING)
end
if !guardian.can_see_private_messages?(user_id) || ignore_private_messages
builder.where("t.archetype != :archetype", archetype: Archetype::private_message)
end
unless guardian.is_admin?
allowed = guardian.secure_category_ids
if allowed.present?
builder.where("( c.read_restricted IS NULL OR
NOT c.read_restricted OR
(c.read_restricted and c.id in (:cats)) )", cats: guardian.secure_category_ids )
else
builder.where("(c.read_restricted IS NULL OR NOT c.read_restricted)")
end
end
end
2013-02-06 03:16:51 +08:00
def self.require_parameters(data, *params)
params.each do |p|
raise Discourse::InvalidParameters.new(p) if data[p].nil?
end
end
end
# == Schema Information
#
# Table name: user_actions
#
# id :integer not null, primary key
# action_type :integer not null
# user_id :integer not null
# target_topic_id :integer
# target_post_id :integer
# target_user_id :integer
# acting_user_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
2014-05-28 09:49:50 +08:00
# idx_unique_rows (action_type,user_id,target_topic_id,target_post_id,acting_user_id) UNIQUE
# index_user_actions_on_acting_user_id (acting_user_id)
# index_user_actions_on_user_id_and_action_type (user_id,action_type)
#