Module: DataMapper::Is::List

Defined in:
lib/dm-is-list/is/list.rb

Overview

dm-is-list

DataMapper plugin for creating and organizing lists.

Installation

Stable

Install the dm-is-list gem using rubygems.

$ gem install dm-is-list

Edge

Download or clone dm-is-list from Github.

$ cd /path/to/dm-is-list

$ rake install

Getting started

First of all, for a better understanding of this gem, make sure you study the ‘dm-is-list/spec/integration/list_spec.rb’ file.


Require dm-is-list in your app.

require 'dm-core'         # must be required first
require 'dm-is-list'

Lets say we have a User class, and we want to give users the possibility of having their own todo-lists.

class User
  include DataMapper::Resource

  property :id, Serial
  property :name, String

  has n, :todos
end

class Todo
  include DataMapper::Resource

  property :id, Serial
  property :title, String
  property :done, DateTime

  belongs_to :user

  # here we define that this should be a list, scoped on :user_id
  is :list, :scope => [:user_id]
end

Once we have our Users and Lists, we might want to work with…

Movements of list items

Any list item can be moved around within the same list easily through the move method.

move( vector )

There are number of convenient vectors that help you move items around within the list.

item = Todo.get(1)
other = Todo.get(2)

item.move(:highest)          # moves to top of list.
item.move(:lowest)           # moves to bottom of list.
item.move(:top)              # moves to top of list.
item.move(:bottom)           # moves to bottom of list.
item.move(:up)               # moves one up (:higher and :up is the same) within the scope.
item.move(:down)             # moves one up (:lower and :down is the same) within the scope.
item.move(:to => position)   # moves item to a specific position.
item.move(:above => other)   # moves item above the other item.*
item.move(:below => other)   # moves item above the other item.*

# * won't move if the other item is in another scope. (should this be enabled?)

The list will act as intelligently as possible and keep positions in a logical running order.

move( Integer )

NOTE! VERY IMPORTANT!

If you set the position manually, and then save, the list will NOT reorganize itself.

item.position = 3      # setting position manually
item.save              # the item will now have position 3, but the list may have two items with the same position.

# alternatively
item.update(:position => 3)    # sets the position manually, but does not reorganize the list positions.

You should therefore always use the item.move(N) syntax instead.

item.move(3)          # does the same as above, but in one call AND *reorganizes* the list.

<hr>

Hold On!

dm-is-list used to work with item.position = 1 type syntax. Why this change?

The main reason behind this change was that the previous version of dm-is-list created a LOT of extra SQL queries in order to support the manual updating of position, and as a result had a quite a few bugs/issues, which have been fixed in this version.

The other reason is that I couldn’t work out how to keep the functionality without adding the extra queries. But perhaps you can ?

<hr>

See “Batch Changing Positions” below for information on how to change the positions on a whole list.

Movements between scopes

When you move items between scopes, the list will try to work with your intentions.

Move the item from list to new list and add the item to the bottom of that list.

item.user_id               # => 1
item.move_to_list(10)      # => the scope id ie User.get(10).id

# results in...
item.user_id                 # => 10
item.position                # => < bottom of the list >

Move the item from list to new list and add at the position given.

item.user_id                 # => 1
item.move_to_list(10, 2)     # => the scope id ie User.get(10).id,  position => 2

# results in...
item.user_id                 # => 10
item.position                # => 2

Batch Changing Positions

A common scenario when working with lists is the sorting of a whole list via something like JQuery’s sortable() functionality.
(Think re-arranging the order of Todo’s according to priority or something similar)

Optimum scenario

The most SQL query efficient way of changing the positions is:

sort_order = [5,4,3,2,1]              # list from AJAX request..

items = Todo.all(:user => @u1)        # loads all 5 items in the list

items.each{ |item| item.update(:position => sort_order.index(item.id) + 1) }   # remember the +1 since array's are indexed from 0

The above code will result in something like these queries.

# SELECT "id", "title", "position", "user_id" FROM "todos" WHERE "user_id" = 1 ORDER BY "position"
# UPDATE "todos" SET "position" = 5 WHERE "id" = 1
# UPDATE "todos" SET "position" = 4 WHERE "id" = 2
# UPDATE "todos" SET "position" = 2 WHERE "id" = 4
# UPDATE "todos" SET "position" = 1 WHERE "id" = 5

Remember! Your sort order list has to be the same length as the found items in the list, or your loop will fail.

Wasteful scenario

