diff --git a/app/models/category.rb b/app/models/category.rb index c2ba44569..5cf290eac 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -1,4 +1,9 @@ +require_dependency "concern/positionable" + class Category < ActiveRecord::Base + + include Concern::Positionable + belongs_to :topic, dependent: :destroy if rails4? belongs_to :topic_only_relative_url, diff --git a/db/migrate/20131018050738_add_position_to_categories.rb b/db/migrate/20131018050738_add_position_to_categories.rb new file mode 100644 index 000000000..65e90a1d1 --- /dev/null +++ b/db/migrate/20131018050738_add_position_to_categories.rb @@ -0,0 +1,11 @@ +class AddPositionToCategories < ActiveRecord::Migration + def up + add_column :categories, :position, :integer + execute "UPDATE categories SET position = id" + change_column :categories, :position, :integer, null: false + end + + def down + remove_column :categories, :position + end +end diff --git a/lib/concern/positionable.rb b/lib/concern/positionable.rb new file mode 100644 index 000000000..6a7c69060 --- /dev/null +++ b/lib/concern/positionable.rb @@ -0,0 +1,31 @@ +module Concern + module Positionable + extend ActiveSupport::Concern + + included do + before_save do + self.position ||= self.class.count + end + end + + def move_to(position) + self.exec_sql " + UPDATE #{self.class.table_name} + SET position = :position + WHERE id = :id", {id: id, position: position} + + self.exec_sql " + UPDATE #{self.class.table_name} t + SET position = x.position - 1 + FROM ( + SELECT i.id, row_number() + OVER(ORDER BY i.position asc, + CASE WHEN i.id = :id THEN 0 ELSE 1 END ASC) AS position + FROM #{self.class.table_name} i + WHERE i.position IS NOT NULL + ) x + WHERE x.id = t.id AND t.position <> x.position - 1 + ", {id: id} + end + end +end diff --git a/spec/components/concern/positionable_spec.rb b/spec/components/concern/positionable_spec.rb new file mode 100644 index 000000000..ec8d22757 --- /dev/null +++ b/spec/components/concern/positionable_spec.rb @@ -0,0 +1,52 @@ +require "spec_helper" +require_dependency "concern/positionable" + +describe Concern::Positionable do + + def positions + TestItem.order('position asc, id asc').pluck(:id) + end + + context "move_to" do + before do + class TestItem < ActiveRecord::Base + include Concern::Positionable + end + + Topic.exec_sql("create temporary table test_items(id int primary key, position int)") + end + + after do + Topic.exec_sql("drop table test_items") + + # import is making my life hard, we need to nuke this out of orbit + des = ActiveSupport::DescendantsTracker.class_variable_get :@@direct_descendants + des[ActiveRecord::Base].delete(TestItem) + Object.send(:remove_const, :TestItem) + end + + it "can position stuff correctly" do + 5.times do |i| + Topic.exec_sql("insert into test_items(id,position) values(#{i}, #{i})") + end + + positions.should == [0,1,2,3,4] + TestItem.find(3).move_to(0) + positions.should == [3,0,1,2,4] + TestItem.pluck(:position).sort.should == [0,1,2,3,4] + + + # this is somewhat odd, but when there is not positioning + # not much we can do + TestItem.find(1).move_to(5) + positions.should == [3,0,2,4,1] + + TestItem.pluck(:position).sort.should == [0,1,2,3,4] + + item = TestItem.new + item.id = 7 + item.save + item.position.should == 5 + end + end +end