Commit a5923aa6 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Revert "Merge branch 'ajk-relative-positioning-improvements' into 'master'"

This reverts merge request !37724
parent 3807fcbf
...@@ -1323,6 +1323,7 @@ Rails/SaveBang: ...@@ -1323,6 +1323,7 @@ Rails/SaveBang:
- 'spec/support/shared_examples/models/members_notifications_shared_example.rb' - 'spec/support/shared_examples/models/members_notifications_shared_example.rb'
- 'spec/support/shared_examples/models/mentionable_shared_examples.rb' - 'spec/support/shared_examples/models/mentionable_shared_examples.rb'
- 'spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb' - 'spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb'
- 'spec/support/shared_examples/models/relative_positioning_shared_examples.rb'
- 'spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb' - 'spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb'
- 'spec/support/shared_examples/models/update_project_statistics_shared_examples.rb' - 'spec/support/shared_examples/models/update_project_statistics_shared_examples.rb'
- 'spec/support/shared_examples/models/with_uploads_shared_examples.rb' - 'spec/support/shared_examples/models/with_uploads_shared_examples.rb'
......
...@@ -3,15 +3,11 @@ ...@@ -3,15 +3,11 @@
# This module makes it possible to handle items as a list, where the order of items can be easily altered # This module makes it possible to handle items as a list, where the order of items can be easily altered
# Requirements: # Requirements:
# #
# The model must have the following named columns: # - Only works for ActiveRecord models
# - id: integer # - relative_position integer field must present on the model
# - relative_position: integer # - This module uses GROUP BY: the model should have a parent relation, example: project -> issues, project is the parent relation (issues table has a parent_id column)
# #
# The model must support a concept of siblings via a child->parent relationship, # Setup like this in the body of your class:
# to enable rebalancing and `GROUP BY` in queries.
# - example: project -> issues, project is the parent relation (issues table has a parent_id column)
#
# Two class methods must be defined when including this concern:
# #
# include RelativePositioning # include RelativePositioning
# #
...@@ -28,162 +24,66 @@ ...@@ -28,162 +24,66 @@
module RelativePositioning module RelativePositioning
extend ActiveSupport::Concern extend ActiveSupport::Concern
STEPS = 10 MIN_POSITION = 0
IDEAL_DISTANCE = 2**(STEPS - 1) + 1 START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2
MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
START_POSITION = 0
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
IDEAL_DISTANCE = 500
MAX_GAP = IDEAL_DISTANCE * 2
MIN_GAP = 2
NoSpaceLeft = Class.new(StandardError)
class_methods do class_methods do
def move_nulls_to_end(objects) def move_nulls_to_end(objects)
move_nulls(objects, at_end: true) objects = objects.reject(&:relative_position)
return if objects.empty?
self.transaction do
max_relative_position = objects.first.max_relative_position
objects.each do |object|
relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
object.update_column(:relative_position, relative_position)
max_relative_position = relative_position
end
end
end end
def move_nulls_to_start(objects) def move_nulls_to_start(objects)
move_nulls(objects, at_end: false) 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
# This method takes two integer values (positions) and # This method takes two integer values (positions) and
# calculates the position between them. The range is huge as # calculates the position between them. The range is huge as
# the maximum integer value is 2147483647. # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
# # when we have enough space. If distance is less than IDEAL_DISTANCE, we are calculating an average number.
# We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
#
# Then we handle one of three cases:
# - If the gap is too small, we raise NoSpaceLeft
# - If the gap is larger than MAX_GAP, we place the new position at most
# IDEAL_DISTANCE from the edge of the gap.
# - otherwise we place the new position at the midpoint.
#
# The new position will always satisfy: pos_before <= midpoint <= pos_after
#
# As a precondition, the gap between pos_before and pos_after MUST be >= 2.
# If the gap is too small, NoSpaceLeft is raised.
#
# This class method should only be called by instance methods of this module, which
# include handling for minimum gap size.
#
# @raises NoSpaceLeft
# @api private
def position_between(pos_before, pos_after) def position_between(pos_before, pos_after)
pos_before ||= MIN_POSITION pos_before ||= MIN_POSITION
pos_after ||= MAX_POSITION pos_after ||= MAX_POSITION
pos_before, pos_after = [pos_before, pos_after].sort pos_before, pos_after = [pos_before, pos_after].sort
gap_width = pos_after - pos_before halfway = (pos_after + pos_before) / 2
midpoint = [pos_after - 1, pos_before + (gap_width / 2)].min distance_to_halfway = pos_after - halfway
if gap_width < MIN_GAP if distance_to_halfway < IDEAL_DISTANCE
raise NoSpaceLeft halfway
elsif gap_width > MAX_GAP else
if pos_before == MIN_POSITION if pos_before == MIN_POSITION
pos_after - IDEAL_DISTANCE pos_after - IDEAL_DISTANCE
elsif pos_after == MAX_POSITION elsif pos_after == MAX_POSITION
pos_before + IDEAL_DISTANCE pos_before + IDEAL_DISTANCE
else else
midpoint halfway
end
else
midpoint
end
end
private
# @api private
def gap_size(object, gaps:, at_end:, starting_from:)
total_width = IDEAL_DISTANCE * gaps
size = if at_end && starting_from + total_width >= MAX_POSITION
(MAX_POSITION - starting_from) / gaps
elsif !at_end && starting_from - total_width <= MIN_POSITION
(starting_from - MIN_POSITION) / gaps
else
IDEAL_DISTANCE
end
# Shift max elements leftwards if there isn't enough space
return [size, starting_from] if size >= MIN_GAP
order = at_end ? :desc : :asc
terminus = object
.send(:relative_siblings) # rubocop:disable GitlabSecurity/PublicSend
.where('relative_position IS NOT NULL')
.order(relative_position: order)
.first
if at_end
terminus.move_sequence_before(true)
max_relative_position = terminus.reset.relative_position
[[(MAX_POSITION - max_relative_position) / gaps, IDEAL_DISTANCE].min, max_relative_position]
else
terminus.move_sequence_after(true)
min_relative_position = terminus.reset.relative_position
[[(min_relative_position - MIN_POSITION) / gaps, IDEAL_DISTANCE].min, min_relative_position]
end
end
# @api private
# @param [Array<RelativePositioning>] objects The objects to give positions to. The relative
# order will be preserved (i.e. when this method returns,
# objects.first.relative_position < objects.last.relative_position)
# @param [Boolean] at_end: The placement.
# If `true`, then all objects with `null` positions are placed _after_
# all siblings with positions. If `false`, all objects with `null`
# positions are placed _before_ all siblings with positions.
def move_nulls(objects, at_end:)
objects = objects.reject(&:relative_position)
return if objects.empty?
representative = objects.first
number_of_gaps = objects.size + 1 # 1 at left, one between each, and one at right
position = if at_end
representative.max_relative_position
else
representative.min_relative_position
end
position ||= START_POSITION # If there are no positioned siblings, start from START_POSITION
gap, position = gap_size(representative, gaps: number_of_gaps, at_end: at_end, starting_from: position)
# Raise if we could not make enough space
raise NoSpaceLeft if gap < MIN_GAP
indexed = objects.each_with_index.to_a
starting_from = at_end ? position : position - (gap * number_of_gaps)
# Some classes are polymorphic, and not all siblings are in the same table.
by_model = indexed.group_by { |pair| pair.first.class }
by_model.each do |model, pairs|
model.transaction do
pairs.each_slice(100) do |batch|
# These are known to be integers, one from the DB, and the other
# calculated by us, and thus safe to interpolate
values = batch.map do |obj, i|
pos = starting_from + gap * (i + 1)
obj.relative_position = pos
"(#{obj.id}, #{pos})"
end.join(', ')
model.connection.exec_query(<<~SQL, "UPDATE #{model.table_name} positions")
WITH cte(cte_id, new_pos) AS (
SELECT *
FROM (VALUES #{values}) as t (id, pos)
)
UPDATE #{model.table_name}
SET relative_position = cte.new_pos
FROM cte
WHERE cte_id = id
SQL
end
end end
end end
end end
...@@ -197,12 +97,11 @@ module RelativePositioning ...@@ -197,12 +97,11 @@ module RelativePositioning
calculate_relative_position('MAX', &block) calculate_relative_position('MAX', &block)
end end
def prev_relative_position(ignoring: nil) def prev_relative_position
prev_pos = nil prev_pos = nil
if self.relative_position if self.relative_position
prev_pos = max_relative_position do |relation| prev_pos = max_relative_position do |relation|
relation = relation.id_not_in(ignoring.id) if ignoring.present?
relation.where('relative_position < ?', self.relative_position) relation.where('relative_position < ?', self.relative_position)
end end
end end
...@@ -210,12 +109,11 @@ module RelativePositioning ...@@ -210,12 +109,11 @@ module RelativePositioning
prev_pos prev_pos
end end
def next_relative_position(ignoring: nil) def next_relative_position
next_pos = nil next_pos = nil
if self.relative_position if self.relative_position
next_pos = min_relative_position do |relation| next_pos = min_relative_position do |relation|
relation = relation.id_not_in(ignoring.id) if ignoring.present?
relation.where('relative_position > ?', self.relative_position) relation.where('relative_position > ?', self.relative_position)
end end
end end
...@@ -227,44 +125,24 @@ module RelativePositioning ...@@ -227,44 +125,24 @@ module RelativePositioning
return move_after(before) unless after return move_after(before) unless after
return move_before(after) unless before return move_before(after) unless before
# If there is no place to insert an item we need to create one by moving the item
# before this and all preceding items until there is a gap
before, after = after, before if after.relative_position < before.relative_position before, after = after, before if after.relative_position < before.relative_position
if (after.relative_position - before.relative_position) < 2
pos_left = before.relative_position after.move_sequence_before
pos_right = after.relative_position before.reset
if pos_right - pos_left < MIN_GAP
# Not enough room! Make space by shifting all previous elements to the left
# if there is enough space, else to the right
gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
if gap.present?
after.move_sequence_before(next_gap: gap)
pos_left -= optimum_delta_for_gap(gap)
else
before.move_sequence_after
pos_right = after.reset.relative_position
end
end end
new_position = self.class.position_between(pos_left, pos_right) self.relative_position = self.class.position_between(before.relative_position, after.relative_position)
self.relative_position = new_position
end end
def move_after(before = self) def move_after(before = self)
pos_before = before.relative_position pos_before = before.relative_position
pos_after = before.next_relative_position(ignoring: self) pos_after = before.next_relative_position
if pos_before == MAX_POSITION || gap_too_small?(pos_after, pos_before)
gap = before.send(:find_next_gap_after) # rubocop:disable GitlabSecurity/PublicSend
if gap.nil? if pos_after && (pos_after - pos_before) < 2
before.move_sequence_before(true) before.move_sequence_after
pos_before = before.reset.relative_position pos_after = before.next_relative_position
else
before.move_sequence_after(next_gap: gap)
pos_after += optimum_delta_for_gap(gap)
end
end end
self.relative_position = self.class.position_between(pos_before, pos_after) self.relative_position = self.class.position_between(pos_before, pos_after)
...@@ -272,168 +150,80 @@ module RelativePositioning ...@@ -272,168 +150,80 @@ module RelativePositioning
def move_before(after = self) def move_before(after = self)
pos_after = after.relative_position pos_after = after.relative_position
pos_before = after.prev_relative_position(ignoring: self) pos_before = after.prev_relative_position
if pos_after == MIN_POSITION || gap_too_small?(pos_before, pos_after) if pos_before && (pos_after - pos_before) < 2
gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend after.move_sequence_before
pos_before = after.prev_relative_position
if gap.nil?
after.move_sequence_after(true)
pos_after = after.reset.relative_position
else
after.move_sequence_before(next_gap: gap)
pos_before -= optimum_delta_for_gap(gap)
end
end end
self.relative_position = self.class.position_between(pos_before, pos_after) self.relative_position = self.class.position_between(pos_before, pos_after)
end end
def move_to_end def move_to_end
max_pos = max_relative_position self.relative_position = self.class.position_between(max_relative_position || START_POSITION, MAX_POSITION)
self.relative_position = max_pos.nil? ? START_POSITION : self.class.position_between(max_pos, MAX_POSITION)
end end
def move_to_start def move_to_start
min_pos = max_relative_position self.relative_position = self.class.position_between(min_relative_position || START_POSITION, MIN_POSITION)
self.relative_position = min_pos.nil? ? START_POSITION : self.class.position_between(MIN_POSITION, min_pos)
end end
# Moves the sequence before the current item to the middle of the next gap # Moves the sequence before the current item to the middle of the next gap
# For example, we have # For example, we have 5 11 12 13 14 15 and the current item is 15
# # This moves the sequence 11 12 13 14 to 8 9 10 11
# 5 . . . . . 11 12 13 14 [15] 16 . 17 def move_sequence_before
# ----------- next_gap = find_next_gap_before
#
# This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
#
# 5 . . 8 9 10 11 . . . [15] 16 . 17
# ---------
#
# Creating a gap to the left of the current item. We can understand this as
# dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
#
# If `include_self` is true, the current item will also be moved, creating a
# gap to the right of the current item:
#
# 5 . . 8 9 10 11 [14] . . . 16 . 17
# --------------
#
# As an optimization, the gap can be precalculated and passed to this method.
#
# @api private
# @raises NoSpaceLeft if the sequence cannot be moved
def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
raise NoSpaceLeft unless next_gap.present?
delta = optimum_delta_for_gap(next_gap) delta = optimum_delta_for_gap(next_gap)
move_sequence(next_gap[:start], relative_position, -delta, include_self) move_sequence(next_gap[:start], relative_position, -delta)
end end
# Moves the sequence after the current item to the middle of the next gap # Moves the sequence after the current item to the middle of the next gap
# For example, we have: # For example, we have 11 12 13 14 15 21 and the current item is 11
# # This moves the sequence 12 13 14 15 to 15 16 17 18
# 8 . 10 [11] 12 13 14 15 . . . . . 21 def move_sequence_after
# ----------- next_gap = find_next_gap_after
#
# This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
#
# 8 . 10 [11] . . . 15 16 17 18 . . 21
# -----------
#
# Creating a gap to the right of the current item. We can understand this as
# dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
#
# If `include_self` is true, the current item will also be moved, creating a
# gap to the left of the current item:
#
# 8 . 10 . . . [14] 15 16 17 18 . . 21
# ----------------
#
# As an optimization, the gap can be precalculated and passed to this method.
#
# @api private
# @raises NoSpaceLeft if the sequence cannot be moved
def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
raise NoSpaceLeft unless next_gap.present?
delta = optimum_delta_for_gap(next_gap) delta = optimum_delta_for_gap(next_gap)
move_sequence(relative_position, next_gap[:start], delta, include_self) move_sequence(relative_position, next_gap[:start], delta)
end end
private private
def gap_too_small?(pos_a, pos_b) # Supposing that we have a sequence of items: 1 5 11 12 13 and the current item is 13
return false unless pos_a && pos_b # This would return: `{ start: 11, end: 5 }`
(pos_a - pos_b).abs < MIN_GAP
end
# Find the first suitable gap to the left of the current position.
#
# Satisfies the relations:
# - gap[:start] <= relative_position
# - abs(gap[:start] - gap[:end]) >= MIN_GAP
# - MIN_POSITION <= gap[:start] <= MAX_POSITION
# - MIN_POSITION <= gap[:end] <= MAX_POSITION
#
# Supposing that the current item is 13, and we have a sequence of items:
#
# 1 . . . 5 . . . . 11 12 [13] 14 . . 17
# ^---------^
#
# Then we return: `{ start: 11, end: 5 }`
#
# Here start refers to the end of the gap closest to the current item.
def find_next_gap_before def find_next_gap_before
items_with_next_pos = scoped_items items_with_next_pos = scoped_items
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos') .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
.where('relative_position <= ?', relative_position) .where('relative_position <= ?', relative_position)
.order(relative_position: :desc) .order(relative_position: :desc)
find_next_gap(items_with_next_pos, MIN_POSITION) find_next_gap(items_with_next_pos).tap do |gap|
end gap[:end] ||= MIN_POSITION
end
# Find the first suitable gap to the right of the current position. end
#
# Satisfies the relations: # Supposing that we have a sequence of items: 13 14 15 20 24 and the current item is 13
# - gap[:start] >= relative_position # This would return: `{ start: 15, end: 20 }`
# - abs(gap[:start] - gap[:end]) >= MIN_GAP
# - MIN_POSITION <= gap[:start] <= MAX_POSITION
# - MIN_POSITION <= gap[:end] <= MAX_POSITION
#
# Supposing the current item is 13, and that we have a sequence of items:
#
# 9 . . . [13] 14 15 . . . . 20 . . . 24
# ^---------^
#
# Then we return: `{ start: 15, end: 20 }`
#
# Here start refers to the end of the gap closest to the current item.
def find_next_gap_after def find_next_gap_after
items_with_next_pos = scoped_items items_with_next_pos = scoped_items
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos') .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
.where('relative_position >= ?', relative_position) .where('relative_position >= ?', relative_position)
.order(:relative_position) .order(:relative_position)
find_next_gap(items_with_next_pos, MAX_POSITION) find_next_gap(items_with_next_pos).tap do |gap|
gap[:end] ||= MAX_POSITION
end
end end
def find_next_gap(items_with_next_pos, end_is_nil) def find_next_gap(items_with_next_pos)
gap = self.class gap = self.class.from(items_with_next_pos, :items_with_next_pos)
.from(items_with_next_pos, :items) .where('ABS(pos - next_pos) > 1 OR next_pos IS NULL')
.where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
.limit(1) .limit(1)
.pluck(:pos, :next_pos) .pluck(:pos, :next_pos)
.first .first
return if gap.nil? || gap.first == end_is_nil { start: gap[0], end: gap[1] }
{ start: gap.first, end: gap.second || end_is_nil }
end end
def optimum_delta_for_gap(gap) def optimum_delta_for_gap(gap)
...@@ -442,10 +232,9 @@ module RelativePositioning ...@@ -442,10 +232,9 @@ module RelativePositioning
[delta, IDEAL_DISTANCE].min [delta, IDEAL_DISTANCE].min
end end
def move_sequence(start_pos, end_pos, delta, include_self = false) def move_sequence(start_pos, end_pos, delta)
relation = include_self ? scoped_items : relative_siblings scoped_items
.where.not(id: self.id)
relation
.where('relative_position BETWEEN ? AND ?', start_pos, end_pos) .where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
.update_all("relative_position = relative_position + #{delta}") .update_all("relative_position = relative_position + #{delta}")
end end
...@@ -466,10 +255,6 @@ module RelativePositioning ...@@ -466,10 +255,6 @@ module RelativePositioning
.first&.last .first&.last
end end
def relative_siblings(relation = scoped_items)
relation.id_not_in(id)
end
def scoped_items def scoped_items
self.class.relative_positioning_query_base(self) self.class.relative_positioning_query_base(self)
end end
......
---
title: Performance and robustness improvements for relative positioning
merge_request: 37724
author:
type: performance
...@@ -8,8 +8,8 @@ module EpicTreeSorting ...@@ -8,8 +8,8 @@ module EpicTreeSorting
class_methods do class_methods do
def relative_positioning_query_base(object) def relative_positioning_query_base(object)
from_union([ from_union([
EpicIssue.select("id, relative_position, epic_id as parent_id, epic_id, 'epic_issue' as object_type").in_epic(object.parent_ids), EpicIssue.select("id, relative_position, epic_id, 'epic_issue' as object_type").in_epic(object.parent_ids),
Epic.select("id, relative_position, parent_id, parent_id as epic_id, 'epic' as object_type").where(parent_id: object.parent_ids) Epic.select("id, relative_position, parent_id as epic_id, 'epic' as object_type").where(parent_id: object.parent_ids)
]) ])
end end
...@@ -19,14 +19,11 @@ module EpicTreeSorting ...@@ -19,14 +19,11 @@ module EpicTreeSorting
end end
included do included do
def move_sequence(start_pos, end_pos, delta, include_self = false) def move_sequence(start_pos, end_pos, delta)
items_to_update = scoped_items items_to_update = scoped_items
.select(:id, :object_type) .select(:id, :object_type)
.where('relative_position BETWEEN ? AND ?', start_pos, end_pos) .where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
.where.not('object_type = ? AND id = ?', self.class.table_name.singularize, self.id)
unless include_self
items_to_update = items_to_update.where.not('object_type = ? AND id = ?', self.class.table_name.singularize, self.id)
end
items_to_update.group_by { |item| item.object_type }.each do |type, group_items| items_to_update.group_by { |item| item.object_type }.each do |type, group_items|
ids = group_items.map(&:id) ids = group_items.map(&:id)
......
...@@ -28,49 +28,9 @@ RSpec.describe EpicIssue do ...@@ -28,49 +28,9 @@ RSpec.describe EpicIssue do
context "relative positioning" do context "relative positioning" do
it_behaves_like "a class that supports relative positioning" do it_behaves_like "a class that supports relative positioning" do
let_it_be(:epic) { create(:epic) } let(:epic) { create(:epic) }
let(:factory) { :epic_issue } let(:factory) { :epic_issue }
let(:default_params) { { epic: epic } } let(:default_params) { { epic: epic } }
end end
context 'with a mixed tree level' do
let_it_be(:epic) { create(:epic) }
let_it_be_with_reload(:left) { create(:epic_issue, epic: epic, relative_position: 100) }
let_it_be_with_reload(:middle) { create(:epic, group: epic.group, parent: epic, relative_position: 101) }
let_it_be_with_reload(:right) { create(:epic_issue, epic: epic, relative_position: 102) }
it 'can create space by using move_sequence_after' do
left.move_sequence_after
[left, middle, right].each(&:reset)
expect(middle.relative_position - left.relative_position).to be > 1
expect(left.relative_position).to be < middle.relative_position
expect(middle.relative_position).to be < right.relative_position
end
it 'can create space by using move_sequence_before' do
right.move_sequence_before
[left, middle, right].each(&:reset)
expect(right.relative_position - middle.relative_position).to be > 1
expect(left.relative_position).to be < middle.relative_position
expect(middle.relative_position).to be < right.relative_position
end
it 'moves nulls to the end' do
leaves = create_list(:epic_issue, 2, epic: epic, relative_position: nil)
nested = create(:epic, group: epic.group, parent: epic, relative_position: nil)
moved = [*leaves, nested]
level = [nested, *leaves, right]
expect do
EpicIssue.move_nulls_to_end(level)
end.not_to change { right.reset.relative_position }
moved.each(&:reset)
expect(moved.map(&:relative_position)).to all(be > right.relative_position)
end
end
end end
end end
...@@ -616,22 +616,12 @@ RSpec.describe Epic do ...@@ -616,22 +616,12 @@ RSpec.describe Epic do
end end
context "relative positioning" do context "relative positioning" do
context 'there is no parent' do
it_behaves_like "a class that supports relative positioning" do it_behaves_like "a class that supports relative positioning" do
let(:factory) { :epic } let(:factory) { :epic }
let(:default_params) { {} } let(:default_params) { {} }
end end
end end
context 'there is a parent' do
it_behaves_like "a class that supports relative positioning" do
let_it_be(:parent) { create(:epic) }
let(:factory) { :epic }
let(:default_params) { { parent: parent, group: parent.group } }
end
end
end
describe '.related_issues' do describe '.related_issues' do
it 'returns epic issues ordered by relative position' do it 'returns epic issues ordered by relative position' do
epic1 = create(:epic, group: group) epic1 = create(:epic, group: group)
......
...@@ -454,14 +454,20 @@ RSpec.describe Issue do ...@@ -454,14 +454,20 @@ RSpec.describe Issue do
end end
describe 'relative positioning with group boards' do describe 'relative positioning with group boards' do
let_it_be(:group) { create(:group) } let(:group) { create(:group) }
let_it_be(:board) { create(:board, group: group) } let!(:board) { create(:board, group: group) }
let_it_be(:project) { create(:project, namespace: group) } let(:project) { create(:project, namespace: group) }
let_it_be(:project1) { create(:project, namespace: group) } let(:project1) { create(:project, namespace: group) }
let_it_be_with_reload(:issue) { create(:issue, project: project) } let(:issue) { build(:issue, project: project) }
let_it_be_with_reload(:issue1) { create(:issue, project: project1, relative_position: issue.relative_position + RelativePositioning::IDEAL_DISTANCE) } let(:issue1) { build(:issue, project: project1) }
let(:new_issue) { build(:issue, project: project1, relative_position: nil) } let(:new_issue) { build(:issue, project: project1, relative_position: nil) }
before do
[issue, issue1].each do |issue|
issue.move_to_end && issue.save
end
end
describe '#max_relative_position' do describe '#max_relative_position' do
it 'returns maximum position' do it 'returns maximum position' do
expect(issue.max_relative_position).to eq issue1.relative_position expect(issue.max_relative_position).to eq issue1.relative_position
...@@ -540,20 +546,18 @@ RSpec.describe Issue do ...@@ -540,20 +546,18 @@ RSpec.describe Issue do
issue1.update relative_position: issue.relative_position issue1.update relative_position: issue.relative_position
new_issue.move_between(issue, issue1) new_issue.move_between(issue, issue1)
[issue, issue1].each(&:reset)
expect(new_issue.relative_position) expect(new_issue.relative_position).to be > issue.relative_position
.to be_between(issue.relative_position, issue1.relative_position).exclusive expect(issue.relative_position).to be < issue1.relative_position
end end
it 'positions issues between other two if distance is 1' do it 'positions issues between other two if distance is 1' do
issue1.update relative_position: issue.relative_position + 1 issue1.update relative_position: issue.relative_position + 1
new_issue.move_between(issue, issue1) new_issue.move_between(issue, issue1)
[issue, issue1].each(&:reset)
expect(new_issue.relative_position) expect(new_issue.relative_position).to be > issue.relative_position
.to be_between(issue.relative_position, issue1.relative_position).exclusive expect(issue.relative_position).to be < issue1.relative_position
end end
it 'positions issue in the middle of other two if distance is big enough' do it 'positions issue in the middle of other two if distance is big enough' do
...@@ -562,20 +566,24 @@ RSpec.describe Issue do ...@@ -562,20 +566,24 @@ RSpec.describe Issue do
new_issue.move_between(issue, issue1) new_issue.move_between(issue, issue1)
expect(new_issue.relative_position) expect(new_issue.relative_position).to eq(8000)
.to be_between(issue.relative_position, issue1.relative_position).exclusive
end end
it 'positions issue closer to the middle if we are at the very top' do it 'positions issue closer to the middle if we are at the very top' do
new_issue.move_between(nil, issue) issue1.update relative_position: 6000
expect(new_issue.relative_position).to eq(issue.relative_position - RelativePositioning::IDEAL_DISTANCE) new_issue.move_between(nil, issue1)
expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE)
end end
it 'positions issue closer to the middle if we are at the very bottom' do it 'positions issue closer to the middle if we are at the very bottom' do
new_issue.move_between(issue1, nil) issue.update relative_position: 6000
issue1.update relative_position: nil
new_issue.move_between(issue, nil)
expect(new_issue.relative_position).to eq(issue1.relative_position + RelativePositioning::IDEAL_DISTANCE) expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
end end
it 'positions issue in the middle of other two if distance is not big enough' do it 'positions issue in the middle of other two if distance is not big enough' do
...@@ -592,10 +600,8 @@ RSpec.describe Issue do ...@@ -592,10 +600,8 @@ RSpec.describe Issue do
issue1.update relative_position: 101 issue1.update relative_position: 101
new_issue.move_between(issue, issue1) new_issue.move_between(issue, issue1)
[issue, issue1].each(&:reset)
expect(new_issue.relative_position) expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position)
.to be_between(issue.relative_position, issue1.relative_position).exclusive
end end
it 'uses rebalancing if there is no place' do it 'uses rebalancing if there is no place' do
...@@ -606,15 +612,12 @@ RSpec.describe Issue do ...@@ -606,15 +612,12 @@ RSpec.describe Issue do
new_issue.move_between(issue1, issue2) new_issue.move_between(issue1, issue2)
new_issue.save! new_issue.save!
[issue, issue1, issue2].each(&:reset)
expect(new_issue.relative_position)
.to be_between(issue1.relative_position, issue2.relative_position).exclusive
expect([issue, issue1, issue2, new_issue].map(&:relative_position).uniq).to have_attributes(size: 4) expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
expect(issue.reload.relative_position).not_to eq(100)
end end
it 'positions issue right if we pass non-sequential parameters' do it 'positions issue right if we pass none-sequential parameters' do
issue.update relative_position: 99 issue.update relative_position: 99
issue1.update relative_position: 101 issue1.update relative_position: 101
issue2 = create(:issue, relative_position: 102, project: project) issue2 = create(:issue, relative_position: 102, project: project)
......
...@@ -169,9 +169,7 @@ RSpec.describe EpicIssues::CreateService do ...@@ -169,9 +169,7 @@ RSpec.describe EpicIssues::CreateService do
end end
context 'when multiple valid issues are given' do context 'when multiple valid issues are given' do
let(:references) { [issue, issue2].map { |i| i.to_reference(full: true) } } subject { assign_issue([issue.to_reference(full: true), issue2.to_reference(full: true)]) }
subject { assign_issue(references) }
let(:created_link1) { EpicIssue.find_by!(issue_id: issue.id) } let(:created_link1) { EpicIssue.find_by!(issue_id: issue.id) }
let(:created_link2) { EpicIssue.find_by!(issue_id: issue2.id) } let(:created_link2) { EpicIssue.find_by!(issue_id: issue2.id) }
...@@ -183,18 +181,13 @@ RSpec.describe EpicIssues::CreateService do ...@@ -183,18 +181,13 @@ RSpec.describe EpicIssues::CreateService do
expect(created_link2).to have_attributes(epic: epic) expect(created_link2).to have_attributes(epic: epic)
end end
it 'places each issue at the start' do
subject
expect(created_link2.relative_position).to be < created_link1.relative_position
end
it 'orders the epic issues to the first place and moves the existing ones down' do it 'orders the epic issues to the first place and moves the existing ones down' do
existing_link = create(:epic_issue, epic: epic, issue: issue3) existing_link = create(:epic_issue, epic: epic, issue: issue3)
subject subject
expect([created_link1, created_link2].map(&:relative_position)) expect(created_link2.relative_position).to be < created_link1.relative_position
.to all(be < existing_link.reset.relative_position) expect(created_link1.relative_position).to be < existing_link.reload.relative_position
end end
it 'returns success status' do it 'returns success status' do
......
...@@ -22,7 +22,6 @@ module Gitlab ...@@ -22,7 +22,6 @@ module Gitlab
# https://www.postgresql.org/docs/9.2/static/datatype-numeric.html # https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
MAX_INT_VALUE = 2147483647 MAX_INT_VALUE = 2147483647
MIN_INT_VALUE = -2147483648
# The max value between MySQL's TIMESTAMP and PostgreSQL's timestampz: # The max value between MySQL's TIMESTAMP and PostgreSQL's timestampz:
# https://www.postgresql.org/docs/9.1/static/datatype-datetime.html # https://www.postgresql.org/docs/9.1/static/datatype-datetime.html
......
...@@ -21,7 +21,7 @@ RSpec.describe 'Issue Boards', :js do ...@@ -21,7 +21,7 @@ RSpec.describe 'Issue Boards', :js do
end end
context 'un-ordered issues' do context 'un-ordered issues' do
let!(:issue4) { create(:labeled_issue, project: project, labels: [label], relative_position: nil) } let!(:issue4) { create(:labeled_issue, project: project, labels: [label]) }
before do before do
visit project_board_path(project, board) visit project_board_path(project, board)
......
...@@ -23,7 +23,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do ...@@ -23,7 +23,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do
context 'relative positioning' do context 'relative positioning' do
it_behaves_like 'a class that supports relative positioning' do it_behaves_like 'a class that supports relative positioning' do
let_it_be(:project) { create(:project) } let(:project) { build(:project) }
let(:factory) { :cycle_analytics_project_stage } let(:factory) { :cycle_analytics_project_stage }
let(:default_params) { { project: project } } let(:default_params) { { project: project } }
end end
......
...@@ -6,7 +6,6 @@ RSpec.describe Issue do ...@@ -6,7 +6,6 @@ RSpec.describe Issue do
include ExternalAuthorizationServiceHelpers include ExternalAuthorizationServiceHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:reusable_project) { create(:project) }
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to(:milestone) } it { is_expected.to belong_to(:milestone) }
...@@ -146,13 +145,13 @@ RSpec.describe Issue do ...@@ -146,13 +145,13 @@ RSpec.describe Issue do
end end
describe '#order_by_position_and_priority' do describe '#order_by_position_and_priority' do
let(:project) { reusable_project } let(:project) { create :project }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) } let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) } let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) }
let!(:issue3) { create(:issue, project: project, relative_position: -200) } let!(:issue3) { create(:issue, project: project, relative_position: 100) }
let!(:issue4) { create(:issue, project: project, relative_position: -100) } let!(:issue4) { create(:issue, project: project, relative_position: 200) }
it 'returns ordered list' do it 'returns ordered list' do
expect(project.issues.order_by_position_and_priority) expect(project.issues.order_by_position_and_priority)
...@@ -161,10 +160,10 @@ RSpec.describe Issue do ...@@ -161,10 +160,10 @@ RSpec.describe Issue do
end end
describe '#sort' do describe '#sort' do
let(:project) { reusable_project } let(:project) { create(:project) }
context "by relative_position" do context "by relative_position" do
let!(:issue) { create(:issue, project: project, relative_position: nil) } let!(:issue) { create(:issue, project: project) }
let!(:issue2) { create(:issue, project: project, relative_position: 2) } let!(:issue2) { create(:issue, project: project, relative_position: 2) }
let!(:issue3) { create(:issue, project: project, relative_position: 1) } let!(:issue3) { create(:issue, project: project, relative_position: 1) }
...@@ -1028,7 +1027,7 @@ RSpec.describe Issue do ...@@ -1028,7 +1027,7 @@ RSpec.describe Issue do
context "relative positioning" do context "relative positioning" do
it_behaves_like "a class that supports relative positioning" do it_behaves_like "a class that supports relative positioning" do
let_it_be(:project) { create(:project) } let(:project) { create(:project) }
let(:factory) { :issue } let(:factory) { :issue }
let(:default_params) { { project: project } } let(:default_params) { { project: project } }
end end
......
...@@ -25,6 +25,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -25,6 +25,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
items = [item1, item2, item3] items = [item1, item2, item3]
described_class.move_nulls_to_end(items) described_class.move_nulls_to_end(items)
items.map(&:reload)
expect(items.sort_by(&:relative_position)).to eq(items) expect(items.sort_by(&:relative_position)).to eq(items)
expect(item1.relative_position).to be(1000) expect(item1.relative_position).to be(1000)
...@@ -34,57 +35,22 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -34,57 +35,22 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item3.next_relative_position).to be_nil expect(item3.next_relative_position).to be_nil
end end
it 'preserves relative position' do
item1.update!(relative_position: nil)
item2.update!(relative_position: nil)
described_class.move_nulls_to_end([item1, item2])
expect(item1.relative_position).to be < item2.relative_position
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])
expect(item1.reset.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE) item1.reload
expect(item1.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE)
end end
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 do
described_class.move_nulls_to_end([item1]) described_class.move_nulls_to_end([item1])
end.not_to change { item1.reset.relative_position } item1.reload
end
it 'manages to move nulls to the end even if there is a sequence at the end' do
bunch = create_items_with_positions(run_at_end)
item1.update!(relative_position: nil)
described_class.move_nulls_to_end([item1])
items = [*bunch, item1]
items.each(&:reset)
expect(items.map(&:relative_position)).to all(be_valid_position)
expect(items.sort_by(&:relative_position)).to eq(items)
end
it 'does not have an N+1 issue' do
create_items_with_positions(10..12)
a, b, c, d, e, f = create_items_with_positions([nil, nil, nil, nil, nil, nil])
baseline = ActiveRecord::QueryRecorder.new do
described_class.move_nulls_to_end([a, e])
end
expect { described_class.move_nulls_to_end([b, c, d]) }
.not_to exceed_query_limit(baseline)
expect { described_class.move_nulls_to_end([f]) } expect(item1.relative_position).to be(1)
.not_to exceed_query_limit(baseline.count)
end end
end end
...@@ -112,19 +78,11 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -112,19 +78,11 @@ RSpec.shared_examples 'a class that supports relative positioning' do
item1.update!(relative_position: nil) item1.update!(relative_position: nil)
described_class.move_nulls_to_start([item1]) described_class.move_nulls_to_start([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
it 'preserves relative position' do
item1.update!(relative_position: nil)
item2.update!(relative_position: nil)
described_class.move_nulls_to_end([item1, item2])
expect(item1.relative_position).to be < item2.relative_position
end
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)
...@@ -143,8 +101,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -143,8 +101,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '#prev_relative_position' do describe '#prev_relative_position' do
it 'returns previous position if there is an item above' do it 'returns previous position if there is an item above' do
item1.update!(relative_position: 5) item1.update(relative_position: 5)
item2.update!(relative_position: 15) item2.update(relative_position: 15)
expect(item2.prev_relative_position).to eq item1.relative_position expect(item2.prev_relative_position).to eq item1.relative_position
end end
...@@ -156,8 +114,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -156,8 +114,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '#next_relative_position' do describe '#next_relative_position' do
it 'returns next position if there is an item below' do it 'returns next position if there is an item below' do
item1.update!(relative_position: 5) item1.update(relative_position: 5)
item2.update!(relative_position: 15) item2.update(relative_position: 15)
expect(item1.next_relative_position).to eq item2.relative_position expect(item1.next_relative_position).to eq item2.relative_position
end end
...@@ -167,172 +125,9 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -167,172 +125,9 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end end
end end
describe '#find_next_gap_before' do
context 'there is no gap' do
let(:items) { create_items_with_positions(run_at_start) }
it 'returns nil' do
items.each do |item|
expect(item.send(:find_next_gap_before)).to be_nil
end
end
end
context 'there is a sequence ending at MAX_POSITION' do
let(:items) { create_items_with_positions(run_at_end) }
let(:gaps) do
items.map { |item| item.send(:find_next_gap_before) }
end
it 'can find the gap at the start for any item in the sequence' do
gap = { start: items.first.relative_position, end: RelativePositioning::MIN_POSITION }
expect(gaps).to all(eq(gap))
end
it 'respects lower bounds' do
gap = { start: items.first.relative_position, end: 10 }
new_item.update!(relative_position: 10)
expect(gaps).to all(eq(gap))
end
end
specify do
item1.update!(relative_position: 5)
(0..10).each do |pos|
item2.update!(relative_position: pos)
gap = item2.send(:find_next_gap_before)
expect(gap[:start]).to be <= item2.relative_position
expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
expect(gap[:start]).to be_valid_position
expect(gap[:end]).to be_valid_position
end
end
it 'deals with there not being any items to the left' do
create_items_with_positions([1, 2, 3])
new_item.update!(relative_position: 0)
expect(new_item.send(:find_next_gap_before)).to eq(start: 0, end: RelativePositioning::MIN_POSITION)
end
it 'finds the next gap to the left, skipping adjacent values' do
create_items_with_positions([1, 9, 10])
new_item.update!(relative_position: 11)
expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 1)
end
it 'finds the next gap to the left' do
create_items_with_positions([2, 10])
new_item.update!(relative_position: 15)
expect(new_item.send(:find_next_gap_before)).to eq(start: 15, end: 10)
new_item.update!(relative_position: 11)
expect(new_item.send(:find_next_gap_before)).to eq(start: 10, end: 2)
new_item.update!(relative_position: 9)
expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 2)
new_item.update!(relative_position: 5)
expect(new_item.send(:find_next_gap_before)).to eq(start: 5, end: 2)
end
end
describe '#find_next_gap_after' do
context 'there is no gap' do
let(:items) { create_items_with_positions(run_at_end) }
it 'returns nil' do
items.each do |item|
expect(item.send(:find_next_gap_after)).to be_nil
end
end
end
context 'there is a sequence starting at MIN_POSITION' do
let(:items) { create_items_with_positions(run_at_start) }
let(:gaps) do
items.map { |item| item.send(:find_next_gap_after) }
end
it 'can find the gap at the end for any item in the sequence' do
gap = { start: items.last.relative_position, end: RelativePositioning::MAX_POSITION }
expect(gaps).to all(eq(gap))
end
it 'respects upper bounds' do
gap = { start: items.last.relative_position, end: 10 }
new_item.update!(relative_position: 10)
expect(gaps).to all(eq(gap))
end
end
specify do
item1.update!(relative_position: 5)
(0..10).each do |pos|
item2.update!(relative_position: pos)
gap = item2.send(:find_next_gap_after)
expect(gap[:start]).to be >= item2.relative_position
expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
expect(gap[:start]).to be_valid_position
expect(gap[:end]).to be_valid_position
end
end
it 'deals with there not being any items to the right' do
create_items_with_positions([1, 2, 3])
new_item.update!(relative_position: 5)
expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: RelativePositioning::MAX_POSITION)
end
it 'finds the next gap to the right, skipping adjacent values' do
create_items_with_positions([1, 2, 10])
new_item.update!(relative_position: 0)
expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
end
it 'finds the next gap to the right' do
create_items_with_positions([2, 10])
new_item.update!(relative_position: 0)
expect(new_item.send(:find_next_gap_after)).to eq(start: 0, end: 2)
new_item.update!(relative_position: 1)
expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
new_item.update!(relative_position: 3)
expect(new_item.send(:find_next_gap_after)).to eq(start: 3, end: 10)
new_item.update!(relative_position: 5)
expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: 10)
end
end
describe '#move_before' do describe '#move_before' do
let(:item3) { create(factory, default_params) }
it 'moves item before' do it 'moves item before' do
[item2, item1].each do |item| [item2, item1].each(&:move_to_end)
item.move_to_end
item.save!
end
expect(item1.relative_position).to be > item2.relative_position
item1.move_before(item2) item1.move_before(item2)
...@@ -340,10 +135,12 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -340,10 +135,12 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end end
context 'when there is no space' do context 'when there is no space' do
let(:item3) { create(factory, default_params) }
before do before do
item1.update!(relative_position: 1000) item1.update(relative_position: 1000)
item2.update!(relative_position: 1001) item2.update(relative_position: 1001)
item3.update!(relative_position: 1002) item3.update(relative_position: 1002)
end end
it 'moves items correctly' do it 'moves items correctly' do
...@@ -352,73 +149,6 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -352,73 +149,6 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item3.relative_position).to be_between(item1.reload.relative_position, item2.reload.relative_position).exclusive expect(item3.relative_position).to be_between(item1.reload.relative_position, item2.reload.relative_position).exclusive
end end
end end
it 'can move the item before an item at the start' do
item1.update!(relative_position: RelativePositioning::START_POSITION)
new_item.move_before(item1)
expect(new_item.relative_position).to be_valid_position
expect(new_item.relative_position).to be < item1.reload.relative_position
end
it 'can move the item before an item at MIN_POSITION' do
item1.update!(relative_position: RelativePositioning::MIN_POSITION)
new_item.move_before(item1)
expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION
expect(new_item.relative_position).to be < item1.reload.relative_position
end
it 'can move the item before an item bunched up at MIN_POSITION' do
item1, item2, item3 = create_items_with_positions(run_at_start)
new_item.move_before(item3)
new_item.save!
items = [item1, item2, new_item, item3]
items.each do |item|
expect(item.reset.relative_position).to be_valid_position
end
expect(items.sort_by(&:relative_position)).to eq(items)
end
context 'leap-frogging to the left' do
before do
start = RelativePositioning::START_POSITION
item1.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 0)
item2.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 1)
item3.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 2)
end
let(:item3) { create(factory, default_params) }
def leap_frog(steps)
a = item1
b = item2
steps.times do |i|
a.move_before(b)
a.save!
a, b = b, a
end
end
it 'can leap-frog STEPS - 1 times before needing to rebalance' do
# This is less efficient than going right, due to the flooring of
# integer division
expect { leap_frog(RelativePositioning::STEPS - 1) }
.not_to change { item3.reload.relative_position }
end
it 'rebalances after leap-frogging STEPS times' do
expect { leap_frog(RelativePositioning::STEPS) }
.to change { item3.reload.relative_position }
end
end
end end
describe '#move_after' do describe '#move_after' do
...@@ -434,17 +164,9 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -434,17 +164,9 @@ RSpec.shared_examples 'a class that supports relative positioning' do
let(:item3) { create(factory, default_params) } let(:item3) { create(factory, default_params) }
before do before do
item1.update!(relative_position: 1000) item1.update(relative_position: 1000)
item2.update!(relative_position: 1001) item2.update(relative_position: 1001)
item3.update!(relative_position: 1002) item3.update(relative_position: 1002)
end
it 'can move the item after an item at MAX_POSITION' do
item1.update!(relative_position: RelativePositioning::MAX_POSITION)
new_item.move_after(item1)
expect(new_item.relative_position).to be_valid_position
expect(new_item.relative_position).to be > item1.reset.relative_position
end end
it 'moves items correctly' do it 'moves items correctly' do
...@@ -453,53 +175,6 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -453,53 +175,6 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item1.relative_position).to be_between(item2.reload.relative_position, item3.reload.relative_position).exclusive expect(item1.relative_position).to be_between(item2.reload.relative_position, item3.reload.relative_position).exclusive
end end
end end
it 'can move the item after an item bunched up at MAX_POSITION' do
item1, item2, item3 = create_items_with_positions(run_at_end)
new_item.move_after(item1)
new_item.save!
items = [item1, new_item, item2, item3]
items.each do |item|
expect(item.reset.relative_position).to be_valid_position
end
expect(items.sort_by(&:relative_position)).to eq(items)
end
context 'leap-frogging' do
before do
start = RelativePositioning::START_POSITION
item1.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 0)
item2.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 1)
item3.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 2)
end
let(:item3) { create(factory, default_params) }
def leap_frog(steps)
a = item1
b = item2
steps.times do |i|
a.move_after(b)
a.save!
a, b = b, a
end
end
it 'can leap-frog STEPS times before needing to rebalance' do
expect { leap_frog(RelativePositioning::STEPS) }
.not_to change { item3.reload.relative_position }
end
it 'rebalances after leap-frogging STEPS+1 times' do
expect { leap_frog(RelativePositioning::STEPS + 1) }
.to change { item3.reload.relative_position }
end
end
end end
describe '#move_to_end' do describe '#move_to_end' do
...@@ -518,17 +193,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -518,17 +193,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '#move_between' do describe '#move_between' do
before do before do
[item1, item2].each do |item| [item1, item2].each do |item1|
item.move_to_end && item.save! item1.move_to_end && item1.save
end
end
shared_examples 'moves item between' do
it 'moves the middle item to between left and right' do
expect do
middle.move_between(left, right)
middle.save!
end.to change { between_exclusive?(left, middle, right) }.from(false).to(true)
end end
end end
...@@ -552,26 +218,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -552,26 +218,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end end
it 'positions items even when after and before positions are the same' do it 'positions items even when after and before positions are the same' do
item2.update! relative_position: item1.relative_position item2.update relative_position: item1.relative_position
new_item.move_between(item1, item2) new_item.move_between(item1, item2)
[item1, item2].each(&:reset)
expect(new_item.relative_position).to be > item1.relative_position expect(new_item.relative_position).to be > item1.relative_position
expect(item1.relative_position).to be < item2.relative_position expect(item1.relative_position).to be < item2.relative_position
end end
context 'the two items are next to each other' do it 'positions items between other two if distance is 1' do
let(:left) { item1 } item2.update relative_position: item1.relative_position + 1
let(:middle) { new_item }
let(:right) { create_item(relative_position: item1.relative_position + 1) } new_item.move_between(item1, item2)
it_behaves_like 'moves item between' expect(new_item.relative_position).to be > item1.relative_position
expect(item1.relative_position).to be < item2.relative_position
end end
it 'positions item in the middle of other two if distance is big enough' do it 'positions item in the middle of other two if distance is big enough' do
item1.update! relative_position: 6000 item1.update relative_position: 6000
item2.update! relative_position: 10000 item2.update relative_position: 10000
new_item.move_between(item1, item2) new_item.move_between(item1, item2)
...@@ -579,8 +245,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -579,8 +245,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end end
it 'positions item closer to the middle if we are at the very top' do it 'positions item closer to the middle if we are at the very top' do
item1.update!(relative_position: 6001) item2.update relative_position: 6000
item2.update!(relative_position: 6000)
new_item.move_between(nil, item2) new_item.move_between(nil, item2)
...@@ -588,53 +253,51 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -588,53 +253,51 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end end
it 'positions item closer to the middle if we are at the very bottom' do it 'positions item closer to the middle if we are at the very bottom' do
new_item.update!(relative_position: 1) new_item.update relative_position: 1
item1.update!(relative_position: 6000) item1.update relative_position: 6000
item2.update!(relative_position: 5999) item2.destroy
new_item.move_between(item1, nil) new_item.move_between(item1, nil)
expect(new_item.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE) expect(new_item.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
end end
it 'positions item in the middle of other two' do it 'positions item in the middle of other two if distance is not big enough' do
item1.update! relative_position: 100 item1.update relative_position: 100
item2.update! relative_position: 400 item2.update relative_position: 400
new_item.move_between(item1, item2) new_item.move_between(item1, item2)
expect(new_item.relative_position).to eq(250) expect(new_item.relative_position).to eq(250)
end end
context 'there is no space' do it 'positions item in the middle of other two is there is no place' do
let(:middle) { new_item } item1.update relative_position: 100
let(:left) { create_item(relative_position: 100) } item2.update relative_position: 101
let(:right) { create_item(relative_position: 101) }
it_behaves_like 'moves item between' new_item.move_between(item1, item2)
end
context 'there is a bunch of items' do expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position).exclusive
let(:items) { create_items_with_positions(100..104) } end
let(:left) { items[1] }
let(:middle) { items[3] }
let(:right) { items[2] }
it_behaves_like 'moves item between' it 'uses rebalancing if there is no place' do
item1.update relative_position: 100
item2.update relative_position: 101
item3 = create_item(relative_position: 102)
new_item.update relative_position: 103
it 'handles bunches correctly' do new_item.move_between(item2, item3)
middle.move_between(left, right) new_item.save!
middle.save!
expect(items.first.reset.relative_position).to be < middle.relative_position expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position).exclusive
end expect(item1.reload.relative_position).not_to eq(100)
end end
it 'positions item right if we pass non-sequential parameters' do it 'positions item right if we pass none-sequential parameters' do
item1.update! relative_position: 99 item1.update relative_position: 99
item2.update! relative_position: 101 item2.update relative_position: 101
item3 = create_item(relative_position: 102) item3 = create_item(relative_position: 102)
new_item.update! relative_position: 103 new_item.update relative_position: 103
new_item.move_between(item1, item3) new_item.move_between(item1, item3)
new_item.save! new_item.save!
...@@ -666,12 +329,6 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -666,12 +329,6 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(positions).to eq([90, 95, 96, 102]) expect(positions).to eq([90, 95, 96, 102])
end end
it 'raises an error if there is no space' do
items = create_items_with_positions(run_at_start)
expect { items.last.move_sequence_before }.to raise_error(RelativePositioning::NoSpaceLeft)
end
it 'finds a gap if there are unused positions' do it 'finds a gap if there are unused positions' do
items = create_items_with_positions([100, 101, 102]) items = create_items_with_positions([100, 101, 102])
...@@ -679,8 +336,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -679,8 +336,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
items.last.save! items.last.save!
positions = items.map { |item| item.reload.relative_position } positions = items.map { |item| item.reload.relative_position }
expect(positions).to eq([50, 51, 102])
expect(positions.last - positions.second).to be > RelativePositioning::MIN_GAP
end end
end end
...@@ -702,33 +358,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do ...@@ -702,33 +358,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
items.first.save! items.first.save!
positions = items.map { |item| item.reload.relative_position } positions = items.map { |item| item.reload.relative_position }
expect(positions.second - positions.first).to be > RelativePositioning::MIN_GAP expect(positions).to eq([100, 601, 602])
end end
it 'raises an error if there is no space' do
items = create_items_with_positions(run_at_end)
expect { items.first.move_sequence_after }.to raise_error(RelativePositioning::NoSpaceLeft)
end
end
def be_valid_position
be_between(RelativePositioning::MIN_POSITION, RelativePositioning::MAX_POSITION)
end
def between_exclusive?(left, middle, right)
a, b, c = [left, middle, right].map { |item| item.reset.relative_position }
return false if a.nil? || b.nil?
return a < b if c.nil?
a < b && b < c
end
def run_at_end(size = 3)
(RelativePositioning::MAX_POSITION - size)..RelativePositioning::MAX_POSITION
end
def run_at_start(size = 3)
(RelativePositioning::MIN_POSITION..).take(size)
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