You can also use this version, but it will create upto 5 times as many SQL queries. :(

sort_order = ['5','4','3','2','1']    # list from AJAX request..

items = Todo.all(:user => @u1)        # loads all 5 items in the list

items.each{ |item| item.move(sort_order.index(item.id).to_i + 1) }   # remember the +1 since array's are indexed from 0

The above code will result in something like these queries:

#  SELECT "id", "title", "position", "user_id" FROM "todos" WHERE "user_id" = 1 ORDER BY "position"

#  SELECT "id", "title", "position", "user_id" FROM "todos" WHERE "user_id" = 1 ORDER BY "position" DESC LIMIT 1
#  SELECT "id" FROM "todos" WHERE "user_id" = 1 AND "id" IN (1, 2, 3, 4, 5) AND "position" BETWEEN 1 AND 5 ORDER BY "position"
#  UPDATE "todos" SET "position" = "position" + -1 WHERE "user_id" = 1 AND "position" BETWEEN 1 AND 5
#  SELECT "id", "position" FROM "todos" WHERE "id" IN (1, 2, 3, 4, 5) ORDER BY "id"
#  UPDATE "todos" SET "position" = 5 WHERE "id" = 1

#  SELECT "id", "title", "position", "user_id" FROM "todos" WHERE "user_id" = 1 ORDER BY "position" DESC LIMIT 1
#  SELECT "id" FROM "todos" WHERE "user_id" = 1 AND "id" IN (1, 2, 3, 4, 5) AND "position" BETWEEN 1 AND 4 ORDER BY "position"
#  UPDATE "todos" SET "position" = "position" + -1 WHERE "user_id" = 1 AND "position" BETWEEN 1 AND 4
#  SELECT "id", "position" FROM "todos" WHERE "id" IN (2, 3, 4, 5) ORDER BY "id"
#  UPDATE "todos" SET "position" = 4 WHERE "id" = 2

#  ...

As you can see it will also do the job, but will be more expensive.

RTFM

As I said above, for a better understanding of this gem/plugin, make sure you study the ‘dm-is-list/spec/integration/list_spec.rb’ tests.

Errors / Bugs

If something is not behaving intuitively, it is a bug, and should be reported. Report it here: datamapper.lighthouseapp.com/

Defined Under Namespace

Modules: ClassMethods, InstanceMethods

Instance Method Summary collapse

Instance Method Details

#is_list(options = {}) ⇒ Object

method for making your model a list.

TODO:: this explanation is confusing. Need to translate into literal code

it will define a :position property if it does not exist, so be sure to have a position-column in your database (will be added automatically on auto_migrate) if the column has a different name, simply make a :position-property and set a custom :field

Examples:

Usage

is :list                                      # put this in your model to make it act as a list.
is :list, :scope => :user_id                  # you can also define scopes
is :list, :scope => [ :user_id, :context_id ] # also works with multiple params

Parameters:

  • options (Hash) (defaults to: {})

    a hash of options

  • :scope<Array> (Hash)

    a customizable set of options



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/dm-is-list/is/list.rb', line 246

def is_list(options={})
  options = { :scope => [], :first => 1 }.merge(options)

  # coerce the scope into an Array
  options[:scope] = Array(options[:scope])

  extend  DataMapper::Is::List::ClassMethods
  include DataMapper::Is::List::InstanceMethods

  unless properties.any? { |p| p.name == :position && p.primitive == Integer }
    property :position, Integer
  end

  @list_options = options

  before :create do
    # if a position has been set before save, then insert it at the position and
    # move the other items in the list accordingly, else if no position has been set
    # then set position to bottom of list
    __send__(:move_without_saving, position || :lowest)

    # on create, set moved to false so we can move the list item after creating it
    # self.moved = false
  end

  before :update do
    # a (new) position has been set => move item to this position (only if position has been set manually)
    # the scope has changed => detach from old list, and possibly move into position
    # the scope and position has changed => detach from old, move to pos in new

    # if the scope has changed, we need to detach our item from the old list
    if list_scope != original_list_scope
      newpos = position
      detach(original_list_scope) # removing from old list
      __send__(:move_without_saving, newpos || :lowest) # moving to pos or bottom of new list
    end

    #  NOTE:: uncommenting the following creates a large number of extra un-wanted SQL queries
    #  hence the commenting out of it.
    # if attribute_dirty?(:position) && !moved
    #   __send__(:move_without_saving, position)
    # end
    # # on update, clean moved to prepare for the next change
    # self.moved = false
  end

  before :destroy do
    detach
  end

  # we need to make sure that STI-models will inherit the list_scope.
  after_class_method :inherited do |retval, target|
    target.instance_variable_set(:@list_options, @list_options.dup)
  end

end