Class: UserStat

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/user_stat.rb

Constant Summary collapse

UPDATE_UNREAD_MINUTES_AGO =
10
UPDATE_UNREAD_USERS_LIMIT =
10_000
MAX_TIME_READ_DIFF =
100

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.cache_last_seen(id, val) ⇒ Object



304
305
306
# File 'app/models/user_stat.rb', line 304

def self.cache_last_seen(id, val)
  Discourse.redis.setex(last_seen_key(id), MAX_TIME_READ_DIFF, val)
end

.ensure_consistency!(last_seen = 1.hour.ago) ⇒ Object



9
10
11
12
13
14
15
# File 'app/models/user_stat.rb', line 9

def self.ensure_consistency!(last_seen = 1.hour.ago)
  reset_bounce_scores
  update_distinct_badge_count
  update_view_counts(last_seen)
  update_first_unread(last_seen)
  update_first_unread_pm(last_seen)
end

.last_seen_cached(id) ⇒ Object



300
301
302
# File 'app/models/user_stat.rb', line 300

def self.last_seen_cached(id)
  Discourse.redis.get(last_seen_key(id))
end

.last_seen_key(id) ⇒ Object



295
296
297
298
# File 'app/models/user_stat.rb', line 295

def self.last_seen_key(id)
  # frozen
  -"user-last-seen:#{id}"
end

.reset_bounce_scoresObject



148
149
150
151
152
153
# File 'app/models/user_stat.rb', line 148

def self.reset_bounce_scores
  UserStat
    .where("reset_bounce_score_after < now()")
    .where("bounce_score > 0")
    .update_all(bounce_score: 0)
end

.update_distinct_badge_count(user_id = nil) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'app/models/user_stat.rb', line 198

def self.update_distinct_badge_count(user_id = nil)
  sql = <<~SQL
    UPDATE user_stats
    SET distinct_badge_count = x.distinct_badge_count
    FROM (
      SELECT users.id user_id, COUNT(distinct user_badges.badge_id) distinct_badge_count
      FROM users
      LEFT JOIN user_badges ON user_badges.user_id = users.id
                            AND (user_badges.badge_id IN (SELECT id FROM badges WHERE enabled))
      GROUP BY users.id
    ) x
    WHERE user_stats.user_id = x.user_id AND user_stats.distinct_badge_count <> x.distinct_badge_count
  SQL

  sql = sql + " AND user_stats.user_id = #{user_id.to_i}" if user_id

  DB.exec sql
end

.update_draft_count(user_id = nil) ⇒ Object



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
# File 'app/models/user_stat.rb', line 221

def self.update_draft_count(user_id = nil)
  if user_id.present?
    draft_count, has_topic_draft =
      DB.query_single <<~SQL, user_id: user_id, new_topic: Draft::NEW_TOPIC
      UPDATE user_stats
      SET draft_count = (SELECT COUNT(*) FROM drafts WHERE user_id = :user_id)
      WHERE user_id = :user_id
      RETURNING draft_count, (SELECT 1 FROM drafts WHERE user_id = :user_id AND draft_key = :new_topic)
    SQL

    MessageBus.publish(
      "/user-drafts/#{user_id}",
      { draft_count: draft_count, has_topic_draft: !!has_topic_draft },
      user_ids: [user_id],
    )
  else
    DB.exec <<~SQL
      UPDATE user_stats
      SET draft_count = new_user_stats.draft_count
      FROM (SELECT user_stats.user_id, COUNT(drafts.id) draft_count
            FROM user_stats
            LEFT JOIN drafts ON user_stats.user_id = drafts.user_id
            GROUP BY user_stats.user_id) new_user_stats
      WHERE user_stats.user_id = new_user_stats.user_id
        AND user_stats.draft_count <> new_user_stats.draft_count
    SQL
  end
end

.update_first_unread(last_seen, limit: UPDATE_UNREAD_USERS_LIMIT) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/models/user_stat.rb', line 78

def self.update_first_unread(last_seen, limit: UPDATE_UNREAD_USERS_LIMIT)
  DB.exec(<<~SQL, min_date: last_seen, limit: limit, now: UPDATE_UNREAD_MINUTES_AGO.minutes.ago)
    UPDATE user_stats us
    SET first_unread_at = COALESCE(Y.min_date, :now)
    FROM (
      SELECT u1.id user_id,
         X.min min_date
      FROM users u1
      LEFT JOIN
        (SELECT u.id AS user_id,
                min(topics.updated_at) min
         FROM users u
         LEFT JOIN topic_users tu ON tu.user_id = u.id
         LEFT JOIN topics ON tu.topic_id = topics.id
         JOIN user_stats AS us ON us.user_id = u.id
         JOIN user_options AS uo ON uo.user_id = u.id
         JOIN categories c ON c.id = topics.category_id
         WHERE u.id IN (
             SELECT id
             FROM users
             WHERE last_seen_at IS NOT NULL
              AND last_seen_at > :min_date
              ORDER BY last_seen_at DESC
              LIMIT :limit
            )
           AND topics.archetype <> 'private_message'
           AND (("topics"."deleted_at" IS NULL
                 AND tu.last_read_post_number < CASE
                                                    WHEN u.admin
                                                         OR u.moderator THEN topics.highest_staff_post_number
                                                    ELSE topics.highest_post_number
                                                END
                 AND COALESCE(tu.notification_level, 1) >= 2)
                OR (1=0))
           AND (topics.visible
                OR u.admin
                OR u.moderator)
           AND topics.deleted_at IS NULL
           AND (NOT c.read_restricted
                OR u.admin
                OR category_id IN
                  (SELECT c2.id
                   FROM categories c2
                   JOIN category_groups cg ON cg.category_id = c2.id
                   JOIN group_users gu ON gu.user_id = u.id
                   AND cg.group_id = gu.group_id
                   WHERE c2.read_restricted ))
           AND NOT EXISTS
             (SELECT 1
              FROM category_users cu
              WHERE last_read_post_number IS NULL
                AND cu.user_id = u.id
                AND cu.category_id = topics.category_id
                AND cu.notification_level = 0)
         GROUP BY u.id,
                  u.username) AS X ON X.user_id = u1.id
      WHERE u1.id IN
          (
           SELECT id
           FROM users
           WHERE last_seen_at IS NOT NULL
            AND last_seen_at > :min_date
            ORDER BY last_seen_at DESC
            LIMIT :limit
          )
    ) Y
    WHERE Y.user_id = us.user_id
  SQL
end

.update_first_unread_pm(last_seen, limit: UPDATE_UNREAD_USERS_LIMIT) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/models/user_stat.rb', line 20

def self.update_first_unread_pm(last_seen, limit: UPDATE_UNREAD_USERS_LIMIT)
  whisperers_group_ids = SiteSetting.whispers_allowed_group_ids

  DB.exec(
    <<~SQL,
  UPDATE user_stats us
  SET first_unread_pm_at = COALESCE(Z.min_date, :now)
  FROM (
    SELECT
      u1.id user_id,
      X.min_date
    FROM users u1
    LEFT JOIN (
      SELECT
        tau.user_id,
        MIN(t.updated_at) min_date
      FROM topic_allowed_users tau
      INNER JOIN topics t ON t.id = tau.topic_id
      INNER JOIN users u ON u.id = tau.user_id
      LEFT JOIN topic_users tu ON t.id = tu.topic_id AND tu.user_id = tau.user_id
      #{whisperers_group_ids.present? ? "LEFT JOIN group_users gu ON gu.group_id IN (:whisperers_group_ids) AND gu.user_id = u.id" : ""}
      WHERE t.deleted_at IS NULL
      AND t.archetype = :archetype
      AND tu.last_read_post_number < CASE
                                     WHEN u.admin OR u.moderator #{whisperers_group_ids.present? ? "OR gu.id IS NOT NULL" : ""}
                                     THEN t.highest_staff_post_number
                                     ELSE t.highest_post_number
                                     END
      AND (COALESCE(tu.notification_level, 1) >= 2)
      AND tau.user_id IN (
        SELECT id
        FROM users
        WHERE last_seen_at IS NOT NULL
        AND last_seen_at > :last_seen
        ORDER BY last_seen_at DESC
        LIMIT :limit
      )
      GROUP BY tau.user_id
    ) AS X ON X.user_id = u1.id
    WHERE u1.id IN (
      SELECT id
      FROM users
      WHERE last_seen_at IS NOT NULL
      AND last_seen_at > :last_seen
      ORDER BY last_seen_at DESC
      LIMIT :limit
    )
  ) AS Z
  WHERE us.user_id = Z.user_id
  SQL
    archetype: Archetype.private_message,
    now: UPDATE_UNREAD_MINUTES_AGO.minutes.ago,
    last_seen: last_seen,
    limit: limit,
    whisperers_group_ids: whisperers_group_ids,
  )
