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