Coder Social home page Coder Social logo

acts_as_list's Introduction

Acts As List

Build Status

Build Status Gem Version

ANNOUNCING: Positioning, the gem

As maintainer of both Acts As List and the Ranked Model gems, I've become intimately aquainted with the strengths and weaknesses of each. I ended up writing a small scale Rails Concern for positioning database rows for a recent project and it worked really well so I've decided to release it as a gem: Positioning

Positioning works similarly to Acts As List in that it maintains a sequential list of integer values as positions. It differs in that it encourages a unique constraints on the position column and supports multiple lists per database table. It borrows Ranked Model's concept of relative positioning. I encourage you to check it out and give it a whirl on your project!

Description

This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a position column defined as an integer on the mapped database table.

0.8.0 Upgrade Notes

There are a couple of changes of behaviour from 0.8.0 onwards:

  • If you specify add_new_at: :top, new items will be added to the top of the list like always. But now, if you specify a position at insert time: .create(position: 3), the position will be respected. In this example, the item will end up at position 3 and will move other items further down the list. Before 0.8.0 the position would be ignored and the item would still be added to the top of the list. #220
  • acts_as_list now copes with disparate position integers (i.e. gaps between the numbers). There has been a change in behaviour for the higher_items method. It now returns items with the first item in the collection being the closest item to the reference item, and the last item in the collection being the furthest from the reference item (a.k.a. the first item in the list). #223

Installation

In your Gemfile:

gem 'acts_as_list'

Or, from the command line:

gem install acts_as_list

Example

At first, you need to add a position column to desired table:

rails g migration AddPositionToTodoItem position:integer
rake db:migrate

After that you can use acts_as_list method in the model:

class TodoList < ActiveRecord::Base
  has_many :todo_items, -> { order(position: :asc) }
end

class TodoItem < ActiveRecord::Base
  belongs_to :todo_list
  acts_as_list scope: :todo_list
end

todo_list = TodoList.find(...)
todo_list.todo_items.first.move_to_bottom
todo_list.todo_items.last.move_higher

Instance Methods Added To ActiveRecord Models

You'll have a number of methods added to each instance of the ActiveRecord model that to which acts_as_list is added.

In acts_as_list, "higher" means further up the list (a lower position), and "lower" means further down the list (a higher position). That can be confusing, so it might make sense to add tests that validate that you're using the right method given your context.

Methods That Change Position and Reorder List

  • list_item.insert_at(2)
  • list_item.move_lower will do nothing if the item is the lowest item
  • list_item.move_higher will do nothing if the item is the highest item
  • list_item.move_to_bottom
  • list_item.move_to_top
  • list_item.remove_from_list

Methods That Change Position Without Reordering List

  • list_item.increment_position
  • list_item.decrement_position
  • list_item.set_list_position(3)

Methods That Return Attributes of the Item's List Position

  • list_item.first?
  • list_item.last?
  • list_item.in_list?
  • list_item.not_in_list?
  • list_item.default_position?
  • list_item.higher_item
  • list_item.higher_items will return all the items above list_item in the list (ordered by the position, ascending)
  • list_item.lower_item
  • list_item.lower_items will return all the items below list_item in the list (ordered by the position, ascending)

Adding acts_as_list To An Existing Model

As it stands acts_as_list requires position values to be set on the model before the instance methods above will work. Adding something like the below to your migration will set the default position. Change the parameters to order if you want a different initial ordering.

class AddPositionToTodoItem < ActiveRecord::Migration
  def change
    add_column :todo_items, :position, :integer
    TodoItem.order(:updated_at).each.with_index(1) do |todo_item, index|
      todo_item.update_column :position, index
    end
  end
end

If you are using the scope option things can get a bit more complicated. Let's say you have acts_as_list scope: :todo_list, you might instead need something like this:

TodoList.all.each do |todo_list|
  todo_list.todo_items.order(:updated_at).each.with_index(1) do |todo_item, index|
    todo_item.update_column :position, index
  end
end

When using PostgreSQL, it is also possible to leave this migration up to the database layer. Inside of the change block you could write:

 execute <<~SQL.squish
   UPDATE todo_items
   SET position = mapping.new_position
   FROM (
     SELECT
       id,
       ROW_NUMBER() OVER (
         PARTITION BY todo_list_id
         ORDER BY updated_at
       ) AS new_position
     FROM todo_items
   ) AS mapping
   WHERE todo_items.id = mapping.id;
 SQL

Notes

All position queries (select, update, etc.) inside gem methods are executed without the default scope (i.e. Model.unscoped), this will prevent nasty issues when the default scope is different from acts_as_list scope.

The position column is set after validations are called, so you should not put a presence validation on the position column.

If you need a scope by a non-association field you should pass an array, containing field name, to a scope:

class TodoItem < ActiveRecord::Base
  # `task_category` is a plain text field (e.g. 'work', 'shopping', 'meeting'), not an association
  acts_as_list scope: [:task_category]
end

You can also add multiple scopes in this fashion:

class TodoItem < ActiveRecord::Base
  belongs_to :todo_list
  acts_as_list scope: [:task_category, :todo_list_id]
end

Furthermore, you can optionally include a hash of fixed parameters that will be included in all queries:

class TodoItem < ActiveRecord::Base
  belongs_to :todo_list
  # or `discarded_at` if using discard
  acts_as_list scope: [:task_category, :todo_list_id, deleted_at: nil]
end

This is useful when using this gem in conjunction with the popular acts_as_paranoid or discard gems.

More Options

  • column default: position. Use this option if the column name in your database is different from position.
  • top_of_list default: 1. Use this option to define the top of the list. Use 0 to make the collection act more like an array in its indexing.
  • add_new_at default: :bottom. Use this option to specify whether objects get added to the :top or :bottom of the list. nil will result in new items not being added to the list on create, i.e, position will be kept nil after create.
  • touch_on_update default: true. Use touch_on_update: false if you don't want to update the timestamps of the associated records.
  • sequential_updates Specifies whether insert_at should update objects positions during shuffling one by one to respect position column unique not null constraint. Defaults to true if position column has unique index, otherwise false. If constraint is deferrable initially deferred (PostgreSQL), overriding it with false will speed up insert_at.

