2013-02-05 14:16:51 -05:00
class UserAction < ActiveRecord :: Base
belongs_to :user
2013-03-23 20:32:59 +05:30
belongs_to :target_post , class_name : " Post "
belongs_to :target_topic , class_name : " Topic "
2013-02-07 16:45:24 +01:00
2013-02-05 14:16:51 -05:00
validates_presence_of :action_type
validates_presence_of :user_id
LIKE = 1
2013-02-07 16:45:24 +01:00
WAS_LIKED = 2
2013-02-05 14:16:51 -05:00
BOOKMARK = 3
NEW_TOPIC = 4
2013-05-01 10:52:31 +10:00
REPLY = 5
2013-02-05 14:16:51 -05:00
RESPONSE = 6
MENTION = 7
QUOTE = 9
EDIT = 11
NEW_PRIVATE_MESSAGE = 12
GOT_PRIVATE_MESSAGE = 13
2015-04-21 14:36:46 -04:00
PENDING = 14
2013-02-05 14:16:51 -05:00
ORDER = Hash [ * [
GOT_PRIVATE_MESSAGE ,
2013-05-17 14:11:33 -04:00
NEW_PRIVATE_MESSAGE ,
2015-04-21 14:36:46 -04:00
PENDING ,
2013-02-05 14:16:51 -05:00
NEW_TOPIC ,
2013-05-01 10:52:31 +10:00
REPLY ,
2013-02-05 14:16:51 -05:00
RESPONSE ,
LIKE ,
WAS_LIKED ,
MENTION ,
QUOTE ,
2013-07-09 18:15:43 +10:00
BOOKMARK ,
2013-02-05 14:16:51 -05:00
EDIT
] . each_with_index . to_a . flatten ]
2013-06-12 11:14:08 +10:00
# 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
2014-06-16 14:49:39 -04:00
def as_json ( options = nil )
@table . as_json ( options )
end
2013-06-12 11:14:08 +10:00
end
2014-04-29 12:59:14 -04:00
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-04-29 16:33:24 +10:00
2013-02-05 14:16:51 -05:00
def self . stats ( user_id , guardian )
2013-02-25 19:42:20 +03:00
2013-04-29 16:33:24 +10:00
# Sam: I tried this in AR and it got complex
builder = UserAction . sql_builder <<SQL
2013-02-05 14:16:51 -05:00
2013-04-29 16:33:24 +10:00
SELECT action_type , COUNT ( * ) count
FROM user_actions a
2015-04-21 14:36:46 -04:00
LEFT JOIN topics t ON t . id = a . target_topic_id
2013-04-29 16:33:24 +10:00
LEFT JOIN posts p on p . id = a . target_post_id
2015-04-21 14:36:46 -04:00
LEFT JOIN posts p2 on p2 . topic_id = a . target_topic_id and p2 . post_number = 1
2013-04-29 16:33:24 +10:00
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 )
2013-04-10 12:50:00 +10:00
2013-04-29 16:33:24 +10:00
apply_common_filters ( builder , user_id , guardian )
2013-02-15 17:08:28 -05:00
2013-04-29 16:33:24 +10:00
results = builder . exec . to_a
2013-02-28 21:54:12 +03:00
results . sort! { | a , b | ORDER [ a . action_type ] < = > ORDER [ b . action_type ] }
2013-02-05 14:16:51 -05:00
results
end
2014-05-02 22:36:52 +02:00
def self . private_messages_stats ( user_id , guardian )
return unless guardian . can_see_private_messages? ( user_id )
2015-09-10 17:07:20 +10:00
2015-12-07 18:37:03 +01:00
# list the stats for: all/mine/unread/groups (topic-based)
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 . archetype = 'private_message'
AND t . id IN ( SELECT topic_id FROM topic_allowed_users WHERE user_id = :user_id )
SQL
all , mine , unread = exec_sql ( sql , user_id : user_id ) . values [ 0 ] . map ( & :to_i )
sql = <<-SQL
2015-12-10 11:39:33 +11:00
SELECT g . name , COUNT ( * ) " count "
FROM topics t
JOIN topic_allowed_groups tg ON topic_id = t . id
JOIN group_users gu ON gu . user_id = :user_id AND gu . group_id = tg . group_id
JOIN groups g ON g . id = gu . group_id
2015-12-07 18:37:03 +01:00
WHERE deleted_at IS NULL
AND archetype = 'private_message'
2015-12-10 11:39:33 +11:00
GROUP BY g . name
2015-12-07 18:37:03 +01:00
SQL
2015-09-10 17:07:20 +10:00
2015-12-10 11:39:33 +11:00
result = { all : all , mine : mine , unread : unread }
exec_sql ( sql , user_id : user_id ) . each do | row |
( result [ :groups ] || = [ ] ) << { name : row [ " name " ] , count : row [ " count " ] . to_i }
end
result
2015-09-10 17:07:20 +10:00
2014-05-02 22:36:52 +02:00
end
2013-02-05 14:16:51 -05:00
def self . stream_item ( action_id , guardian )
2013-02-28 21:54:12 +03:00
stream ( action_id : action_id , guardian : guardian ) . first
2013-02-05 14:16:51 -05:00
end
2015-04-21 14:36:46 -04:00
def self . stream_queued ( opts = nil )
opts || = { }
2013-02-28 21:54:12 +03:00
offset = opts [ :offset ] || 0
limit = opts [ :limit ] || 60
2015-04-21 14:36:46 -04: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-05 14:16:51 -05:00
action_types = opts [ :action_types ]
2015-04-21 14:36:46 -04:00
user_id = opts [ :user_id ]
action_id = opts [ :action_id ]
2013-02-05 14:16:51 -05:00
guardian = opts [ :guardian ]
ignore_private_messages = opts [ :ignore_private_messages ]
2015-04-21 14:36:46 -04:00
offset = opts [ :offset ] || 0
limit = opts [ :limit ] || 60
2013-02-05 14:16:51 -05:00
2013-02-25 19:42:20 +03: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-21 14:36:46 -04:00
builder = SqlBuilder . new <<-SQL
SELECT
a . id ,
t . title , a . action_type , a . created_at , t . id topic_id ,
2015-05-11 22:21:16 +05:30
t . closed AS topic_closed , t . archived AS topic_archived ,
2015-04-21 14:36:46 -04: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 ,
2015-07-31 14:22:28 -04:00
p . action_code ,
2015-04-21 14:36:46 -04: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-05 14:16:51 -05:00
2013-04-29 16:33:24 +10:00
apply_common_filters ( builder , user_id , guardian , ignore_private_messages )
2013-02-05 14:16:51 -05:00
if action_id
builder . where ( " a.id = :id " , id : action_id . to_i )
2013-02-07 16:45:24 +01:00
else
2013-02-05 14:16:51 -05:00
builder . where ( " a.user_id = :user_id " , user_id : user_id . to_i )
2013-02-07 16:45:24 +01:00
builder . where ( " a.action_type in (:action_types) " , action_types : action_types ) if action_types && action_types . length > 0
2013-05-27 10:22:37 +10:00
builder
. order_by ( " a.created_at desc " )
. offset ( offset . to_i )
. limit ( limit . to_i )
2013-02-05 14:16:51 -05:00
end
2013-05-27 10:22:37 +10:00
2013-06-12 11:14:08 +10:00
builder . map_exec ( UserActionRow )
2013-02-05 14:16:51 -05:00
end
def self . log_action! ( hash )
2015-04-21 14:36:46 -04: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
2013-07-23 09:48:18 +10:00
require_parameters ( hash , * required_parameters )
2014-06-04 17:41:11 +02:00
2013-02-05 14:16:51 -05:00
transaction ( requires_new : true ) do
2013-02-07 16:45:24 +01:00
begin
2013-08-16 17:04:30 +10:00
# TODO there are conditions when this is called and user_id was already rolled back and is invalid.
2013-07-23 09:48:18 +10:00
# protect against dupes, for some reason this is failing in some cases
2014-08-15 03:24:55 +05:30
action = self . find_by ( hash . select { | k , _ | required_parameters . include? ( k ) } )
2013-07-23 09:48:18 +10:00
return action if action
action = self . new ( hash )
2013-02-05 14:16:51 -05:00
if hash [ :created_at ]
2013-02-07 16:45:24 +01:00
action . created_at = hash [ :created_at ]
2013-02-05 14:16:51 -05:00
end
action . save!
2013-04-05 15:29:46 +11:00
user_id = hash [ :user_id ]
2014-05-06 14:41:59 +01:00
topic = Topic . includes ( :category ) . find_by ( id : hash [ :target_topic_id ] )
2013-05-16 15:03:03 +10:00
2016-01-24 16:39:01 +11:00
if topic && ! topic . private_message?
update_like_count ( user_id , hash [ :action_type ] , 1 )
end
2013-05-16 15:03:03 +10:00
# move into Topic perhaps
group_ids = nil
2013-07-14 11:24:16 +10:00
if topic && topic . category && topic . category . read_restricted
2013-05-28 09:13:53 +10:00
group_ids = topic . category . groups . pluck ( " groups.id " )
2013-05-16 15:03:03 +10:00
end
2013-09-04 15:35:10 -04:00
if action . user
2015-05-04 12:21:00 +10:00
MessageBus . publish ( " /users/ #{ action . user . username . downcase } " , action . id , user_ids : [ user_id ] , group_ids : group_ids )
2013-09-04 15:35:10 -04:00
end
2013-07-17 16:40:15 +10:00
action
2013-05-16 15:03:03 +10:00
2013-02-05 14:16:51 -05: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 )
2014-05-06 14:41:59 +01:00
if action = UserAction . find_by ( hash . except ( :created_at ) )
2013-02-07 16:45:24 +01:00
action . destroy
2015-05-04 12:21:00 +10:00
MessageBus . publish ( " /user/ #{ hash [ :user_id ] } " , { user_action_id : action . id , remove : true } )
2013-02-05 14:16:51 -05:00
end
2013-04-05 15:29:46 +11:00
2013-05-28 09:13:53 +10:00
update_like_count ( hash [ :user_id ] , hash [ :action_type ] , - 1 )
end
2013-07-17 16:40:15 +10:00
def self . synchronize_target_topic_ids ( post_ids = nil )
2013-07-22 17:48:24 +10:00
2013-07-23 12:43:34 +10:00
# 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
2013-07-17 16:40:15 +10:00
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 09:13:53 +10:00
def self . update_like_count ( user_id , action_type , delta )
2013-04-05 15:29:46 +11:00
if action_type == LIKE
2013-10-04 13:28:49 +10:00
UserStat . where ( user_id : user_id ) . update_all ( " likes_given = likes_given + #{ delta . to_i } " )
2013-04-05 15:29:46 +11:00
elsif action_type == WAS_LIKED
2013-10-04 13:28:49 +10:00
UserStat . where ( user_id : user_id ) . update_all ( " likes_received = likes_received + #{ delta . to_i } " )
2013-04-05 15:29:46 +11:00
end
2013-02-05 14:16:51 -05:00
end
2013-04-29 16:33:24 +10:00
def self . apply_common_filters ( builder , user_id , guardian , ignore_private_messages = false )
2014-02-28 11:14:36 -05:00
# 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
2013-04-29 16:33:24 +10:00
unless guardian . can_see_deleted_posts?
2014-02-28 11:14:36 -05:00
builder . where ( " p.deleted_at is null and p2.deleted_at is null " )
2014-02-11 17:16:58 +11:00
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 )
2013-04-29 16:33:24 +10:00
end
2015-09-22 00:50:52 +02:00
visible_post_types = Topic . visible_post_types ( guardian . user )
builder . where ( " COALESCE(p.post_type, p2.post_type) IN (:visible_post_types) " , visible_post_types : visible_post_types )
2013-08-22 09:18:54 +10:00
unless ( guardian . user && guardian . user . id == user_id ) || guardian . is_staff?
2014-10-03 15:37:51 +10:00
builder . where ( " t.visible " )
2013-04-29 16:33:24 +10:00
end
2015-04-21 14:36:46 -04:00
unless guardian . can_see_notifications? ( User . where ( id : user_id ) . first )
2016-03-20 18:27:28 +00:00
builder . where ( " a.action_type not in ( #{ BOOKMARK } ) " )
2015-04-21 14:36:46 -04:00
builder . where ( 'a.action_type <> :pending' , pending : UserAction :: PENDING )
end
2013-04-29 16:33:24 +10:00
if ! guardian . can_see_private_messages? ( user_id ) || ignore_private_messages
builder . where ( " t.archetype != :archetype " , archetype : Archetype :: private_message )
end
2014-02-07 14:11:52 +11:00
unless guardian . is_admin?
2013-04-29 16:33:24 +10:00
allowed = guardian . secure_category_ids
if allowed . present?
2013-07-14 11:24:16 +10:00
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 )
2013-04-29 16:33:24 +10:00
else
2013-07-14 11:24:16 +10:00
builder . where ( " (c.read_restricted IS NULL OR NOT c.read_restricted) " )
2013-04-29 16:33:24 +10:00
end
end
end
2013-02-05 14:16:51 -05:00
def self . require_parameters ( data , * params )
params . each do | p |
raise Discourse :: InvalidParameters . new ( p ) if data [ p ] . nil?
end
end
end
2013-05-24 12:48:32 +10:00
# == 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
2014-08-27 15:19:25 +10:00
# created_at :datetime not null
# updated_at :datetime not null
2015-09-18 10:41:10 +10:00
# queued_post_id :integer
2013-05-24 12:48:32 +10:00
#
# Indexes
#
2014-05-28 11:49:50 +10:00
# idx_unique_rows (action_type,user_id,target_topic_id,target_post_id,acting_user_id) UNIQUE
2015-09-18 10:41:10 +10:00
# idx_user_actions_speed_up_user_all (user_id,created_at,action_type)
2014-05-28 11:49:50 +10:00
# index_user_actions_on_acting_user_id (acting_user_id)
2015-09-18 10:41:10 +10:00
# index_user_actions_on_target_post_id (target_post_id)
2014-05-28 11:49:50 +10:00
# index_user_actions_on_user_id_and_action_type (user_id,action_type)
2013-05-24 12:48:32 +10:00
#