Revision 9533
Added by Jean-Philippe Lang about 13 years ago
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 |
Also available in: Unified diff
Moved Rails plugins required by the core to lib/plugins.