Revision 2304
Added by Jean-Philippe Lang over 16 years ago
trunk/app/controllers/admin_controller.rb | ||
---|---|---|
26 | 26 |
end |
27 | 27 |
|
28 | 28 |
def projects |
29 |
sort_init 'name', 'asc' |
|
30 |
sort_update %w(name is_public created_on) |
|
31 |
|
|
32 | 29 |
@status = params[:status] ? params[:status].to_i : 1 |
33 | 30 |
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) |
34 | 31 |
|
... | ... | |
37 | 34 |
c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name] |
38 | 35 |
end |
39 | 36 |
|
40 |
@project_count = Project.count(:conditions => c.conditions) |
|
41 |
@project_pages = Paginator.new self, @project_count, |
|
42 |
per_page_option, |
|
43 |
params['page'] |
|
44 |
@projects = Project.find :all, :order => sort_clause, |
|
45 |
:conditions => c.conditions, |
|
46 |
:limit => @project_pages.items_per_page, |
|
47 |
:offset => @project_pages.current.offset |
|
37 |
@projects = Project.find :all, :order => 'lft', |
|
38 |
:conditions => c.conditions |
|
48 | 39 |
|
49 | 40 |
render :action => "projects", :layout => false if request.xhr? |
50 | 41 |
end |
trunk/app/controllers/projects_controller.rb | ||
---|---|---|
43 | 43 |
|
44 | 44 |
# Lists visible projects |
45 | 45 |
def index |
46 |
projects = Project.find :all, |
|
47 |
:conditions => Project.visible_by(User.current), |
|
48 |
:include => :parent |
|
49 | 46 |
respond_to do |format| |
50 | 47 |
format.html { |
51 |
@project_tree = projects.group_by {|p| p.parent || p} |
|
52 |
@project_tree.keys.each {|p| @project_tree[p] -= [p]} |
|
48 |
@projects = Project.visible.find(:all, :order => 'lft') |
|
53 | 49 |
} |
54 | 50 |
format.atom { |
55 |
render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i), |
|
56 |
:title => "#{Setting.app_title}: #{l(:label_project_latest)}") |
|
51 |
projects = Project.visible.find(:all, :order => 'created_on DESC', |
|
52 |
:limit => Setting.feeds_limit.to_i) |
|
53 |
render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") |
|
57 | 54 |
} |
58 | 55 |
end |
59 | 56 |
end |
... | ... | |
62 | 59 |
def add |
63 | 60 |
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") |
64 | 61 |
@trackers = Tracker.all |
65 |
@root_projects = Project.find(:all, |
|
66 |
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", |
|
67 |
:order => 'name') |
|
68 | 62 |
@project = Project.new(params[:project]) |
69 | 63 |
if request.get? |
70 | 64 |
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? |
... | ... | |
74 | 68 |
else |
75 | 69 |
@project.enabled_module_names = params[:enabled_modules] |
76 | 70 |
if @project.save |
71 |
@project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id') |
|
77 | 72 |
flash[:notice] = l(:notice_successful_create) |
78 | 73 |
redirect_to :controller => 'admin', :action => 'projects' |
79 | 74 |
end |
... | ... | |
88 | 83 |
end |
89 | 84 |
|
90 | 85 |
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} |
91 |
@subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current)) |
|
86 |
@subprojects = @project.children.visible |
|
87 |
@ancestors = @project.ancestors.visible |
|
92 | 88 |
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") |
93 | 89 |
@trackers = @project.rolled_up_trackers |
94 | 90 |
|
... | ... | |
110 | 106 |
end |
111 | 107 |
|
112 | 108 |
def settings |
113 |
@root_projects = Project.find(:all, |
|
114 |
:conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], |
|
115 |
:order => 'name') |
|
116 | 109 |
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") |
117 | 110 |
@issue_category ||= IssueCategory.new |
118 | 111 |
@member ||= @project.members.new |
... | ... | |
126 | 119 |
if request.post? |
127 | 120 |
@project.attributes = params[:project] |
128 | 121 |
if @project.save |
122 |
@project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id') |
|
129 | 123 |
flash[:notice] = l(:notice_successful_update) |
130 | 124 |
redirect_to :action => 'settings', :id => @project |
131 | 125 |
else |
trunk/app/controllers/reports_controller.rb | ||
---|---|---|
61 | 61 |
render :template => "reports/issue_report_details" |
62 | 62 |
when "subproject" |
63 | 63 |
@field = "project_id" |
64 |
@rows = @project.active_children
|
|
64 |
@rows = @project.descendants.active
|
|
65 | 65 |
@data = issues_by_subproject |
66 | 66 |
@report_title = l(:field_subproject) |
67 | 67 |
render :template => "reports/issue_report_details" |
... | ... | |
72 | 72 |
@categories = @project.issue_categories |
73 | 73 |
@assignees = @project.members.collect { |m| m.user } |
74 | 74 |
@authors = @project.members.collect { |m| m.user } |
75 |
@subprojects = @project.active_children
|
|
75 |
@subprojects = @project.descendants.active
|
|
76 | 76 |
issues_by_tracker |
77 | 77 |
issues_by_version |
78 | 78 |
issues_by_priority |
... | ... | |
229 | 229 |
#{Issue.table_name} i, #{IssueStatus.table_name} s |
230 | 230 |
where |
231 | 231 |
i.status_id=s.id |
232 |
and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
|
|
233 |
group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
|
|
232 |
and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
|
|
233 |
group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
|
|
234 | 234 |
@issues_by_subproject ||= [] |
235 | 235 |
end |
236 | 236 |
end |
trunk/app/controllers/search_controller.rb | ||
---|---|---|
34 | 34 |
when 'my_projects' |
35 | 35 |
User.current.memberships.collect(&:project) |
36 | 36 |
when 'subprojects' |
37 |
@project ? ([ @project ] + @project.active_children) : nil
|
|
37 |
@project ? (@project.self_and_descendants.active) : nil
|
|
38 | 38 |
else |
39 | 39 |
@project |
40 | 40 |
end |
trunk/app/controllers/users_controller.rb | ||
---|---|---|
83 | 83 |
end |
84 | 84 |
@auth_sources = AuthSource.find(:all) |
85 | 85 |
@roles = Role.find_all_givable |
86 |
@projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
|
|
86 |
@projects = Project.active.find(:all, :order => 'lft')
|
|
87 | 87 |
@membership ||= Member.new |
88 | 88 |
@memberships = @user.memberships |
89 | 89 |
end |
trunk/app/helpers/admin_helper.rb | ||
---|---|---|
20 | 20 |
options_for_select([[l(:label_all), ''], |
21 | 21 |
[l(:status_active), 1]], selected) |
22 | 22 |
end |
23 |
|
|
24 |
def css_project_classes(project) |
|
25 |
s = 'project' |
|
26 |
s << ' root' if project.root? |
|
27 |
s << ' child' if project.child? |
|
28 |
s << (project.leaf? ? ' leaf' : ' parent') |
|
29 |
s |
|
30 |
end |
|
23 | 31 |
end |
trunk/app/helpers/application_helper.rb | ||
---|---|---|
156 | 156 |
end |
157 | 157 |
s |
158 | 158 |
end |
159 |
|
|
160 |
# Renders the project quick-jump box |
|
161 |
def render_project_jump_box |
|
162 |
# Retrieve them now to avoid a COUNT query |
|
163 |
projects = User.current.projects.all |
|
164 |
if projects.any? |
|
165 |
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' + |
|
166 |
"<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" + |
|
167 |
'<option disabled="disabled">---</option>' |
|
168 |
s << project_tree_options_for_select(projects) do |p| |
|
169 |
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p) } |
|
170 |
end |
|
171 |
s << '</select>' |
|
172 |
s |
|
173 |
end |
|
174 |
end |
|
175 |
|
|
176 |
def project_tree_options_for_select(projects, options = {}) |
|
177 |
s = '' |
|
178 |
project_tree(projects) do |project, level| |
|
179 |
name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '') |
|
180 |
tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)} |
|
181 |
tag_options.merge!(yield(project)) if block_given? |
|
182 |
s << content_tag('option', name_prefix + h(project), tag_options) |
|
183 |
end |
|
184 |
s |
|
185 |
end |
|
186 |
|
|
187 |
# Yields the given block for each project with its level in the tree |
|
188 |
def project_tree(projects, &block) |
|
189 |
ancestors = [] |
|
190 |
projects.sort_by(&:lft).each do |project| |
|
191 |
while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) |
|
192 |
ancestors.pop |
|
193 |
end |
|
194 |
yield project, ancestors.size |
|
195 |
ancestors << project |
|
196 |
end |
|
197 |
end |
|
159 | 198 |
|
160 | 199 |
# Truncates and returns the string as a single line |
161 | 200 |
def truncate_single_line(string, *args) |
trunk/app/helpers/projects_helper.rb | ||
---|---|---|
33 | 33 |
] |
34 | 34 |
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} |
35 | 35 |
end |
36 |
|
|
37 |
def parent_project_select_tag(project) |
|
38 |
options = '<option></option>' + project_tree_options_for_select(project.possible_parents, :selected => project.parent) |
|
39 |
content_tag('select', options, :name => 'project[parent_id]') |
|
40 |
end |
|
41 |
|
|
42 |
# Renders a tree of projects as a nested set of unordered lists |
|
43 |
# The given collection may be a subset of the whole project tree |
|
44 |
# (eg. some intermediate nodes are private and can not be seen) |
|
45 |
def render_project_hierarchy(projects) |
|
46 |
s = '' |
|
47 |
if projects.any? |
|
48 |
ancestors = [] |
|
49 |
projects.each do |project| |
|
50 |
if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) |
|
51 |
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n" |
|
52 |
else |
|
53 |
ancestors.pop |
|
54 |
s << "</li>" |
|
55 |
while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) |
|
56 |
ancestors.pop |
|
57 |
s << "</ul></li>\n" |
|
58 |
end |
|
59 |
end |
|
60 |
classes = (ancestors.empty? ? 'root' : 'child') |
|
61 |
s << "<li class='#{classes}'><div class='#{classes}'>" + |
|
62 |
link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") |
|
63 |
s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank? |
|
64 |
s << "</div>\n" |
|
65 |
ancestors << project |
|
66 |
end |
|
67 |
s << ("</li></ul>\n" * ancestors.size) |
|
68 |
end |
|
69 |
s |
|
70 |
end |
|
36 | 71 |
end |
trunk/app/helpers/search_helper.rb | ||
---|---|---|
44 | 44 |
def project_select_tag |
45 | 45 |
options = [[l(:label_project_all), 'all']] |
46 | 46 |
options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? |
47 |
options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.active_children.empty?
|
|
47 |
options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
|
|
48 | 48 |
options << [@project.name, ''] unless @project.nil? |
49 | 49 |
select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 |
50 | 50 |
end |
trunk/app/helpers/users_helper.rb | ||
---|---|---|
25 | 25 |
end |
26 | 26 |
|
27 | 27 |
# Options for the new membership projects combo-box |
28 |
def projects_options_for_select(projects)
|
|
28 |
def options_for_membership_project_select(user, projects)
|
|
29 | 29 |
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") |
30 |
projects_by_root = projects.group_by(&:root) |
|
31 |
projects_by_root.keys.sort.each do |root| |
|
32 |
options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root))) |
|
33 |
projects_by_root[root].sort.each do |project| |
|
34 |
next if project == root |
|
35 |
options << content_tag('option', '» ' + h(project.name), :value => project.id) |
|
36 |
end |
|
30 |
options << project_tree_options_for_select(projects) do |p| |
|
31 |
{:disabled => (user.projects.include?(p))} |
|
37 | 32 |
end |
38 | 33 |
options |
39 | 34 |
end |
trunk/app/models/project.rb | ||
---|---|---|
43 | 43 |
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", |
44 | 44 |
:association_foreign_key => 'custom_field_id' |
45 | 45 |
|
46 |
acts_as_tree :order => "name", :counter_cache => true
|
|
46 |
acts_as_nested_set :order => 'name', :dependent => :destroy
|
|
47 | 47 |
acts_as_attachable :view_permission => :view_files, |
48 | 48 |
:delete_permission => :manage_files |
49 | 49 |
|
... | ... | |
66 | 66 |
before_destroy :delete_all_members |
67 | 67 |
|
68 | 68 |
named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } |
69 |
named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} |
|
70 |
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } } |
|
69 | 71 |
|
70 | 72 |
def identifier=(identifier) |
71 | 73 |
super unless identifier_frozen? |
... | ... | |
78 | 80 |
def issues_with_subprojects(include_subprojects=false) |
79 | 81 |
conditions = nil |
80 | 82 |
if include_subprojects |
81 |
ids = [id] + child_ids
|
|
83 |
ids = [id] + descendants.collect(&:id)
|
|
82 | 84 |
conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"] |
83 | 85 |
end |
84 | 86 |
conditions ||= ["#{Project.table_name}.id = ?", id] |
... | ... | |
118 | 120 |
end |
119 | 121 |
if options[:project] |
120 | 122 |
project_statement = "#{Project.table_name}.id = #{options[:project].id}" |
121 |
project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
|
|
123 |
project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
|
|
122 | 124 |
base_statement = "(#{project_statement}) AND (#{base_statement})" |
123 | 125 |
end |
124 | 126 |
if user.admin? |
... | ... | |
141 | 143 |
|
142 | 144 |
def project_condition(with_subprojects) |
143 | 145 |
cond = "#{Project.table_name}.id = #{id}" |
144 |
cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
|
|
146 |
cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
|
|
145 | 147 |
cond |
146 | 148 |
end |
147 | 149 |
|
... | ... | |
164 | 166 |
self.status == STATUS_ACTIVE |
165 | 167 |
end |
166 | 168 |
|
169 |
# Archives the project and its descendants recursively |
|
167 | 170 |
def archive |
168 | 171 |
# Archive subprojects if any |
169 | 172 |
children.each do |subproject| |
... | ... | |
172 | 175 |
update_attribute :status, STATUS_ARCHIVED |
173 | 176 |
end |
174 | 177 |
|
178 |
# Unarchives the project |
|
179 |
# All its ancestors must be active |
|
175 | 180 |
def unarchive |
176 |
return false if parent && !parent.active?
|
|
181 |
return false if ancestors.detect {|a| !a.active?}
|
|
177 | 182 |
update_attribute :status, STATUS_ACTIVE |
178 | 183 |
end |
179 | 184 |
|
180 |
def active_children |
|
181 |
children.select {|child| child.active?} |
|
185 |
# Returns an array of projects the project can be moved to |
|
186 |
def possible_parents |
|
187 |
@possible_parents ||= (Project.active.find(:all) - self_and_descendants) |
|
182 | 188 |
end |
183 | 189 |
|
190 |
# Sets the parent of the project |
|
191 |
# Argument can be either a Project, a String, a Fixnum or nil |
|
192 |
def set_parent!(p) |
|
193 |
unless p.nil? || p.is_a?(Project) |
|
194 |
if p.to_s.blank? |
|
195 |
p = nil |
|
196 |
else |
|
197 |
p = Project.find_by_id(p) |
|
198 |
return false unless p |
|
199 |
end |
|
200 |
end |
|
201 |
if p == parent && !p.nil? |
|
202 |
# Nothing to do |
|
203 |
true |
|
204 |
elsif p.nil? || (p.active? && move_possible?(p)) |
|
205 |
# Insert the project so that target's children or root projects stay alphabetically sorted |
|
206 |
sibs = (p.nil? ? self.class.roots : p.children) |
|
207 |
to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase } |
|
208 |
if to_be_inserted_before |
|
209 |
move_to_left_of(to_be_inserted_before) |
|
210 |
elsif p.nil? |
|
211 |
if sibs.empty? |
|
212 |
# move_to_root adds the project in first (ie. left) position |
|
213 |
move_to_root |
|
214 |
else |
|
215 |
move_to_right_of(sibs.last) unless self == sibs.last |
|
216 |
end |
|
217 |
else |
|
218 |
# move_to_child_of adds the project in last (ie.right) position |
|
219 |
move_to_child_of(p) |
|
220 |
end |
|
221 |
true |
|
222 |
else |
|
223 |
# Can not move to the given target |
|
224 |
false |
|
225 |
end |
|
226 |
end |
|
227 |
|
|
184 | 228 |
# Returns an array of the trackers used by the project and its sub projects |
185 | 229 |
def rolled_up_trackers |
186 | 230 |
@rolled_up_trackers ||= |
187 | 231 |
Tracker.find(:all, :include => :projects, |
188 | 232 |
:select => "DISTINCT #{Tracker.table_name}.*", |
189 |
:conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
|
|
233 |
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt],
|
|
190 | 234 |
:order => "#{Tracker.table_name}.position") |
191 | 235 |
end |
192 | 236 |
|
... | ... | |
225 | 269 |
|
226 | 270 |
# Returns a short description of the projects (first lines) |
227 | 271 |
def short_description(length = 255) |
228 |
description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
|
|
272 |
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
|
|
229 | 273 |
end |
230 | 274 |
|
231 | 275 |
def allows_to?(action) |
... | ... | |
257 | 301 |
|
258 | 302 |
protected |
259 | 303 |
def validate |
260 |
errors.add(parent_id, " must be a root project") if parent and parent.parent |
|
261 |
errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0 |
|
262 | 304 |
errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/) |
263 | 305 |
end |
264 | 306 |
|
trunk/app/models/query.rb | ||
---|---|---|
174 | 174 |
unless @project.versions.empty? |
175 | 175 |
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } |
176 | 176 |
end |
177 |
unless @project.active_children.empty?
|
|
178 |
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
|
|
177 |
unless @project.descendants.active.empty?
|
|
178 |
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
|
|
179 | 179 |
end |
180 | 180 |
add_custom_fields_filters(@project.all_issue_custom_fields) |
181 | 181 |
else |
... | ... | |
257 | 257 |
|
258 | 258 |
def project_statement |
259 | 259 |
project_clauses = [] |
260 |
if project && !@project.active_children.empty?
|
|
260 |
if project && !@project.descendants.active.empty?
|
|
261 | 261 |
ids = [project.id] |
262 | 262 |
if has_filter?("subproject_id") |
263 | 263 |
case operator_for("subproject_id") |
... | ... | |
268 | 268 |
# main project only |
269 | 269 |
else |
270 | 270 |
# all subprojects |
271 |
ids += project.child_ids
|
|
271 |
ids += project.descendants.collect(&:id)
|
|
272 | 272 |
end |
273 | 273 |
elsif Setting.display_subprojects_issues? |
274 |
ids += project.child_ids
|
|
274 |
ids += project.descendants.collect(&:id)
|
|
275 | 275 |
end |
276 | 276 |
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') |
277 | 277 |
elsif project |
trunk/app/views/admin/projects.rhtml | ||
---|---|---|
17 | 17 |
|
18 | 18 |
<table class="list"> |
19 | 19 |
<thead><tr> |
20 |
<%= sort_header_tag('name', :caption => l(:label_project)) %>
|
|
20 |
<th><%=l(:label_project)%></th>
|
|
21 | 21 |
<th><%=l(:field_description)%></th> |
22 |
<th><%=l(:label_subproject_plural)%></th> |
|
23 |
<%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %> |
|
24 |
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> |
|
22 |
<th><%=l(:field_is_public)%></th> |
|
23 |
<th><%=l(:field_created_on)%></th> |
|
25 | 24 |
<th></th> |
26 | 25 |
<th></th> |
27 | 26 |
</tr></thead> |
28 | 27 |
<tbody> |
29 | 28 |
<% for project in @projects %> |
30 |
<tr class="<%= cycle("odd", "even") %>"> |
|
31 |
<td><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> |
|
32 |
<td><%= textilizable project.short_description, :project => project %> |
|
33 |
<td align="center"><%= project.children.size %> |
|
34 |
<td align="center"><%= image_tag 'true.png' if project.is_public? %> |
|
35 |
<td align="center"><%= format_date(project.created_on) %> |
|
29 |
<tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>"> |
|
30 |
<td class="name" style="padding-left: <%= project.level %>em;"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td> |
|
31 |
<td><%= textilizable project.short_description, :project => project %></td> |
|
32 |
<td align="center"><%= image_tag 'true.png' if project.is_public? %></td> |
|
33 |
<td align="center"><%= format_date(project.created_on) %></td> |
|
36 | 34 |
<td align="center" style="width:10%"> |
37 | 35 |
<small> |
38 | 36 |
<%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %> |
... | ... | |
47 | 45 |
</tbody> |
48 | 46 |
</table> |
49 | 47 |
|
50 |
<p class="pagination"><%= pagination_links_full @project_pages, @project_count %></p> |
|
51 |
|
|
52 | 48 |
<% html_title(l(:label_project_plural)) -%> |
trunk/app/views/layouts/_project_selector.rhtml | ||
---|---|---|
1 |
<% user_projects_by_root = User.current.projects.find(:all, :include => :parent).group_by(&:root) %> |
|
2 |
<select onchange="if (this.value != '') { window.location = this.value; }"> |
|
3 |
<option selected="selected" value=""><%= l(:label_jump_to_a_project) %></option> |
|
4 |
<option disabled="disabled" value="">---</option> |
|
5 |
<% user_projects_by_root.keys.sort.each do |root| %> |
|
6 |
<%= content_tag('option', h(root.name), :value => url_for(:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item)) %> |
|
7 |
<% user_projects_by_root[root].sort.each do |project| %> |
|
8 |
<% next if project == root %> |
|
9 |
<%= content_tag('option', ('» ' + h(project.name)), :value => url_for(:controller => 'projects', :action => 'show', :id => project, :jump => current_menu_item)) %> |
|
10 |
<% end %> |
|
11 |
<% end %> |
|
12 |
</select> |
|
13 | 0 |
trunk/app/views/layouts/base.rhtml | ||
---|---|---|
34 | 34 |
<%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>: |
35 | 35 |
<%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %> |
36 | 36 |
<% end %> |
37 |
<%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
|
|
37 |
<%= render_project_jump_box %>
|
|
38 | 38 |
</div> |
39 | 39 |
|
40 | 40 |
<h1><%= h(@project && [email protected]_record? ? @project.name : Setting.app_title) %></h1> |
trunk/app/views/projects/_form.rhtml | ||
---|---|---|
4 | 4 |
<!--[form:project]--> |
5 | 5 |
<p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p> |
6 | 6 |
|
7 |
<% if User.current.admin? and !@root_projects.empty? %>
|
|
8 |
<p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p>
|
|
7 |
<% if User.current.admin? && [email protected]_parents.empty? %>
|
|
8 |
<p><label><%= l(:field_parent) %></label><%= parent_project_select_tag(@project) %></p>
|
|
9 | 9 |
<% end %> |
10 | 10 |
|
11 | 11 |
<p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p> |
trunk/app/views/projects/activity.rhtml | ||
---|---|---|
48 | 48 |
<p><% @activity.event_types.each do |t| %> |
49 | 49 |
<label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> |
50 | 50 |
<% end %></p> |
51 |
<% if @project && @project.active_children.any? %>
|
|
51 |
<% if @project && @project.descendants.active.any? %>
|
|
52 | 52 |
<p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p> |
53 | 53 |
<%= hidden_field_tag 'with_subprojects', 0 %> |
54 | 54 |
<% end %> |
trunk/app/views/projects/destroy.rhtml | ||
---|---|---|
3 | 3 |
<p><strong><%=h @project_to_destroy %></strong><br /> |
4 | 4 |
<%=l(:text_project_destroy_confirmation)%> |
5 | 5 |
|
6 |
<% if @project_to_destroy.children.any? %>
|
|
7 |
<br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.children.sort.collect{|p| p.to_s}.join(', ')))) %>
|
|
6 |
<% if @project_to_destroy.descendants.any? %>
|
|
7 |
<br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %>
|
|
8 | 8 |
<% end %> |
9 | 9 |
</p> |
10 | 10 |
<p> |
trunk/app/views/projects/index.rhtml | ||
---|---|---|
6 | 6 |
|
7 | 7 |
<h2><%=l(:label_project_plural)%></h2> |
8 | 8 |
|
9 |
<% @project_tree.keys.sort.each do |project| %> |
|
10 |
<h3><%= link_to h(project.name), {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %></h3> |
|
11 |
<%= textilizable(project.short_description, :project => project) %> |
|
9 |
<%= render_project_hierarchy(@projects)%> |
|
12 | 10 |
|
13 |
<% if @project_tree[project].any? %> |
|
14 |
<p><%= l(:label_subproject_plural) %>: |
|
15 |
<%= @project_tree[project].sort.collect {|subproject| |
|
16 |
link_to(h(subproject.name), {:action => 'show', :id => subproject}, :class => (User.current.member_of?(subproject) ? "icon icon-fav" : ""))}.join(', ') %></p> |
|
17 |
<% end %> |
|
18 |
<% end %> |
|
19 |
|
|
20 | 11 |
<% if User.current.logged? %> |
21 | 12 |
<p style="text-align:right;"> |
22 |
<span class="icon icon-fav"><%= l(:label_my_projects) %></span>
|
|
13 |
<span class="my-project"><%= l(:label_my_projects) %></span>
|
|
23 | 14 |
</p> |
24 | 15 |
<% end %> |
25 | 16 |
|
trunk/app/views/projects/show.rhtml | ||
---|---|---|
4 | 4 |
<%= textilizable @project.description %> |
5 | 5 |
<ul> |
6 | 6 |
<% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %> |
7 |
<% if @subprojects.any? %> |
|
8 |
<li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li> |
|
9 |
<% end %> |
|
10 |
<% if @project.parent %> |
|
11 |
<li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li> |
|
7 |
<% if @subprojects.any? %> |
|
8 |
<li><%=l(:label_subproject_plural)%>: |
|
9 |
<%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li> |
|
10 |
<% end %> |
|
11 |
<% if @ancestors.any? %> |
|
12 |
<li><%=l(:field_parent)%>: |
|
13 |
<%= @ancestors.collect {|p| link_to(h(p), :action => 'show', :id => p)}.join(" » ") %></li> |
|
12 | 14 |
<% end %> |
13 | 15 |
<% @project.custom_values.each do |custom_value| %> |
14 | 16 |
<% if !custom_value.value.empty? %> |
trunk/app/views/users/_memberships.rhtml | ||
---|---|---|
31 | 31 |
<p> |
32 | 32 |
<label><%=l(:label_project_new)%></label><br/> |
33 | 33 |
<% form_tag({ :action => 'edit_membership', :id => @user }) do %> |
34 |
<%= select_tag 'membership[project_id]', projects_options_for_select(@projects) %>
|
|
34 |
<%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, @projects) %>
|
|
35 | 35 |
<%= l(:label_role) %>: |
36 | 36 |
<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %> |
37 | 37 |
<%= submit_tag l(:button_add) %> |
trunk/db/migrate/104_add_projects_lft_and_rgt.rb | ||
---|---|---|
1 |
class AddProjectsLftAndRgt < ActiveRecord::Migration |
|
2 |
def self.up |
|
3 |
add_column :projects, :lft, :integer |
|
4 |
add_column :projects, :rgt, :integer |
|
5 |
end |
|
6 |
|
|
7 |
def self.down |
|
8 |
remove_column :projects, :lft |
|
9 |
remove_column :projects, :rgt |
|
10 |
end |
|
11 |
end |
|
0 | 12 |
trunk/db/migrate/105_build_projects_tree.rb | ||
---|---|---|
1 |
class BuildProjectsTree < ActiveRecord::Migration |
|
2 |
def self.up |
|
3 |
Project.rebuild! |
|
4 |
end |
|
5 |
|
|
6 |
def self.down |
|
7 |
end |
|
8 |
end |
|
0 | 9 |
trunk/public/stylesheets/application.css | ||
---|---|---|
85 | 85 |
table.list td.id { width: 2%; text-align: center;} |
86 | 86 |
table.list td.checkbox { width: 15px; padding: 0px;} |
87 | 87 |
|
88 |
tr.project td.name a { padding-left: 16px; white-space:nowrap; } |
|
89 |
tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; } |
|
90 |
|
|
88 | 91 |
tr.issue { text-align: center; white-space: nowrap; } |
89 | 92 |
tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; } |
90 | 93 |
tr.issue td.subject { text-align: left; } |
... | ... | |
235 | 238 |
form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; } |
236 | 239 |
form#issue-form .attributes select { min-width: 30%; } |
237 | 240 |
|
241 |
ul.projects { margin: 0; padding-left: 1em; } |
|
242 |
ul.projects.root { margin: 0; padding: 0; } |
|
243 |
ul.projects ul { border-left: 3px solid #e0e0e0; } |
|
244 |
ul.projects li { list-style-type:none; } |
|
245 |
ul.projects li.root { margin-bottom: 1em; } |
|
246 |
ul.projects li.child { margin-top: 1em;} |
|
247 |
ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; } |
|
248 |
.my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; } |
|
249 |
|
|
238 | 250 |
ul.properties {padding:0; font-size: 0.9em; color: #777;} |
239 | 251 |
ul.properties li {list-style-type:none;} |
240 | 252 |
ul.properties li span {font-style:italic;} |
trunk/test/fixtures/projects.yml | ||
---|---|---|
10 | 10 |
is_public: true |
11 | 11 |
identifier: ecookbook |
12 | 12 |
parent_id: |
13 |
lft: 1 |
|
14 |
rgt: 10 |
|
13 | 15 |
projects_002: |
14 | 16 |
created_on: 2006-07-19 19:14:19 +02:00 |
15 | 17 |
name: OnlineStore |
... | ... | |
21 | 23 |
is_public: false |
22 | 24 |
identifier: onlinestore |
23 | 25 |
parent_id: |
26 |
lft: 11 |
|
27 |
rgt: 12 |
|
24 | 28 |
projects_003: |
25 | 29 |
created_on: 2006-07-19 19:15:21 +02:00 |
26 | 30 |
name: eCookbook Subproject 1 |
... | ... | |
32 | 36 |
is_public: true |
33 | 37 |
identifier: subproject1 |
34 | 38 |
parent_id: 1 |
39 |
lft: 6 |
|
40 |
rgt: 7 |
|
35 | 41 |
projects_004: |
36 | 42 |
created_on: 2006-07-19 19:15:51 +02:00 |
37 | 43 |
name: eCookbook Subproject 2 |
... | ... | |
43 | 49 |
is_public: true |
44 | 50 |
identifier: subproject2 |
45 | 51 |
parent_id: 1 |
52 |
lft: 8 |
|
53 |
rgt: 9 |
|
46 | 54 |
projects_005: |
47 | 55 |
created_on: 2006-07-19 19:15:51 +02:00 |
48 | 56 |
name: Private child of eCookbook |
... | ... | |
52 | 60 |
description: This is a private subproject of a public project |
53 | 61 |
homepage: "" |
54 | 62 |
is_public: false |
55 |
identifier: private_child
|
|
63 |
identifier: private-child
|
|
56 | 64 |
parent_id: 1 |
65 |
lft: 2 |
|
66 |
rgt: 5 |
|
67 |
projects_006: |
|
68 |
created_on: 2006-07-19 19:15:51 +02:00 |
|
69 |
name: Child of private child |
|
70 |
updated_on: 2006-07-19 19:17:07 +02:00 |
|
71 |
projects_count: 0 |
|
72 |
id: 6 |
|
73 |
description: This is a public subproject of a private project |
|
74 |
homepage: "" |
|
75 |
is_public: true |
|
76 |
identifier: project6 |
|
77 |
parent_id: 5 |
|
78 |
lft: 3 |
|
79 |
rgt: 4 |
|
57 | 80 |
|
trunk/test/functional/projects_controller_test.rb | ||
---|---|---|
38 | 38 |
get :index |
39 | 39 |
assert_response :success |
40 | 40 |
assert_template 'index' |
41 |
assert_not_nil assigns(:project_tree) |
|
42 |
# Root project as hash key |
|
43 |
assert assigns(:project_tree).keys.include?(Project.find(1)) |
|
44 |
# Subproject in corresponding value |
|
45 |
assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3)) |
|
41 |
assert_not_nil assigns(:projects) |
|
42 |
|
|
43 |
assert_tag :ul, :child => {:tag => 'li', |
|
44 |
:descendant => {:tag => 'a', :content => 'eCookbook'}, |
|
45 |
:child => { :tag => 'ul', |
|
46 |
:descendant => { :tag => 'a', |
|
47 |
:content => 'Child of private child' |
|
48 |
} |
|
49 |
} |
|
50 |
} |
|
51 |
|
|
52 |
assert_no_tag :a, :content => /Private child of eCookbook/ |
|
46 | 53 |
end |
47 | 54 |
|
48 | 55 |
def test_index_atom |
trunk/test/unit/project_test.rb | ||
---|---|---|
45 | 45 |
assert_equal "activerecord_error_blank", @ecookbook.errors.on(:name) |
46 | 46 |
end |
47 | 47 |
|
48 |
def test_public_projects |
|
49 |
public_projects = Project.find(:all, :conditions => ["is_public=?", true]) |
|
50 |
assert_equal 3, public_projects.length |
|
51 |
assert_equal true, public_projects[0].is_public? |
|
52 |
end |
|
53 |
|
|
54 | 48 |
def test_archive |
55 | 49 |
user = @ecookbook.members.first.user |
56 | 50 |
@ecookbook.archive |
... | ... | |
60 | 54 |
assert !user.projects.include?(@ecookbook) |
61 | 55 |
# Subproject are also archived |
62 | 56 |
assert [email protected]? |
63 |
assert @ecookbook.active_children.empty?
|
|
57 |
assert @ecookbook.descendants.active.empty?
|
|
64 | 58 |
end |
65 | 59 |
|
66 | 60 |
def test_unarchive |
... | ... | |
95 | 89 |
assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty? |
96 | 90 |
end |
97 | 91 |
|
98 |
def test_subproject_ok
|
|
92 |
def test_move_an_orphan_project_to_a_root_project
|
|
99 | 93 |
sub = Project.find(2) |
100 |
sub.parent = @ecookbook |
|
101 |
assert sub.save |
|
94 |
sub.set_parent! @ecookbook |
|
102 | 95 |
assert_equal @ecookbook.id, sub.parent.id |
103 | 96 |
@ecookbook.reload |
104 | 97 |
assert_equal 4, @ecookbook.children.size |
105 | 98 |
end |
106 | 99 |
|
107 |
def test_subproject_invalid
|
|
100 |
def test_move_an_orphan_project_to_a_subproject
|
|
108 | 101 |
sub = Project.find(2) |
109 |
sub.parent = @ecookbook_sub1 |
|
110 |
assert !sub.save |
|
102 |
assert sub.set_parent!(@ecookbook_sub1) |
|
111 | 103 |
end |
112 | 104 |
|
113 |
def test_subproject_invalid_2
|
|
105 |
def test_move_a_root_project_to_a_project
|
|
114 | 106 |
sub = @ecookbook |
115 |
sub.parent = Project.find(2) |
|
116 |
assert !sub.save |
|
107 |
assert sub.set_parent!(Project.find(2)) |
|
117 | 108 |
end |
118 | 109 |
|
110 |
def test_should_not_move_a_project_to_its_children |
|
111 |
sub = @ecookbook |
|
112 |
assert !(sub.set_parent!(Project.find(3))) |
|
113 |
end |
|
114 |
|
|
115 |
def test_set_parent_should_add_roots_in_alphabetical_order |
|
116 |
ProjectCustomField.delete_all |
|
117 |
Project.delete_all |
|
118 |
Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil) |
|
119 |
Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil) |
|
120 |
Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil) |
|
121 |
Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil) |
|
122 |
|
|
123 |
assert_equal 4, Project.count |
|
124 |
assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft) |
|
125 |
end |
|
126 |
|
|
127 |
def test_set_parent_should_add_children_in_alphabetical_order |
|
128 |
ProjectCustomField.delete_all |
|
129 |
parent = Project.create!(:name => 'Parent', :identifier => 'parent') |
|
130 |
Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent) |
|
131 |
Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent) |
|
132 |
Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent) |
|
133 |
Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent) |
|
134 |
|
|
135 |
parent.reload |
|
136 |
assert_equal 4, parent.children.size |
|
137 |
assert_equal parent.children.sort_by(&:name), parent.children |
|
138 |
end |
|
139 |
|
|
140 |
def test_rebuild_should_sort_children_alphabetically |
|
141 |
ProjectCustomField.delete_all |
|
142 |
parent = Project.create!(:name => 'Parent', :identifier => 'parent') |
|
143 |
Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent) |
|
144 |
Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent) |
|
145 |
Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent) |
|
146 |
Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent) |
|
147 |
|
|
148 |
Project.update_all("lft = NULL, rgt = NULL") |
|
149 |
Project.rebuild! |
|
150 |
|
|
151 |
parent.reload |
|
152 |
assert_equal 4, parent.children.size |
|
153 |
assert_equal parent.children.sort_by(&:name), parent.children |
|
154 |
end |
|
155 |
|
|
156 |
def test_parent |
|
157 |
p = Project.find(6).parent |
|
158 |
assert p.is_a?(Project) |
|
159 |
assert_equal 5, p.id |
|
160 |
end |
|
161 |
|
|
162 |
def test_ancestors |
|
163 |
a = Project.find(6).ancestors |
|
164 |
assert a.first.is_a?(Project) |
|
165 |
assert_equal [1, 5], a.collect(&:id) |
|
166 |
end |
|
167 |
|
|
168 |
def test_root |
|
169 |
r = Project.find(6).root |
|
170 |
assert r.is_a?(Project) |
|
171 |
assert_equal 1, r.id |
|
172 |
end |
|
173 |
|
|
174 |
def test_children |
|
175 |
c = Project.find(1).children |
|
176 |
assert c.first.is_a?(Project) |
|
177 |
assert_equal [5, 3, 4], c.collect(&:id) |
|
178 |
end |
|
179 |
|
|
180 |
def test_descendants |
|
181 |
d = Project.find(1).descendants |
|
182 |
assert d.first.is_a?(Project) |
|
183 |
assert_equal [5, 6, 3, 4], d.collect(&:id) |
|
184 |
end |
|
185 |
|
|
119 | 186 |
def test_rolled_up_trackers |
120 | 187 |
parent = Project.find(1) |
121 | 188 |
parent.trackers = Tracker.find([1,2]) |
trunk/vendor/plugins/awesome_nested_set/MIT-LICENSE | ||
---|---|---|
1 |
Copyright (c) 2007 [name of plugin creator] |
|
2 |
|
|
3 |
Permission is hereby granted, free of charge, to any person obtaining |
|
4 |
a copy of this software and associated documentation files (the |
|
5 |
"Software"), to deal in the Software without restriction, including |
|
6 |
without limitation the rights to use, copy, modify, merge, publish, |
|
7 |
distribute, sublicense, and/or sell copies of the Software, and to |
|
8 |
permit persons to whom the Software is furnished to do so, subject to |
|
9 |
the following conditions: |
|
10 |
|
|
11 |
The above copyright notice and this permission notice shall be |
|
12 |
included in all copies or substantial portions of the Software. |
|
13 |
|
|
14 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|
15 |
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|
16 |
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|
17 |
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|
18 |
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|
19 |
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|
20 |
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|
0 | 21 |
trunk/vendor/plugins/awesome_nested_set/README.rdoc | ||
---|---|---|
1 |
= AwesomeNestedSet |
|
2 |
|
|
3 |
Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but awesomer. |
|
4 |
|
|
5 |
== What makes this so awesome? |
|
6 |
|
|
7 |
This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support. |
|
8 |
|
|
9 |
== Installation |
|
10 |
|
|
11 |
If you are on Rails 2.1 or later: |
|
12 |
|
|
13 |
script/plugin install git://github.com/collectiveidea/awesome_nested_set.git |
|
14 |
|
|
15 |
== Usage |
|
16 |
|
|
17 |
To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id: |
|
18 |
|
|
19 |
class CreateCategories < ActiveRecord::Migration |
|
20 |
def self.up |
|
21 |
create_table :categories do |t| |
|
22 |
t.string :name |
|
23 |
t.integer :parent_id |
|
24 |
t.integer :lft |
|
25 |
t.integer :rgt |
|
26 |
end |
|
27 |
end |
|
28 |
|
|
29 |
def self.down |
|
30 |
drop_table :categories |
|
31 |
end |
|
32 |
end |
|
33 |
|
|
34 |
Enable the nested set functionality by declaring acts_as_nested_set on your model |
|
35 |
|
|
36 |
class Category < ActiveRecord::Base |
|
37 |
acts_as_nested_set |
|
38 |
end |
|
39 |
|
|
40 |
Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet::SingletonMethods for more info. |
|
41 |
|
|
42 |
== View Helper |
|
43 |
|
|
44 |
The view helper is called #nested_set_options. |
|
45 |
|
|
46 |
Example usage: |
|
47 |
|
|
48 |
<%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %> |
|
49 |
|
|
50 |
<%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %> |
|
51 |
|
|
52 |
See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers. |
|
53 |
|
|
54 |
== References |
|
55 |
|
|
56 |
You can learn more about nested sets at: |
|
57 |
|
|
58 |
http://www.dbmsmag.com/9603d06.html |
|
59 |
http://threebit.net/tutorials/nestedset/tutorial1.html |
|
60 |
http://api.rubyonrails.com/classes/ActiveRecord/Acts/NestedSet/ClassMethods.html |
|
61 |
http://opensource.symetrie.com/trac/better_nested_set/ |
|
62 |
|
|
63 |
|
|
64 |
Copyright (c) 2008 Collective Idea, released under the MIT license |
|
0 | 65 |
trunk/vendor/plugins/awesome_nested_set/Rakefile | ||
---|---|---|
1 |
require 'rake' |
|
2 |
require 'rake/testtask' |
|
3 |
require 'rake/rdoctask' |
|
4 |
require 'rake/gempackagetask' |
|
5 |
require 'rcov/rcovtask' |
|
6 |
require "load_multi_rails_rake_tasks" |
|
7 |
|
|
8 |
spec = eval(File.read("#{File.dirname(__FILE__)}/awesome_nested_set.gemspec")) |
|
9 |
PKG_NAME = spec.name |
|
10 |
PKG_VERSION = spec.version |
|
11 |
|
|
12 |
Rake::GemPackageTask.new(spec) do |pkg| |
|
13 |
pkg.need_zip = true |
|
14 |
pkg.need_tar = true |
|
15 |
end |
|
16 |
|
|
17 |
|
|
18 |
desc 'Default: run unit tests.' |
|
19 |
task :default => :test |
|
20 |
|
|
21 |
desc 'Test the awesome_nested_set plugin.' |
|
22 |
Rake::TestTask.new(:test) do |t| |
|
23 |
t.libs << 'lib' |
|
24 |
t.pattern = 'test/**/*_test.rb' |
|
25 |
t.verbose = true |
|
26 |
end |
|
27 |
|
|
28 |
desc 'Generate documentation for the awesome_nested_set plugin.' |
|
29 |
Rake::RDocTask.new(:rdoc) do |rdoc| |
|
30 |
rdoc.rdoc_dir = 'rdoc' |
|
31 |
rdoc.title = 'AwesomeNestedSet' |
|
32 |
rdoc.options << '--line-numbers' << '--inline-source' |
|
33 |
rdoc.rdoc_files.include('README.rdoc') |
|
34 |
rdoc.rdoc_files.include('lib/**/*.rb') |
|
35 |
end |
|
36 |
|
|
37 |
namespace :test do |
|
38 |
desc "just rcov minus html output" |
|
39 |
Rcov::RcovTask.new(:coverage) do |t| |
|
40 |
# t.libs << 'test' |
|
41 |
t.test_files = FileList['test/**/*_test.rb'] |
|
42 |
t.output_dir = 'coverage' |
|
43 |
t.verbose = true |
|
44 |
t.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,lib/awesome_nested_set/named_scope.rb --sort coverage) |
|
45 |
end |
|
46 |
end |
|
0 | 47 |
trunk/vendor/plugins/awesome_nested_set/awesome_nested_set.gemspec | ||
---|---|---|
1 |
Gem::Specification.new do |s| |
|
2 |
s.name = "awesome_nested_set" |
|
3 |
s.version = "1.1.1" |
|
4 |
s.summary = "An awesome replacement for acts_as_nested_set and better_nested_set." |
|
5 |
s.description = s.summary |
|
6 |
|
|
7 |
s.files = %w(init.rb MIT-LICENSE Rakefile README.rdoc lib/awesome_nested_set.rb lib/awesome_nested_set/compatability.rb lib/awesome_nested_set/helper.rb lib/awesome_nested_set/named_scope.rb rails/init.rb test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml) |
|
8 |
|
|
9 |
s.add_dependency "activerecord", ['>= 1.1'] |
|
10 |
|
|
11 |
s.has_rdoc = true |
|
12 |
s.extra_rdoc_files = [ "README.rdoc"] |
|
13 |
s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"] |
|
14 |
|
|
15 |
s.test_files = %w(test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml) |
|
16 |
s.require_path = 'lib' |
|
17 |
s.author = "Collective Idea" |
|
18 |
s.email = "[email protected]" |
|
19 |
s.homepage = "http://collectiveidea.com" |
|
20 |
end |
|
0 | 21 |
trunk/vendor/plugins/awesome_nested_set/init.rb | ||
---|---|---|
1 |
require File.dirname(__FILE__) + "/rails/init" |
|
0 | 2 |
trunk/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb | ||
---|---|---|
1 |
# Rails <2.x doesn't define #except |
|
2 |
class Hash #:nodoc: |
|
3 |
# Returns a new hash without the given keys. |
|
4 |
def except(*keys) |
|
5 |
clone.except!(*keys) |
|
6 |
end unless method_defined?(:except) |
|
7 |
|
|
8 |
# Replaces the hash without the given keys. |
|
9 |
def except!(*keys) |
|
10 |
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key) |
|
11 |
keys.each { |key| delete(key) } |
|
12 |
self |
|
13 |
end unless method_defined?(:except!) |
|
14 |
end |
|
15 |
|
|
16 |
# NamedScope is new to Rails 2.1 |
|
17 |
unless defined? ActiveRecord::NamedScope |
|
18 |
require 'awesome_nested_set/named_scope' |
|
19 |
ActiveRecord::Base.class_eval do |
|
20 |
include CollectiveIdea::NamedScope |
|
21 |
end |
|
22 |
end |
|
23 |
|
|
24 |
# Rails 1.2.x doesn't define #quoted_table_name |
|
25 |
class ActiveRecord::Base #:nodoc: |
|
26 |
def self.quoted_table_name |
|
27 |
self.connection.quote_column_name(self.table_name) |
|
28 |
end unless methods.include?('quoted_table_name') |
|
29 |
end |
|
0 | 30 |
trunk/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/helper.rb | ||
---|---|---|
1 |
module CollectiveIdea #:nodoc: |
|
2 |
module Acts #:nodoc: |
|
3 |
module NestedSet #:nodoc: |
|
4 |
# This module provides some helpers for the model classes using acts_as_nested_set. |
|
5 |
# It is included by default in all views. |
|
6 |
# |
|
7 |
module Helper |
|
8 |
# Returns options for select. |
|
9 |
# You can exclude some items from the tree. |
|
10 |
# You can pass a block receiving an item and returning the string displayed in the select. |
|
11 |
# |
|
12 |
# == Params |
|
13 |
# * +class_or_item+ - Class name or top level times |
|
14 |
# * +mover+ - The item that is being move, used to exlude impossible moves |
|
15 |
# * +&block+ - a block that will be used to display: { |item| ... item.name } |
|
16 |
# |
|
17 |
# == Usage |
|
18 |
# |
|
19 |
# <%= f.select :parent_id, nested_set_options(Category, @category) {|i| |
|
20 |
# "#{'–' * i.level} #{i.name}" |
|
21 |
# }) %> |
|
22 |
# |
|
23 |
def nested_set_options(class_or_item, mover = nil) |
|
24 |
class_or_item = class_or_item.roots if class_or_item.is_a?(Class) |
|
25 |
items = Array(class_or_item) |
|
26 |
result = [] |
|
27 |
items.each do |root| |
|
28 |
result += root.self_and_descendants.map do |i| |
|
29 |
if mover.nil? || mover.new_record? || mover.move_possible?(i) |
|
30 |
[yield(i), i.id] |
|
31 |
end |
|
32 |
end.compact |
|
33 |
end |
|
34 |
result |
|
35 |
end |
|
36 |
|
|
37 |
end |
|
38 |
end |
|
39 |
end |
|
40 |
end |
|
0 | 41 |
trunk/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb | ||
---|---|---|
1 |
# Taken from Rails 2.1 |
|
2 |
module CollectiveIdea #:nodoc: |
|
3 |
module NamedScope #:nodoc: |
|
4 |
# All subclasses of ActiveRecord::Base have two named_scopes: |
|
5 |
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and |
|
6 |
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: |
|
7 |
# |
|
8 |
# Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions) |
|
9 |
# |
|
10 |
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing |
|
11 |
# intermediate values (scopes) around as first-class objects is convenient. |
|
12 |
def self.included(base) |
|
13 |
base.class_eval do |
|
14 |
extend ClassMethods |
|
15 |
named_scope :scoped, lambda { |scope| scope } |
|
16 |
end |
|
17 |
end |
|
18 |
|
|
19 |
module ClassMethods #:nodoc: |
|
20 |
def scopes |
|
21 |
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}) |
|
22 |
end |
|
23 |
|
|
24 |
# Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query, |
|
25 |
# such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>. |
|
26 |
# |
|
27 |
# class Shirt < ActiveRecord::Base |
|
28 |
# named_scope :red, :conditions => {:color => 'red'} |
|
29 |
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true] |
|
30 |
# end |
|
31 |
# |
|
32 |
# The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, |
|
33 |
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>. |
|
34 |
# |
|
35 |
# Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object |
|
36 |
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>, |
|
37 |
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just |
|
38 |
# as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>, |
|
39 |
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array. |
|
40 |
# |
|
41 |
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only. |
|
42 |
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments |
|
43 |
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. |
|
44 |
# |
|
45 |
# All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to |
|
46 |
# <tt>has_many</tt> associations. If, |
|
47 |
# |
|
48 |
# class Person < ActiveRecord::Base |
|
49 |
# has_many :shirts |
|
50 |
# end |
|
51 |
# |
|
52 |
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean |
|
53 |
# only shirts. |
|
54 |
# |
|
55 |
# Named scopes can also be procedural. |
|
56 |
# |
|
57 |
# class Shirt < ActiveRecord::Base |
|
58 |
# named_scope :colored, lambda { |color| |
|
59 |
# { :conditions => { :color => color } } |
|
60 |
# } |
|
61 |
# end |
|
62 |
# |
|
63 |
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. |
|
64 |
# |
|
65 |
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations: |
|
66 |
# |
|
67 |
# class Shirt < ActiveRecord::Base |
|
68 |
# named_scope :red, :conditions => {:color => 'red'} do |
|
69 |
# def dom_id |
|
70 |
# 'red_shirts' |
|
71 |
# end |
|
72 |
# end |
|
73 |
# end |
|
74 |
# |
|
75 |
# |
|
76 |
# For testing complex named scopes, you can examine the scoping options using the |
|
77 |
# <tt>proxy_options</tt> method on the proxy itself. |
|
78 |
# |
|
79 |
# class Shirt < ActiveRecord::Base |
|
80 |
# named_scope :colored, lambda { |color| |
|
81 |
# { :conditions => { :color => color } } |
|
82 |
# } |
|
83 |
# end |
|
84 |
# |
|
85 |
# expected_options = { :conditions => { :colored => 'red' } } |
|
86 |
# assert_equal expected_options, Shirt.colored('red').proxy_options |
|
87 |
def named_scope(name, options = {}, &block) |
|
88 |
scopes[name] = lambda do |parent_scope, *args| |
|
89 |
Scope.new(parent_scope, case options |
|
90 |
when Hash |
|
91 |
options |
|
92 |
when Proc |
|
93 |
options.call(*args) |
|
94 |
end, &block) |
|
95 |
end |
|
96 |
(class << self; self end).instance_eval do |
|
97 |
define_method name do |*args| |
|
98 |
scopes[name].call(self, *args) |
|
99 |
end |
|
100 |
end |
|
101 |
end |
|
102 |
end |
|
103 |
|
|
104 |
class Scope #:nodoc: |
|
105 |
attr_reader :proxy_scope, :proxy_options |
|
106 |
[].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ } |
|
107 |
delegate :scopes, :with_scope, :to => :proxy_scope |
|
108 |
|
|
109 |
def initialize(proxy_scope, options, &block) |
|
110 |
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend] |
|
111 |
extend Module.new(&block) if block_given? |
|
112 |
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend) |
|
113 |
end |
|
114 |
|
|
115 |
def reload |
|
116 |
load_found; self |
|
117 |
end |
|
118 |
|
|
119 |
protected |
|
120 |
def proxy_found |
|
121 |
@found || load_found |
|
122 |
end |
|
123 |
|
|
124 |
private |
|
125 |
def method_missing(method, *args, &block) |
|
126 |
if scopes.include?(method) |
|
127 |
scopes[method].call(self, *args) |
|
128 |
else |
|
129 |
with_scope :find => proxy_options do |
|
130 |
proxy_scope.send(method, *args, &block) |
|
131 |
end |
|
132 |
end |
|
133 |
end |
|
134 |
|
|
135 |
def load_found |
|
136 |
@found = find(:all) |
|
137 |
end |
|
138 |
end |
|
139 |
end |
|
140 |
end |
|
0 | 141 |
trunk/vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb | ||
---|---|---|
1 |
module CollectiveIdea #:nodoc: |
|
2 |
module Acts #:nodoc: |
|
3 |
module NestedSet #:nodoc: |
|
4 |
def self.included(base) |
|
5 |
base.extend(SingletonMethods) |
|
6 |
end |
|
7 |
|
|
8 |
# This acts provides Nested Set functionality. Nested Set is a smart way to implement |
|
9 |
# an _ordered_ tree, with the added feature that you can select the children and all of their |
|
10 |
# descendants with a single query. The drawback is that insertion or move need some complex |
|
11 |
# sql queries. But everything is done here by this module! |
|
12 |
# |
|
13 |
# Nested sets are appropriate each time you want either an orderd tree (menus, |
|
14 |
# commercial categories) or an efficient way of querying big trees (threaded posts). |
|
15 |
# |
|
16 |
# == API |
|
17 |
# |
|
18 |
# Methods names are aligned with acts_as_tree as much as possible, to make replacment from one |
|
19 |
# by another easier, except for the creation: |
|
20 |
# |
|
21 |
# in acts_as_tree: |
|
22 |
# item.children.create(:name => "child1") |
|
23 |
# |
|
24 |
# in acts_as_nested_set: |
|
25 |
# # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1 |
|
26 |
# child = MyClass.new(:name => "child1") |
|
27 |
# child.save |
|
28 |
# # now move the item to its right place |
|
29 |
# child.move_to_child_of my_item |
|
30 |
# |
|
31 |
# You can pass an id or an object to: |
|
32 |
# * <tt>#move_to_child_of</tt> |
|
33 |
# * <tt>#move_to_right_of</tt> |
|
34 |
# * <tt>#move_to_left_of</tt> |
|
35 |
# |
|
36 |
module SingletonMethods |
|
37 |
# Configuration options are: |
|
38 |
# |
|
39 |
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id) |
|
40 |
# * +:left_column+ - column name for left boundry data, default "lft" |
|
41 |
# * +:right_column+ - column name for right boundry data, default "rgt" |
|
42 |
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" |
|
43 |
# (if it hasn't been already) and use that as the foreign key restriction. You |
|
44 |
# can also pass an array to scope by multiple attributes. |
|
45 |
# Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt> |
|
46 |
# * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the |
|
47 |
# child objects are destroyed alongside this object by calling their destroy |
|
48 |
# method. If set to :delete_all (default), all the child objects are deleted |
|
49 |
# without calling their destroy method. |
|
50 |
# |
|
51 |
# See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and |
|
52 |
# CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added |
|
53 |
# to acts_as_nested_set models |
|
54 |
def acts_as_nested_set(options = {}) |
|
55 |
options = { |
|
56 |
:parent_column => 'parent_id', |
|
57 |
:left_column => 'lft', |
Also available in: Unified diff
Merged nested projects branch. Removes limit on subproject nesting (#594).