Project

General

Profile

« Previous | Next » 

Revision 9533

Moved Rails plugins required by the core to lib/plugins.

View differences:

trunk/config/initializers/00-core_plugins.rb
1
# Loads the core plugins located in lib/plugins
2
Dir.glob(File.join(Rails.root, "lib/plugins/*")).sort.each do |directory|
3
  if File.directory?(directory)
4
    lib = File.join(directory, "lib")
5
    if File.directory?(lib)
6
      $:.unshift lib
7
      ActiveSupport::Dependencies.autoload_paths += [lib]
8
    end
9
    initializer = File.join(directory, "init.rb")
10
    if File.file?(initializer)
11
      config = config = RedmineApp::Application.config
12
      eval(File.read(initializer), binding, initializer)
13
    end
14
  end
15
end
0 16

  
trunk/lib/plugins/acts_as_activity_provider/init.rb
1
require File.dirname(__FILE__) + '/lib/acts_as_activity_provider'
2
ActiveRecord::Base.send(:include, Redmine::Acts::ActivityProvider)
0 3

  
trunk/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
module Redmine
19
  module Acts
20
    module ActivityProvider
21
      def self.included(base)
22
        base.extend ClassMethods
23
      end
24

  
25
      module ClassMethods
26
        def acts_as_activity_provider(options = {})
27
          unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods)
28
            cattr_accessor :activity_provider_options
29
            send :include, Redmine::Acts::ActivityProvider::InstanceMethods
30
          end
31

  
32
          options.assert_valid_keys(:type, :permission, :timestamp, :author_key, :find_options)
33
          self.activity_provider_options ||= {}
34

  
35
          # One model can provide different event types
36
          # We store these options in activity_provider_options hash
37
          event_type = options.delete(:type) || self.name.underscore.pluralize
38

  
39
          options[:timestamp] ||= "#{table_name}.created_on"
40
          options[:find_options] ||= {}
41
          options[:author_key] = "#{table_name}.#{options[:author_key]}" if options[:author_key].is_a?(Symbol)
42
          self.activity_provider_options[event_type] = options
43
        end
44
      end
45

  
46
      module InstanceMethods
47
        def self.included(base)
48
          base.extend ClassMethods
49
        end
50

  
51
        module ClassMethods
52
          # Returns events of type event_type visible by user that occured between from and to
53
          def find_events(event_type, user, from, to, options)
54
            provider_options = activity_provider_options[event_type]
55
            raise "#{self.name} can not provide #{event_type} events." if provider_options.nil?
56

  
57
            scope = self
58

  
59
            if from && to
60
              scope = scope.scoped(:conditions => ["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
61
            end
62

  
63
            if options[:author]
64
              return [] if provider_options[:author_key].nil?
65
              scope = scope.scoped(:conditions => ["#{provider_options[:author_key]} = ?", options[:author].id])
66
            end
67

  
68
            if options[:limit]
69
              # id and creation time should be in same order in most cases
70
              scope = scope.scoped(:order => "#{table_name}.id DESC", :limit => options[:limit])
71
            end
72

  
73
            if provider_options.has_key?(:permission)
74
              scope = scope.scoped(:conditions => Project.allowed_to_condition(user, provider_options[:permission] || :view_project, options))
75
            elsif respond_to?(:visible)
76
              scope = scope.visible(user, options)
77
            else
78
              ActiveSupport::Deprecation.warn "acts_as_activity_provider with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
79
              scope = scope.scoped(:conditions => Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym, options))
80
            end
81

  
82
            scope.all(provider_options[:find_options].dup)
83
          end
84
        end
85
      end
86
    end
87
  end
88
end
0 89

  
trunk/lib/plugins/acts_as_attachable/init.rb
1
require File.dirname(__FILE__) + '/lib/acts_as_attachable'
2
ActiveRecord::Base.send(:include, Redmine::Acts::Attachable)
0 3

  
trunk/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
module Redmine
19
  module Acts
20
    module Attachable
21
      def self.included(base)
22
        base.extend ClassMethods
23
      end
24

  
25
      module ClassMethods
26
        def acts_as_attachable(options = {})
27
          cattr_accessor :attachable_options
28
          self.attachable_options = {}
29
          attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
30
          attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
31

  
32
          has_many :attachments, options.merge(:as => :container,
33
                                               :order => "#{Attachment.table_name}.created_on",
34
                                               :dependent => :destroy)
35
          send :include, Redmine::Acts::Attachable::InstanceMethods
36
          before_save :attach_saved_attachments
37
        end
38
      end
39

  
40
      module InstanceMethods
41
        def self.included(base)
42
          base.extend ClassMethods
43
        end
44

  
45
        def attachments_visible?(user=User.current)
46
          (respond_to?(:visible?) ? visible?(user) : true) &&
47
            user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
48
        end
49

  
50
        def attachments_deletable?(user=User.current)
51
          (respond_to?(:visible?) ? visible?(user) : true) &&
52
            user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
53
        end
54

  
55
        def saved_attachments
56
          @saved_attachments ||= []
57
        end
58

  
59
        def unsaved_attachments
60
          @unsaved_attachments ||= []
61
        end
62

  
63
        def save_attachments(attachments, author=User.current)
64
          if attachments.is_a?(Hash)
65
            attachments = attachments.values
66
          end
67
          if attachments.is_a?(Array)
68
            attachments.each do |attachment|
69
              a = nil
70
              if file = attachment['file']
71
                next unless file.size > 0
72
                a = Attachment.create(:file => file, :author => author)
73
              elsif token = attachment['token']
74
                a = Attachment.find_by_token(token)
75
                next unless a
76
                a.filename = attachment['filename'] unless attachment['filename'].blank?
77
                a.content_type = attachment['content_type']
78
              end
79
              next unless a
80
              a.description = attachment['description'].to_s.strip
81
              if a.new_record?
82
                unsaved_attachments << a
83
              else
84
                saved_attachments << a
85
              end
86
            end
87
          end
88
          {:files => saved_attachments, :unsaved => unsaved_attachments}
89
        end
90

  
91
        def attach_saved_attachments
92
          saved_attachments.each do |attachment|
93
            self.attachments << attachment
94
          end
95
        end
96

  
97
        module ClassMethods
98
        end
99
      end
100
    end
101
  end
102
end
0 103

  
trunk/lib/plugins/acts_as_customizable/init.rb
1
require File.dirname(__FILE__) + '/lib/acts_as_customizable'
2
ActiveRecord::Base.send(:include, Redmine::Acts::Customizable)
0 3

  
trunk/lib/plugins/acts_as_customizable/lib/acts_as_customizable.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
module Redmine
19
  module Acts
20
    module Customizable
21
      def self.included(base)
22
        base.extend ClassMethods
23
      end
24

  
25
      module ClassMethods
26
        def acts_as_customizable(options = {})
27
          return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
28
          cattr_accessor :customizable_options
29
          self.customizable_options = options
30
          has_many :custom_values, :as => :customized,
31
                                   :include => :custom_field,
32
                                   :order => "#{CustomField.table_name}.position",
33
                                   :dependent => :delete_all,
34
                                   :validate => false
35
          send :include, Redmine::Acts::Customizable::InstanceMethods
36
          validate :validate_custom_field_values
37
          after_save :save_custom_field_values
38
        end
39
      end
40

  
41
      module InstanceMethods
42
        def self.included(base)
43
          base.extend ClassMethods
44
        end
45

  
46
        def available_custom_fields
47
          CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
48
                                 :order => 'position')
49
        end
50

  
51
        # Sets the values of the object's custom fields
52
        # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
53
        def custom_fields=(values)
54
          values_to_hash = values.inject({}) do |hash, v|
55
            v = v.stringify_keys
56
            if v['id'] && v.has_key?('value')
57
              hash[v['id']] = v['value']
58
            end
59
            hash
60
          end
61
          self.custom_field_values = values_to_hash
62
        end
63

  
64
        # Sets the values of the object's custom fields
65
        # values is a hash like {'1' => 'foo', 2 => 'bar'}
66
        def custom_field_values=(values)
67
          values = values.stringify_keys
68

  
69
          custom_field_values.each do |custom_field_value|
70
            key = custom_field_value.custom_field_id.to_s
71
            if values.has_key?(key)
72
              value = values[key]
73
              if value.is_a?(Array)
74
                value = value.reject(&:blank?).uniq
75
                if value.empty?
76
                  value << ''
77
                end
78
              end
79
              custom_field_value.value = value
80
            end
81
          end
82
          @custom_field_values_changed = true
83
        end
84

  
85
        def custom_field_values
86
          @custom_field_values ||= available_custom_fields.collect do |field|
87
            x = CustomFieldValue.new
88
            x.custom_field = field
89
            x.customized = self
90
            if field.multiple?
91
              values = custom_values.select { |v| v.custom_field == field }
92
              if values.empty?
93
                values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
94
              end
95
              x.value = values.map(&:value)
96
            else
97
              cv = custom_values.detect { |v| v.custom_field == field }
98
              cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
99
              x.value = cv.value
100
            end
101
            x
102
          end
103
        end
104

  
105
        def visible_custom_field_values
106
          custom_field_values.select(&:visible?)
107
        end
108

  
109
        def custom_field_values_changed?
110
          @custom_field_values_changed == true
111
        end
112

  
113
        def custom_value_for(c)
114
          field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
115
          custom_values.detect {|v| v.custom_field_id == field_id }
116
        end
117

  
118
        def custom_field_value(c)
119
          field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
120
          custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
121
        end
122

  
123
        def validate_custom_field_values
124
          if new_record? || custom_field_values_changed?
125
            custom_field_values.each(&:validate_value)
126
          end
127
        end
128

  
129
        def save_custom_field_values
130
          target_custom_values = []
131
          custom_field_values.each do |custom_field_value|
132
            if custom_field_value.value.is_a?(Array)
133
              custom_field_value.value.each do |v|
134
                target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
135
                target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
136
                target_custom_values << target
137
              end
138
            else
139
              target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
140
              target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
141
              target.value = custom_field_value.value
142
              target_custom_values << target
143
            end
144
          end
145
          self.custom_values = target_custom_values
146
          custom_values.each(&:save)
147
          @custom_field_values_changed = false
148
          true
149
        end
150

  
151
        def reset_custom_values!
152
          @custom_field_values = nil
153
          @custom_field_values_changed = true
154
        end
155

  
156
        module ClassMethods
157
        end
158
      end
159
    end
160
  end
161
end
0 162

  
trunk/lib/plugins/acts_as_event/init.rb
1
require File.dirname(__FILE__) + '/lib/acts_as_event'
2
ActiveRecord::Base.send(:include, Redmine::Acts::Event)
0 3

  
trunk/lib/plugins/acts_as_event/lib/acts_as_event.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
module Redmine
19
  module Acts
20
    module Event
21
      def self.included(base)
22
        base.extend ClassMethods
23
      end
24

  
25
      module ClassMethods
26
        def acts_as_event(options = {})
27
          return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods)
28
          default_options = { :datetime => :created_on,
29
                              :title => :title,
30
                              :description => :description,
31
                              :author => :author,
32
                              :url => {:controller => 'welcome'},
33
                              :type => self.name.underscore.dasherize }
34

  
35
          cattr_accessor :event_options
36
          self.event_options = default_options.merge(options)
37
          send :include, Redmine::Acts::Event::InstanceMethods
38
        end
39
      end
40

  
41
      module InstanceMethods
42
        def self.included(base)
43
          base.extend ClassMethods
44
        end
45

  
46
        %w(datetime title description author type).each do |attr|
47
          src = <<-END_SRC
48
            def event_#{attr}