Disabling temporarily

If you need to temporarily disable acts_as_list during specific operations such as mass-update or imports:

TodoItem.acts_as_list_no_update do
  perform_mass_update
end

In an acts_as_list_no_update block, all callbacks are disabled, and positions are not updated. New records will be created with the default value from the database. It is your responsibility to correctly manage positions values.

You can also pass an array of classes as an argument to disable database updates on just those classes. It can be any ActiveRecord class that has acts_as_list enabled.

class TodoList < ActiveRecord::Base
  has_many :todo_items, -> { order(position: :asc) }
  acts_as_list
end

class TodoItem < ActiveRecord::Base
  belongs_to :todo_list
  has_many :todo_attachments, -> { order(position: :asc) }

  acts_as_list scope: :todo_list
end

class TodoAttachment < ActiveRecord::Base
  belongs_to :todo_item
  acts_as_list scope: :todo_item
end

TodoItem.acts_as_list_no_update([TodoAttachment]) do
  TodoItem.find(10).update(position: 2)
  TodoAttachment.find(10).update(position: 1)
  TodoAttachment.find(11).update(position: 2)
  TodoList.find(2).update(position: 3) # For this instance the callbacks will be called because we haven't passed the class as an argument
end

Troubleshooting Database Deadlock Errors

When using this gem in an app with a high amount of concurrency, you may see "deadlock" errors raised by your database server. It's difficult for the gem to provide a solution that fits every app. Here are some steps you can take to mitigate and handle these kinds of errors.

1) Use the Most Concise API

One easy way to reduce deadlocks is to use the most concise gem API available for what you want to accomplish. In this specific example, the more concise API for creating a list item at a position results in one transaction instead of two, and it issues fewer SQL statements. Issuing fewer statements tends to lead to faster transactions. Faster transactions are less likely to deadlock.

Example:

# Good
TodoItem.create(todo_list: todo_list, position: 1)

# Bad
item = TodoItem.create(todo_list: todo_list)
item.insert_at(1)

2) Rescue then Retry

Deadlocks are always a possibility when updating tables rows concurrently. The general advice from MySQL documentation is to catch these errors and simply retry the transaction; it will probably succeed on another attempt. (see How to Minimize and Handle Deadlocks) Retrying transactions sounds simple, but there are many details that need to be chosen on a per-app basis: How many retry attempts should be made? Should there be a wait time between attempts? What other statements were in the transaction that got rolled back?

Here a simple example of rescuing from deadlock and retrying the operation:

  • ActiveRecord::Deadlocked is available in Rails >= 5.1.0.
  • If you have Rails < 5.1.0, you will need to rescue ActiveRecord::StatementInvalid and check #cause.
  attempts_left = 2
  while attempts_left > 0
    attempts_left -= 1
    begin
      TodoItem.transaction do
        TodoItem.create(todo_list: todo_list, position: 1)
      end
      attempts_left = 0
    rescue ActiveRecord::Deadlocked
      raise unless attempts_left > 0
    end
  end

You can also use the approach suggested in this StackOverflow post: https://stackoverflow.com/questions/4027659/activerecord3-deadlock-retry

3) Lock Parent Record

In addition to reacting to deadlocks, it is possible to reduce their frequency with more pessimistic locking. This approach uses the parent record as a mutex for the entire list. This kind of locking is very effective at reducing the frequency of deadlocks while updating list items. However, there are some things to keep in mind:

  • This locking pattern needs to be used around every call that modifies the list; even if it does not reorder list items.
  • This locking pattern effectively serializes operations on the list. The throughput of operations on the list will decrease.
  • Locking the parent record may lead to deadlock elsewhere if some other code also locks the parent table.

Example:

todo_list = TodoList.create(name: "The List")
todo_list.with_lock do
  item = TodoItem.create(description: "Buy Groceries", todo_list: todo_list, position: 1)
end

Versions

