Think of acts_as_tree + acts_as_listed, but doing what you want: root categories, subcategories, positions (and pretty view helpers).
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.



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, 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
 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
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
  has_many :pictures, :counter_cache => true

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'

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

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

=== 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. 

 Returns the category with the id 1

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

 Returns an array with [child1, root1] (root1.ancestors will return an empty array [], because root has none)
 Returns the same array, but ids instead of categories [2,1]
 Returns an array with [child1, subchild1, subchild2] (subchild1.descendants will return an empty array [], because it has none)
 Returns the same array, but ids instead of categories [2,3,4]
 Returns an array with all siblings [root2] (child1.siblings returns an empty array [], because it has no 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)
  Returns the array that you provided for hidden=...
  Returns true, because root1 is a hidden categorie now.
  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:


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
    render :nothing => true
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.