Permalink
Browse files

first commit

  • Loading branch information...
Josep M. Bach
Josep M. Bach committed Mar 18, 2011
0 parents commit bb7696c6db910e421754f85081ee5eb60b61b978
Showing with 689 additions and 0 deletions.
  1. +7 −0 .gitignore
  2. +2 −0 .rspec
  3. +1 −0 .rvmrc
  4. +3 −0 Gemfile
  5. +47 −0 Rakefile
  6. +92 −0 Readme.md
  7. +204 −0 lib/resort.rb
  8. +4 −0 lib/resort/version.rb
  9. +27 −0 resort.gemspec
  10. +283 −0 spec/resort_spec.rb
  11. +19 −0 spec/spec_helper.rb
@@ -0,0 +1,7 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+graph.png
+.yardoc/*
+doc/*
2 .rspec
@@ -0,0 +1,2 @@
+--color
+--format documentation
1 .rvmrc
@@ -0,0 +1 @@
+rvm --create use ruby-1.9.2@resort
@@ -0,0 +1,3 @@
+source "http://rubygems.org"
+
+gemspec
@@ -0,0 +1,47 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+require 'rspec/core/rake_task'
+desc "Run resort specs"
+RSpec::Core::RakeTask.new
+
+require 'yard'
+YARD::Rake::YardocTask.new(:docs) do |t|
+ t.files = ['lib/**/*.rb']
+ t.options = ['-m', 'markdown', '--no-private', '-r', 'Readme.md', '--title', 'Resort documentation']
+end
+
+site = 'doc'
+source_branch = 'master'
+deploy_branch = 'gh-pages'
+
+desc "generate and deploy documentation website to github pages"
+multitask :pages do
+ puts ">>> Deploying #{deploy_branch} branch to Github Pages <<<"
+ require 'git'
+ repo = Git.open('.')
+ puts "\n>>> Checking out #{deploy_branch} branch <<<\n"
+ repo.branch("#{deploy_branch}").checkout
+ (Dir["*"] - [site]).each { |f| rm_rf(f) }
+ Dir["#{site}/*"].each {|f| mv(f, "./")}
+ rm_rf(site)
+ puts "\n>>> Moving generated site files <<<\n"
+ Dir["**/*"].each {|f| repo.add(f) }
+ repo.status.deleted.each {|f, s| repo.remove(f)}
+ puts "\n>>> Commiting: Site updated at #{Time.now.utc} <<<\n"
+ message = ENV["MESSAGE"] || "Site updated at #{Time.now.utc}"
+ repo.commit(message)
+ puts "\n>>> Pushing generated site to #{deploy_branch} branch <<<\n"
+ repo.push
+ puts "\n>>> Github Pages deploy complete <<<\n"
+ repo.branch("#{source_branch}").checkout
+end
+
+task :doc => [:docs]
+
+desc "Generate and open class diagram (needs Graphviz installed)"
+task :graph do |t|
+ `bundle exec yard graph -d --full --no-private | dot -Tpng -o graph.png && open graph.png`
+end
+
+task :default => [:spec]
@@ -0,0 +1,92 @@
+#resort
+
+Resort provides sorting capabilities to your Rails 3 models.
+
+##Install
+
+ $ gem install resort
+
+Or in your Gemfile:
+
+ gem 'resort'
+
+##Rationale
+
+Most other sorting plugins work with an absolute `position` attribute that sets
+the _weight_ of a given element within a tree. This field has no semantic sense,
+since "84" by itself gives you absolutely no information about an element's
+position or its relations with other elements of the tree.
+
+Resort is implemented quite like a [binary tree](http://en.wikipedia.org/wiki/Binary_tree),
+rather than relying on absolute position values. This way, every model
+references a `next` and a `previous`, which seems a bit more sensible :)
+
+##Usage
+
+You must add two fields (`next_id` and `first`) to your model's table:
+
+ class AddResortFieldsToProducts < ActiveRecord::Migration
+ def self.up
+ add_column :products, :next_id, :integer
+ add_column :products, :first, :boolean
+ end
+
+ def self.down
+ remove_column :products, :next_id
+ remove_column :products, :first
+ end
+ end
+
+Then in your Product model:
+
+ class Product < ActiveRecord::Base
+ resort!
+ end
+
+**NOTE**: By default, Resort will treat _all products_ as a single big tree.
+If you wanted to limit the tree scope, i.e. treating every ProductLine as a
+separate tree of sortable products, you must override the `siblings` method:
+
+ class Product < ActiveRecord::Base
+ resort!
+
+ def siblings
+ # Tree contains only products from my own product line
+ self.product_line.products
+ end
+ end
+
+###API
+
+Every time a product is created, it will be appended after the last element.
+
+Moreover, now a `product` responds to the following methods:
+
+* `first?` &mdash; Returns true if the element is the first of the tree.
+* `append_to(other_element)` &mdash; Appends the element _after_ another element.
+
+And the class Product has a new scope named `ordered` that returns the
+products in order.
+
+##Under the hood
+
+Run the test suite by typing:
+
+ rake spec
+
+You can also build the documentation with the following command:
+
+ rake docs
+
+## Note on Patches/Pull Requests
+
+* Fork the project.
+* Make your feature addition or bug fix.
+* Add tests for it. This is important so I don't break it in a
+ future version unintentionally.
+* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
+* Send us a pull request. Bonus points for topic branches.
+
+## Copyright
+
+Copyright (c) 2011 Codegram. See LICENSE for details.
@@ -0,0 +1,204 @@
+# # Resort
+#
+# A tool that allows any ActiveRecord model to be sorted.
+#
+# Unlike most Rails sorting plugins (acts_as_list, etc), Resort is based
+# on linked lists rather than absolute position fields.
+#
+# @example Using Resort in an ActiveRecord model
+# # In the migration
+# create_table :products do |t|
+# t.text :name
+# t.references :next
+# t.boolean :first
+# end
+#
+# # Model
+# class Product < ActiveRecord::Base
+# resort!
+#
+# # A sortable model must implement #siblings method, which should
+# # return and ActiveRecord::Relation with all the models to be
+# # considered as `peers` in the list representing the sorted
+# # products, i.e. its siblings.
+# def siblings
+# self.class.scoped
+# end
+# end
+#
+# product = Product.create(:name => 'Bread')
+# product.first? # => true
+#
+# another_product = Product.create(:name => 'Milk')
+# yet_another_product = Product.create(:name => 'Salami')
+#
+# yet_another_product.append_to(product)
+#
+# Product.ordered.map(&:name)
+# # => ['Bread', 'Salami', 'Milk']
+module Resort
+ # The module encapsulating all the Resort functionality.
+ #
+ # @todo Refactor into a more OO solution, maybe implementing a LinkedList
+ # object.
+ module Sortable
+ class << self
+ # When included, extends the includer with {ClassMethods}, and includes
+ # {InstanceMethods} in it.
+ #
+ # It also establishes the required relationships. It is necessary that
+ # the includer table has the following database columns:
+ #
+ # t.references :next
+ # t.boolean :first
+ #
+ # @param [ActiveRecord::Base] base the includer `ActiveRecord` model.
+ def included(base)
+ base.extend ClassMethods
+ base.send :include, InstanceMethods
+
+ base.has_one :previous, :class_name => base.name, :foreign_key => 'next_id', :inverse_of => :next
+ base.belongs_to :next, :class_name => base.name, :inverse_of => :previous
+
+ base.after_create :include_in_list!
+ base.after_destroy :delete_from_list
+ end
+ end
+
+ # Class methods to be used from the model class.
+ module ClassMethods
+ # Returns the first element of the list.
+ #
+ # @return [ActiveRecord::Base] the first element of the list.
+ def first_in_order
+ where(:first => true).first
+ end
+
+ # Returns eager-loaded Components in order.
+ #
+ # OPTIMIZE: Avoid creating as many hashes.
+ # @return [Array<ActiveRecord::Base>] the ordered elements
+ def ordered
+ ordered_elements = []
+ elements = {}
+
+ scoped.each do |element|
+ if element.first?
+ ordered_elements << element
+ else
+ elements[element.id] = element
+ end
+ end
+
+ elements.length.times do
+ ordered_elements << elements[ordered_elements.last.next_id]
+ end
+ ordered_elements
+ end
+ end
+
+ # Instance methods to use.
+ module InstanceMethods
+
+ # Default definition of siblings, i.e. every instance of the model.
+ #
+ # Can be overriden to specify a different scope for the siblings.
+ # For example, if we wanted to limit a products tree inside a ProductLine
+ # scope, we would do the following:
+ #
+ # class Product < ActiveRecord::Base
+ # belongs_to :product_line
+ #
+ # resort!
+ #
+ # def siblings
+ # self.product_line.products
+ # end
+ #
+ # This way, every product line is an independent tree of sortable
+ # products.
+ #
+ # @return [ActiveRecord::Relation] the element's siblings relation.
+ def siblings
+ self.class.scoped
+ end
+ # Includes the object in the linked list.
+ #
+ # If there are no other objects, it prepends the object so that it is
+ # in the first position. Otherwise, it appends it to the end of the
+ # empty list.
+ def include_in_list!
+ _siblings.count > 0 ? push\
+ : prepend
+ end
+
+ # Puts the object in the first position of the list.
+ def prepend
+ return if first?
+
+ if _siblings.count > 0
+ delete_from_list
+ _siblings.where(:first => true).first.append_to(self)
+ end
+
+ self.update_attribute(:first, true)
+ end
+
+ # Puts the object in the last position of the list.
+ def push
+ return if last?
+ last_element = _siblings.where(:next_id => nil).first
+ self.append_to(last_element)
+ end
+
+ # Puts the object right after another object in the list.
+ def append_to(another)
+ if self.next
+ delete_from_list
+ elsif last?
+ self.previous.update_attribute(:next_id, nil)
+ self.previous = nil
+ end
+
+ self.update_attribute(:next_id, another.next_id)
+ another.update_attribute(:next_id, self.id)
+ end
+
+ private
+
+ def delete_from_list
+ if first? && self.next
+ self.update_attribute(:first, nil) unless frozen?
+ self.next.first = true
+ self.next.previous = nil
+ self.next.save!
+ elsif self.previous
+ previous.next = self.next
+ previous.save!
+ self.update_attribute(:next_id, nil) unless frozen?
+ end
+ end
+
+ def last?
+ self.previous && !self.next
+ end
+
+ def _siblings
+ table = self.class.arel_table
+ siblings.where(table[:id].not_eq(self.id))
+ end
+ end
+ end
+ # Helper class methods to be injected into ActiveRecord::Base class.
+ # They will be available to every model.
+ module ClassMethods
+ # Helper class method to include Resort::Sortable in an ActiveRecord
+ # model.
+ def resort!
+ include Sortable
+ end
+ end
+end
+
+require 'active_record' unless defined?(ActiveRecord)
+ActiveRecord::Base.extend Resort::ClassMethods
@@ -0,0 +1,4 @@
+module Resort
+ # Resort's version number
+ VERSION = "0.0.1"
+end
@@ -0,0 +1,27 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "resort/version"
+
+Gem::Specification.new do |s|
+ s.name = "resort"
+ s.version = Resort::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Oriol Gual", "Josep M. Bach", "Josep Jaume Rey"]
+ s.email = ["info@codegram.com"]
+ s.homepage = "http://codegram.github.com/resort"
+ s.summary = %q{Positionless model sorting for Rails 3.}
+ s.description = %q{Positionless model sorting for Rails 3.}
+
+ s.rubyforge_project = "resort"
+
+ s.add_runtime_dependency 'activerecord', '~> 3.0.5'
+ s.add_development_dependency 'sqlite3'
+ s.add_development_dependency 'rspec', '~> 2.5.0'
+ s.add_development_dependency 'yard'
+ s.add_development_dependency 'bluecloth'
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
Oops, something went wrong.

0 comments on commit bb7696c

Please sign in to comment.