discourse/app/models/user_action.rb

274 lines
8.5 KiB
Ruby

class UserAction < ActiveRecord::Base
belongs_to :user
belongs_to :target_post, class_name: "Post"
belongs_to :target_topic, class_name: "Topic"
attr_accessible :acting_user_id, :action_type, :target_topic_id, :target_post_id, :target_user_id, :user_id
validates_presence_of :action_type
validates_presence_of :user_id
LIKE = 1
WAS_LIKED = 2
BOOKMARK = 3
NEW_TOPIC = 4
REPLY = 5
RESPONSE= 6
MENTION = 7
QUOTE = 9
STAR = 10
EDIT = 11
NEW_PRIVATE_MESSAGE = 12
GOT_PRIVATE_MESSAGE = 13
ORDER = Hash[*[
GOT_PRIVATE_MESSAGE,
NEW_PRIVATE_MESSAGE,
BOOKMARK,
NEW_TOPIC,
REPLY,
RESPONSE,
LIKE,
WAS_LIKED,
MENTION,
QUOTE,
STAR,
EDIT
].each_with_index.to_a.flatten]
def self.stats(user_id, guardian)
# Sam: I tried this in AR and it got complex
builder = UserAction.sql_builder <<SQL
SELECT action_type, COUNT(*) count
FROM user_actions 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
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] }
results
end
def self.stream_item(action_id, guardian)
stream(action_id: action_id, guardian: guardian).first
end
def self.stream(opts={})
user_id = opts[:user_id]
offset = opts[:offset] || 0
limit = opts[:limit] || 60
action_id = opts[:action_id]
action_types = opts[:action_types]
guardian = opts[:guardian]
ignore_private_messages = opts[:ignore_private_messages]
# 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.
builder = SqlBuilder.new("
SELECT
t.title, a.action_type, a.created_at, t.id topic_id,
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.reply_to_post_number,
pu.email ,pu.username, pu.name, pu.id user_id,
u.email acting_email, u.username acting_username, u.name acting_name, u.id acting_user_id,
coalesce(p.cooked, p2.cooked) cooked
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*/
")
apply_common_filters(builder, user_id, guardian, ignore_private_messages)
if action_id
builder.where("a.id = :id", id: action_id.to_i)
data = builder.exec.to_a
else
builder.where("a.user_id = :user_id", user_id: user_id.to_i)
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")
builder.offset(offset.to_i)
builder.limit(limit.to_i)
data = builder.exec.to_a
end
data.each do |row|
row["action_type"] = row["action_type"].to_i
row["created_at"] = DateTime.parse(row["created_at"])
# we should probably cache the excerpts in the db at some point
row["excerpt"] = PrettyText.excerpt(row["cooked"],300) if row["cooked"]
row["cooked"] = nil
row["avatar_template"] = User.avatar_template(row["email"])
row["acting_avatar_template"] = User.avatar_template(row["acting_email"])
row.delete("email")
row.delete("acting_email")
row["slug"] = Slug.for(row["title"])
end
data
end
# slightly different to standard stream, it collapses replies
def self.private_message_stream(action_type, opts)
user_id = opts[:user_id]
return [] unless opts[:guardian].can_see_private_messages?(user_id)
builder = SqlBuilder.new("
SELECT
t.title, :action_type action_type, p.created_at, t.id topic_id,
:user_id AS target_user_id, au.name AS target_name, au.username AS target_username,
coalesce(p.post_number, 1) post_number,
p.reply_to_post_number,
pu.email ,pu.username, pu.name, pu.id user_id,
pu.email acting_email, pu.username acting_username, pu.name acting_name, pu.id acting_user_id,
p.cooked
FROM topics t
JOIN posts p ON p.topic_id = t.id and p.post_number = t.highest_post_number
JOIN users pu ON pu.id = p.user_id
JOIN users au ON au.id = :user_id
WHERE archetype = 'private_message' and EXISTS (
select 1 from user_actions a where a.user_id = :user_id and a.target_topic_id = t.id and action_type = :action_type)
ORDER BY p.created_at desc
/*offset*/
/*limit*/
")
builder.offset((opts[:offset] || 0).to_i)
builder.limit((opts[:limit] || 60).to_i)
data = builder.exec(user_id: user_id, action_type: action_type).to_a
data.each do |row|
row["action_type"] = row["action_type"].to_i
row["created_at"] = DateTime.parse(row["created_at"])
# we should probably cache the excerpts in the db at some point
row["excerpt"] = PrettyText.excerpt(row["cooked"],300) if row["cooked"]
row["cooked"] = nil
row["avatar_template"] = User.avatar_template(row["email"])
row["acting_avatar_template"] = User.avatar_template(row["acting_email"])
row.delete("email")
row.delete("acting_email")
row["slug"] = Slug.for(row["title"])
end
data
end
def self.log_action!(hash)
require_parameters(hash, :action_type, :user_id, :acting_user_id, :target_topic_id, :target_post_id)
transaction(requires_new: true) do
begin
action = new(hash)
if hash[:created_at]
action.created_at = hash[:created_at]
end
action.save!
action_type = hash[:action_type]
user_id = hash[:user_id]
if action_type == LIKE
User.update_all('likes_given = likes_given + 1', id: user_id)
elsif action_type == WAS_LIKED
User.update_all('likes_received = likes_received + 1', id: user_id)
end
topic = Topic.includes(:category).where(id: hash[:target_topic_id]).first
# move into Topic perhaps
group_ids = nil
if topic && topic.category && topic.category.secure
group_ids = topic.category.groups.select("groups.id").map{|g| g.id}
end
MessageBus.publish("/users/#{action.user.username.downcase}",
action.id,
user_ids: [user_id],
group_ids: group_ids )
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.where(hash).first
action.destroy
MessageBus.publish("/user/#{hash[:user_id]}", {user_action_id: action.id, remove: true})
end
action_type = hash[:action_type]
user_id = hash[:user_id]
if action_type == LIKE
User.update_all('likes_given = likes_given - 1', id: user_id)
elsif action_type == WAS_LIKED
User.update_all('likes_received = likes_received - 1', id: user_id)
end
end
protected
def self.apply_common_filters(builder,user_id,guardian,ignore_private_messages=false)
unless guardian.can_see_deleted_posts?
builder.where("p.deleted_at is null and p2.deleted_at is null and t.deleted_at is null")
end
unless guardian.user && guardian.user.id == user_id
builder.where("a.action_type not in (#{BOOKMARK})")
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_staff?
allowed = guardian.secure_category_ids
if allowed.present?
builder.where("( c.secure IS NULL OR
c.secure = 'f' OR
(c.secure = 't' and c.id in (:cats)) )", cats: guardian.secure_category_ids )
else
builder.where("(c.secure IS NULL OR c.secure = 'f')")
end
end
end
def self.require_parameters(data, *params)
params.each do |p|
raise Discourse::InvalidParameters.new(p) if data[p].nil?
end
end
end