noosfero | 7 new commits pushed to repository
Bráulio Bhavamitra
gitlab at gitlab.com
Tue Feb 10 16:22:21 BRST 2015
Bráulio Bhavamitra pushed to refs/heads/master at <a href="https://gitlab.com/noosfero/noosfero">Noosfero / noosfero</a>
Commits:
<a href="https://gitlab.com/noosfero/noosfero/commit/95ebdaf6cdf408c42692148482de6d8257c3b419">95ebdaf6</a> by Braulio Bhavamitra
Optimize category tree lookup with a materialized path
- - - - -
<a href="https://gitlab.com/noosfero/noosfero/commit/8923158bf6bc9f7ae4b485381b3cde99de123b1d">8923158b</a> by Braulio Bhavamitra
Improve migration performance
- - - - -
<a href="https://gitlab.com/noosfero/noosfero/commit/cb068077017e5d664fe413048dfcfcb102efb44f">cb068077</a> by Braulio Bhavamitra
Speed up build_ancestry
- - - - -
<a href="https://gitlab.com/noosfero/noosfero/commit/9cd867163dc935789dcc560d930d7f410098c504">9cd86716</a> by Braulio Bhavamitra
Update schema.rb
- - - - -
<a href="https://gitlab.com/noosfero/noosfero/commit/4d1b6e6745061d6853885d933bdc4c01a4acf0e8">4d1b6e67</a> by Braulio Bhavamitra
Move path methods into PathMethods
- - - - -
<a href="https://gitlab.com/noosfero/noosfero/commit/9f0569e64995596b4e30a5a5f4fe618da0e8054b">9f0569e6</a> by Braulio Bhavamitra
Ensure callbacks aren't called
- - - - -
<a href="https://gitlab.com/noosfero/noosfero/commit/00b69cdc0d426fad8bdefc5652814bd780de19d8">00b69cdc</a> by Bráulio Bhavamitra
Merge branch 'ai2809' into 'master'
Use materialized path for parents lookup
http://noosfero.org/Development/ActionItem2809
See merge request !7
- - - - -
Changes:
=====================================
db/migrate/20120820120000_index_parent_id_from_all_tables.rb
=====================================
--- /dev/null
+++ b/db/migrate/20120820120000_index_parent_id_from_all_tables.rb
@@ -0,0 +1,17 @@
+class IndexParentIdFromAllTables < ActiveRecord::Migration
+ def self.up
+ add_index :article_versions, :parent_id
+ add_index :categories, :parent_id
+ add_index :images, :parent_id
+ add_index :tags, :parent_id
+ add_index :thumbnails, :parent_id
+ end
+
+ def self.down
+ remove_index :article_versions, :parent_id
+ remove_index :categories, :parent_id
+ remove_index :images, :parent_id
+ remove_index :tags, :parent_id
+ remove_index :thumbnails, :parent_id
+ end
+end
=====================================
db/migrate/20120820142056_add_ancestry_to_categories.rb
=====================================
--- /dev/null
+++ b/db/migrate/20120820142056_add_ancestry_to_categories.rb
@@ -0,0 +1,11 @@
+class AddAncestryToCategories < ActiveRecord::Migration
+ def self.up
+ add_column :categories, :ancestry, :text
+
+ Category.build_ancestry
+ end
+
+ def self.down
+ remove_column :categories, :ancestry
+ end
+end
=====================================
db/schema.rb
=====================================
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -104,6 +104,7 @@ ActiveRecord::Schema.define(:version => 20150122165042) do
add_index "article_versions", ["path", "profile_id"], :name => "index_article_versions_on_path_and_profile_id"
add_index "article_versions", ["path"], :name => "index_article_versions_on_path"
add_index "article_versions", ["published_at", "id"], :name => "index_article_versions_on_published_at_and_id"
+ add_index "article_versions", ["parent_id"], :name => "index_article_versions_on_parent_id"
create_table "articles", :force => true do |t|
t.string "name"
@@ -217,8 +218,11 @@ ActiveRecord::Schema.define(:version => 20150122165042) do
t.string "acronym"
t.string "abbreviation"
t.string "display_color", :limit => 6
+ t.text "ancestry"
end
+ add_index "categories", ["parent_id"], :name => "index_categories_on_parent_id"
+
create_table "categories_profiles", :id => false, :force => true do |t|
t.integer "profile_id"
t.integer "category_id"
@@ -251,6 +255,7 @@ ActiveRecord::Schema.define(:version => 20150122165042) do
t.string "source_type"
t.string "user_agent"
t.string "referrer"
+ t.integer "group_id"
end
add_index "comments", ["source_id", "spam"], :name => "index_comments_on_source_id_and_spam"
@@ -357,6 +362,8 @@ ActiveRecord::Schema.define(:version => 20150122165042) do
t.boolean "thumbnails_processed", :default => false
end
+ add_index "images", ["parent_id"], :name => "index_images_on_parent_id"
+
create_table "inputs", :force => true do |t|
t.integer "product_id", :null => false
t.integer "product_category_id", :null => false
@@ -649,7 +656,11 @@ ActiveRecord::Schema.define(:version => 20150122165042) do
t.boolean "pending", :default => false
end
+<<<<<<< HEAD
add_index "tags", ["name"], :name => "index_tags_on_name", :unique => true
+=======
+ add_index "tags", ["parent_id"], :name => "index_tags_on_parent_id"
+>>>>>>> Update schema.rb
create_table "tasks", :force => true do |t|
t.text "data"
@@ -689,6 +700,8 @@ ActiveRecord::Schema.define(:version => 20150122165042) do
t.string "thumbnail"
end
+ add_index "thumbnails", ["parent_id"], :name => "index_thumbnails_on_parent_id"
+
create_table "units", :force => true do |t|
t.string "singular", :null => false
t.string "plural", :null => false
=====================================
lib/acts_as_filesystem.rb
=====================================
--- a/lib/acts_as_filesystem.rb
+++ b/lib/acts_as_filesystem.rb
@@ -1,6 +1,6 @@
module ActsAsFileSystem
- module ClassMethods
+ module ActsMethods
# Declares the ActiveRecord model to acts like a filesystem: objects are
# arranged in a tree (liks acts_as_tree), and . The underlying table must
@@ -14,66 +14,81 @@ module ActsAsFileSystem
# the parent, a "/" and the slug of the object)
# * children_count - a cache of the number of children elements.
def acts_as_filesystem
-
- include ActsAsFileSystem::InstanceMethods
-
# a filesystem is a tree
- acts_as_tree :order => 'name', :counter_cache => :children_count
+ acts_as_tree :counter_cache => :children_count
- # calculate the right path
- before_create do |record|
- if record.path == record.slug && (! record.top_level?)
- record.path = record.calculate_path
- end
- true
+ extend ClassMethods
+ include InstanceMethods
+ if self.has_path?
+ after_update :update_children_path
+ before_create :set_path
+ include InstanceMethods::PathMethods
end
- # when renaming a category, all children categories must have their paths
- # recalculated
- after_update do |record|
- if record.recalculate_path
- record.children.each do |item|
- item.path = item.calculate_path
- item.recalculate_path = true
- item.save!
- end
+ before_save :set_ancestry
+ end
+
+ end
+
+ module ClassMethods
+
+ def build_ancestry(parent_id = nil, ancestry = '')
+ ActiveRecord::Base.transaction do
+ self.base_class.all(:conditions => {:parent_id => parent_id}).each do |node|
+ node.ancestry = ancestry
+ node.send :create_or_update_without_callbacks
+
+ build_ancestry node.id, (ancestry.empty? ? "#{node.formatted_ancestry_id}" :
+ "#{ancestry}#{node.ancestry_sep}#{node.formatted_ancestry_id}")
end
- record.recalculate_path = false
- true
end
+ #raise "Couldn't reach and set ancestry on every record" if self.base_class.count(:conditions => ['ancestry is null']) != 0
+ end
+
+ def has_path?
+ (['name', 'slug', 'path'] - self.column_names).blank?
end
+
end
module InstanceMethods
- # used to know when to trigger batch renaming
- attr_accessor :recalculate_path
- # calculates the full name of a category by accessing the name of all its
- # ancestors.
- #
- # If you have this category hierarchy:
- # Category "A"
- # Category "B"
- # Category "C"
- #
- # Then Category "C" will have "A/B/C" as its full name.
- def full_name(sep = '/')
- self.hierarchy.map {|item| item.name || '?' }.join(sep)
+ def ancestry_column
+ 'ancestry'
+ end
+ def ancestry_sep
+ '.'
+ end
+ def has_ancestry?
+ self.class.column_names.include? self.ancestry_column
+ end
+
+ def formatted_ancestry_id
+ "%010d" % self.id if self.id
end
- # gets the name without leading parents. Usefull when dividing categories
- # in top-level groups and full names must not include the top-level
- # category which is already a emphasized label
- def full_name_without_leading(count, sep = '/')
- parts = self.full_name(sep).split(sep)
- count.times { parts.shift }
- parts.join(sep)
+ def ancestry
+ self[ancestry_column]
+ end
+ def ancestor_ids
+ return nil if !has_ancestry? or ancestry.nil?
+ @ancestor_ids ||= ancestry.split(ancestry_sep).map{ |id| id.to_i }
+ end
+
+ def ancestry=(value)
+ self[ancestry_column] = value
+ end
+ def set_ancestry
+ return unless self.has_ancestry?
+ if self.ancestry.nil? or (new_record? or parent_id_changed?) or recalculate_path
+ self.ancestry = self.hierarchy(true)[0...-1].map{ |p| p.formatted_ancestry_id }.join(ancestry_sep)
+ end
end
- # calculates the level of the category in the category hierarchy. Top-level
- # categories have level 0; the children of the top-level categories have
- # level 1; the children of categories with level 1 have level 2, and so on.
+ # calculates the level of the record in the records hierarchy. Top-level
+ # records have level 0; the children of the top-level records have
+ # level 1; the children of records with level 1 have level 2, and so on.
#
# A level 0
# / \
@@ -82,61 +97,36 @@ module ActsAsFileSystem
# E F G H level 2
# ...
def level
- self.parent ? (self.parent.level + 1) : 0
+ self.hierarchy.size - 1
end
- # Is this category a top-level category?
+ # Is this record a top-level record?
def top_level?
self.parent.nil?
end
- # Is this category a leaf in the hierarchy tree of categories?
+ # Is this record a leaf in the hierarchy tree of records?
#
- # Being a leaf means that this category has no subcategories.
+ # Being a leaf means that this record has no subrecord.
def leaf?
self.children.empty?
end
- def set_name(value)
- if self.name != value
- self.recalculate_path = true
- end
- self[:name] = value
- end
-
- # sets the name of the category. Also sets #slug accordingly.
- def name=(value)
- self.set_name(value)
- unless self.name.blank?
- self.slug = self.name.to_slug
- end
- end
-
- # sets the slug of the category. Also sets the path with the new slug value.
- def slug=(value)
- self[:slug] = value
- unless self.slug.blank?
- self.path = self.calculate_path
+ def top_ancestor
+ if has_ancestry? and !ancestry.nil?
+ self.class.base_class.find_by_id self.top_ancestor_id
+ else
+ self.hierarchy.first
end
end
-
- # calculates the full path to this category using parent's path.
- def calculate_path
- if self.top_level?
- self.slug
+ def top_ancestor_id
+ if has_ancestry? and !ancestry.nil?
+ self.ancestor_ids.first
else
- self.parent.calculate_path + "/" + self.slug
+ self.hierarchy.first.id
end
end
- def top_ancestor
- self.top_level? ? self : self.parent.top_ancestor
- end
-
- def explode_path
- path.split(/\//)
- end
-
# returns the full hierarchy from the top-level item to this one. For
# example, if item1 has a children item2 and item2 has a children item3,
# then item3's hierarchy would be [item1, item2, item3].
@@ -145,16 +135,21 @@ module ActsAsFileSystem
# when the ActiveRecord object was modified in some way, or just after
# changing parent)
def hierarchy(reload = false)
- if reload
- @hierarchy = nil
- end
+ @hierarchy = nil if reload or recalculate_path
- unless @hierarchy
+ if @hierarchy.nil?
@hierarchy = []
- item = self
- while item
- @hierarchy.unshift(item)
- item = item.parent
+
+ if !reload and !recalculate_path and ancestor_ids
+ objects = self.class.base_class.all(:conditions => {:id => ancestor_ids})
+ ancestor_ids.each{ |id| @hierarchy << objects.find{ |t| t.id == id } }
+ @hierarchy << self
+ else
+ item = self
+ while item
+ @hierarchy.unshift(item)
+ item = item.parent
+ end
end
end
@@ -181,8 +176,86 @@ module ActsAsFileSystem
res
end
+ #####
+ # Path methods
+ # These methods are used when _path_, _name_ and _slug_ attributes exist
+ # and should be calculated based on the tree
+ #####
+ module PathMethods
+ # used to know when to trigger batch renaming
+ attr_accessor :recalculate_path
+
+ # calculates the full path to this record using parent's path.
+ def calculate_path
+ self.hierarchy.map{ |obj| obj.slug }.join('/')
+ end
+ def set_path
+ if self.path == self.slug && !self.top_level?
+ self.path = self.calculate_path
+ end
+ end
+ def explode_path
+ path.split(/\//)
+ end
+
+ def update_children_path
+ if self.recalculate_path
+ self.children.each do |child|
+ child.path = child.calculate_path
+ child.recalculate_path = true
+ child.save!
+ end
+ end
+ self.recalculate_path = false
+ end
+
+ # calculates the full name of a record by accessing the name of all its
+ # ancestors.
+ #
+ # If you have this record hierarchy:
+ # Record "A"
+ # Record "B"
+ # Record "C"
+ #
+ # Then Record "C" will have "A/B/C" as its full name.
+ def full_name(sep = '/')
+ self.hierarchy.map {|item| item.name || '?' }.join(sep)
+ end
+
+ # gets the name without leading parents. Useful when dividing records
+ # in top-level groups and full names must not include the top-level
+ # record which is already a emphasized label
+ def full_name_without_leading(count, sep = '/')
+ parts = self.full_name(sep).split(sep)
+ count.times { parts.shift }
+ parts.join(sep)
+ end
+
+ def set_name(value)
+ if self.name != value
+ self.recalculate_path = true
+ end
+ self[:name] = value
+ end
+
+ # sets the name of the record. Also sets #slug accordingly.
+ def name=(value)
+ self.set_name(value)
+ unless self.name.blank?
+ self.slug = self.name.to_slug
+ end
+ end
+
+ # sets the slug of the record. Also sets the path with the new slug value.
+ def slug=(value)
+ self[:slug] = value
+ unless self.slug.blank?
+ self.path = self.calculate_path
+ end
+ end
+ end
end
end
-ActiveRecord::Base.extend ActsAsFileSystem::ClassMethods
+ActiveRecord::Base.extend ActsAsFileSystem::ActsMethods
=====================================
test/unit/acts_as_filesystem_test.rb
=====================================
--- a/test/unit/acts_as_filesystem_test.rb
+++ b/test/unit/acts_as_filesystem_test.rb
@@ -7,13 +7,34 @@ class ActsAsFilesystemTest < ActiveSupport::TestCase
should 'provide a hierarchy list' do
profile = create_user('testinguser').person
- a1 = profile.articles.build(:name => 'a1'); a1.save!
- a2 = profile.articles.build(:name => 'a2'); a2.parent = a1; a2.save!
- a3 = profile.articles.build(:name => 'a3'); a3.parent = a2; a3.save!
+ a1 = profile.articles.create(:name => 'a1')
+ a2 = profile.articles.create(:name => 'a2', :parent => a1)
+ a3 = profile.articles.create(:name => 'a3', :parent => a2)
assert_equal [a1, a2, a3], a3.hierarchy
end
+ should 'set ancestry' do
+ c1 = create(Category, :name => 'c1')
+ c2 = create(Category, :name => 'c2', :parent => c1)
+ c3 = create(Category, :name => 'c3', :parent => c2)
+
+ assert_not_nil c1.ancestry
+ assert_not_nil c2.ancestry
+ assert_equal "%010d.%010d" % [c1.id, c2.id], c3.ancestry
+ assert_equal [c1, c2, c3], c3.hierarchy
+ end
+
+ should 'provide the level' do
+ c1 = create(Category, :name => 'c1')
+ c2 = create(Category, :name => 'c2', :parent => c1)
+ c3 = create(Category, :name => 'c3', :parent => c2)
+
+ assert_equal 0, c1.level
+ assert_equal 1, c2.level
+ assert_equal 2, c3.level
+ end
+
should 'be able to optionally reload the hierarchy' do
a = Article.new
list = a.hierarchy
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://listas.softwarelivre.org/pipermail/noosfero-dev/attachments/20150210/9281de06/attachment-0001.html>
More information about the Noosfero-dev
mailing list