Version 0.9.0 adds acts_as_list_no_update (#244) and compatibility with not-null and uniqueness constraints on the database (#246). These additions shouldn't break compatibility with existing implementations.

As of version 0.7.5 Rails 5 is supported.

All versions 0.1.5 onwards require Rails 3.0.x and higher.

A note about data integrity

We often hear complaints that position values are repeated, incorrect etc. For example, #254. To ensure data integrity, you should rely on your database. There are two things you can do:

  1. Use constraints. If you model Item that belongs_to an Order, and it has a position column, then add a unique constraint on items with [:order_id, :position]. Think of it as a list invariant. What are the properties of your list that don't change no matter how many items you have in it? One such propery is that each item has a distinct position. Another could be that position is always greater than 0. It is strongly recommended that you rely on your database to enforce these invariants or constraints. Here are the docs for PostgreSQL and MySQL.
  2. Use mutexes or row level locks. At its heart the duplicate problem is that of handling concurrency. Adding a contention resolution mechanism like locks will solve it to some extent. But it is not a solution or replacement for constraints. Locks are also prone to deadlocks.

As a library, acts_as_list may not always have all the context needed to apply these tools. They are much better suited at the application level.

Roadmap

  1. Sort based feature

Contributing to acts_as_list

  • Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
  • Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
  • Fork the project
  • Start a feature/bugfix branch
  • Commit and push until you are happy with your contribution
  • Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
  • I would recommend using Rails 3.1.x and higher for testing the build before a pull request. The current test harness does not quite work with 3.0.x. The plugin itself works, but the issue lies with testing infrastructure.

Copyright

Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license

acts_as_list's People

Contributors

aepstein avatar albin-willman avatar botandrose-machine avatar brendon avatar chaffeqa avatar chrisortman avatar conzett avatar danielross avatar dependabot[bot] avatar dubroe avatar fabn avatar forrest avatar greatghoul avatar jeremy avatar jpalumickas avatar krzysiek1507 avatar ledestin avatar miks avatar nzkoz avatar petergoldstein avatar philippfranke avatar ramontayag avatar randoum avatar rdvdijk avatar sanemat avatar soffes avatar swanandp avatar swelther avatar tibastral avatar tvdeyen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

acts_as_list's Issues

has_many :through with acts as list

I want to use your acts as list gem with these models :

module ActiveRoad
  class PhysicalRoad < ActiveRoad::Base
    has_many :physical_road_junctions
    has_many :junctions, :through => :physical_road_junctions, :dependent => :destroy, :uniq => true
  end
end
module ActiveRoad
  class PhysicalRoadJunction < ActiveRoad::Base
    attr_accessible :position  

    belongs_to :physical_road
    belongs_to :junction

    acts_as_list :scope => :physical_road
  end
end
module ActiveRoad
  class Junction < ActiveRoad::Base
    has_many :physical_road_junctions
    has_many :physical_roads, :through => :physical_road_junctions
  end
end

But when I use the scope of acts_as_list an exception is raised :

ActiveRoad::Junction junction connected to physical roads #physical_roads should be addable
     Failure/Error: subject.physical_roads << new_road
     NameError:
       uninitialized constant ActiveRecord::NamedScope
     # (eval):25:in `scope_condition'
     # ./spec/models/active_road/junction_spec.rb:18:in `block (4 levels) in <top (required)>'

Should we use this gem in a special way with has_many :through relations?

Best Regards
Luc Donnet

insert_at creates gaps

I am using version 0.3.0 and want this feature.

Given

ranking = Ranking.create(name: 'Dummy')
ranking.items.create(ranking_id: ranking.id, name: 'Thing 1')
ranking.items.create(ranking_id: ranking.id, name: 'Thing 2')
ranking.items.create(ranking_id: ranking.id, name: 'Thing 3')

When

item = ranking.items.new(name: 'Thing 2.0')
item.insert_at(2)

Then

ranking.items.order('position ASC').all.map{|i| [i.position, i.thing.name]}.should == [
  [1, "Thing 1"], [2, "Thing 2.0"], [3, "Thing 2"], [4, "Thing 3"]
]

But I got

ranking.items.order('position ASC').all.map{|i| [i.position, i.thing.name]}.should == [
  [1, "Thing 1"], [2, "Thing 2.0"], [4, "Thing 2"], [5, "Thing 3"]
]

Graph like behaviour

I can store a relation that is a graph in fact. Is it expected? I think it should throw an exception when there is a recycling path (For example A->B->A->B....). Here is the test (please note that this test passes; however i am expecting it to fail):

category = Category.new
category.name="Parent"
category.save
child = category.children.create("name" => "child_1")
child.children = [category]
assert category.save
loadedChild = Category.find_by_name("child_1")
assert_equal "Parent", loadedChild.parent.name
assert_equal "Parent", loadedChild.children[0].name

move_higher/move_lower vs move_to_top/move_to_bottom act differently when item is already at top or bottom

If trying to move an item higher when it is already at the top of list (via move_higher), the return is nil (because it checks for higher_item). But a move_to_top method only checks if the item is in_list?.

(move_lower and move_to_bottom act the same way)

Shouldn't these two groups of methods either both return nil or both be successful? Either would make sense, but the inconsistency doesn't make sense to me.

add info about v0.1.5 require Rails 3

I tried upgrading to 0.1.5 in my 2.3 app and received a NoMethodError 'unscoped' error.

unscoped was introduced in Rails 3.

v0.1.4 list.rb line 213

acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")

v0.1.5 list.rb line 225

acts_as_list_class.unscoped.find(:first, :conditions => conditions, :order => "#{position_column} DESC")

It should probably be noted in the README that 0.1.5 require Rails 3 and up.

(e)

ActiveRecord dependency causes rake assets:compile to fail without access to a database

When I include the gem acts_as_list in my Gemfile, intentionally change my connection string for PostgreSQL database to an invalid DB, and run rake assets:precompile, I receive an ActiveRecord error. It appears that acts_as_list tries to connect to the database at all times, even during asset compilation.

If you refer to the Heroku post at https://devcenter.heroku.com/articles/rails-asset-pipeline, you will see that this problem unfortunately makes acts_as_list incompatible with Heroku.

I have worked around the problem by doing something along the lines of:

gem 'acts_as_list', require: false

And I have an initializer which does this:

require 'acts_as_list' unless Rails.groups.include?('assets')

Here is the error log which unfortunately does not clearly show where the problem lies:

$ rake assets:precompile
/Users/matt/.rvm/rubies/ruby-1.9.3-p286/bin/ruby /Users/matt/.rvm/gems/ruby-1.9.3-p286/bin/rake assets:precompile:all RAILS_ENV=production RAILS_GROUPS=assets
rake aborted!
FATAL:  database "mysite_deploymenta" does not exist
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/postgresql_adapter.rb:1208:in `initialize'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/postgresql_adapter.rb:1208:in `new'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/postgresql_adapter.rb:1208:in `connect'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/postgresql_adapter.rb:326:in `initialize'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/postgresql_adapter.rb:28:in `new'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/postgresql_adapter.rb:28:in `postgresql_connection'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:315:in `new_connection'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:325:in `checkout_new_connection'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:247:in `block (2 levels) in checkout'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:242:in `loop'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:242:in `block in checkout'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:239:in `checkout'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:102:in `block in connection'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:101:in `connection'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_pool.rb:410:in `retrieve_connection'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_specification.rb:171:in `retrieve_connection'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/connection_adapters/abstract/connection_specification.rb:145:in `connection'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/model_schema.rb:308:in `clear_cache!'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activerecord-3.2.12/lib/active_record/railtie.rb:97:in `block (2 levels) in <class:Railtie>'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activesupport-3.2.12/lib/active_support/callbacks.rb:418:in `_run__3455161023029107272__prepare__737315814947044187__callbacks'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activesupport-3.2.12/lib/active_support/callbacks.rb:405:in `__run_callback'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activesupport-3.2.12/lib/active_support/callbacks.rb:385:in `_run_prepare_callbacks'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activesupport-3.2.12/lib/active_support/callbacks.rb:81:in `run_callbacks'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/actionpack-3.2.12/lib/action_dispatch/middleware/reloader.rb:74:in `prepare!'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/actionpack-3.2.12/lib/action_dispatch/middleware/reloader.rb:48:in `prepare!'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/application/finisher.rb:47:in `block in <module:Finisher>'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/initializable.rb:30:in `instance_exec'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/initializable.rb:30:in `run'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/initializable.rb:55:in `block in run_initializers'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/initializable.rb:54:in `each'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/initializable.rb:54:in `run_initializers'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/application.rb:136:in `initialize!'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/railtie/configurable.rb:30:in `method_missing'
/Users/matt/Dropbox/Projects/easybacklog/easybacklog/config/environment.rb:5:in `<top (required)>'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activesupport-3.2.12/lib/active_support/dependencies.rb:251:in `require'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activesupport-3.2.12/lib/active_support/dependencies.rb:251:in `block in require'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activesupport-3.2.12/lib/active_support/dependencies.rb:236:in `load_dependency'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/activesupport-3.2.12/lib/active_support/dependencies.rb:251:in `require'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/application.rb:103:in `require_environment!'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/railties-3.2.12/lib/rails/application.rb:297:in `block (2 levels) in initialize_tasks'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:246:in `call'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:246:in `block in execute'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:241:in `each'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:241:in `execute'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:184:in `block in invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:177:in `invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:170:in `invoke'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/actionpack-3.2.12/lib/sprockets/assets.rake:93:in `block (2 levels) in <top (required)>'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:246:in `call'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:246:in `block in execute'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:241:in `each'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:241:in `execute'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:184:in `block in invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:177:in `invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:205:in `block in invoke_prerequisites'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:203:in `each'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:203:in `invoke_prerequisites'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:183:in `block in invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:177:in `invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:170:in `invoke'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/actionpack-3.2.12/lib/sprockets/assets.rake:60:in `block (3 levels) in <top (required)>'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:246:in `call'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:246:in `block in execute'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:241:in `each'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:241:in `execute'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:184:in `block in invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:177:in `invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:170:in `invoke'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:143:in `invoke_task'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:101:in `block (2 levels) in top_level'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:101:in `each'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:101:in `block in top_level'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:110:in `run_with_threads'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:95:in `top_level'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:73:in `block in run'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:160:in `standard_exception_handling'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:70:in `run'
Tasks: TOP => environment
(See full trace by running task with --trace)
rake aborted!
Command failed with status (1): [/Users/matt/.rvm/rubies/ruby-1.9.3-p286/bi...]
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/file_utils.rb:53:in `block in create_shell_runner'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/file_utils.rb:45:in `call'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/file_utils.rb:45:in `sh'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/file_utils_ext.rb:37:in `sh'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/file_utils.rb:80:in `ruby'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/file_utils_ext.rb:37:in `ruby'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/actionpack-3.2.12/lib/sprockets/assets.rake:12:in `ruby_rake_task'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/actionpack-3.2.12/lib/sprockets/assets.rake:21:in `invoke_or_reboot_rake_task'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/actionpack-3.2.12/lib/sprockets/assets.rake:29:in `block (2 levels) in <top (required)>'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:246:in `call'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:246:in `block in execute'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:241:in `each'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:241:in `execute'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:184:in `block in invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:177:in `invoke_with_call_chain'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/task.rb:170:in `invoke'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:143:in `invoke_task'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:101:in `block (2 levels) in top_level'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:101:in `each'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:101:in `block in top_level'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:110:in `run_with_threads'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:95:in `top_level'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:73:in `block in run'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:160:in `standard_exception_handling'
/Users/matt/.rvm/gems/ruby-1.9.3-p286/gems/rake-10.0.4/lib/rake/application.rb:70:in `run'
Tasks: TOP => assets:precompile
(See full trace by running task with --trace)

Feature: sort

I would love to see a sort function in acts_as_list. It would rewrite all positions according to the returns of a sort block, without - of course - triggering any duplicate key issues.

Remove use of update_attribute

Calling move_to_bottom causes the following warning:

DEPRECATION WARNING: update_attribute is deprecated and will be removed in Rails 4. If you want to skip mass-assignment protection, callbacks, and modifying updated_at, use update_column. If you do want those things, use update_attributes.

Updated Gem?

As the title really - is there going to be an updated acts_as_list gem with the recent changes?

Thanks
Luke

decrement_positions_on_lower_items method

I found an odd behaviour when destroing a list item. when it's invoked decrement_positions_on_lower_items method al the lower items' position are set to deleted item's position. for example:

[ < Item id:1, position: 1 >, < Item id:2, position: 2 >, < Item id:3, position: 3 >, < Item id:4, position: 4 > ]

if I delete the item with id = 2 the resulting list is:

[ < Item id:1, position: 1 >, < Item id:3, position: 2 >, < Item id:4, position: 2 > ]

instead of:

[ < Item id:1, position: 1 >, < Item id:3, position: 2 >, < Item id:4, position: 3 > ]

I resolved overriding decrement_positions_on_lower_items method:

def decrement_positions_on_lower_items
return unless in_list?
acts_as_list_class.each do |l|
l.update(
"#{position_column} = (#{position_column} - 1)",
"#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
)
end
end

eval mistakenly resolved the module path

Hi

I found acts_as_list doesn't work well with enumerize gem 'https://github.com/twinslash/enumerize' because eval resolve incorrect module path.

In "lib/acts_as_list/active_record/acts/list.rb"

            include ActiveRecord::Acts::List::InstanceMethods

The module path can not resolved correctly in eval in some situations.
Could you just fix it like below?

            include ::ActiveRecord::Acts::List::InstanceMethods

position: 0 now makes model pushed to top?

After update i got weird issues with sorting.
After investigation i have found that new items got pushed to top, and not to bottom as described...
So model with position: 0 results into position: 1.
Is this new behaviour, or i have some issues in my code? Maybe with scope(i have scope on multiple fields) but how it relates to this?

insert_at fails on postgresql w/ non-null constraint on postion_column

Switched from mysql 5.1.49 to postgresql 8.3.12 using gem 'pg', '0.11.0'.

Our model acts_as_list, and our databases (both Mysql and PostgreSQL) have non-null constraints on the position column. Calling insert_at(1) on a model instance (that was at position 5, e.g.) works fine in Mysql, but in PostgreSQL, it caused error:

ActiveRecord::StatementInvalid: PGError: ERROR: null value in column "position" violates not-null constraint

I'm posting a pull-request with the fix.

Performance Improvements

Acts_as_list hits the database a lot when moving items. I like the concept used by https://github.com/mixonic/ranked-model :

When an item is given a new position, it assigns itself a rank number between two neighbors. This allows several movements of items before no digits are available between two neighbors. When this occurs, ranked-model will try to shift other records out of the way.

What do you think? Should we change it?

has_many :through or has_many_and_belongs_to_many support

Hello,

I have not been able to work out how to make this work with a has_many :through association.

The easiest example I can think of is Playlists have many Songs and Songs belong to many Playlists. The position would have to be added to the intermediary table, because the position can be different in different playlists.

Does this gem support those associations?

acts_as_list :scope => "doesnt_seem_to_work"

I'm trying to use acts_as_list in combination with has_ancestry, but when I apply a scope it returns this:

Code: acts_as_list :scope => "ancestry" #string column

Returns:

ActiveRecord::StatementInvalid in FaqsController#create

PGError: ERROR: argument of WHERE must be type boolean, not type character varying
LINE 1: SELECT "faqs".* FROM "faqs" WHERE (ancestry) ORDER BY posit...
^
: SELECT "faqs".* FROM "faqs" WHERE (ancestry) ORDER BY position DESC LIMIT 1

Duplicated positions when creating parent and children from scratch in 0.1.5

I have a problem after upgrade to 0.1.5.

class TodoList < ActiveRecord::Base
   has_many :todo_items, :order => "position"
end

class TodoItem < ActiveRecord::Base
   belongs_to :todo_list
   acts_as_list :scope => :todo_list
end

list = TodoList.new
3.times { list.todo_items.build }
list.save

Creating the parent and the children from scratch generate odd values for positions (sometimes the same repeated value). It used to work fine with 0.1.4 (I'm using rails 3.0.10)

As far I checked it was broken after move the move_to_bottom callback to a before_validation callback, it worked with 0.1.4 because move_to_bottom was a before_create callback.

Mysql2 error

Hi again, I'm getting a errors when I go to create a new item or when updating an item.

I have 8 items, their order starts with 0 and goes to 7. When I go to create a new item and hit create, I actually get a routing error:

No route matches {:controller=>"admin/featured_items", :id=>nil, :action=>"edit"}

When I go to update an item, this comes up when I hit Update:

Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order = 8)' at line 1: SELECT COUNT(*) FROM `featured_items`  WHERE (1 = 1 AND order = 8)

I'm running rails 3.2.8. Have you seen this before?

The item does get does get created. It just gives an error.

Creating an item with a nil scope should not add it to the list

Thank for this great gem.
I have a quesiton/issue related to default scope. Thanks for any help.

class NewContent < ActiveRecord::Base
  acts_as_list add_new_at: :top, scope: :collection
end

n = NewContent.create
puts n.position 
   => 1

But collection_id is null.

This is creates a huge performance problem because every time I create a NewContent it is going to add to the list of item with collection_id set to null.

Cannot update to version 0.1.7

In a rails 3.2.7 app running on ruby 1.9.3p194, I have gem 'acts_as_list', git: 'git://github.com/swanandp/acts_as_list.git' in the Gemfile.

Running bundle outdated tells me:

  Outdated gems included in the bundle:
    * acts_as_list (0.1.7 > 0.1.6 caa41b9)
    ...snip...

Running bundle update does not update the version to 0.1.7. To be thorough, I have performed these steps:

  • removed acts_as_list from my Gemfile
  • run bundle install
  • run gem uninstall acts_as_list
  • added gem 'acts_as_list', git: 'git://github.com/swanandp/acts_as_list.git' back into the Gemfile
  • re-run bundle install

I continue to get 0.1.6 version and the warning that it is outdated.

Batch setting of position

I have a issue when trying to batch assign the position of elements in a list, and then when the items are saved, the order gets duplicated in the middle of the list.

I have a category model, a product model, and a category_product_mappings model. Acts as list is on the mapping model to manage the order of the products in the category.

The category model has:
accepts_nested_attributes_for :category_product_mappings, allow_destroy: true

In the edit view for the category, the user can change the order of the products. We use js on the client to renumber the products on the moves, updating the hidden position field for the mappings fields.

When we submit the edit form, the category.update_attributes will update all the mapping objects correctly, and then save them. When the save occurs, acts_as_list will trigger it's own resequencing due to the callback:

after_update :update_positions

This will throw off the ordering as shown below
Original Order:
Prod1: 0
Prod2: 1
Prod3: 2
Prod4: 3
Prod5: 4
Prod6: 5
Prod7: 6
Prod8: 7
Prod9: 8
Prod10: 9

after reversing the order of the products and saving the position is:

Prod10: 0
Prod9: 1
Prod8: 2
Prod7: 3
Prod6: 3
Prod5: 4
Prod4: 6
Prod3: 7
Prod2: 8
Prod1: 9

Is seems when we get to the middle of the list, the shuffle_positions_on_intermediate_items call doesn't like what I have done with the position elements by setting them directly through accepts_nested_attributes.

Any thoughts on how I can get around this?

Problem when inserting straight at top of list

Hi all,

Let's define

>>> p_1 = Item.find(1)
>>> p_2 = Item.find(2)

Say I do

>>> p_1.insert_at(1)
>>> p_2.insert_at(1)

Everything works as expected :

>>> p_1.reload.position
2
>>> p_2.position
1

p_2 replaces p_1 at the top of the list and p_1 gets pushed down.

However, if I start again with nil positions and do

>>> p_1.position = 1
>>> p_1.save
>>> p_2.position = 1
>>> p_2.save

Then I end up with

>>> p_1.reload.position
0
>>> p_2.reload.position
1

I think the problems stems from the update_positions method that is called after update.

The method does

old_position = send("#{position_column}_was").to_i

And nil.to_i yields 0.

Thoughts?

Use alternative column name?

is there a way to use this with a column that's not named "position"? I have tables that have already existed and I was hoping to be able to specify this per use. For instance, I have an "order" column instead.

Update position when scope changes

In update, position remains the same when scope changes. I think that a solution should be to trigger add_to_list_bottom but I can't think of any global way, especially in complex scope cases. When it is just a field I use something like this

class TodoItem < ActiveRecord::Base
    belongs_to :todo_list
    acts_as_list :scope => :todo_list
    before_update :change_position

   def change_position
     add_to_list_bottom if todo_list_id_changed?
   end
 end

Is there a better way?

Mass-assignment issue with 0.1.8

I upgraded from 0.1.7 to 0.1.8 and I'm not getting a mass-assignment error. With 0.1.7 I get

DEPRECATION WARNING: update_attribute is deprecated and will be removed in Rails 4. If you want to skip mass-assignment protection, callbacks, and modifying updated_at, use update_column. If you do want those things, use update_attributes. (called from move_to at /Users/samsoffes/Code/cheddarapp.com/app/models/task.rb:82)

with all tests green. Under 0.1.8 this goes away (yay! ) but now I get this:

ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: position
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activemodel-3.2.7/lib/active_model/mass_assignment_security/sanitizer.rb:48:in `process_removed_attributes'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activemodel-3.2.7/lib/active_model/mass_assignment_security/sanitizer.rb:20:in `debug_protected_attribute_removal'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activemodel-3.2.7/lib/active_model/mass_assignment_security/sanitizer.rb:12:in `sanitize'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activemodel-3.2.7/lib/active_model/mass_assignment_security.rb:230:in `sanitize_for_mass_assignment'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/attribute_assignment.rb:75:in `assign_attributes'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/persistence.rb:227:in `block in update_attributes!'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/transactions.rb:295:in `block in with_transaction_returning_status'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/connection_adapters/abstract/database_statements.rb:192:in `transaction'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/transactions.rb:208:in `transaction'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/transactions.rb:293:in `with_transaction_returning_status'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/persistence.rb:226:in `update_attributes!'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/acts_as_list-0.1.8/lib/acts_as_list/active_record/acts/list.rb:233:in `assume_bottom_position'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/acts_as_list-0.1.8/lib/acts_as_list/active_record/acts/list.rb:121:in `block in move_to_bottom'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/connection_adapters/abstract/database_statements.rb:192:in `transaction'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/activerecord-3.2.7/lib/active_record/transactions.rb:208:in `transaction'
    /Users/samsoffes/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/acts_as_list-0.1.8/lib/acts_as_list/active_record/acts/list.rb:119:in `move_to_bottom'
...

Duplicate positions and lost items

We're using acts_as_list in an Ajax UI to allow users to re-order items in a list by dragging and dropping items.

If the user drags and drops a few items in quick succession, this can lead to items being lost and duplicated in the list, e.g. "Item A, Item B, Item C" becomes "Item A, Item C, Item C".

It seems the gem doesn't cope well with overlapping operations, and there is no locking. There doesn't seem to be any behaviour to protect against two items having the same position value.

I have some ideas about how to approach this problem but I wanted to get some guidance first.

Any thoughts?

validates error

I don't know if it is an issue with acts_as_list or just my code but i ran into the problem that if i have a validates :uniqueness on the position column, an error would come up when creating a new list_item saying that the position was not unique. I removed the validation and my app works fine now but thought it might be worth mentioning.

Enhancement: Expose scope object

A acts_as_list object should expose its scope object/parent.

I'd love to patch and submit a pull request for this myself, but I'd like to discuss naming options, possible alternative solutions, and whether this is a sensible idea at all with you guys first.

Workaround

I #send a substring of the scope name to the object

object.send(object.scope_name[0..-3])

Why?

This would allow to create awesome nested paths without needing to know about the scope explicitly, such as this reusable navigation snippet:

link_to "Next Object", [:show, acts_as_list_object.scoped_to, acts_as_list_object.higher_item]

Naming

Here are some possible names, feel free to chime in on how you like them:

  • scoped_to
  • scope_object
  • scope_parent
  • parent imho ambiguous

Adding multiple creates strange ordering

I'm now onto an object (license) that has_many of a certain object (progress_point). When I go to add multiple progress points to a license, it gives me mixed results.

The first time I ran it, it gave my progress points "position" (called sequence) values of 1, 2, and 3.

Second time, it ordered my items as 3, 2, 1.

The third time: 1, 3, 2.

class License < ActiveRecord::Base
  has_many :progress_points, :order => 'sequence'  
end
class ProgressPoint < ActiveRecord::Base
  belongs_to :license
  acts_as_list :column => :sequence, :scope => :license
end

Any ideas on why it would do this?

Also, this is how I'm doing the multiple add functionality if you're curious:
http://railscasts.com/episodes/197-nested-model-form-part-2

Using insert_at with values with type String

  • Rails 4.0.4
  • Ruby 2.1.1
  • simple_form gem

For model Foo, inserting at say position utilizing strong parameters yields

> Foo.insert_at(foo_params[:position])
> ArgumentError: comparison of Fixnum with String failed

Parameters for my case are passed as String. To get it to insert, I must convert the parameter position to type Integer. I accomplished this in the foo_params method.

What's the feasibility of having the (String|Integer) => Integer conversion included in the insert_at method (and any other applicable method)?

Create element in default position

When I try create a new element in the default position create it at the bottom of the list.

Example:

*********************************
* Tasks
*********************************
* id
* name
* position , NOT NULL, DEFAULT 0
*********************************

class Task
 acts_as_list  top_of_list: 0, add_new_at: :bottom
end

# existing elements
task position: 0 id: 20
task position: 1 id: 35
task position: 2 id: 15
task position: 3 id: 14
task position: 4 id: 16
task position: 5 id: 17
task position: 6 id: 18

# create new element
task = Task.create(position: 0, name: 'I want to be at the top')

task.position => 7 # but I wanted to create it at position 0

How can I create a new element forcing the default position?

position not updated with move_higher or move_lover

I have a strange behavior with acts_as_list, Rails 3.1 and Ruby 1.9.2p290.

Steps to reproduce

  • Create a test application
    rails new asl_test
    cd asl_test
  • Add

gem 'acts_as_list', :git => 'https://github.com/swanandp/acts_as_list.git'

in gemfile.

  • Update gems

bundle update

  • Generate a model
    rails generate model Hand side:string
  • Generate a second model
    rails generate model Finger position:integer name:string hand_id:integer
  • Migrate the db
    bundle exec rake db:migrate
  • Modify the hand model
    class Hand < ActiveRecord::Base
    has_many :fingers
    end
  • Modify the finger model
    class Finger < ActiveRecord::Base
    belongs_to :hand
    acts_as_list :scope => :hand
    end
  • Now go to the console
    rails console
  • Create a hand
    lefty = Hand.create(:side => "left")
  • Add some fingers
    lefty.fingers.create(:name => "thumb")
    lefty.fingers.create(:name => "index")
    lefty.fingers.create(:name => "ring")
    lefty.fingers.create(:name => "middle")
    lefty.fingers.create(:name => "baby")

Let's see the fingers
lefty.fingers.each do |finger| puts "#{finger.position}: #{finger.name}" end

That gives what was inserted.
1: thumb
2: index
3: ring
4: middle
5: baby

But one finger is not at the good place so try to correct that.
lefty.fingers[3].move_lower

Let's see the fingers now
lefty.fingers.each do |finger| puts "#{finger.position}: #{finger.name}" end

The result is not what I expected
1: thumb
2: index
3: ring
5: middle
5: baby

I don't know why middle and ring where not interverted and why there are two fingers whose position is 5.

Change bundler dependency from ~>1.0.0 to ~>1.0

The bundler dependency needs to be updated to play nicely with the forthcoming bundler 1.1. Actually, I'm not sure if bundler even needs to be a dependency for this project. So maybe, the line can just be taken out entirely.

$ gem dependency -R bundler
Gem bundler-1.0.21
  ronn (>= 0, development)
  rspec (>= 0, development)
  Used by
    Ascii85-1.0.1 (bundler (>= 1.0.0, development))
    acts_as_list-0.1.4 (bundler (~> 1.0.0, development))
    rails-3.1.1 (bundler (~> 1.0))
    thor-0.14.6 (bundler (~> 1.0, development))

Gem bundler-1.1.rc
  ronn (>= 0, development)
  rspec (~> 2.0, development)
  Used by
    Ascii85-1.0.1 (bundler (>= 1.0.0, development))
    rails-3.1.1 (bundler (~> 1.0))
    thor-0.14.6 (bundler (~> 1.0, development))

Ability to move multiple at once

Hi, I have encountered an issue when building a list of choices and the ability to order then according to the user.

I am currently able to order each choices individually, but I was wondering if there was an ability to do a batch order, so something on the lines of:

C1 - Choose
C2 - Choose
C3 - Choose
C4

If someone decide to move up base on that selection, the list would be broken. It would become

C2
C3
C1
C4

Which is not what I want, I would like the list to stay the same. There are also other scenarios which I face, and I was wondering if there was a method for batch up and batch down. Any suggestion?

New gem release

Hi there, is there any chance of getting the latest changes released as an updated gem?

Cheers,

Brendon

MySQL: Position column MUST NOT have default

If I set my position column to have default 0 in MySQL, then acts_as_list does not work. If I change the SQL table to drop the default, then acts_as_list works fine. This should be documented somewhere, as the older "official" one worked fine with the default 0 until we upgraded to this one.

Thanks,

Matt

increment_positions_on_lower_items called twice on insert_at with new item

I found unexpected behavior when I call insert_at(position) on a new item (as is shown in the test_list.rb in the gem). If I insert a newly created (and not yet saved object), increment_positions_on_lower_items gets called twice, so that given positions [1,2], new_object.insert_at(2) results in positions [1,2,4].

See the following:

## GET THE PARENT
ruby-1.9.2-p290 :030 > shoot_list = ShootList.first
  ShootList Load (0.6ms)  SELECT "shoot_lists".* FROM "shoot_lists" LIMIT 1
 => #<ShootList id: 1, name: "xyx", manuscript_id: 1, lighting_config_id: nil, imaging_setup_id: nil, imaging_mode: nil, imaging_system_id: nil, created_at: "2011-11-11 22:26:39", updated_at: "2011-11-11 22:26:39"> 

### TWO ORDERED CHILDREN IN THE LIST
ruby-1.9.2-p290 :031 > shoot_list.shot_sequences.map(&:position)
  ShotSequence Load (0.7ms)  SELECT "shot_sequences".* FROM "shot_sequences" WHERE "shot_sequences"."shoot_list_id" = 1 ORDER BY position
 => [1, 2] 

## CREATE A NEW CHILD AND INSERT IT
ruby-1.9.2-p290 :032 > shot_sequence = ShotSequence.new(:subject => "elephant", :shoot_list => shoot_list)
 => #<ShotSequence id: nil, operator_instructions: nil, subject: "elephant", rotation_degrees: 0, position: nil, created_at: nil, updated_at: nil, shoot_list_id: 1> 
ruby-1.9.2-p290 :033 > shot_sequence.insert_at(2)
  SQL (2.2ms)  UPDATE "shot_sequences" SET position = (position + 1) WHERE ("shot_sequences"."shoot_list_id" = 1 AND position >= 2)
   (0.1ms)  BEGIN
  SQL (0.4ms)  UPDATE "shot_sequences" SET position = (position + 1) WHERE ("shot_sequences"."shoot_list_id" = 1 AND position >= 2)
  SQL (0.7ms)  INSERT INTO "shot_sequences" ("created_at", "operator_instructions", "position", "rotation_degrees", "shoot_list_id", "subject", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id"  [["created_at", Mon, 14 Nov 2011 12:34:05 EST -05:00], ["operator_instructions", nil], ["position", 2], ["rotation_degrees", 0], ["shoot_list_id", 1], ["subject", "elephant"], ["updated_at", Mon, 14 Nov 2011 12:34:05 EST -05:00]]
   (0.4ms)  COMMIT
 => true 

### RELOAD THE PARENT AND LIST THE CHILDREN
ruby-1.9.2-p290 :035 > shoot_list = ShootList.first
  ShootList Load (0.6ms)  SELECT "shoot_lists".* FROM "shoot_lists" LIMIT 1
 => #<ShootList id: 1, name: "xyx", manuscript_id: 1, lighting_config_id: nil, imaging_setup_id: nil, imaging_mode: nil, imaging_system_id: nil, created_at: "2011-11-11 22:26:39", updated_at: "2011-11-11 22:26:39"> 
ruby-1.9.2-p290 :036 > shoot_list.shot_sequences.map(&:position)
  ShotSequence Load (0.8ms)  SELECT "shot_sequences".* FROM "shot_sequences" WHERE "shot_sequences"."shoot_list_id" = 1 ORDER BY position
 => [1, 2, 4] 
ruby-1.9.2-p290 :037 > 

Note that two update calls are made to do the position increment. I stuck a backtrace printout in increment_positions_on_lower_items(position) and found the first call was made on insert and the second when the before_create callback is made.

Creating the item, and then inserting it in the correct position gets the desired result.

move_lower and move_higher not working returning nil

Hi,

I am using Rails 3.2.6 application with acts_as_list(0.1.6) gem included in my Gemfile.

I have the code as follows

class Product < ActiveRecord::Base
  attr_accessible :name, :price
  has_many :product_stores
  has_many :stores, :through => :product_stores, :order => 'product_stores.position'
end

class ProductStore < ActiveRecord::Base
  attr_accessible :position, :product_id, :store_id
  default_scope :order => 'position'

  belongs_to :store
  belongs_to :product

  acts_as_list :scope => :product
end

class Store < ActiveRecord::Base
  attr_accessible :description, :name
  has_many :product_stores
  has_many :products, :through => :product_stores, :order => 'product_stores.position'
end

I have position(integer) column in the product_stores table . But the move_lower and move_higher functions does not work returning nil and the product is not moved to any position without updating the position. e.g

ruby-1.9.2-p290 :008 > store.product_stores.where(:product_id => 2).first.move_lower
  ProductStore Load (0.2ms)  SELECT `product_stores`.* FROM `product_stores` WHERE `product_stores`.`store_id` = 1 AND `product_stores`.`product_id` = 2 LIMIT 1
  ProductStore Load (0.3ms)  SELECT `product_stores`.* FROM `product_stores` WHERE (`product_stores`.`product_id` = 2 AND position = 2) ORDER BY position LIMIT 1
 => nil 
ruby-1.9.2-p290 :009 > 

Hw to resolve it ?

act_as_list didn't install with bundle install

Added gem 'act_as_list' to my Gemfile. bundle install gives this error
Could not find gem 'act_as_list (>= 0) x86-mingw32' in the gems available on this machine.
Using Ruby1.9.3, Rails 3.2.13 on Windows 7 64bit environment. Bundler is version 1.2.3

Shuffle list

In my project I've got shuffle feature. I can make pull request if anyone like it.

Scope for Polymorphic association + ManyToMany

I have this setup: "Topic" and "Comment" has many "Assets" through polymorphic join table.
I want my topics and comments to have their assets ordered. To make my act_as_list work for Topics and Comments separately it should use default scope. How to do that?

Order is reversed when adding multiple rows at once

Using code similar to this:

list = List.new title: "List 1"
list.items.new text: "Item 1"
list.items.new text: "Item 2"
list.items.new text: "Item 3"
list.save!

The items get inserted into the list in reverse order.

This is because before_validation :add_to_list_bottom is run for all rows before any row is saved, so they all get position set to 1. Then each item that is saved causes the ones before it to be pushed further down the list.

when position is null all new items get inserted in position 1

I'm using 'remove_from_list' and a deleted_at attribute to remove items from list. The problem is that when creating a new record, if there are other records in the list with position of null, all new records get set to position 1. A workaround I've used is to set a default scope of :conditions => (:position ^ nil), but it seems to me at least, that the private method 'bottom_item' should have a default condition of position not nil.

Bug when use #insert_at on an invalid ActiveRecord object

Hi,

I found something that seems to be a bug.

If I have an invalid ActiveRecord object (record.valid? => false) and calls record.insert_at(new_position), it updates all other records using #shuffle_positions_on_intermediate_items. But to save the current record it uses #set_list_position method that calls #save!on the record. Causing the save to abort since the record is invalid and leaving the database corrupted with a dup position.

To me seems #set_list_position should not call #save!. It should just save the record ignoring if it's invalid as already happens on #shuffle_positions_on_intermediate_items that uses #update_all method that ignore ActiveRecord validation.

The new implementation should be something like:

def set_list_position(new_position)
  self.update_column(position_column, new_position)
end

I can PR with a fix if you think is useful.

don't work perfectly with default_scope

when i set default_scope order: 'position asc' in model, will make some function dont work correct like move_to_bottom.

move_to_bottom uses position desc to get bottom item position, but order position in default_scope will overwrite it.

maybe use max or count function to get bottom item position is better?

Limiting the list size

Would it be possible for there to be a way to limit the size of the list you can have? For example, if I have 100 items and try to set two items as item #100, the previously set one should go down to 99 (and this should trickle down the rest of the list)?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.