FIX: CategoryUser#batch_set (#7787)

* Remove unused method

* Prefabricate user in category_user_spec.rb

* FIX: Remove notification_level from category_users unique indexes

* FIX: CategoryUser#batch_set wasn't updating pre-existing records

* Improve tests for CategoryUser#batch_set

* FIX: changed was being reported incorrectly

* DEV: Rewrote query to do a bulk insert

* DEV: remove unnecessary parentheses
This commit is contained in:
Daniel Waterworth 2019-06-25 03:13:27 +01:00 committed by Sam
parent 6de254f642
commit bc03c509ab
3 changed files with 112 additions and 38 deletions

View File

@ -10,10 +10,6 @@ class CategoryUser < ActiveRecord::Base
self.where(user: user, notification_level: notification_levels[level])
end
def self.lookup_by_category(user, category)
self.where(user: user, category: category)
end
def self.notification_levels
NotificationLevels.all
end
@ -23,21 +19,43 @@ class CategoryUser < ActiveRecord::Base
end
def self.batch_set(user, level, category_ids)
records = CategoryUser.where(user: user, notification_level: notification_levels[level])
old_ids = records.pluck(:category_id)
level_num = notification_levels[level]
category_ids = Category.where(id: category_ids).pluck(:id)
changed = false
category_ids = Category.where('id in (?)', category_ids).pluck(:id)
remove = (old_ids - category_ids)
if remove.present?
records.where('category_id in (?)', remove).destroy_all
changed = true
# Update pre-existing category users
unless category_ids.empty?
changed ||=
CategoryUser
.where(user_id: user.id, category_id: category_ids)
.where.not(notification_level: level_num)
.update_all(notification_level: level_num) > 0
end
(category_ids - old_ids).each do |id|
CategoryUser.create!(user: user, category_id: id, notification_level: notification_levels[level])
changed = true
# Remove extraneous category users
changed ||=
CategoryUser
.where(user_id: user.id, notification_level: level_num)
.where.not(category_id: category_ids)
.delete_all > 0
# Create new category users
unless category_ids.empty?
# TODO: rewrite this when we upgrade to rails 6
changed ||=
DB.exec(
"
INSERT INTO category_users (user_id, category_id, notification_level)
SELECT :user_id, category_id, :level_num
FROM
(VALUES (:category_ids)) AS category_ids (category_id)
ON CONFLICT DO NOTHING
",
user_id: user.id,
level_num: level_num,
category_ids: category_ids
) > 0
end
if changed
@ -188,6 +206,6 @@ end
#
# Indexes
#
# idx_category_users_u1 (user_id,category_id,notification_level) UNIQUE
# idx_category_users_u2 (category_id,user_id,notification_level) UNIQUE
# idx_category_users_category_id_user_id (category_id,user_id) UNIQUE
# idx_category_users_user_id_category_id (user_id,category_id) UNIQUE
#

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class RemoveNotificationLevelFromCategoryUserIndexes < ActiveRecord::Migration[5.2]
def up
execute <<SQL
DELETE FROM category_users cu USING category_users cu1
WHERE cu.user_id = cu1.user_id AND
cu.category_id = cu1.category_id AND
cu.notification_level < cu1.notification_level
SQL
remove_index :category_users, name: 'idx_category_users_u1'
remove_index :category_users, name: 'idx_category_users_u2'
add_index :category_users, [:user_id, :category_id],
name: 'idx_category_users_user_id_category_id', unique: true
add_index :category_users, [:category_id, :user_id],
name: 'idx_category_users_category_id_user_id', unique: true
end
def down
remove_index :category_users, name: 'idx_category_users_user_id_category_id'
remove_index :category_users, name: 'idx_category_users_category_id_user_id'
add_index :category_users, [:user_id, :category_id, :notification_level],
name: 'idx_category_users_u1', unique: true
add_index :category_users, [:category_id, :user_id, :notification_level],
name: 'idx_category_users_u2', unique: true
end
end

View File

@ -5,6 +5,7 @@ require 'rails_helper'
require_dependency 'post_creator'
describe CategoryUser do
fab!(:user) { Fabricate(:user) }
def tracking
CategoryUser.notification_levels[:tracking]
@ -14,26 +15,60 @@ describe CategoryUser do
CategoryUser.notification_levels[:regular]
end
it 'allows batch set' do
user = Fabricate(:user)
category1 = Fabricate(:category)
category2 = Fabricate(:category)
context '#batch_set' do
fab!(:category) { Fabricate(:category) }
watching = CategoryUser.where(user_id: user.id, notification_level: CategoryUser.notification_levels[:watching])
def category_ids_at_level(level)
CategoryUser.where(
user_id: user.id,
notification_level: CategoryUser.notification_levels[level]
).pluck(:category_id)
end
CategoryUser.batch_set(user, :watching, [category1.id, category2.id])
expect(watching.pluck(:category_id).sort).to eq [category1.id, category2.id]
it "should add new records where required" do
CategoryUser.batch_set(user, :watching, [category.id])
CategoryUser.batch_set(user, :watching, [])
expect(watching.count).to eq 0
expect(category_ids_at_level(:watching)).to eq([category.id])
end
CategoryUser.batch_set(user, :watching, [category2.id])
expect(watching.count).to eq 1
it "should change existing records where required" do
CategoryUser.create!(
user_id: user.id,
category_id: category.id,
notification_level: CategoryUser.notification_levels[:muted]
)
CategoryUser.batch_set(user, :watching, [category.id])
expect(category_ids_at_level(:watching)).to eq([category.id])
expect(category_ids_at_level(:muted)).to eq([])
end
it "should delete extraneous records where required" do
CategoryUser.create!(
user_id: user.id,
category_id: category.id,
notification_level: CategoryUser.notification_levels[:watching]
)
CategoryUser.batch_set(user, :watching, [])
expect(category_ids_at_level(:watching)).to eq([])
end
it "should return true when something changed" do
expect(CategoryUser.batch_set(user, :watching, [category.id])).to eq(true)
end
it "should return false when nothing changed" do
CategoryUser.batch_set(user, :watching, [category.id])
expect(CategoryUser.batch_set(user, :watching, [category.id])).to eq(false)
end
end
it 'should correctly auto_track' do
tracking_user = Fabricate(:user)
user = Fabricate(:user)
topic = Fabricate(:post).topic
TopicUser.change(user.id, topic.id, total_msecs_viewed: 10)
@ -48,7 +83,6 @@ describe CategoryUser do
it 'allows updating notification level' do
category = Fabricate(:category)
user = Fabricate(:user)
CategoryUser.set_notification_level_for_category(user,
NotificationLevels.all[:watching_first_post],
@ -78,8 +112,6 @@ describe CategoryUser do
muted_category = Fabricate(:category)
tracked_category = Fabricate(:category)
user = Fabricate(:user)
early_watched_post = create_post(category: watched_category)
CategoryUser.create!(user: user, category: watched_category, notification_level: CategoryUser.notification_levels[:watching])
@ -104,8 +136,6 @@ describe CategoryUser do
end
it "topics that move to a tracked category should auto track" do
user = Fabricate(:user)
first_post = create_post
tracked_category = first_post.topic.category
@ -120,7 +150,6 @@ describe CategoryUser do
end
it "unwatches categories that have been changed" do
user = Fabricate(:user)
watched_category = Fabricate(:category)
CategoryUser.create!(user: user, category: watched_category, notification_level: CategoryUser.notification_levels[:watching])
@ -141,7 +170,6 @@ describe CategoryUser do
# this is done so as to maintain topic notification state when topic category is changed and the new category has same notification level for the user
# see: https://meta.discourse.org/t/changing-topic-from-one-watched-category-to-another-watched-category-makes-topic-new-again/36517/15
user = Fabricate(:user)
watched_category_1 = Fabricate(:category)
watched_category_2 = Fabricate(:category)
category_3 = Fabricate(:category)
@ -167,7 +195,6 @@ describe CategoryUser do
end
it "deletes TopicUser record when topic category is changed, and new category has different notification level" do
user = Fabricate(:user)
watched_category = Fabricate(:category)
tracked_category = Fabricate(:category)
CategoryUser.create!(user: user, category: watched_category, notification_level: CategoryUser.notification_levels[:watching])
@ -183,7 +210,6 @@ describe CategoryUser do
end
it "is destroyed when a user is deleted" do
user = Fabricate(:user)
category = Fabricate(:category)
CategoryUser.create!(user: user, category: category, notification_level: CategoryUser.notification_levels[:watching])