Commit 07bc6987 authored by Markus Koller's avatar Markus Koller

Add RelativePositioning.move_nulls_to_start helper

This mirrors the existing `move_nulls_to_end` and can be used to
backfill existing records.

The records are positioned at the start before any other records that
already have a position set, while keeping the sequential order that
the records are passed in.

This also switches from `save` to `update_column` to avoid unnecessary
queries for things like uniqueness validation.
parent a0adf4a2
...@@ -32,17 +32,32 @@ module RelativePositioning ...@@ -32,17 +32,32 @@ module RelativePositioning
class_methods do class_methods do
def move_nulls_to_end(objects) def move_nulls_to_end(objects)
objects = objects.reject(&:relative_position) objects = objects.reject(&:relative_position)
return if objects.empty? return if objects.empty?
max_relative_position = objects.first.max_relative_position
self.transaction do self.transaction do
max_relative_position = objects.first.max_relative_position
objects.each do |object| objects.each do |object|
relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
object.relative_position = relative_position object.update_column(:relative_position, relative_position)
max_relative_position = relative_position max_relative_position = relative_position
object.save(touch: false) end
end
end
def move_nulls_to_start(objects)
objects = objects.reject(&:relative_position)
return if objects.empty?
self.transaction do
min_relative_position = objects.first.min_relative_position
objects.reverse_each do |object|
relative_position = position_between(MIN_POSITION, min_relative_position || START_POSITION)
object.update_column(:relative_position, relative_position)
min_relative_position = relative_position
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'a class that supports relative positioning' do RSpec.shared_examples 'a class that supports relative positioning' do
let(:item1) { create(factory, default_params) } let(:item1) { create_item }
let(:item2) { create(factory, default_params) } let(:item2) { create_item }
let(:new_item) { create(factory, default_params) } let(:new_item) { create_item }
def create_item(params) def create_item(params = {})
create(factory, params.merge(default_params)) create(factory, params.merge(default_params))
end end
...@@ -16,21 +16,30 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -16,21 +16,30 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end end
describe '.move_nulls_to_end' do describe '.move_nulls_to_end' do
let(:item3) { create_item }
it 'moves items with null relative_position to the end' do it 'moves items with null relative_position to the end' do
item1.update!(relative_position: nil) item1.update!(relative_position: 1000)
item2.update!(relative_position: nil) item2.update!(relative_position: nil)
item3.update!(relative_position: nil)
described_class.move_nulls_to_end([item1, item2]) items = [item1, item2, item3]
described_class.move_nulls_to_end(items)
items.map(&:reload)
expect(item2.prev_relative_position).to eq item1.relative_position expect(items.sort_by(&:relative_position)).to eq(items)
expect(item1.prev_relative_position).to eq nil expect(item1.relative_position).to be(1000)
expect(item2.next_relative_position).to eq nil expect(item1.prev_relative_position).to be_nil
expect(item1.next_relative_position).to eq(item2.relative_position)
expect(item2.next_relative_position).to eq(item3.relative_position)
expect(item3.next_relative_position).to be_nil
end end
it 'moves the item near the start position when there are no existing positions' do it 'moves the item near the start position when there are no existing positions' do
item1.update!(relative_position: nil) item1.update!(relative_position: nil)
described_class.move_nulls_to_end([item1]) described_class.move_nulls_to_end([item1])
item1.reload
expect(item1.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE) expect(item1.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE)
end end
...@@ -38,9 +47,49 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -38,9 +47,49 @@ RSpec.shared_examples 'a class that supports relative positioning' do
it 'does not perform any moves if all items have their relative_position set' do it 'does not perform any moves if all items have their relative_position set' do
item1.update!(relative_position: 1) item1.update!(relative_position: 1)
expect(item1).not_to receive(:save)
described_class.move_nulls_to_end([item1]) described_class.move_nulls_to_end([item1])
item1.reload
expect(item1.relative_position).to be(1)
end
end
describe '.move_nulls_to_start' do
let(:item3) { create_item }
it 'moves items with null relative_position to the start' do
item1.update!(relative_position: nil)
item2.update!(relative_position: nil)
item3.update!(relative_position: 1000)
items = [item1, item2, item3]
described_class.move_nulls_to_start(items)
items.map(&:reload)
expect(items.sort_by(&:relative_position)).to eq(items)
expect(item1.prev_relative_position).to eq nil
expect(item1.next_relative_position).to eq item2.relative_position
expect(item2.next_relative_position).to eq item3.relative_position
expect(item3.next_relative_position).to eq nil
expect(item3.relative_position).to be(1000)
end
it 'moves the item near the start position when there are no existing positions' do
item1.update!(relative_position: nil)
described_class.move_nulls_to_start([item1])
item1.reload
expect(item1.relative_position).to eq(described_class::START_POSITION - described_class::IDEAL_DISTANCE)
end
it 'does not perform any moves if all items have their relative_position set' do
item1.update!(relative_position: 1)
described_class.move_nulls_to_start([item1])
item1.reload
expect(item1.relative_position).to be(1)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment