Commit 00b69cdc0d426fad8bdefc5652814bd780de19d8

Authored by Braulio Bhavamitra
2 parents 980d4cac 9f0569e6

Merge branch 'ai2809' into 'master'

Use materialized path for parents lookup

http://noosfero.org/Development/ActionItem2809

See merge request !7
db/migrate/20120820120000_index_parent_id_from_all_tables.rb 0 → 100644
@@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
  1 +class IndexParentIdFromAllTables < ActiveRecord::Migration
  2 + def self.up
  3 + add_index :article_versions, :parent_id
  4 + add_index :categories, :parent_id
  5 + add_index :images, :parent_id
  6 + add_index :tags, :parent_id
  7 + add_index :thumbnails, :parent_id
  8 + end
  9 +
  10 + def self.down
  11 + remove_index :article_versions, :parent_id
  12 + remove_index :categories, :parent_id
  13 + remove_index :images, :parent_id
  14 + remove_index :tags, :parent_id
  15 + remove_index :thumbnails, :parent_id
  16 + end
  17 +end
db/migrate/20120820142056_add_ancestry_to_categories.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +class AddAncestryToCategories < ActiveRecord::Migration
  2 + def self.up
  3 + add_column :categories, :ancestry, :text
  4 +
  5 + Category.build_ancestry
  6 + end
  7 +
  8 + def self.down
  9 + remove_column :categories, :ancestry
  10 + end
  11 +end
@@ -104,6 +104,7 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do @@ -104,6 +104,7 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do
104 add_index "article_versions", ["path", "profile_id"], :name => "index_article_versions_on_path_and_profile_id" 104 add_index "article_versions", ["path", "profile_id"], :name => "index_article_versions_on_path_and_profile_id"
105 add_index "article_versions", ["path"], :name => "index_article_versions_on_path" 105 add_index "article_versions", ["path"], :name => "index_article_versions_on_path"
106 add_index "article_versions", ["published_at", "id"], :name => "index_article_versions_on_published_at_and_id" 106 add_index "article_versions", ["published_at", "id"], :name => "index_article_versions_on_published_at_and_id"
  107 + add_index "article_versions", ["parent_id"], :name => "index_article_versions_on_parent_id"
107 108
108 create_table "articles", :force => true do |t| 109 create_table "articles", :force => true do |t|
109 t.string "name" 110 t.string "name"
@@ -217,8 +218,11 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do @@ -217,8 +218,11 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do
217 t.string "acronym" 218 t.string "acronym"
218 t.string "abbreviation" 219 t.string "abbreviation"
219 t.string "display_color", :limit => 6 220 t.string "display_color", :limit => 6
  221 + t.text "ancestry"
220 end 222 end
221 223
  224 + add_index "categories", ["parent_id"], :name => "index_categories_on_parent_id"
  225 +
222 create_table "categories_profiles", :id => false, :force => true do |t| 226 create_table "categories_profiles", :id => false, :force => true do |t|
223 t.integer "profile_id" 227 t.integer "profile_id"
224 t.integer "category_id" 228 t.integer "category_id"
@@ -251,6 +255,7 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do @@ -251,6 +255,7 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do
251 t.string "source_type" 255 t.string "source_type"
252 t.string "user_agent" 256 t.string "user_agent"
253 t.string "referrer" 257 t.string "referrer"
  258 + t.integer "group_id"
254 end 259 end
255 260
256 add_index "comments", ["source_id", "spam"], :name => "index_comments_on_source_id_and_spam" 261 add_index "comments", ["source_id", "spam"], :name => "index_comments_on_source_id_and_spam"
@@ -357,6 +362,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do @@ -357,6 +362,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do
357 t.boolean "thumbnails_processed", :default => false 362 t.boolean "thumbnails_processed", :default => false
358 end 363 end
359 364
  365 + add_index "images", ["parent_id"], :name => "index_images_on_parent_id"
  366 +
360 create_table "inputs", :force => true do |t| 367 create_table "inputs", :force => true do |t|
361 t.integer "product_id", :null => false 368 t.integer "product_id", :null => false
362 t.integer "product_category_id", :null => false 369 t.integer "product_category_id", :null => false
@@ -649,7 +656,11 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do @@ -649,7 +656,11 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do
649 t.boolean "pending", :default => false 656 t.boolean "pending", :default => false
650 end 657 end
651 658
  659 +<<<<<<< HEAD
652 add_index "tags", ["name"], :name => "index_tags_on_name", :unique => true 660 add_index "tags", ["name"], :name => "index_tags_on_name", :unique => true
  661 +=======
  662 + add_index "tags", ["parent_id"], :name => "index_tags_on_parent_id"
  663 +>>>>>>> Update schema.rb
653 664
654 create_table "tasks", :force => true do |t| 665 create_table "tasks", :force => true do |t|
655 t.text "data" 666 t.text "data"
@@ -689,6 +700,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do @@ -689,6 +700,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150122165042) do
689 t.string "thumbnail" 700 t.string "thumbnail"
690 end 701 end
691 702
  703 + add_index "thumbnails", ["parent_id"], :name => "index_thumbnails_on_parent_id"
  704 +
692 create_table "units", :force => true do |t| 705 create_table "units", :force => true do |t|
693 t.string "singular", :null => false 706 t.string "singular", :null => false
694 t.string "plural", :null => false 707 t.string "plural", :null => false
lib/acts_as_filesystem.rb
1 module ActsAsFileSystem 1 module ActsAsFileSystem
2 2
3 - module ClassMethods 3 + module ActsMethods
4 4
5 # Declares the ActiveRecord model to acts like a filesystem: objects are 5 # Declares the ActiveRecord model to acts like a filesystem: objects are
6 # arranged in a tree (liks acts_as_tree), and . The underlying table must 6 # arranged in a tree (liks acts_as_tree), and . The underlying table must
@@ -14,66 +14,81 @@ module ActsAsFileSystem @@ -14,66 +14,81 @@ module ActsAsFileSystem
14 # the parent, a "/" and the slug of the object) 14 # the parent, a "/" and the slug of the object)
15 # * children_count - a cache of the number of children elements. 15 # * children_count - a cache of the number of children elements.
16 def acts_as_filesystem 16 def acts_as_filesystem
17 -  
18 - include ActsAsFileSystem::InstanceMethods  
19 -  
20 # a filesystem is a tree 17 # a filesystem is a tree
21 - acts_as_tree :order => 'name', :counter_cache => :children_count 18 + acts_as_tree :counter_cache => :children_count
22 19
23 - # calculate the right path  
24 - before_create do |record|  
25 - if record.path == record.slug && (! record.top_level?)  
26 - record.path = record.calculate_path  
27 - end  
28 - true 20 + extend ClassMethods
  21 + include InstanceMethods
  22 + if self.has_path?
  23 + after_update :update_children_path
  24 + before_create :set_path
  25 + include InstanceMethods::PathMethods
29 end 26 end
30 27
31 - # when renaming a category, all children categories must have their paths  
32 - # recalculated  
33 - after_update do |record|  
34 - if record.recalculate_path  
35 - record.children.each do |item|  
36 - item.path = item.calculate_path  
37 - item.recalculate_path = true  
38 - item.save!  
39 - end 28 + before_save :set_ancestry
  29 + end
  30 +
  31 + end
  32 +
  33 + module ClassMethods
  34 +
  35 + def build_ancestry(parent_id = nil, ancestry = '')
  36 + ActiveRecord::Base.transaction do
  37 + self.base_class.all(:conditions => {:parent_id => parent_id}).each do |node|
  38 + node.ancestry = ancestry
  39 + node.send :create_or_update_without_callbacks
  40 +
  41 + build_ancestry node.id, (ancestry.empty? ? "#{node.formatted_ancestry_id}" :
  42 + "#{ancestry}#{node.ancestry_sep}#{node.formatted_ancestry_id}")
40 end 43 end
41 - record.recalculate_path = false  
42 - true  
43 end 44 end
44 45
  46 + #raise "Couldn't reach and set ancestry on every record" if self.base_class.count(:conditions => ['ancestry is null']) != 0
  47 + end
  48 +
  49 + def has_path?
  50 + (['name', 'slug', 'path'] - self.column_names).blank?
45 end 51 end
  52 +
46 end 53 end
47 54
48 module InstanceMethods 55 module InstanceMethods
49 - # used to know when to trigger batch renaming  
50 - attr_accessor :recalculate_path  
51 56
52 - # calculates the full name of a category by accessing the name of all its  
53 - # ancestors.  
54 - #  
55 - # If you have this category hierarchy:  
56 - # Category "A"  
57 - # Category "B"  
58 - # Category "C"  
59 - #  
60 - # Then Category "C" will have "A/B/C" as its full name.  
61 - def full_name(sep = '/')  
62 - self.hierarchy.map {|item| item.name || '?' }.join(sep) 57 + def ancestry_column
  58 + 'ancestry'
  59 + end
  60 + def ancestry_sep
  61 + '.'
  62 + end
  63 + def has_ancestry?
  64 + self.class.column_names.include? self.ancestry_column
  65 + end
  66 +
  67 + def formatted_ancestry_id
  68 + "%010d" % self.id if self.id
63 end 69 end
64 70
65 - # gets the name without leading parents. Usefull when dividing categories  
66 - # in top-level groups and full names must not include the top-level  
67 - # category which is already a emphasized label  
68 - def full_name_without_leading(count, sep = '/')  
69 - parts = self.full_name(sep).split(sep)  
70 - count.times { parts.shift }  
71 - parts.join(sep) 71 + def ancestry
  72 + self[ancestry_column]
  73 + end
  74 + def ancestor_ids
  75 + return nil if !has_ancestry? or ancestry.nil?
  76 + @ancestor_ids ||= ancestry.split(ancestry_sep).map{ |id| id.to_i }
  77 + end
  78 +
  79 + def ancestry=(value)
  80 + self[ancestry_column] = value
  81 + end
  82 + def set_ancestry
  83 + return unless self.has_ancestry?
  84 + if self.ancestry.nil? or (new_record? or parent_id_changed?) or recalculate_path
  85 + self.ancestry = self.hierarchy(true)[0...-1].map{ |p| p.formatted_ancestry_id }.join(ancestry_sep)
  86 + end
72 end 87 end
73 88
74 - # calculates the level of the category in the category hierarchy. Top-level  
75 - # categories have level 0; the children of the top-level categories have  
76 - # level 1; the children of categories with level 1 have level 2, and so on. 89 + # calculates the level of the record in the records hierarchy. Top-level
  90 + # records have level 0; the children of the top-level records have
  91 + # level 1; the children of records with level 1 have level 2, and so on.
77 # 92 #
78 # A level 0 93 # A level 0
79 # / \ 94 # / \
@@ -82,61 +97,36 @@ module ActsAsFileSystem @@ -82,61 +97,36 @@ module ActsAsFileSystem
82 # E F G H level 2 97 # E F G H level 2
83 # ... 98 # ...
84 def level 99 def level
85 - self.parent ? (self.parent.level + 1) : 0 100 + self.hierarchy.size - 1
86 end 101 end
87 102
88 - # Is this category a top-level category? 103 + # Is this record a top-level record?
89 def top_level? 104 def top_level?
90 self.parent.nil? 105 self.parent.nil?
91 end 106 end
92 107
93 - # Is this category a leaf in the hierarchy tree of categories? 108 + # Is this record a leaf in the hierarchy tree of records?
94 # 109 #
95 - # Being a leaf means that this category has no subcategories. 110 + # Being a leaf means that this record has no subrecord.
96 def leaf? 111 def leaf?
97 self.children.empty? 112 self.children.empty?
98 end 113 end
99 114
100 - def set_name(value)  
101 - if self.name != value  
102 - self.recalculate_path = true  
103 - end  
104 - self[:name] = value  
105 - end  
106 -  
107 - # sets the name of the category. Also sets #slug accordingly.  
108 - def name=(value)  
109 - self.set_name(value)  
110 - unless self.name.blank?  
111 - self.slug = self.name.to_slug  
112 - end  
113 - end  
114 -  
115 - # sets the slug of the category. Also sets the path with the new slug value.  
116 - def slug=(value)  
117 - self[:slug] = value  
118 - unless self.slug.blank?  
119 - self.path = self.calculate_path 115 + def top_ancestor
  116 + if has_ancestry? and !ancestry.nil?
  117 + self.class.base_class.find_by_id self.top_ancestor_id
  118 + else
  119 + self.hierarchy.first
120 end 120 end
121 end 121 end
122 -  
123 - # calculates the full path to this category using parent's path.  
124 - def calculate_path  
125 - if self.top_level?  
126 - self.slug 122 + def top_ancestor_id
  123 + if has_ancestry? and !ancestry.nil?
  124 + self.ancestor_ids.first
127 else 125 else
128 - self.parent.calculate_path + "/" + self.slug 126 + self.hierarchy.first.id
129 end 127 end
130 end 128 end
131 129
132 - def top_ancestor  
133 - self.top_level? ? self : self.parent.top_ancestor  
134 - end  
135 -  
136 - def explode_path  
137 - path.split(/\//)  
138 - end  
139 -  
140 # returns the full hierarchy from the top-level item to this one. For 130 # returns the full hierarchy from the top-level item to this one. For
141 # example, if item1 has a children item2 and item2 has a children item3, 131 # example, if item1 has a children item2 and item2 has a children item3,
142 # then item3's hierarchy would be [item1, item2, item3]. 132 # then item3's hierarchy would be [item1, item2, item3].
@@ -145,16 +135,21 @@ module ActsAsFileSystem @@ -145,16 +135,21 @@ module ActsAsFileSystem
145 # when the ActiveRecord object was modified in some way, or just after 135 # when the ActiveRecord object was modified in some way, or just after
146 # changing parent) 136 # changing parent)
147 def hierarchy(reload = false) 137 def hierarchy(reload = false)
148 - if reload  
149 - @hierarchy = nil  
150 - end 138 + @hierarchy = nil if reload or recalculate_path
151 139
152 - unless @hierarchy 140 + if @hierarchy.nil?
153 @hierarchy = [] 141 @hierarchy = []
154 - item = self  
155 - while item  
156 - @hierarchy.unshift(item)  
157 - item = item.parent 142 +
  143 + if !reload and !recalculate_path and ancestor_ids
  144 + objects = self.class.base_class.all(:conditions => {:id => ancestor_ids})
  145 + ancestor_ids.each{ |id| @hierarchy << objects.find{ |t| t.id == id } }
  146 + @hierarchy << self
  147 + else
  148 + item = self
  149 + while item
  150 + @hierarchy.unshift(item)
  151 + item = item.parent
  152 + end
158 end 153 end
159 end 154 end
160 155
@@ -181,8 +176,86 @@ module ActsAsFileSystem @@ -181,8 +176,86 @@ module ActsAsFileSystem
181 res 176 res
182 end 177 end
183 178
  179 + #####
  180 + # Path methods
  181 + # These methods are used when _path_, _name_ and _slug_ attributes exist
  182 + # and should be calculated based on the tree
  183 + #####
  184 + module PathMethods
  185 + # used to know when to trigger batch renaming
  186 + attr_accessor :recalculate_path
  187 +
  188 + # calculates the full path to this record using parent's path.
  189 + def calculate_path
  190 + self.hierarchy.map{ |obj| obj.slug }.join('/')
  191 + end
  192 + def set_path
  193 + if self.path == self.slug && !self.top_level?
  194 + self.path = self.calculate_path
  195 + end
  196 + end
  197 + def explode_path
  198 + path.split(/\//)
  199 + end
  200 +
  201 + def update_children_path
  202 + if self.recalculate_path
  203 + self.children.each do |child|
  204 + child.path = child.calculate_path
  205 + child.recalculate_path = true
  206 + child.save!
  207 + end
  208 + end
  209 + self.recalculate_path = false
  210 + end
  211 +
  212 + # calculates the full name of a record by accessing the name of all its
  213 + # ancestors.
  214 + #
  215 + # If you have this record hierarchy:
  216 + # Record "A"
  217 + # Record "B"
  218 + # Record "C"
  219 + #
  220 + # Then Record "C" will have "A/B/C" as its full name.
  221 + def full_name(sep = '/')
  222 + self.hierarchy.map {|item| item.name || '?' }.join(sep)
  223 + end
  224 +
  225 + # gets the name without leading parents. Useful when dividing records
  226 + # in top-level groups and full names must not include the top-level
  227 + # record which is already a emphasized label
  228 + def full_name_without_leading(count, sep = '/')
  229 + parts = self.full_name(sep).split(sep)
  230 + count.times { parts.shift }
  231 + parts.join(sep)
  232 + end
  233 +
  234 + def set_name(value)
  235 + if self.name != value
  236 + self.recalculate_path = true
  237 + end
  238 + self[:name] = value
  239 + end
  240 +
  241 + # sets the name of the record. Also sets #slug accordingly.
  242 + def name=(value)
  243 + self.set_name(value)
  244 + unless self.name.blank?
  245 + self.slug = self.name.to_slug
  246 + end
  247 + end
  248 +
  249 + # sets the slug of the record. Also sets the path with the new slug value.
  250 + def slug=(value)
  251 + self[:slug] = value
  252 + unless self.slug.blank?
  253 + self.path = self.calculate_path
  254 + end
  255 + end
  256 + end
184 end 257 end
185 end 258 end
186 259
187 -ActiveRecord::Base.extend ActsAsFileSystem::ClassMethods 260 +ActiveRecord::Base.extend ActsAsFileSystem::ActsMethods
188 261
test/unit/acts_as_filesystem_test.rb
@@ -7,13 +7,34 @@ class ActsAsFilesystemTest &lt; ActiveSupport::TestCase @@ -7,13 +7,34 @@ class ActsAsFilesystemTest &lt; ActiveSupport::TestCase
7 should 'provide a hierarchy list' do 7 should 'provide a hierarchy list' do
8 profile = create_user('testinguser').person 8 profile = create_user('testinguser').person
9 9
10 - a1 = profile.articles.build(:name => 'a1'); a1.save!  
11 - a2 = profile.articles.build(:name => 'a2'); a2.parent = a1; a2.save!  
12 - a3 = profile.articles.build(:name => 'a3'); a3.parent = a2; a3.save! 10 + a1 = profile.articles.create(:name => 'a1')
  11 + a2 = profile.articles.create(:name => 'a2', :parent => a1)
  12 + a3 = profile.articles.create(:name => 'a3', :parent => a2)
13 13
14 assert_equal [a1, a2, a3], a3.hierarchy 14 assert_equal [a1, a2, a3], a3.hierarchy
15 end 15 end
16 16
  17 + should 'set ancestry' do
  18 + c1 = create(Category, :name => 'c1')
  19 + c2 = create(Category, :name => 'c2', :parent => c1)
  20 + c3 = create(Category, :name => 'c3', :parent => c2)
  21 +
  22 + assert_not_nil c1.ancestry
  23 + assert_not_nil c2.ancestry
  24 + assert_equal "%010d.%010d" % [c1.id, c2.id], c3.ancestry
  25 + assert_equal [c1, c2, c3], c3.hierarchy
  26 + end
  27 +
  28 + should 'provide the level' do
  29 + c1 = create(Category, :name => 'c1')
  30 + c2 = create(Category, :name => 'c2', :parent => c1)
  31 + c3 = create(Category, :name => 'c3', :parent => c2)
  32 +
  33 + assert_equal 0, c1.level
  34 + assert_equal 1, c2.level
  35 + assert_equal 2, c3.level
  36 + end
  37 +
17 should 'be able to optionally reload the hierarchy' do 38 should 'be able to optionally reload the hierarchy' do
18 a = Article.new 39 a = Article.new
19 list = a.hierarchy 40 list = a.hierarchy