-
Notifications
You must be signed in to change notification settings - Fork 16
/
README
211 lines (143 loc) · 8.62 KB
/
README
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
=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.