49
              option = event_options[:#{attr}]
50
              if option.is_a?(Proc)
51
                option.call(self)
52
              elsif option.is_a?(Symbol)
53
                send(option)
54
              else
55
                option
56
              end
57
            end
58
          END_SRC
59
          class_eval src, __FILE__, __LINE__
60
        end
61

  
62
        def event_date
63
          event_datetime.to_date
64
        end
65

  
66
        def event_url(options = {})
67
          option = event_options[:url]
68
          if option.is_a?(Proc)
69
            option.call(self).merge(options)
70
          elsif option.is_a?(Hash)
71
            option.merge(options)
72
          elsif option.is_a?(Symbol)
73
            send(option).merge(options)
74
          else
75
            option
76
          end
77
        end
78

  
79
        # Returns the mail adresses of users that should be notified
80
        def recipients
81
          notified = project.notified_users
82
          notified.reject! {|user| !visible?(user)}
83
          notified.collect(&:mail)
84
        end
85

  
86
        module ClassMethods
87
        end
88
      end
89
    end
90
  end
91
end
0 92

  
trunk/lib/plugins/acts_as_list/README
1
ActsAsList
2
==========
3

  
4
This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a +position+ column defined as an integer on the mapped database table.
5

  
6

  
7
Example
8
=======
9

  
10
  class TodoList < ActiveRecord::Base
11
    has_many :todo_items, :order => "position"
12
  end
13

  
14
  class TodoItem < ActiveRecord::Base
15
    belongs_to :todo_list
16
    acts_as_list :scope => :todo_list
17
  end
18

  
19
  todo_list.first.move_to_bottom
20
  todo_list.last.move_higher
21

  
22

  
23
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
0 24

  
trunk/lib/plugins/acts_as_list/init.rb
1
$:.unshift "#{File.dirname(__FILE__)}/lib"
2
require 'active_record/acts/list'
3
ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
0 4

  
trunk/lib/plugins/acts_as_list/lib/active_record/acts/list.rb
1
module ActiveRecord
2
  module Acts #:nodoc:
3
    module List #:nodoc:
4
      def self.included(base)
5
        base.extend(ClassMethods)
6
      end
7

  
8
      # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9
      # The class that has this specified needs to have a +position+ column defined as an integer on
10
      # the mapped database table.
11
      #
12
      # Todo list example:
13
      #
14
      #   class TodoList < ActiveRecord::Base
15
      #     has_many :todo_items, :order => "position"
16
      #   end
17
      #
18
      #   class TodoItem < ActiveRecord::Base
19
      #     belongs_to :todo_list
20
      #     acts_as_list :scope => :todo_list
21
      #   end
22
      #
23
      #   todo_list.first.move_to_bottom
24
      #   todo_list.last.move_higher
25
      module ClassMethods
26
        # Configuration options are:
27
        #
28
        # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29
        # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt> 
30
        #   (if it hasn't already been added) and use that as the foreign key restriction. It's also possible 
31
        #   to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
        #   Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
        def acts_as_list(options = {})
34
          configuration = { :column => "position", :scope => "1 = 1" }
35
          configuration.update(options) if options.is_a?(Hash)
36

  
37
          configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38

  
39
          if configuration[:scope].is_a?(Symbol)
40
            scope_condition_method = %(
41
              def scope_condition
42
                if #{configuration[:scope].to_s}.nil?
43
                  "#{configuration[:scope].to_s} IS NULL"
44
                else
45
                  "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46
                end
47
              end
48
            )
49
          else
50
            scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51
          end
52

  
53
          class_eval <<-EOV
54
            include ActiveRecord::Acts::List::InstanceMethods
55

  
56
            def acts_as_list_class
57
              ::#{self.name}
58
            end
59

  
60
            def position_column
61
              '#{configuration[:column]}'
62
            end
63

  
64
            #{scope_condition_method}
65

  
66
            before_destroy :remove_from_list
67
            before_create  :add_to_list_bottom
68
          EOV
69
        end
70
      end
71

  
72
      # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73
      # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74
      # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75
      # the first in the list of all chapters.
76
      module InstanceMethods
77
        # Insert the item at the given position (defaults to the top position of 1).
78
        def insert_at(position = 1)
79
          insert_at_position(position)
80
        end
81

  
82
        # Swap positions with the next lower item, if one exists.
83
        def move_lower
84
          return unless lower_item
85

  
86
          acts_as_list_class.transaction do
87
            lower_item.decrement_position
88
            increment_position
89
          end
90
        end
91

  
92
        # Swap positions with the next higher item, if one exists.
93
        def move_higher
94
          return unless higher_item
95

  
96
          acts_as_list_class.transaction do
97
            higher_item.increment_position
98
            decrement_position
99
          end
100
        end
101

  
102
        # Move to the bottom of the list. If the item is already in the list, the items below it have their
103
        # position adjusted accordingly.
104
        def move_to_bottom
105
          return unless in_list?
106
          acts_as_list_class.transaction do
107
            decrement_positions_on_lower_items
108
            assume_bottom_position
109
          end
110
        end
111

  
112
        # Move to the top of the list. If the item is already in the list, the items above it have their
113
        # position adjusted accordingly.
114
        def move_to_top
115
          return unless in_list?
116
          acts_as_list_class.transaction do
117
            increment_positions_on_higher_items
118
            assume_top_position
119
          end
120
        end
121
        
122
        # Move to the given position
123
        def move_to=(pos)
124
          case pos.to_s
125
          when 'highest'
126
            move_to_top
127
          when 'higher'
128
            move_higher
129
          when 'lower'
130
            move_lower
131
          when 'lowest'
132
            move_to_bottom
133
          end
134
        end
135

  
136
        # Removes the item from the list.
137
        def remove_from_list
138
          if in_list?
139
            decrement_positions_on_lower_items
140
            update_attribute position_column, nil
141
          end
142
        end
143

  
144
        # Increase the position of this item without adjusting the rest of the list.
145
        def increment_position
146
          return unless in_list?
147
          update_attribute position_column, self.send(position_column).to_i + 1
148
        end
149

  
150
        # Decrease the position of this item without adjusting the rest of the list.
151
        def decrement_position
152
          return unless in_list?
153
          update_attribute position_column, self.send(position_column).to_i - 1
154
        end
155

  
156
        # Return +true+ if this object is the first in the list.
157
        def first?
158
          return false unless in_list?
159
          self.send(position_column) == 1
160
        end
161

  
162
        # Return +true+ if this object is the last in the list.
163
        def last?
164
          return false unless in_list?
165
          self.send(position_column) == bottom_position_in_list
166
        end
167

  
168
        # Return the next higher item in the list.
169
        def higher_item
170
          return nil unless in_list?
171
          acts_as_list_class.find(:first, :conditions =>
172
            "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
173
          )
174
        end
175

  
176
        # Return the next lower item in the list.
177
        def lower_item
178
          return nil unless in_list?
179
          acts_as_list_class.find(:first, :conditions =>
180
            "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
181
          )
182
        end
183

  
184
        # Test if this record is in a list
185
        def in_list?
186
          !send(position_column).nil?
187
        end
188

  
189
        private
190
          def add_to_list_top
191
            increment_positions_on_all_items
192
          end
193

  
194
          def add_to_list_bottom
195
            self[position_column] = bottom_position_in_list.to_i + 1
196
          end
197

  
198
          # Overwrite this method to define the scope of the list changes
199
          def scope_condition() "1" end
200

  
201
          # Returns the bottom position number in the list.
202
          #   bottom_position_in_list    # => 2
203
          def bottom_position_in_list(except = nil)
204
            item = bottom_item(except)
205
            item ? item.send(position_column) : 0
206
          end
207

  
208
          # Returns the bottom item
209
          def bottom_item(except = nil)
210
            conditions = scope_condition
211
            conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
212
            acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
213
          end
214

  
215
          # Forces item to assume the bottom position in the list.
216
          def assume_bottom_position
217
            update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
218
          end
219

  
220
          # Forces item to assume the top position in the list.
221
          def assume_top_position
222
            update_attribute(position_column, 1)
223
          end
224

  
225
          # This has the effect of moving all the higher items up one.
226
          def decrement_positions_on_higher_items(position)
227
            acts_as_list_class.update_all(
228
              "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
229
            )
230
          end
231

  
232
          # This has the effect of moving all the lower items up one.
233
          def decrement_positions_on_lower_items
234
            return unless in_list?
235
            acts_as_list_class.update_all(
236
              "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
237
            )
238
          end
239

  
240
          # This has the effect of moving all the higher items down one.
241
          def increment_positions_on_higher_items
242
            return unless in_list?
243
            acts_as_list_class.update_all(
244
              "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
245
            )
246
          end
247

  
248
          # This has the effect of moving all the lower items down one.
249
          def increment_positions_on_lower_items(position)
250
            acts_as_list_class.update_all(
251
              "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
252
           )
253
          end
254

  
255
          # Increments position (<tt>position_column</tt>) of all items in the list.
256
          def increment_positions_on_all_items
257
            acts_as_list_class.update_all(
258
              "#{position_column} = (#{position_column} + 1)",  "#{scope_condition}"
259
            )
260
          end
261

  
262
          def insert_at_position(position)
263
            remove_from_list
264
            increment_positions_on_lower_items(position)
265
            self.update_attribute(position_column, position)
266
          end
267
      end 
268
    end
269
  end
270
end
0 271

  
trunk/lib/plugins/acts_as_list/test/list_test.rb
1
require 'test/unit'
2

  
3
require 'rubygems'
4
gem 'activerecord', '>= 1.15.4.7794'
5
require 'active_record'
6

  
7
require "#{File.dirname(__FILE__)}/../init"
8

  
9
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
10

  
11
def setup_db
12
  ActiveRecord::Schema.define(:version => 1) do
13
    create_table :mixins do |t|
14
      t.column :pos, :integer
15
      t.column :parent_id, :integer
16
      t.column :created_at, :datetime      
17
      t.column :updated_at, :datetime
18
    end
19
  end
20
end
21

  
22
def teardown_db
23
  ActiveRecord::Base.connection.tables.each do |table|
24
    ActiveRecord::Base.connection.drop_table(table)
25
  end
26
end
27

  
28
class Mixin < ActiveRecord::Base
29
end
30

  
31
class ListMixin < Mixin
32
  acts_as_list :column => "pos", :scope => :parent
33

  
34
  def self.table_name() "mixins" end
35
end
36

  
37
class ListMixinSub1 < ListMixin
38
end
39

  
40
class ListMixinSub2 < ListMixin
41
end
42

  
43
class ListWithStringScopeMixin < ActiveRecord::Base
44
  acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
45

  
46
  def self.table_name() "mixins" end
47
end
48

  
49

  
50
class ListTest < Test::Unit::TestCase
51

  
52
  def setup
53
    setup_db
54
    (1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
55
  end
56

  
57
  def teardown
58
    teardown_db
59
  end
60

  
61
  def test_reordering
62
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
63

  
64
    ListMixin.find(2).move_lower
65
    assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
66

  
67
    ListMixin.find(2).move_higher
68
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
69

  
70
    ListMixin.find(1).move_to_bottom
71
    assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
72

  
73
    ListMixin.find(1).move_to_top
74
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
75

  
76
    ListMixin.find(2).move_to_bottom
77
    assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
78

  
79
    ListMixin.find(4).move_to_top
80
    assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
81
  end
82

  
83
  def test_move_to_bottom_with_next_to_last_item
84
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
85
    ListMixin.find(3).move_to_bottom
86
    assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
87
  end
88

  
89
  def test_next_prev
90
    assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
91
    assert_nil ListMixin.find(1).higher_item
92
    assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
93
    assert_nil ListMixin.find(4).lower_item
94
  end
95

  
96
  def test_injection
97
    item = ListMixin.new(:parent_id => 1)
98
    assert_equal "parent_id = 1", item.scope_condition
99
    assert_equal "pos", item.position_column
100
  end
101

  
102
  def test_insert
103
    new = ListMixin.create(:parent_id => 20)
104
    assert_equal 1, new.pos
105
    assert new.first?
106
    assert new.last?
107

  
108
    new = ListMixin.create(:parent_id => 20)
109
    assert_equal 2, new.pos
110
    assert !new.first?
111
    assert new.last?
112

  
113
    new = ListMixin.create(:parent_id => 20)
114
    assert_equal 3, new.pos
115
    assert !new.first?
116
    assert new.last?
117

  
118
    new = ListMixin.create(:parent_id => 0)
119
    assert_equal 1, new.pos
120
    assert new.first?
121
    assert new.last?
122
  end
123

  
124
  def test_insert_at
125
    new = ListMixin.create(:parent_id => 20)
126
    assert_equal 1, new.pos
127

  
128
    new = ListMixin.create(:parent_id => 20)
129
    assert_equal 2, new.pos
130

  
131
    new = ListMixin.create(:parent_id => 20)
132
    assert_equal 3, new.pos
133

  
134
    new4 = ListMixin.create(:parent_id => 20)
135
    assert_equal 4, new4.pos
136

  
137
    new4.insert_at(3)
138
    assert_equal 3, new4.pos
139

  
140
    new.reload
141
    assert_equal 4, new.pos
142

  
143
    new.insert_at(2)
144
    assert_equal 2, new.pos
145

  
146
    new4.reload
147
    assert_equal 4, new4.pos
148

  
149
    new5 = ListMixin.create(:parent_id => 20)
150
    assert_equal 5, new5.pos
151

  
152
    new5.insert_at(1)
153
    assert_equal 1, new5.pos
154

  
155
    new4.reload
156
    assert_equal 5, new4.pos
157
  end
158

  
159
  def test_delete_middle
160
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
161

  
162
    ListMixin.find(2).destroy
163

  
164
    assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
165

  
166
    assert_equal 1, ListMixin.find(1).pos
167
    assert_equal 2, ListMixin.find(3).pos
168
    assert_equal 3, ListMixin.find(4).pos
169

  
170
    ListMixin.find(1).destroy
171

  
172
    assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
173

  
174
    assert_equal 1, ListMixin.find(3).pos
175
    assert_equal 2, ListMixin.find(4).pos
176
  end
177

  
178
  def test_with_string_based_scope
179
    new = ListWithStringScopeMixin.create(:parent_id => 500)
180
    assert_equal 1, new.pos
181
    assert new.first?
182
    assert new.last?
183
  end
184

  
185
  def test_nil_scope
186
    new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
187
    new2.move_higher
188
    assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
189
  end
190
  
191
  
192
  def test_remove_from_list_should_then_fail_in_list? 
193
    assert_equal true, ListMixin.find(1).in_list?
194
    ListMixin.find(1).remove_from_list
195
    assert_equal false, ListMixin.find(1).in_list?
196
  end 
197
  
198
  def test_remove_from_list_should_set_position_to_nil 
199
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
200
  
201
    ListMixin.find(2).remove_from_list 
202
  
203
    assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
204
  
205
    assert_equal 1,   ListMixin.find(1).pos
206
    assert_equal nil, ListMixin.find(2).pos
207
    assert_equal 2,   ListMixin.find(3).pos
208
    assert_equal 3,   ListMixin.find(4).pos
209
  end 
210
  
211
  def test_remove_before_destroy_does_not_shift_lower_items_twice 
212
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
213
  
214
    ListMixin.find(2).remove_from_list 
215
    ListMixin.find(2).destroy 
216
  
217
    assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
218
  
219
    assert_equal 1, ListMixin.find(1).pos
220
    assert_equal 2, ListMixin.find(3).pos
221
    assert_equal 3, ListMixin.find(4).pos
222
  end 
223
  
224
end
225

  
226
class ListSubTest < Test::Unit::TestCase
227

  
228
  def setup
229
    setup_db
230
    (1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
231
  end
232

  
233
  def teardown
234
    teardown_db
235
  end
236

  
237
  def test_reordering
238
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
239

  
240
    ListMixin.find(2).move_lower
241
    assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
242

  
243
    ListMixin.find(2).move_higher
244
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
245

  
246
    ListMixin.find(1).move_to_bottom
247
    assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
248

  
249
    ListMixin.find(1).move_to_top
250
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
251

  
252
    ListMixin.find(2).move_to_bottom
253
    assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
254

  
255
    ListMixin.find(4).move_to_top
256
    assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
257
  end
258

  
259
  def test_move_to_bottom_with_next_to_last_item
260
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
261
    ListMixin.find(3).move_to_bottom
262
    assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
263
  end
264

  
265
  def test_next_prev
266
    assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
267
    assert_nil ListMixin.find(1).higher_item
268
    assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
269
    assert_nil ListMixin.find(4).lower_item
270
  end
271

  
272
  def test_injection
273
    item = ListMixin.new("parent_id"=>1)
274
    assert_equal "parent_id = 1", item.scope_condition
275
    assert_equal "pos", item.position_column
276
  end
277

  
278
  def test_insert_at
279
    new = ListMixin.create("parent_id" => 20)
280
    assert_equal 1, new.pos
281

  
282
    new = ListMixinSub1.create("parent_id" => 20)
283
    assert_equal 2, new.pos
284

  
285
    new = ListMixinSub2.create("parent_id" => 20)
286
    assert_equal 3, new.pos
287

  
288
    new4 = ListMixin.create("parent_id" => 20)
289
    assert_equal 4, new4.pos
290

  
291
    new4.insert_at(3)
292
    assert_equal 3, new4.pos
293

  
294
    new.reload
295
    assert_equal 4, new.pos
296

  
297
    new.insert_at(2)
298
    assert_equal 2, new.pos
299

  
300
    new4.reload
301
    assert_equal 4, new4.pos
302

  
303
    new5 = ListMixinSub1.create("parent_id" => 20)
304
    assert_equal 5, new5.pos
305

  
306
    new5.insert_at(1)
307
    assert_equal 1, new5.pos
308

  
309
    new4.reload
310
    assert_equal 5, new4.pos
311
  end
312

  
313
  def test_delete_middle
314
    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
315

  
316
    ListMixin.find(2).destroy
317

  
318
    assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
319

  
320
    assert_equal 1, ListMixin.find(1).pos
321
    assert_equal 2, ListMixin.find(3).pos
322
    assert_equal 3, ListMixin.find(4).pos
323

  
324
    ListMixin.find(1).destroy
325

  
326
    assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
327

  
328
    assert_equal 1, ListMixin.find(3).pos
329
    assert_equal 2, ListMixin.find(4).pos
330
  end
331

  
332
end
0 333

  
trunk/lib/plugins/acts_as_searchable/init.rb
1
require File.dirname(__FILE__) + '/lib/acts_as_searchable'
2
ActiveRecord::Base.send(:include, Redmine::Acts::Searchable)
0 3

  
trunk/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
module Redmine
19
  module Acts
20
    module Searchable
21
      def self.included(base)
22
        base.extend ClassMethods
23
      end
24

  
25
      module ClassMethods
26
        # Options:
27
        # * :columns - a column or an array of columns to search
28
        # * :project_key - project foreign key (default to project_id)
29
        # * :date_column - name of the datetime column (default to created_on)
30
        # * :sort_order - name of the column used to sort results (default to :date_column or created_on)
31
        # * :permission - permission required to search the model (default to :view_"objects")
32
        def acts_as_searchable(options = {})
33
          return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
34

  
35
          cattr_accessor :searchable_options
36
          self.searchable_options = options
37

  
38
          if searchable_options[:columns].nil?
39
            raise 'No searchable column defined.'
40
          elsif !searchable_options[:columns].is_a?(Array)
41
            searchable_options[:columns] = [] << searchable_options[:columns]
42
          end
43

  
44
          searchable_options[:project_key] ||= "#{table_name}.project_id"
45
          searchable_options[:date_column] ||= "#{table_name}.created_on"
46
          searchable_options[:order_column] ||= searchable_options[:date_column]
47

  
48
          # Should we search custom fields on this model ?
49
          searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
50

  
51
          send :include, Redmine::Acts::Searchable::InstanceMethods
52
        end
53
      end
54

  
55
      module InstanceMethods
56
        def self.included(base)
57
          base.extend ClassMethods
58
        end
59

  
60
        module ClassMethods
61
          # Searches the model for the given tokens
62
          # projects argument can be either nil (will search all projects), a project or an array of projects
63
          # Returns the results and the results count
64
          def search(tokens, projects=nil, options={})
65
            if projects.is_a?(Array) && projects.empty?
66
              # no results
67
              return [[], 0]
68
            end
69

  
70
            # TODO: make user an argument
71
            user = User.current
72
            tokens = [] << tokens unless tokens.is_a?(Array)
73
            projects = [] << projects unless projects.nil? || projects.is_a?(Array)
74

  
75
            find_options = {:include => searchable_options[:include]}
76
            find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
77

  
78
            limit_options = {}
79
            limit_options[:limit] = options[:limit] if options[:limit]
80
            if options[:offset]
81
              limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
82
            end
83

  
84
            columns = searchable_options[:columns]
85
            columns = columns[0..0] if options[:titles_only]
86

  
87
            token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
88

  
89
            if !options[:titles_only] && searchable_options[:search_custom_fields]
90
              searchable_custom_field_ids = CustomField.find(:all,
91
                                                             :select => 'id',
92
                                                             :conditions => { :type => "#{self.name}CustomField",
93
                                                                              :searchable => true }).collect(&:id)
94
              if searchable_custom_field_ids.any?
95
                custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
96
                  " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
97
                  " AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))"
98
                token_clauses << custom_field_sql
99
              end
100
            end
101

  
102
            sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
103

  
104
            find_options[:conditions] = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
105

  
106
            scope = self
107
            project_conditions = []
108
            if searchable_options.has_key?(:permission)
109
              project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project)
110
            elsif respond_to?(:visible)
111
              scope = scope.visible(user)
112
            else
113
              ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
114
              project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym)
115
            end
116
            # TODO: use visible scope options instead
117
            project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
118
            project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ')
119

  
120
            results = []
121
            results_count = 0
122

  
123
            scope = scope.scoped({:conditions => project_conditions}).scoped(find_options)
124
            results_count = scope.count(:all)
125
            results = scope.find(:all, limit_options)
126

  
127
            [results, results_count]
128
          end
129
        end
130
      end
131
    end
132
  end
133
end
0 134

  
trunk/lib/plugins/acts_as_tree/README
1
acts_as_tree
2
============
3

  
4
Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
5
association. This requires that you have a foreign key column, which by default is called +parent_id+.
6

  
7
  class Category < ActiveRecord::Base
8
    acts_as_tree :order => "name"
9
  end
10

  
11
  Example:
12
  root
13
   \_ child1
14
        \_ subchild1
15
        \_ subchild2
16

  
17
  root      = Category.create("name" => "root")
18
  child1    = root.children.create("name" => "child1")
19
  subchild1 = child1.children.create("name" => "subchild1")
20

  
21
  root.parent   # => nil
22
  child1.parent # => root
23
  root.children # => [child1]
24
  root.children.first.children.first # => subchild1
25

  
26
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license  
0 27

  
trunk/lib/plugins/acts_as_tree/Rakefile
1
require 'rake'
2
require 'rake/testtask'
3
require 'rake/rdoctask'
4

  
5
desc 'Default: run unit tests.'
6
task :default => :test
7

  
8
desc 'Test acts_as_tree plugin.'
9
Rake::TestTask.new(:test) do |t|
10
  t.libs << 'lib'
11
  t.pattern = 'test/**/*_test.rb'
12
  t.verbose = true
13
end
14

  
15
desc 'Generate documentation for acts_as_tree plugin.'
16
Rake::RDocTask.new(:rdoc) do |rdoc|
17
  rdoc.rdoc_dir = 'rdoc'
18
  rdoc.title    = 'acts_as_tree'
19
  rdoc.options << '--line-numbers' << '--inline-source'
20
  rdoc.rdoc_files.include('README')
21
  rdoc.rdoc_files.include('lib/**/*.rb')
22
end
0 23

  
trunk/lib/plugins/acts_as_tree/init.rb
1
ActiveRecord::Base.send :include, ActiveRecord::Acts::Tree
0 2

  
trunk/lib/plugins/acts_as_tree/lib/active_record/acts/tree.rb
1
module ActiveRecord
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff