Project

General

Profile

« Previous | Next » 

Revision 2304

Merged nested projects branch. Removes limit on subproject nesting (#594).

View differences:

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 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
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', '&#187; ' + 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', ('&#187; ' + 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(" &#187; ") %></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',
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff