Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Think of acts_as_tree + acts_as_listed, but doing what you want: root categories, subcategories, positions (and pretty view helpers).
Ruby
branch: master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
lib
test
.gitignore
README
Rakefile
init.rb

README

=Introduction

Let me explain to you what I mean by *acts_as_category*, which is yet another acts_as plugin for Ruby on Rails ActiveRecord models. Copyright is 2008 by www.funkensturm.de, released under the MIT/X11 license, which is free for all to do whatever you want with it.

*acts_as_tree* provides functionality for trees, but lacks some things:
- It has no descendants method or things like ancestors_ids
- It doesn't validate parent_id whatsoever, which means that you can make a category a parent of itself, etc.
- It has no caching for ancestors and descendants
- It won't help if you want certain users to see only certain nodes

*acts_as_list* is not exactly what I want either:
- It also has no validation or features to hide entries
- It doesn't support scriptaculous sortable_list
- It has more than I need, providing all these move_just_a_little_bit_higher methods
- Last but not least, it won't work together with acts_as_tree unless you hack around with the scope code

So I came up with *acts_as_category*, and this is what it does:
- It provides a structure for infinite categories and their subcategories (similar to acts_as_tree)
- It validates that no category will be the parent of its own descendant and all of these foreign key things
- You can define (through a class variable) that certain categories should be hidden to the current user
- There is a variety of instance methods such as ancestors, descendants, descendants_ids, root?, etc.
- It has view helpers to create menus, select boxes, drag and drop ajax lists, etc.
- It provides sorting by a position column, including admin methods that take parameters from the helpers
- There are automatic cache columns for children, ancestors and descendants (good for fast menu output)
- It is well commented and documented, so that Rails beginners will learn from it, or easily make changes
- A full unit test comes along with it

What can *acts_as_category* *not* do?
- You can't simply turn of certain features it has, in order to speed up your application
- I consider it efficient code, though I am sure, here or there you could tweak it, so don't blame me ;)
- ActiveRecord's find method won't respect hidden categories feature (but I provide alternative methods)
- "update" and "update_attributes" must not be used to change the parent_id, because there is no validation callback
- It can't make you a coffee

= Tutorial

=== Installation

Just copy the *acts_as_category* directory into "<i>/vendor/plugins/</i>" in your Rails application.

To generate <b>HTML documentation</b> for all your plugins, run "<i>rake doc:plugins</i>".
To generate it just for this plugin, go to "<i>/vendor/plugins/acts_as_category</i>" and run "<i>rake rdoc</i>".

To run the <b>Unit Test</b> that comes with this plugin, please read the comments in "<i>/vendor/plugins/acts_as_category/test/acts_as_category_test.rb</i>".

=== Including acts_as_category in your model

To make it work, you need a ActiveRecord Model, which provides certain table columns. Like so:

 class Category < ActiveRecord::Base
  acts_as_category
 end
 
 create_table :categories do |t|
   t.column :parent_id,         :integer
   t.column :position,          :integer
   t.column :children_count,    :integer
   t.column :ancestors_count,   :integer
   t.column :descendants_count, :integer
 end
 
You can change all their names, or add additional fields like "name", "description", etc. Natually it allows more associations, e.g. to your pictures in a gallery or such:
 
 class Category < ActiveRecord::Base
  acts_as_category
  has_many :pictures, :counter_cache => true
 end

To change the names of the table columns, just pass on the correct parameters with the alternate names:

  class Category < ActiveRecord::Base
   acts_as_category :foreign_key => 'parent', :position => 'sortby', cache_ancestors => 'count_of_ancestors'
  end

Sorting is by position (default), or by anything else you want:

  class Category < ActiveRecord::Base
   acts_as_category :order => 'name'
  end

=== Usage

If everything is set up, you can actually use the plugin. Let's say you have trees like this and your model is called *Category*.

  root1                   root2
   \_ child1               \_ child2
        \_ subchild1            \subchild3
        \_ subchild2                \subchild4