end

.update_time_read!(id) ⇒ Object

attempt to add total read time to user based on previous time this was called



275
276
277
278
279
280
281
282
283
284
285
# File 'app/models/user_stat.rb', line 275

def self.update_time_read!(id)
  if last_seen = last_seen_cached(id)
    diff = (Time.now.to_f - last_seen.to_f).round
    if diff > 0 && diff < MAX_TIME_READ_DIFF
      update_args = ["time_read = time_read + ?", diff]
      UserStat.where(user_id: id).update_all(update_args)
      UserVisit.where(user_id: id, visited_at: Time.zone.now.to_date).update_all(update_args)
    end
  end
  cache_last_seen(id, Time.now.to_f)
end

.update_view_counts(last_seen = 1.hour.ago) ⇒ Object

Updates the denormalized view counts for all users



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'app/models/user_stat.rb', line 156

def self.update_view_counts(last_seen = 1.hour.ago)
  # NOTE: we only update the counts for users we have seen in the last hour
  #  this avoids a very expensive query that may run on the entire user base
  #  we also ensure we only touch the table if data changes

  # Update denormalized topics_entered
  DB.exec(<<~SQL, seen_at: last_seen)
    UPDATE user_stats SET topics_entered = X.c
     FROM
    (SELECT v.user_id, COUNT(topic_id) AS c
     FROM topic_views AS v
     WHERE v.user_id IN (
        SELECT u1.id FROM users u1 where u1.last_seen_at > :seen_at
     )
     GROUP BY v.user_id) AS X
    WHERE
      X.user_id = user_stats.user_id AND
      X.c <> topics_entered
  SQL

  # Update denormalized posts_read_count
  DB.exec(<<~SQL, seen_at: last_seen)
    WITH filtered_users AS (
      SELECT id FROM users u
      JOIN user_stats ON user_id = u.id
      WHERE last_seen_at > :seen_at
      AND posts_read_count < 10000
    )
    UPDATE user_stats SET posts_read_count = X.c
    FROM (SELECT pt.user_id, COUNT(*) as c
          FROM filtered_users AS u
          JOIN post_timings AS pt ON pt.user_id = u.id
          JOIN topics t ON t.id = pt.topic_id
          WHERE t.archetype = 'regular'
          AND t.deleted_at IS NULL
          GROUP BY pt.user_id
         ) AS X
    WHERE X.user_id = user_stats.user_id
    AND X.c <> posts_read_count
  SQL
end

Instance Method Details

#any_postsObject



269
270
271
# File 'app/models/user_stat.rb', line 269

def any_posts
  user.posts.exists?
end

#calc_topic_reply_count!(start_time = nil) ⇒ Object

topic_reply_count is a count of posts in other users’ topics



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'app/models/user_stat.rb', line 251

def calc_topic_reply_count!(start_time = nil)
  sql = <<~SQL
    SELECT COUNT(DISTINCT posts.topic_id) AS count
    FROM posts
    INNER JOIN topics ON topics.id = posts.topic_id
    WHERE posts.user_id = ?
    AND topics.user_id <> posts.user_id
    AND posts.deleted_at IS NULL AND topics.deleted_at IS NULL
    AND topics.archetype <> 'private_message'
    #{start_time.nil? ? "" : "AND posts.created_at > ?"}
  SQL
  if start_time.nil?
    DB.query_single(sql, self.user_id).first
  else
    DB.query_single(sql, self.user_id, start_time).first
  end
end

#reset_bounce_score!Object



291
292
293
# File 'app/models/user_stat.rb', line 291

def reset_bounce_score!
  update_columns(reset_bounce_score_after: nil, bounce_score: 0)
end

#update_distinct_badge_countObject



217
218
219
# File 'app/models/user_stat.rb', line 217

def update_distinct_badge_count
  self.class.update_distinct_badge_count(self.user_id)
end

#update_pending_postsObject



308
309
310
311
312
313
314
315
316
# File 'app/models/user_stat.rb', line 308

def update_pending_posts
  update(pending_posts_count: user.pending_posts.count)
  MessageBus.publish(
    "/u/#{user.username_lower}/counters",
    { pending_posts_count: pending_posts_count },
    user_ids: [user.id],
    group_ids: [Group::AUTO_GROUPS[:staff]],
  )
end

#update_time_read!Object



287
288
289
# File 'app/models/user_stat.rb', line 287

def update_time_read!
  UserStat.update_time_read!(id)
end