Class: PostAction

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
RateLimiter::OnCreateRecord, Trashable
Defined in:
app/models/post_action.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Trashable

#recover!, #trash!, #trashed?

Methods included from RateLimiter::OnCreateRecord

#default_rate_limiter, #disable_rate_limits!, included

Class Method Details

.copy(original_post, target_post) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
# File 'app/models/post_action.rb', line 126

def self.copy(original_post, target_post)
  cols_to_copy = (column_names - %w[id post_id]).join(", ")

  DB.exec <<~SQL
  INSERT INTO post_actions(post_id, #{cols_to_copy})
  SELECT #{target_post.id}, #{cols_to_copy}
  FROM post_actions
  WHERE post_id = #{original_post.id}
  SQL

  target_post.post_actions.each { |post_action| post_action.update_counters }
end

.count_per_day_for_type(post_action_type, opts = nil) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'app/models/post_action.rb', line 65

def self.count_per_day_for_type(post_action_type, opts = nil)
  opts ||= {}
  result = unscoped.where(post_action_type_id: post_action_type)
  result =
    result.where(
      "post_actions.created_at >= ?",
      opts[:start_date] || (opts[:since_days_ago] || 30).days.ago,
    )
  result = result.where("post_actions.created_at <= ?", opts[:end_date]) if opts[:end_date]
  if opts[:category_id]
    if opts[:include_subcategories]
      result =
        result.joins(post: :topic).where(
          "topics.category_id IN (?)",
          Category.subcategory_ids(opts[:category_id]),
        )
    else
      result = result.joins(post: :topic).where("topics.category_id = ?", opts[:category_id])
    end
  end

  if opts[:group_ids]
    result =
      result
        .joins("INNER JOIN users ON users.id = post_actions.user_id")
        .joins("INNER JOIN group_users ON group_users.user_id = users.id")
        .where("group_users.group_id IN (?)", opts[:group_ids])
  end

  result.group("date(post_actions.created_at)").order("date(post_actions.created_at)").count
end

.counts_for(collection, user) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'app/models/post_action.rb', line 23

def self.counts_for(collection, user)
  return {} if collection.blank? || !user

  collection_ids = collection.map(&:id)
  user_id = user.try(:id) || 0

  post_actions = PostAction.where(post_id: collection_ids, user_id: user_id)

   = {}
  post_actions.each do |post_action|
    [post_action.post_id] ||= {}
    [post_action.post_id][post_action.post_action_type_id] = post_action
  end

  
end

.limit_action!(user, post, post_action_type_id) ⇒ Object



122
123
124
# File 'app/models/post_action.rb', line 122

def self.limit_action!(user, post, post_action_type_id)
  RateLimiter.new(user, "post_action-#{post.id}_#{post_action_type_id}", 4, 1.minute).performed!
end

.lookup_for(user, topics, post_action_type_id) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'app/models/post_action.rb', line 40

def self.lookup_for(user, topics, post_action_type_id)
  return if topics.blank?
  # in critical path 2x faster than AR
  #
  topic_ids = topics.map(&:id)
  map = {}

  builder = DB.build <<~SQL
    SELECT p.topic_id, p.post_number
    FROM post_actions pa
    JOIN posts p ON pa.post_id = p.id
    WHERE p.deleted_at IS NULL AND pa.deleted_at IS NULL AND
       pa.post_action_type_id = :post_action_type_id AND
       pa.user_id = :user_id AND
       p.topic_id IN (:topic_ids)
    ORDER BY p.topic_id, p.post_number
  SQL

  builder
    .query(user_id: user.id, post_action_type_id: post_action_type_id, topic_ids: topic_ids)
    .each { |row| (map[row.topic_id] ||= []) << row.post_number }

  map
end

.remove_act(user, post, post_action_type_id) ⇒ Object



139
140
141
142
143
144
145
146
147
# File 'app/models/post_action.rb', line 139

def self.remove_act(user, post, post_action_type_id)
  Discourse.deprecate(
    "PostAction.remove_act is deprecated. Use `PostActionDestroyer` instead.",
    output_in_test: true,
    drop_from: "2.9.0",
  )

  PostActionDestroyer.new(user, post, post_action_type_id).perform
end

Instance Method Details

#add_moderator_post_if_needed(moderator, disposition, delete_post = false) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'app/models/post_action.rb', line 97

def add_moderator_post_if_needed(moderator, disposition, delete_post = false)
  return if !SiteSetting.auto_respond_to_flag_actions
  return if related_post.nil? || related_post.topic.nil?
  return if staff_already_replied?(related_post.topic)
  message_key = +"flags_dispositions.#{disposition}"
  message_key << "_and_deleted" if delete_post

  I18n.with_locale(SiteSetting.default_locale) do
    related_post.topic.add_moderator_post(moderator, I18n.t(message_key))
  end

  # archive message for moderators
  GroupArchivedMessage.archive!(Group[:moderators].id, related_post.topic)
end

#ensure_unique_actionsObject



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'app/models/post_action.rb', line 194

def ensure_unique_actions
  post_action_type_ids = is_flag? ? PostActionType.notify_flag_types.values : post_action_type_id

  acted =
    PostAction
      .where(user_id: user_id)
      .where(post_id: post_id)
      .where(post_action_type_id: post_action_type_ids)
      .where(deleted_at: nil)
      .where(disagreed_at: nil)
      .where(targets_topic: targets_topic)
      .exists?

  errors.add(:post_action_type_id) if acted
end

#is_flag?Boolean

Returns:

  • (Boolean)


161
162
163
# File 'app/models/post_action.rb', line 161

def is_flag?
  !!PostActionType.notify_flag_types[post_action_type_id]
end

#is_like?Boolean

Returns:

  • (Boolean)


157
158
159
# File 'app/models/post_action.rb', line 157

def is_like?
  post_action_type_id == PostActionType.types[:like]
end

#is_private_message?Boolean

Returns:

  • (Boolean)


165
166
167
168
# File 'app/models/post_action.rb', line 165

def is_private_message?
  post_action_type_id == PostActionType.types[:notify_user] ||
    post_action_type_id == PostActionType.types[:notify_moderators]
end

#post_action_rate_limiterObject

A custom rate limiter for this model



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'app/models/post_action.rb', line 171

def post_action_rate_limiter
  return unless is_flag? || is_like?

  return @rate_limiter if @rate_limiter.present?

  %w[like flag].each do |type|
    if public_send("is_#{type}?")
      limit = SiteSetting.get("max_#{type}s_per_day")

      if (is_flag? || is_like?) && user && user.trust_level >= 2
        multiplier =
          SiteSetting.get("tl#{user.trust_level}_additional_#{type}s_per_day_multiplier").to_f
        multiplier = 1.0 if multiplier < 1.0

        limit = (limit * multiplier).to_i
      end

      @rate_limiter = RateLimiter.new(user, "create_#{type}", limit, 1.day.to_i)
      return @rate_limiter
    end
  end
end

#post_action_type_keyObject



210
211
212
# File 'app/models/post_action.rb', line 210

def post_action_type_key
  PostActionType.types[post_action_type_id]
end

#remove_act!(user) ⇒ Object



149
150
151
152
153
154
155
# File 'app/models/post_action.rb', line 149

def remove_act!(user)
  trash!(user)
  # NOTE: save is called to ensure all callbacks are called
  # trash will not trigger callbacks, and triggering after_commit
  # is not trivial
  save
end

#staff_already_replied?(topic) ⇒ Boolean

Returns:

  • (Boolean)


112
113
114
115
116
117
118
119
120
# File 'app/models/post_action.rb', line 112

def staff_already_replied?(topic)
  topic
    .posts
    .where(
      "user_id IN (SELECT id FROM users WHERE moderator OR admin) OR (post_type != :regular_post_type)",
      regular_post_type: Post.types[:regular],
    )
    .exists?
end

#update_countersObject



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'app/models/post_action.rb', line 214

def update_counters
  # Update denormalized counts
  column = "#{post_action_type_key}_count"
  count = PostAction.where(post_id: post_id).where(post_action_type_id: post_action_type_id).count

  # We probably want to refactor this method to something cleaner.
  case post_action_type_key
  when :like
    # 'like_score' is weighted higher for staff accounts
    score =
      PostAction
        .joins(:user)
        .where(post_id: post_id)
        .sum(
          "CASE WHEN users.moderator OR users.admin THEN #{SiteSetting.staff_like_weight} ELSE 1 END",
        )
    Post.where(id: post_id).update_all [
                   "like_count = :count, like_score = :score",
                   count: count,
                   score: score,
                 ]
  else
    if ActiveRecord::Base.connection.column_exists?(:posts, column)
      Post.where(id: post_id).update_all ["#{column} = ?", count]
    end
  end

  topic_id = Post.with_deleted.where(id: post_id).pick(:topic_id)

  # topic_user
  if post_action_type_key == :like
    TopicUser.update_post_action_cache(
      user_id: user_id,
      topic_id: topic_id,
      post_action_type: post_action_type_key,
    )
  end

  Topic.find_by(id: topic_id)&.update_action_counts if column == "like_count"
end