Then you can run the following methods. For more specific information about return values, please look at the HTML documentation generated by RDoc. 

 Category.get(1)
 Returns the category with the id 1

 Category.roots
 Returns an array with all root categories [root1, root2]
 
 (For the rest let's assume, that root1 = Category.get(1), etc...)
 
 root1.root?
 Will return true, because root is a root category (child1.root? will return false)
  
 child1.parent
 Returns root (root.parent will return nil, because root has none)
 
 root.children
 Returns an array with [subchild1, subchild2].

 subchild1.ancestors
 Returns an array with [child1, root1] (root1.ancestors will return an empty array [], because root has none)
 
 subchild1.ancestors_ids
 Returns the same array, but ids instead of categories [2,1]
 
 root1.descendants
 Returns an array with [child1, subchild1, subchild2] (subchild1.descendants will return an empty array [], because it has none)
 
 root1.descendants_ids
 Returns the same array, but ids instead of categories [2,3,4]
 
 root1.siblings
 Returns an array with all siblings [root2] (child1.siblings returns an empty array [], because it has no siblings)
 
 subchild1.self_and_siblings
 Returns an array [subchild1, subchild2], just like siblings, only with itself as well

=== Usage with "hidden"

Let's bring the *hidden* feature into the game. It let's you hide categories for certain users.
  
  Category.hidden = [1,2,3]
  This will hide the categories with the ids 1, 2 and 3 (say root1, child1, subchid2)
  
  Category.hidden
  Returns the array that you provided for hidden=...
  
  root1.hidden?
  Returns true, because root1 is a hidden categorie now.
  
  Category.get(1)
  Returns nil now, because root1, having the id 1, is hidden
  
  ...
  
Note that you can still use find(1) to get hidden categories. So you should never use it unless you must. However, if you do have to use it, you can generate an SQL addition for your condition like so:

  Category.find(:all, :condition => Category.excluded_sql, [... other options])

That will be considered: 

  Category.find(:all, :condition => "id NOT IN (1,2,3)", [... e.g. other options here])
  
If you use a SQL query, you can use the parameter _true_ to add an "AND":

  Category.find(:all, :condition => "WHERE parent_id > 5" Category.excluded_sql(true))
  
Will be considered:

   Category.find(:all, :condition => "WHERE parent_id > 5 AND id NOT IN (1,2,3))

In general you can say, that these methods do respect the _hidden_ feature and will not let you access hidden categories:

  Category.get(id)
  Category.roots
  Category.excluded
  Category.excluded=
  Category.excluded_sql

  self.hidden?
  self.children
  self.children.size
  self.children.empty?
  self.children_ids
  self.descendants
  self.descendants_ids
  self.siblings
  self.self_and_siblings
  
Q:: Why is _find_ not respecting hidden?
A:: I didn't feel comfortable overwriting the find method for Categories and it is not really needed.

Q:: Why are _ancestors_, _ancestors_ids_ and <i>self.parent</i> not respecting hidden?
A:: Because the whole idea of hidden is to exclude descendants of an hidden Category as well, thus the ancestors will never be hidden.
  
=== Add positioning for ordering

Let's say you have a gallery and use acts_as_category on your categories. Then the categories will not be ordered by name (unless you want them to), but by a individual order. For this we have the position column.

You can manually update these positions, but I strongly recommend to let this be done by the sortable_category helper and the Category.update_positions(params) method like so:

In your layout, make sure that you have all the JavaScripts included, that will allow drag and drop with scriptaculous, etc. For the beginning, let's add all:

  <%= javascript_include_tag :all %>

Then, in your view, you can call this little helper to generate a drag and drop list where you can re-sort the positions. Remember to provide the name of the model to use:

  <%= sortable_categories Category %>

Finally, in your controller create an action method like this:

  def update_positions
    Category.update_positions(params)
    render :nothing => true
  end
  
And you can already try it. You can change the URL to that action method like this:

  <%= sortable_categories(Category, {:action => :update_positions}) %>
  <%= sortable_categories(Category, {:controller => :mycontroller, :action => :update_positions}) %>

=== Have fun.
Something went wrong with that request. Please try again.