Class: TopicUser
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- TopicUser
- Defined in:
- app/models/topic_user.rb
Constant Summary collapse
- UPDATE_TOPIC_USER_SQL =
Update the last read and the last seen post count, but only if it doesn’t exist. This would be a lot easier if psql supported some kind of upsert
<<~SQL UPDATE topic_users SET last_read_post_number = LEAST( CASE WHEN :whisperer THEN highest_staff_post_number ELSE highest_post_number END , GREATEST(:post_number, tu.last_read_post_number) ), total_msecs_viewed = LEAST(tu.total_msecs_viewed + :msecs,86400000), notification_level = case when tu.notifications_reason_id is null and (tu.total_msecs_viewed + :msecs) > coalesce(uo.auto_track_topics_after_msecs,:threshold) and coalesce(uo.auto_track_topics_after_msecs, :threshold) >= 0 and t.archetype = 'regular' then :tracking else tu.notification_level end FROM topic_users tu join topics t on t.id = tu.topic_id join users u on u.id = :user_id join user_options uo on uo.user_id = :user_id WHERE tu.topic_id = topic_users.topic_id AND tu.user_id = topic_users.user_id AND tu.topic_id = :topic_id AND tu.user_id = :user_id RETURNING topic_users.notification_level, tu.notification_level old_level, tu.last_read_post_number, t.archetype SQL
- INSERT_TOPIC_USER_SQL =
"INSERT INTO topic_users (user_id, topic_id, last_read_post_number, last_visited_at, first_visited_at, notification_level) SELECT :user_id, :topic_id, :post_number, :now, :now, :new_status FROM topics AS ft JOIN users u on u.id = :user_id WHERE ft.id = :topic_id AND NOT EXISTS(SELECT 1 FROM topic_users AS ftu WHERE ftu.user_id = :user_id and ftu.topic_id = :topic_id)"
Instance Attribute Summary collapse
-
#post_action_data ⇒ Object
used for serialization.
Class Method Summary collapse
- .auto_notification(user_id, topic_id, reason, notification_level) ⇒ Object
- .auto_notification_for_staging(user_id, topic_id, reason, notification_level = ) ⇒ Object
-
.cap_unread!(user_id, count) ⇒ Object
cap number of unread topics at count, bumping up last_read if needed.
-
.change(user_id, topic_id, attrs) ⇒ Object
Change attributes for a user (creates a record when none is present).
- .create_lookup(topic_users) ⇒ Object
- .create_missing_record(user_id, topic_id, attrs) ⇒ Object
- .ensure_consistency!(topic_id = nil) ⇒ Object
- .get(topic, user) ⇒ Object
-
.lookup_for(user, topics) ⇒ Object
Find the information specific to a user in a forum topic.
- .notification_level_change(user_id, topic_id, notification_level, reason_id) ⇒ Object
-
.notification_levels ⇒ Object
Enums.
- .notification_reasons ⇒ Object
- .track_visit!(topic_id, user_id) ⇒ Object
- .unwatch_categories!(user, category_ids) ⇒ Object
- .update_last_read(user, topic_id, post_number, new_posts_read, msecs, opts = {}) ⇒ Object
- .update_last_read_post_number(topic_id: nil) ⇒ Object
-
.update_post_action_cache(user_id: nil, post_id: nil, topic_id: nil, post_action_type: :like) ⇒ Object
Update the cached topic_user.liked column based on data from the post_actions table.
Instance Method Summary collapse
Instance Attribute Details
#post_action_data ⇒ Object
used for serialization
12 13 14 |
# File 'app/models/topic_user.rb', line 12 def post_action_data @post_action_data end |
Class Method Details
.auto_notification(user_id, topic_id, reason, notification_level) ⇒ Object
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'app/models/topic_user.rb', line 56 def auto_notification(user_id, topic_id, reason, notification_level) should_change = TopicUser .where(user_id: user_id, topic_id: topic_id) .where( "notifications_reason_id IS NULL OR (notification_level < :max AND notification_level > :min)", max: notification_level, min: notification_levels[:regular], ) .exists? if should_change change( user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason, ) end end |
.auto_notification_for_staging(user_id, topic_id, reason, notification_level = ) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'app/models/topic_user.rb', line 77 def auto_notification_for_staging( user_id, topic_id, reason, notification_level = notification_levels[:watching] ) change( user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason, ) end |
.cap_unread!(user_id, count) ⇒ Object
cap number of unread topics at count, bumping up last_read if needed
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 |
# File 'app/models/topic_user.rb', line 515 def self.cap_unread!(user_id, count) sql = <<SQL UPDATE topic_users tu SET last_read_post_number = max_number FROM ( SELECT MAX(post_number) max_number, p.topic_id FROM posts p WHERE deleted_at IS NULL GROUP BY p.topic_id ) m WHERE tu.user_id = :user_id AND m.topic_id = tu.topic_id AND tu.topic_id IN ( #{TopicTrackingState.report_raw_sql(skip_new: true, select: "topics.id")} offset :count ) SQL DB.exec(sql, user_id: user_id, count: count) end |
.change(user_id, topic_id, attrs) ⇒ Object
Change attributes for a user (creates a record when none is present). First it tries an update since there’s more likely to be an existing record than not. If the update returns 0 rows affected it then creates the row instead.
143 144 145 146 147 148 149 150 151 152 153 154 155 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 |
# File 'app/models/topic_user.rb', line 143 def change(user_id, topic_id, attrs) # For plugin compatibility, remove after 01 Jan 2022 attrs.delete(:highest_seen_post_number) if attrs[:highest_seen_post_number] # Sometimes people pass objs instead of the ids. We can handle that. topic_id = topic_id.id if topic_id.is_a?(::Topic) user_id = user_id.id if user_id.is_a?(::User) topic_id = topic_id.to_i user_id = user_id.to_i TopicUser.transaction do attrs = attrs.dup if attrs[:notification_level] attrs[:notifications_changed_at] ||= DateTime.now attrs[:notifications_reason_id] ||= TopicUser.notification_reasons[:user_changed] end attrs_array = attrs.to_a attrs_sql = attrs_array.map { |t| "#{t[0]} = ?" }.join(", ") vals = attrs_array.map { |t| t[1] } rows = TopicUser.where(topic_id: topic_id, user_id: user_id).update_all([attrs_sql, *vals]) create_missing_record(user_id, topic_id, attrs) if rows == 0 end if attrs[:notification_level] notification_level_change( user_id, topic_id, attrs[:notification_level], attrs[:notifications_reason_id], ) end rescue ActiveRecord::RecordNotUnique # In case of a race condition to insert, do nothing end |
.create_lookup(topic_users) ⇒ Object
126 127 128 129 130 131 132 |
# File 'app/models/topic_user.rb', line 126 def create_lookup(topic_users) topic_users = topic_users.to_a result = {} return result if topic_users.blank? topic_users.each { |ftu| result[ftu.topic_id] = ftu } result end |
.create_missing_record(user_id, topic_id, attrs) ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 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 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'app/models/topic_user.rb', line 195 def create_missing_record(user_id, topic_id, attrs) now = DateTime.now unless attrs[:notification_level] category_notification_level = CategoryUser .where(user_id: user_id) .where("category_id IN (SELECT category_id FROM topics WHERE id = :id)", id: topic_id) .where( "notification_level IN (:levels)", levels: [ CategoryUser.notification_levels[:watching], CategoryUser.notification_levels[:tracking], ], ) .order("notification_level DESC") .limit(1) .pluck(:notification_level) .first tag_notification_level = TagUser .where(user_id: user_id) .where("tag_id IN (SELECT tag_id FROM topic_tags WHERE topic_id = :id)", id: topic_id) .where( "notification_level IN (:levels)", levels: [ CategoryUser.notification_levels[:watching], CategoryUser.notification_levels[:tracking], ], ) .order("notification_level DESC") .limit(1) .pluck(:notification_level) .first if category_notification_level && !(tag_notification_level && (tag_notification_level > category_notification_level)) attrs[:notification_level] = category_notification_level attrs[:notifications_changed_at] = DateTime.now attrs[:notifications_reason_id] = ( if category_notification_level == CategoryUser.notification_levels[:watching] TopicUser.notification_reasons[:auto_watch_category] else TopicUser.notification_reasons[:auto_track_category] end ) elsif tag_notification_level attrs[:notification_level] = tag_notification_level attrs[:notifications_changed_at] = DateTime.now attrs[:notifications_reason_id] = ( if tag_notification_level == TagUser.notification_levels[:watching] TopicUser.notification_reasons[:auto_watch_tag] else TopicUser.notification_reasons[:auto_track_tag] end ) end end unless attrs[:notification_level] if Topic..where(id: topic_id).exists? && Notification.where( user_id: user_id, topic_id: topic_id, notification_type: Notification.types[:invited_to_private_message], ).exists? group_notification_level = Group .joins( "LEFT OUTER JOIN group_users gu ON gu.group_id = groups.id AND gu.user_id = #{user_id}", ) .joins("LEFT OUTER JOIN topic_allowed_groups tag ON tag.topic_id = #{topic_id}") .where("gu.id IS NOT NULL AND tag.id IS NOT NULL") .pluck(:default_notification_level) .first if group_notification_level.present? attrs[:notification_level] = group_notification_level else attrs[:notification_level] = notification_levels[:watching] end else auto_track_after = UserOption.where(user_id: user_id).pick(:auto_track_topics_after_msecs) auto_track_after ||= SiteSetting.default_other_auto_track_topics_after_msecs if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed].to_i || 0) attrs[:notification_level] ||= notification_levels[:tracking] end end end TopicUser.create!( attrs.merge!( user_id: user_id, topic_id: topic_id, first_visited_at: now, last_visited_at: now, ), ) DiscourseEvent.trigger(:topic_first_visited_by_user, topic_id, user_id) end |
.ensure_consistency!(topic_id = nil) ⇒ Object
535 536 537 538 |
# File 'app/models/topic_user.rb', line 535 def self.ensure_consistency!(topic_id = nil) update_post_action_cache(topic_id:) update_last_read_post_number(topic_id:) end |
.get(topic, user) ⇒ Object
134 135 136 137 138 |
# File 'app/models/topic_user.rb', line 134 def get(topic, user) topic = topic.id if topic.is_a?(Topic) user = user.id if user.is_a?(User) TopicUser.find_by(topic_id: topic, user_id: user) end |
.lookup_for(user, topics) ⇒ Object
Find the information specific to a user in a forum topic
118 119 120 121 122 123 124 |
# File 'app/models/topic_user.rb', line 118 def lookup_for(user, topics) # If the user isn't logged in, there's no last read posts return {} if user.blank? || topics.blank? topic_ids = topics.map(&:id) create_lookup(TopicUser.where(topic_id: topic_ids, user_id: user.id)) end |
.notification_level_change(user_id, topic_id, notification_level, reason_id) ⇒ Object
182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'app/models/topic_user.rb', line 182 def notification_level_change(user_id, topic_id, notification_level, reason_id) = { notification_level_change: notification_level } [:notifications_reason_id] = reason_id if reason_id MessageBus.publish("/topic/#{topic_id}", , user_ids: [user_id]) DiscourseEvent.trigger( :topic_notification_level_changed, notification_level, user_id, topic_id, ) end |
.notification_levels ⇒ Object
Enums
34 35 36 |
# File 'app/models/topic_user.rb', line 34 def notification_levels NotificationLevels.topic_levels end |
.notification_reasons ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'app/models/topic_user.rb', line 38 def notification_reasons @notification_reasons ||= Enum.new( created_topic: 1, user_changed: 2, user_interacted: 3, created_post: 4, auto_watch: 5, auto_watch_category: 6, auto_mute_category: 7, auto_track_category: 8, plugin_changed: 9, auto_watch_tag: 10, auto_mute_tag: 11, auto_track_tag: 12, ) end |
.track_visit!(topic_id, user_id) ⇒ Object
299 300 301 302 303 304 305 306 |
# File 'app/models/topic_user.rb', line 299 def track_visit!(topic_id, user_id) now = DateTime.now rows = TopicUser.where(topic_id: topic_id, user_id: user_id).update_all(last_visited_at: now) if rows == 0 change(user_id, topic_id, last_visited_at: now, first_visited_at: now) DiscourseEvent.trigger(:user_first_visit_to_topic, user_id: user_id, topic_id: topic_id) end end |
.unwatch_categories!(user, category_ids) ⇒ Object
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 |
# File 'app/models/topic_user.rb', line 91 def unwatch_categories!(user, category_ids) track_threshold = user.user_option.auto_track_topics_after_msecs sql = <<~SQL UPDATE topic_users tu SET notification_level = CASE WHEN t.user_id = :user_id THEN :watching WHEN total_msecs_viewed > :track_threshold AND :track_threshold >= 0 THEN :tracking ELSE :regular end FROM topics t WHERE t.id = tu.topic_id AND tu.notification_level <> :muted AND category_id IN (:category_ids) AND tu.user_id = :user_id SQL DB.exec( sql, watching: notification_levels[:watching], tracking: notification_levels[:tracking], regular: notification_levels[:regular], muted: notification_levels[:muted], category_ids: category_ids, user_id: user.id, track_threshold: track_threshold, ) end |
.update_last_read(user, topic_id, post_number, new_posts_read, msecs, opts = {}) ⇒ Object
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'app/models/topic_user.rb', line 357 def update_last_read(user, topic_id, post_number, new_posts_read, msecs, opts = {}) return if post_number.blank? msecs = 0 if msecs.to_i < 0 args = { user_id: user.id, topic_id: topic_id, post_number: post_number, now: DateTime.now, msecs: msecs, tracking: notification_levels[:tracking], threshold: SiteSetting.default_other_auto_track_topics_after_msecs, whisperer: user.whisperer?, } rows = DB.query(UPDATE_TOPIC_USER_SQL, args) if rows.length == 1 before = rows[0].old_level.to_i after = rows[0].notification_level.to_i before_last_read = rows[0].last_read_post_number.to_i archetype = rows[0].archetype if before_last_read < post_number # The user read at least one new post publish_read( topic_id: topic_id, post_number: post_number, user: user, notification_level: after, private_message: archetype == Archetype., ) end user.update_posts_read!(new_posts_read, mobile: opts[:mobile]) if new_posts_read > 0 notification_level_change(user.id, topic_id, after, nil) if before != after end if rows.length == 0 # The user read at least one post in a topic that they haven't viewed before. args[:new_status] = notification_levels[:regular] if ( user.user_option.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs ) == 0 args[:new_status] = notification_levels[:tracking] end publish_read( topic_id: topic_id, post_number: post_number, user: user, notification_level: args[:new_status], private_message: Topic.exists?(archetype: Archetype., id: topic_id), ) user.update_posts_read!(new_posts_read, mobile: opts[:mobile]) begin DB.exec(INSERT_TOPIC_USER_SQL, args) rescue PG::UniqueViolation # if record is inserted between two statements this can happen # we retry once to avoid failing the req if opts[:retry] raise else opts[:retry] = true update_last_read(user, topic_id, post_number, new_posts_read, msecs, opts) end end notification_level_change(user.id, topic_id, args[:new_status], nil) end end |
.update_last_read_post_number(topic_id: nil) ⇒ Object
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 |
# File 'app/models/topic_user.rb', line 540 def self.update_last_read_post_number(topic_id: nil) # TODO this needs some reworking, when we mark stuff skipped # we up these numbers so they are not in-sync # the simple fix is to add a column here, but table is already quite big # long term we want to split up topic_users and allow for this better builder = DB.build <<~SQL UPDATE topic_users t SET last_read_post_number = LEAST(GREATEST(last_read, last_read_post_number), max_post_number) FROM ( SELECT topic_id, user_id, MAX(post_number) last_read FROM post_timings GROUP BY topic_id, user_id ) as X JOIN ( SELECT p.topic_id, MAX(p.post_number) max_post_number from posts p GROUP BY p.topic_id ) as Y on Y.topic_id = X.topic_id /*where*/ SQL builder.where <<~SQL X.topic_id = t.topic_id AND X.user_id = t.user_id AND ( last_read_post_number <> LEAST(GREATEST(last_read, last_read_post_number), max_post_number) ) SQL builder.where("t.topic_id = :topic_id", topic_id: topic_id) if topic_id builder.exec end |
.update_post_action_cache(user_id: nil, post_id: nil, topic_id: nil, post_action_type: :like) ⇒ Object
Update the cached topic_user.liked column based on data from the post_actions table. This is useful when posts have moved around, or to ensure integrity of the data.
By default this will update data for all topics and all users. The parameters can be used to shrink the scope, and make it faster. user_id, post_id and topic_id can optionally be arrays of ids.
Providing post_id will automatically scope to the relevant user_id and topic_id. A provided ‘topic_id` value will always take precedence, which is useful when a post has been moved between topics.
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 |
# File 'app/models/topic_user.rb', line 458 def self.update_post_action_cache( user_id: nil, post_id: nil, topic_id: nil, post_action_type: :like ) raise ArgumentError, "post_action_type must equal :like" if post_action_type != :like raise ArgumentError, "post_id and user_id cannot be supplied together" if user_id && post_id action_type_name = "liked" builder = DB.build <<~SQL UPDATE topic_users tu SET #{action_type_name} = x.state FROM ( SELECT CASE WHEN EXISTS ( SELECT 1 FROM post_actions pa JOIN posts p on p.id = pa.post_id JOIN topics t ON t.id = p.topic_id WHERE pa.deleted_at IS NULL AND p.deleted_at IS NULL AND t.deleted_at IS NULL AND pa.post_action_type_id = :action_type_id AND tu2.topic_id = t.id AND tu2.user_id = pa.user_id LIMIT 1 ) THEN true ELSE false END state, tu2.topic_id, tu2.user_id FROM topic_users tu2 /*where*/ ) x WHERE x.topic_id = tu.topic_id AND x.user_id = tu.user_id AND x.state != tu.#{action_type_name} SQL builder.where("tu2.user_id IN (:user_id)", user_id: user_id) if user_id builder.where("tu2.topic_id IN (:topic_id)", topic_id: topic_id) if topic_id if post_id if !topic_id builder.where( "tu2.topic_id IN (SELECT topic_id FROM posts WHERE id IN (:post_id))", post_id: post_id, ) end builder.where(<<~SQL, post_id: post_id) tu2.user_id IN ( SELECT user_id FROM post_actions WHERE post_id IN (:post_id) AND post_action_type_id = :action_type_id ) SQL end builder.exec(action_type_id: PostActionType.types[post_action_type]) end |
Instance Method Details
#topic_bookmarks ⇒ Object
27 28 29 |
# File 'app/models/topic_user.rb', line 27 def topic_bookmarks Bookmark.where(topic: topic, user: user) end |