##// END OF EJS Templates
Adds subtasking (#443) including:...
Jean-Philippe Lang -
r3459:8e3d1b694ab4
parent child
Show More
@@ -0,0 +1,9
1 <ul>
2 <% if @issues.any? -%>
3 <% @issues.each do |issue| -%>
4 <%= content_tag 'li', h("#{issue.tracker} ##{issue.id}: #{issue.subject}"), :id => issue.id %>
5 <% end -%>
6 <% else -%>
7 <%= content_tag("li", l(:label_none), :style => 'display:none') %>
8 <% end -%>
9 </ul>
@@ -0,0 +1,17
1 class AddIssuesNestedSetsColumns < ActiveRecord::Migration
2 def self.up
3 add_column :issues, :parent_id, :integer, :default => nil
4 add_column :issues, :root_id, :integer, :default => nil
5 add_column :issues, :lft, :integer, :default => nil
6 add_column :issues, :rgt, :integer, :default => nil
7
8 Issue.update_all("parent_id = NULL, root_id = id, lft = 1, rgt = 2")
9 end
10
11 def self.down
12 remove_column :issues, :parent_id
13 remove_column :issues, :root_id
14 remove_column :issues, :lft
15 remove_column :issues, :rgt
16 end
17 end
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,300
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19
20 class IssueNestedSetTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
23 :versions,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations,
26 :issues,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :time_entries
29
30 self.use_transactional_fixtures = false
31
32 def test_create_root_issue
33 issue1 = create_issue!
34 issue2 = create_issue!
35 issue1.reload
36 issue2.reload
37
38 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
39 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
40 end
41
42 def test_create_child_issue
43 parent = create_issue!
44 child = create_issue!(:parent_issue_id => parent.id)
45 parent.reload
46 child.reload
47
48 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
49 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
50 end
51
52 def test_creating_a_child_in_different_project_should_not_validate
53 issue = create_issue!
54 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, :subject => 'child', :parent_issue_id => issue.id)
55 assert !child.save
56 assert_not_nil child.errors.on(:parent_issue_id)
57 end
58
59 def test_move_a_root_to_child
60 parent1 = create_issue!
61 parent2 = create_issue!
62 child = create_issue!(:parent_issue_id => parent1.id)
63
64 parent2.parent_issue_id = parent1.id
65 parent2.save!
66 child.reload
67 parent1.reload
68 parent2.reload
69
70 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
71 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
72 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
73 end
74
75 def test_move_a_child_to_root
76 parent1 = create_issue!
77 parent2 = create_issue!
78 child = create_issue!(:parent_issue_id => parent1.id)
79
80 child.parent_issue_id = nil
81 child.save!
82 child.reload
83 parent1.reload
84 parent2.reload
85
86 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
87 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
88 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
89 end
90
91 def test_move_a_child_to_another_issue
92 parent1 = create_issue!
93 parent2 = create_issue!
94 child = create_issue!(:parent_issue_id => parent1.id)
95
96 child.parent_issue_id = parent2.id
97 child.save!
98 child.reload
99 parent1.reload
100 parent2.reload
101
102 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
103 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
104 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
105 end
106
107 def test_move_a_child_with_descendants_to_another_issue
108 parent1 = create_issue!
109 parent2 = create_issue!
110 child = create_issue!(:parent_issue_id => parent1.id)
111 grandchild = create_issue!(:parent_issue_id => child.id)
112
113 parent1.reload
114 parent2.reload
115 child.reload
116 grandchild.reload
117
118 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
119 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
120 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
121 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
122
123 child.reload.parent_issue_id = parent2.id
124 child.save!
125 child.reload
126 grandchild.reload
127 parent1.reload
128 parent2.reload
129
130 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
131 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
132 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
133 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
134 end
135
136 def test_move_a_child_with_descendants_to_another_project
137 parent1 = create_issue!
138 child = create_issue!(:parent_issue_id => parent1.id)
139 grandchild = create_issue!(:parent_issue_id => child.id)
140
141 assert child.reload.move_to_project(Project.find(2))
142 child.reload
143 grandchild.reload
144 parent1.reload
145
146 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
147 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
148 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
149 end
150
151 def test_invalid_move_to_another_project
152 parent1 = create_issue!
153 child = create_issue!(:parent_issue_id => parent1.id)
154 grandchild = create_issue!(:parent_issue_id => child.id, :tracker_id => 2)
155 Project.find(2).tracker_ids = [1]
156
157 parent1.reload
158 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
159
160 # child can not be moved to Project 2 because its child is on a disabled tracker
161 assert_equal false, Issue.find(child.id).move_to_project(Project.find(2))
162 child.reload
163 grandchild.reload
164 parent1.reload
165
166 # no change
167 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
168 assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt]
169 assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
170 end
171
172 def test_moving_an_issue_to_a_descendant_should_not_validate
173 parent1 = create_issue!
174 parent2 = create_issue!
175 child = create_issue!(:parent_issue_id => parent1.id)
176 grandchild = create_issue!(:parent_issue_id => child.id)
177
178 child.reload
179 child.parent_issue_id = grandchild.id
180 assert !child.save
181 assert_not_nil child.errors.on(:parent_issue_id)
182 end
183
184 def test_moving_an_issue_should_keep_valid_relations_only
185 issue1 = create_issue!
186 issue2 = create_issue!
187 issue3 = create_issue!(:parent_issue_id => issue2.id)
188 issue4 = create_issue!
189 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
190 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
191 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
192 issue2.reload
193 issue2.parent_issue_id = issue1.id
194 issue2.save!
195 assert !IssueRelation.exists?(r1.id)
196 assert !IssueRelation.exists?(r2.id)
197 assert IssueRelation.exists?(r3.id)
198 end
199
200 def test_destroy_should_destroy_children
201 issue1 = create_issue!
202 issue2 = create_issue!
203 issue3 = create_issue!(:parent_issue_id => issue2.id)
204 issue4 = create_issue!(:parent_issue_id => issue1.id)
205 issue2.reload.destroy
206 issue1.reload
207 issue4.reload
208 assert !Issue.exists?(issue2.id)
209 assert !Issue.exists?(issue3.id)
210 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
211 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
212 end
213
214 def test_parent_priority_should_be_the_highest_child_priority
215 parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
216 # Create children
217 child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
218 assert_equal 'High', parent.reload.priority.name
219 child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
220 assert_equal 'Immediate', child1.reload.priority.name
221 assert_equal 'Immediate', parent.reload.priority.name
222 child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
223 assert_equal 'Immediate', parent.reload.priority.name
224 # Destroy a child
225 child1.destroy
226 assert_equal 'Low', parent.reload.priority.name
227 # Update a child
228 child3.reload.priority = IssuePriority.find_by_name('Normal')
229 child3.save!
230 assert_equal 'Normal', parent.reload.priority.name
231 end
232
233 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
234 parent = create_issue!
235 create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
236 create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
237 create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
238 parent.reload
239 assert_equal Date.parse('2010-01-25'), parent.start_date
240 assert_equal Date.parse('2010-02-22'), parent.due_date
241 end
242
243 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
244 parent = create_issue!
245 create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
246 assert_equal 20, parent.reload.done_ratio
247 create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
248 assert_equal 45, parent.reload.done_ratio
249
250 child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
251 assert_equal 30, parent.reload.done_ratio
252
253 create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
254 assert_equal 30, child.reload.done_ratio
255 assert_equal 40, parent.reload.done_ratio
256 end
257
258 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
259 parent = create_issue!
260 create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
261 assert_equal 20, parent.reload.done_ratio
262 create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
263 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
264 end
265
266 def test_parent_estimate_should_be_sum_of_leaves
267 parent = create_issue!
268 create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
269 assert_equal nil, parent.reload.estimated_hours
270 create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
271 assert_equal 5, parent.reload.estimated_hours
272 create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
273 assert_equal 12, parent.reload.estimated_hours
274 end
275
276 def test_project_copy_should_copy_issue_tree
277 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
278 i1 = create_issue!(:project_id => p.id, :subject => 'i1')
279 i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
280 i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
281 i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
282 i5 = create_issue!(:project_id => p.id, :subject => 'i5')
283 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
284 c.copy(p, :only => 'issues')
285 c.reload
286
287 assert_equal 5, c.issues.count
288 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
289 assert ic1.root?
290 assert_equal ic1, ic2.parent
291 assert_equal ic1, ic3.parent
292 assert_equal ic2, ic4.parent
293 assert ic5.root?
294 end
295
296 # Helper that creates an issue with default attributes
297 def create_issue!(attributes={})
298 Issue.create!({:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test'}.merge(attributes))
299 end
300 end
@@ -21,7 +21,7 class IssuesController < ApplicationController
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :update, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview, :auto_complete]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
@@ -164,7 +164,8 class IssuesController < ApplicationController
164 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
164 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
165 respond_to do |format|
165 respond_to do |format|
166 format.html {
166 format.html {
167 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
167 redirect_to(params[:continue] ? { :action => 'new', :issue => {:tracker_id => @issue.tracker,
168 :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
168 { :action => 'show', :id => @issue })
169 { :action => 'show', :id => @issue })
169 }
170 }
170 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
171 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
@@ -247,6 +248,7 class IssuesController < ApplicationController
247
248
248 # Bulk edit a set of issues
249 # Bulk edit a set of issues
249 def bulk_edit
250 def bulk_edit
251 @issues.sort!
250 if request.post?
252 if request.post?
251 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
253 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
252 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
254 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
@@ -254,6 +256,7 class IssuesController < ApplicationController
254
256
255 unsaved_issue_ids = []
257 unsaved_issue_ids = []
256 @issues.each do |issue|
258 @issues.each do |issue|
259 issue.reload
257 journal = issue.init_journal(User.current, params[:notes])
260 journal = issue.init_journal(User.current, params[:notes])
258 issue.safe_attributes = attributes
261 issue.safe_attributes = attributes
259 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
262 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
@@ -271,6 +274,7 class IssuesController < ApplicationController
271 end
274 end
272
275
273 def move
276 def move
277 @issues.sort!
274 @copy = params[:copy_options] && params[:copy_options][:copy]
278 @copy = params[:copy_options] && params[:copy_options][:copy]
275 @allowed_projects = []
279 @allowed_projects = []
276 # find projects to which the user is allowed to move the issue
280 # find projects to which the user is allowed to move the issue
@@ -289,6 +293,7 class IssuesController < ApplicationController
289 unsaved_issue_ids = []
293 unsaved_issue_ids = []
290 moved_issues = []
294 moved_issues = []
291 @issues.each do |issue|
295 @issues.each do |issue|
296 issue.reload
292 changed_attributes = {}
297 changed_attributes = {}
293 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
298 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
294 unless params[valid_attribute].blank?
299 unless params[valid_attribute].blank?
@@ -297,7 +302,7 class IssuesController < ApplicationController
297 end
302 end
298 issue.init_journal(User.current)
303 issue.init_journal(User.current)
299 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
304 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
300 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
305 if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
301 moved_issues << r
306 moved_issues << r
302 else
307 else
303 unsaved_issue_ids << issue.id
308 unsaved_issue_ids << issue.id
@@ -456,6 +461,18 class IssuesController < ApplicationController
456 render :partial => 'common/preview'
461 render :partial => 'common/preview'
457 end
462 end
458
463
464 def auto_complete
465 @issues = []
466 q = params[:q].to_s
467 if q.match(/^\d+$/)
468 @issues << @project.issues.visible.find_by_id(q.to_i)
469 end
470 unless q.blank?
471 @issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
472 end
473 render :layout => false
474 end
475
459 private
476 private
460 def find_issue
477 def find_issue
461 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
478 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@@ -94,7 +94,7 class TimelogController < ApplicationController
94 elsif @issue.nil?
94 elsif @issue.nil?
95 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
95 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
96 else
96 else
97 sql_condition = "#{TimeEntry.table_name}.issue_id = #{@issue.id}"
97 sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
98 end
98 end
99
99
100 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
100 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
@@ -166,7 +166,7 class TimelogController < ApplicationController
166 elsif @issue.nil?
166 elsif @issue.nil?
167 cond << @project.project_condition(Setting.display_subprojects_issues?)
167 cond << @project.project_condition(Setting.display_subprojects_issues?)
168 else
168 else
169 cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
169 cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
170 end
170 end
171
171
172 retrieve_date_range
172 retrieve_date_range
@@ -176,7 +176,7 class TimelogController < ApplicationController
176 respond_to do |format|
176 respond_to do |format|
177 format.html {
177 format.html {
178 # Paginate results
178 # Paginate results
179 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
179 @entry_count = TimeEntry.count(:include => [:project, :issue], :conditions => cond.conditions)
180 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
180 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
181 @entries = TimeEntry.find(:all,
181 @entries = TimeEntry.find(:all,
182 :include => [:project, :activity, :user, {:issue => :tracker}],
182 :include => [:project, :activity, :user, {:issue => :tracker}],
@@ -184,7 +184,7 class TimelogController < ApplicationController
184 :order => sort_clause,
184 :order => sort_clause,
185 :limit => @entry_pages.items_per_page,
185 :limit => @entry_pages.items_per_page,
186 :offset => @entry_pages.current.offset)
186 :offset => @entry_pages.current.offset)
187 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
187 @total_hours = TimeEntry.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
188
188
189 render :layout => !request.xhr?
189 render :layout => !request.xhr?
190 }
190 }
@@ -31,6 +31,34 module IssuesHelper
31 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
31 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
32 end
32 end
33
33
34 def render_issue_subject_with_tree(issue)
35 s = ''
36 issue.ancestors.each do |ancestor|
37 s << '<div>' + content_tag('p', link_to_issue(ancestor))
38 end
39 s << '<div>' + content_tag('h3', h(issue.subject))
40 s << '</div>' * (issue.ancestors.size + 1)
41 s
42 end
43
44 def render_descendants_tree(issue)
45 s = '<form><table class="list issues">'
46 ancestors = []
47 issue.descendants.sort_by(&:lft).each do |child|
48 level = child.level - issue.level - 1
49 s << content_tag('tr',
50 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil)) +
51 content_tag('td', link_to_issue(child), :class => 'subject',
52 :style => "padding-left: #{level * 20}px") +
53 content_tag('td', h(child.status)) +
54 content_tag('td', link_to_user(child.assigned_to)) +
55 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
56 :class => "issue-#{child.id} hascontextmenu")
57 end
58 s << '</form></table>'
59 s
60 end
61
34 def render_custom_fields_rows(issue)
62 def render_custom_fields_rows(issue)
35 return if issue.custom_field_values.empty?
63 return if issue.custom_field_values.empty?
36 ordered_values = []
64 ordered_values = []
@@ -32,6 +32,7 class Issue < ActiveRecord::Base
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_nested_set :scope => 'root_id'
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
37 acts_as_customizable
37 acts_as_watchable
38 acts_as_watchable
@@ -68,7 +69,9 class Issue < ActiveRecord::Base
68
69
69 before_create :default_assign
70 before_create :default_assign
70 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
71 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
71 after_save :create_journal
72 after_save :update_nested_set_attributes, :update_parent_attributes, :create_journal
73 after_destroy :destroy_children
74 after_destroy :update_parent_attributes
72
75
73 # Returns true if usr or current user is allowed to view the issue
76 # Returns true if usr or current user is allowed to view the issue
74 def visible?(usr=nil)
77 def visible?(usr=nil)
@@ -90,18 +93,24 class Issue < ActiveRecord::Base
90
93
91 def copy_from(arg)
94 def copy_from(arg)
92 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
95 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
93 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
96 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
94 self.custom_values = issue.custom_values.collect {|v| v.clone}
97 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
95 self.status = issue.status
98 self.status = issue.status
96 self
99 self
97 end
100 end
98
101
99 # Moves/copies an issue to a new project and tracker
102 # Moves/copies an issue to a new project and tracker
100 # Returns the moved/copied issue on success, false on failure
103 # Returns the moved/copied issue on success, false on failure
101 def move_to(new_project, new_tracker = nil, options = {})
104 def move_to_project(*args)
102 options ||= {}
103 issue = options[:copy] ? self.clone : self
104 ret = Issue.transaction do
105 ret = Issue.transaction do
106 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
107 end || false
108 end
109
110 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
111 options ||= {}
112 issue = options[:copy] ? self.class.new.copy_from(self) : self
113
105 if new_project && issue.project_id != new_project.id
114 if new_project && issue.project_id != new_project.id
106 # delete issue relations
115 # delete issue relations
107 unless Setting.cross_project_issue_relations?
116 unless Setting.cross_project_issue_relations?
@@ -117,9 +126,13 class Issue < ActiveRecord::Base
117 issue.fixed_version = nil
126 issue.fixed_version = nil
118 end
127 end
119 issue.project = new_project
128 issue.project = new_project
129 if issue.parent && issue.parent.project_id != issue.project_id
130 issue.parent_issue_id = nil
131 end
120 end
132 end
121 if new_tracker
133 if new_tracker
122 issue.tracker = new_tracker
134 issue.tracker = new_tracker
135 issue.reset_custom_values!
123 end
136 end
124 if options[:copy]
137 if options[:copy]
125 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
138 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
@@ -137,13 +150,18 class Issue < ActiveRecord::Base
137 unless options[:copy]
150 unless options[:copy]
138 # Manually update project_id on related time entries
151 # Manually update project_id on related time entries
139 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
152 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
153
154 issue.children.each do |child|
155 unless child.move_to_project_without_transaction(new_project)
156 # Move failed and transaction was rollback'd
157 return false
140 end
158 end
141 true
142 else
143 raise ActiveRecord::Rollback
144 end
159 end
145 end
160 end
146 ret ? issue : false
161 else
162 return false
163 end
164 issue
147 end
165 end
148
166
149 def priority_id=(pid)
167 def priority_id=(pid)
@@ -177,6 +195,7 class Issue < ActiveRecord::Base
177 SAFE_ATTRIBUTES = %w(
195 SAFE_ATTRIBUTES = %w(
178 tracker_id
196 tracker_id
179 status_id
197 status_id
198 parent_issue_id
180 category_id
199 category_id
181 assigned_to_id
200 assigned_to_id
182 priority_id
201 priority_id
@@ -203,6 +222,19 class Issue < ActiveRecord::Base
203 attrs.delete('status_id')
222 attrs.delete('status_id')
204 end
223 end
205 end
224 end
225
226 unless leaf?
227 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
228 end
229
230 if attrs.has_key?('parent_issue_id')
231 if !user.allowed_to?(:manage_subtasks, project)
232 attrs.delete('parent_issue_id')
233 elsif !attrs['parent_issue_id'].blank?
234 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
235 end
236 end
237
206 self.attributes = attrs
238 self.attributes = attrs
207 end
239 end
208
240
@@ -249,6 +281,22 class Issue < ActiveRecord::Base
249 errors.add :tracker_id, :inclusion
281 errors.add :tracker_id, :inclusion
250 end
282 end
251 end
283 end
284
285 # Checks parent issue assignment
286 if @parent_issue
287 if @parent_issue.project_id != project_id
288 errors.add :parent_issue_id, :not_same_project
289 elsif !new_record?
290 # moving an existing issue
291 if @parent_issue.root_id != root_id
292 # we can always move to another tree
293 elsif move_possible?(@parent_issue)
294 # move accepted inside tree
295 else
296 errors.add :parent_issue_id, :not_a_valid_parent
297 end
298 end
299 end
252 end
300 end
253
301
254 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
302 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
@@ -340,13 +388,13 class Issue < ActiveRecord::Base
340 notified.collect(&:mail)
388 notified.collect(&:mail)
341 end
389 end
342
390
343 # Returns the total number of hours spent on this issue.
391 # Returns the total number of hours spent on this issue and its descendants
344 #
392 #
345 # Example:
393 # Example:
346 # spent_hours => 0
394 # spent_hours => 0.0
347 # spent_hours => 50
395 # spent_hours => 50.2
348 def spent_hours
396 def spent_hours
349 @spent_hours ||= time_entries.sum(:hours) || 0
397 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
350 end
398 end
351
399
352 def relations
400 def relations
@@ -386,6 +434,16 class Issue < ActiveRecord::Base
386 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
434 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
387 end
435 end
388
436
437 def <=>(issue)
438 if issue.nil?
439 -1
440 elsif root_id != issue.root_id
441 (root_id || 0) <=> (issue.root_id || 0)
442 else
443 (lft || 0) <=> (issue.lft || 0)
444 end
445 end
446
389 def to_s
447 def to_s
390 "#{tracker} ##{id}: #{subject}"
448 "#{tracker} ##{id}: #{subject}"
391 end
449 end
@@ -442,6 +500,24 class Issue < ActiveRecord::Base
442 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
500 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
443 end
501 end
444
502
503 def parent_issue_id=(arg)
504 parent_issue_id = arg.blank? ? nil : arg.to_i
505 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
506 @parent_issue.id
507 else
508 @parent_issue = nil
509 nil
510 end
511 end
512
513 def parent_issue_id
514 if instance_variable_defined? :@parent_issue
515 @parent_issue.nil? ? nil : @parent_issue.id
516 else
517 parent_id
518 end
519 end
520
445 # Extracted from the ReportsController.
521 # Extracted from the ReportsController.
446 def self.by_tracker(project)
522 def self.by_tracker(project)
447 count_and_group_by(:project => project,
523 count_and_group_by(:project => project,
@@ -495,6 +571,95 class Issue < ActiveRecord::Base
495
571
496 private
572 private
497
573
574 def update_nested_set_attributes
575 if root_id.nil?
576 # issue was just created
577 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
578 set_default_left_and_right
579 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
580 if @parent_issue
581 move_to_child_of(@parent_issue)
582 end
583 reload
584 elsif parent_issue_id != parent_id
585 # moving an existing issue
586 if @parent_issue && @parent_issue.root_id == root_id
587 # inside the same tree
588 move_to_child_of(@parent_issue)
589 else
590 # to another tree
591 unless root?
592 move_to_right_of(root)
593 reload
594 end
595 old_root_id = root_id
596 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
597 target_maxright = nested_set_scope.maximum(right_column_name) || 0
598 offset = target_maxright + 1 - lft
599 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
600 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
601 self[left_column_name] = lft + offset
602 self[right_column_name] = rgt + offset
603 if @parent_issue
604 move_to_child_of(@parent_issue)
605 end
606 end
607 reload
608 # delete invalid relations of all descendants
609 self_and_descendants.each do |issue|
610 issue.relations.each do |relation|
611 relation.destroy unless relation.valid?
612 end
613 end
614 end
615 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
616 end
617
618 def update_parent_attributes
619 if parent_id && p = Issue.find_by_id(parent_id)
620 # priority = highest priority of children
621 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
622 p.priority = IssuePriority.find_by_position(priority_position)
623 end
624
625 # start/due dates = lowest/highest dates of children
626 p.start_date = p.children.minimum(:start_date)
627 p.due_date = p.children.maximum(:due_date)
628 if p.start_date && p.due_date && p.due_date < p.start_date
629 p.start_date, p.due_date = p.due_date, p.start_date
630 end
631
632 # done ratio = weighted average ratio of leaves
633 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio?
634 leaves_count = p.leaves.count
635 if leaves_count > 0
636 average = p.leaves.average(:estimated_hours).to_f
637 if average == 0
638 average = 1
639 end
640 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
641 progress = done / (average * leaves_count)
642 p.done_ratio = progress.round
643 end
644 end
645
646 # estimate = sum of leaves estimates
647 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
648 p.estimated_hours = nil if p.estimated_hours == 0.0
649
650 # ancestors will be recursively updated
651 p.save(false)
652 end
653 end
654
655 def destroy_children
656 unless leaf?
657 children.each do |child|
658 child.destroy
659 end
660 end
661 end
662
498 # Update issues so their versions are not pointing to a
663 # Update issues so their versions are not pointing to a
499 # fixed_version that is not shared with the issue's project
664 # fixed_version that is not shared with the issue's project
500 def self.update_versions(conditions=nil)
665 def self.update_versions(conditions=nil)
@@ -562,7 +727,7 class Issue < ActiveRecord::Base
562 def create_journal
727 def create_journal
563 if @current_journal
728 if @current_journal
564 # attributes changes
729 # attributes changes
565 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
730 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
566 @current_journal.details << JournalDetail.new(:property => 'attr',
731 @current_journal.details << JournalDetail.new(:property => 'attr',
567 :prop_key => c,
732 :prop_key => c,
568 :old_value => @issue_before_change.send(c),
733 :old_value => @issue_before_change.send(c),
@@ -48,6 +48,7 class IssueRelation < ActiveRecord::Base
48 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
48 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
49 errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
49 errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
50 errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
50 errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
51 errors.add_to_base :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
51 end
52 end
52 end
53 end
53
54
@@ -557,9 +557,12 class Project < ActiveRecord::Base
557 # value. Used to map the two togeather for issue relations.
557 # value. Used to map the two togeather for issue relations.
558 issues_map = {}
558 issues_map = {}
559
559
560 project.issues.each do |issue|
560 # Get issues sorted by root_id, lft so that parent issues
561 # get copied before their children
562 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
561 new_issue = Issue.new
563 new_issue = Issue.new
562 new_issue.copy_from(issue)
564 new_issue.copy_from(issue)
565 new_issue.project = self
563 # Reassign fixed_versions by name, since names are unique per
566 # Reassign fixed_versions by name, since names are unique per
564 # project and the versions for self are not yet saved
567 # project and the versions for self are not yet saved
565 if issue.fixed_version
568 if issue.fixed_version
@@ -570,6 +573,13 class Project < ActiveRecord::Base
570 if issue.category
573 if issue.category
571 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
574 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
572 end
575 end
576 # Parent issue
577 if issue.parent_id
578 if copied_parent = issues_map[issue.parent_id]
579 new_issue.parent_issue_id = copied_parent.id
580 end
581 end
582
573 self.issues << new_issue
583 self.issues << new_issue
574 issues_map[issue.id] = new_issue
584 issues_map[issue.id] = new_issue
575 end
585 end
@@ -7,7 +7,7
7 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
7 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
8 <% end %>
8 <% end %>
9
9
10 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
10 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %></p>
11 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
11 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
12 <% unless @project.issue_categories.empty? %>
12 <% unless @project.issue_categories.empty? %>
13 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
13 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
@@ -31,10 +31,10
31 </div>
31 </div>
32
32
33 <div class="splitcontentright">
33 <div class="splitcontentright">
34 <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
34 <p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
35 <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
35 <p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
36 <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
36 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p>
37 <% if Issue.use_field_for_done_ratio? %>
37 <% if @issue.leaf? && Issue.use_field_for_done_ratio? %>
38 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
38 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
39 <% end %>
39 <% end %>
40 </div>
40 </div>
@@ -5,6 +5,16
5 :with => "Form.serialize('issue-form')" %>
5 :with => "Form.serialize('issue-form')" %>
6
6
7 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
7 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
8
9 <% unless (@issue.new_record? && @issue.parent_issue_id.nil?) || !User.current.allowed_to?(:manage_subtasks, @project) %>
10 <p><%= f.text_field :parent_issue_id, :size => 10 %></p>
11 <div id="parent_issue_candidates" class="autocomplete"></div>
12 <%= javascript_tag "observeParentIssueField('#{url_for(:controller => :issues,
13 :action => :auto_complete,
14 :id => @issue,
15 :project_id => @project) }')" %>
16 <% end %>
17
8 <p><%= f.text_area :description,
18 <p><%= f.text_area :description,
9 :cols => 60,
19 :cols => 60,
10 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
20 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
@@ -79,8 +79,10 t_height = g_height + headers_height
79 # Tasks subjects
79 # Tasks subjects
80 #
80 #
81 top = headers_height + 8
81 top = headers_height + 8
82 @gantt.events.each do |i| %>
82 @gantt.events.each do |i|
83 <div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:4px;overflow:hidden;"><small>
83 left = 4 + (i.is_a?(Issue) ? i.level * 16 : 0)
84 %>
85 <div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:<%= left %>px;overflow:hidden;"><small>
84 <% if i.is_a? Issue %>
86 <% if i.is_a? Issue %>
85 <%= h("#{i.project} -") unless @project && @project == i.project %>
87 <%= h("#{i.project} -") unless @project && @project == i.project %>
86 <%= link_to_issue i %>
88 <%= link_to_issue i %>
@@ -189,15 +191,16 top = headers_height + 10
189 i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
191 i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
190 d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
192 d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
191 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
193 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
194 css = "task " + (i.leaf? ? 'leaf' : 'parent')
192 %>
195 %>
193 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="task task_todo">&nbsp;</div>
196 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="<%= css %> task_todo"><div class="left"></div>&nbsp;<div class="right"></div></div>
194 <% if l_width > 0 %>
197 <% if l_width > 0 %>
195 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="task task_late">&nbsp;</div>
198 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="<%= css %> task_late">&nbsp;</div>
196 <% end %>
199 <% end %>
197 <% if d_width > 0 %>
200 <% if d_width > 0 %>
198 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="task task_done">&nbsp;</div>
201 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="<%= css %> task_done">&nbsp;</div>
199 <% end %>
202 <% end %>
200 <div style="top:<%= top %>px;left:<%= i_left + i_width + 5 %>px;background:#fff;" class="task">
203 <div style="top:<%= top %>px;left:<%= i_left + i_width + 8 %>px;background:#fff;" class="<%= css %>">
201 <%= i.status.name %>
204 <%= i.status.name %>
202 <%= (i.done_ratio).to_i %>%
205 <%= (i.done_ratio).to_i %>%
203 </div>
206 </div>
@@ -4,7 +4,10
4
4
5 <div class="<%= @issue.css_classes %> details">
5 <div class="<%= @issue.css_classes %> details">
6 <%= avatar(@issue.author, :size => "50") %>
6 <%= avatar(@issue.author, :size => "50") %>
7 <h3><%=h @issue.subject %></h3>
7
8 <div class="subject">
9 <%= render_issue_subject_with_tree(@issue) %>
10 </div>
8 <p class="author">
11 <p class="author">
9 <%= authoring @issue.created_on, @issue.author %>.
12 <%= authoring @issue.created_on, @issue.author %>.
10 <% if @issue.created_on != @issue.updated_on %>
13 <% if @issue.created_on != @issue.updated_on %>
@@ -56,6 +59,17
56
59
57 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
60 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
58
61
62 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
63 <hr />
64 <div id="issue_tree">
65 <div class="contextual">
66 <%= link_to(l(:button_add), {:controller => 'issues', :action => 'new', :project_id => @project, :issue => {:parent_issue_id => @issue}}) if User.current.allowed_to?(:manage_subtasks, @project) %>
67 </div>
68 <p><strong><%=l(:label_subtask_pural)%></strong></p>
69 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
70 </div>
71 <% end %>
72
59 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
73 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
60 <hr />
74 <hr />
61 <div id="relations">
75 <div id="relations">
@@ -113,4 +127,8
113 <% content_for :header_tags do %>
127 <% content_for :header_tags do %>
114 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
128 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
115 <%= stylesheet_link_tag 'scm' %>
129 <%= stylesheet_link_tag 'scm' %>
130 <%= javascript_include_tag 'context_menu' %>
131 <%= stylesheet_link_tag 'context_menu' %>
116 <% end %>
132 <% end %>
133 <div id="context-menu" style="display: none;"></div>
134 <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> No newline at end of file
@@ -112,6 +112,7 en:
112 greater_than_start_date: "must be greater than start date"
112 greater_than_start_date: "must be greater than start date"
113 not_same_project: "doesn't belong to the same project"
113 not_same_project: "doesn't belong to the same project"
114 circular_dependency: "This relation would create a circular dependency"
114 circular_dependency: "This relation would create a circular dependency"
115 cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks"
115
116
116 actionview_instancetag_blank_option: Please select
117 actionview_instancetag_blank_option: Please select
117
118
@@ -277,6 +278,7 en:
277 field_content: Content
278 field_content: Content
278 field_group_by: Group results by
279 field_group_by: Group results by
279 field_sharing: Sharing
280 field_sharing: Sharing
281 field_parent_issue: Parent task
280
282
281 setting_app_title: Application title
283 setting_app_title: Application title
282 setting_app_subtitle: Application subtitle
284 setting_app_subtitle: Application subtitle
@@ -386,6 +388,7 en:
386 permission_delete_messages: Delete messages
388 permission_delete_messages: Delete messages
387 permission_delete_own_messages: Delete own messages
389 permission_delete_own_messages: Delete own messages
388 permission_export_wiki_pages: Export wiki pages
390 permission_export_wiki_pages: Export wiki pages
391 permission_manage_subtasks: Manage subtasks
389
392
390 project_module_issue_tracking: Issue tracking
393 project_module_issue_tracking: Issue tracking
391 project_module_time_tracking: Time tracking
394 project_module_time_tracking: Time tracking
@@ -750,6 +753,7 en:
750 label_missing_api_access_key: Missing an API access key
753 label_missing_api_access_key: Missing an API access key
751 label_api_access_key_created_on: "API access key created {{value}} ago"
754 label_api_access_key_created_on: "API access key created {{value}} ago"
752 label_profile: Profile
755 label_profile: Profile
756 label_subtask_plural: Subtasks
753
757
754 button_login: Login
758 button_login: Login
755 button_submit: Submit
759 button_submit: Submit
@@ -136,6 +136,7 fr:
136 greater_than_start_date: "doit être postérieure à la date de début"
136 greater_than_start_date: "doit être postérieure à la date de début"
137 not_same_project: "n'appartient pas au même projet"
137 not_same_project: "n'appartient pas au même projet"
138 circular_dependency: "Cette relation créerait une dépendance circulaire"
138 circular_dependency: "Cette relation créerait une dépendance circulaire"
139 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
139
140
140 actionview_instancetag_blank_option: Choisir
141 actionview_instancetag_blank_option: Choisir
141
142
@@ -299,6 +300,7 fr:
299 field_group_by: Grouper par
300 field_group_by: Grouper par
300 field_sharing: Partage
301 field_sharing: Partage
301 field_active: Actif
302 field_active: Actif
303 field_parent_issue: Tâche parente
302
304
303 setting_app_title: Titre de l'application
305 setting_app_title: Titre de l'application
304 setting_app_subtitle: Sous-titre de l'application
306 setting_app_subtitle: Sous-titre de l'application
@@ -408,6 +410,7 fr:
408 permission_delete_own_messages: Supprimer ses propres messages
410 permission_delete_own_messages: Supprimer ses propres messages
409 permission_export_wiki_pages: Exporter les pages
411 permission_export_wiki_pages: Exporter les pages
410 permission_manage_project_activities: Gérer les activités
412 permission_manage_project_activities: Gérer les activités
413 permission_manage_subtasks: Gérer les sous-tâches
411
414
412 project_module_issue_tracking: Suivi des demandes
415 project_module_issue_tracking: Suivi des demandes
413 project_module_time_tracking: Suivi du temps passé
416 project_module_time_tracking: Suivi du temps passé
@@ -765,6 +768,7 fr:
765 label_close_versions: Fermer les versions terminées
768 label_close_versions: Fermer les versions terminées
766 label_revision_id: Revision {{value}}
769 label_revision_id: Revision {{value}}
767 label_profile: Profil
770 label_profile: Profil
771 label_subtask_pural: Sous-tâches
768
772
769 button_login: Connexion
773 button_login: Connexion
770 button_submit: Soumettre
774 button_submit: Soumettre
@@ -47,13 +47,14 Redmine::AccessControl.map do |map|
47 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member
47 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member
48 # Issues
48 # Issues
49 map.permission :view_issues, {:projects => :roadmap,
49 map.permission :view_issues, {:projects => :roadmap,
50 :issues => [:index, :changes, :show, :context_menu],
50 :issues => [:index, :changes, :show, :context_menu, :auto_complete],
51 :versions => [:show, :status_by],
51 :versions => [:show, :status_by],
52 :queries => :index,
52 :queries => :index,
53 :reports => [:issue_report, :issue_report_details]}
53 :reports => [:issue_report, :issue_report_details]}
54 map.permission :add_issues, {:issues => [:new, :update_form]}
54 map.permission :add_issues, {:issues => [:new, :update_form]}
55 map.permission :edit_issues, {:issues => [:edit, :update, :reply, :bulk_edit, :update_form]}
55 map.permission :edit_issues, {:issues => [:edit, :update, :reply, :bulk_edit, :update_form]}
56 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
56 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
57 map.permission :manage_subtasks, {}
57 map.permission :add_issue_notes, {:issues => [:edit, :update, :reply]}
58 map.permission :add_issue_notes, {:issues => [:edit, :update, :reply]}
58 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
59 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
59 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
60 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
@@ -53,6 +53,7 module Redmine
53 :add_issues,
53 :add_issues,
54 :edit_issues,
54 :edit_issues,
55 :manage_issue_relations,
55 :manage_issue_relations,
56 :manage_subtasks,
56 :add_issue_notes,
57 :add_issue_notes,
57 :save_queries,
58 :save_queries,
58 :view_gantt,
59 :view_gantt,
@@ -52,8 +52,29 module Redmine
52 @date_to = (@date_from >> @months) - 1
52 @date_to = (@date_from >> @months) - 1
53 end
53 end
54
54
55
55 def events=(e)
56 def events=(e)
56 @events = e.sort {|x,y| x.start_date <=> y.start_date }
57 @events = e
58 # Adds all ancestors
59 root_ids = e.select {|i| i.is_a?(Issue) && i.parent_id? }.collect(&:root_id).uniq
60 if root_ids.any?
61 # Retrieves all nodes
62 parents = Issue.find_all_by_root_id(root_ids, :conditions => ["rgt - lft > 1"])
63 # Only add ancestors
64 @events += parents.select {|p| @events.detect {|i| i.is_a?(Issue) && p.is_ancestor_of?(i)}}
65 end
66 @events.uniq!
67 # Sort issues by hierarchy and start dates
68 @events.sort! {|x,y|
69 if x.is_a?(Issue) && y.is_a?(Issue)
70 gantt_issue_compare(x, y, @events)
71 else
72 gantt_start_compare(x, y)
73 end
74 }
75 # Removes issues that have no start or end date
76 @events.reject! {|i| i.is_a?(Issue) && (i.start_date.nil? || i.due_before.nil?) }
77 @events
57 end
78 end
58
79
59 def params
80 def params
@@ -218,6 +239,36 module Redmine
218 imgl.format = format
239 imgl.format = format
219 imgl.to_blob
240 imgl.to_blob
220 end if Object.const_defined?(:Magick)
241 end if Object.const_defined?(:Magick)
242
243 private
244
245 def gantt_issue_compare(x, y, issues)
246 if x.parent_id == y.parent_id
247 gantt_start_compare(x, y)
248 elsif x.is_ancestor_of?(y)
249 -1
250 elsif y.is_ancestor_of?(x)
251 1
252 else
253 ax = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(x) && !i.is_ancestor_of?(y) }.sort_by(&:lft).first
254 ay = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(y) && !i.is_ancestor_of?(x) }.sort_by(&:lft).first
255 if ax.nil? && ay.nil?
256 gantt_start_compare(x, y)
257 else
258 gantt_issue_compare(ax || x, ay || y, issues)
259 end
260 end
261 end
262
263 def gantt_start_compare(x, y)
264 if x.start_date.nil?
265 -1
266 elsif y.start_date.nil?
267 1
268 else
269 x.start_date <=> y.start_date
270 end
271 end
221 end
272 end
222 end
273 end
223 end
274 end
@@ -194,6 +194,18 function randomKey(size) {
194 return key;
194 return key;
195 }
195 }
196
196
197 function observeParentIssueField(url) {
198 new Ajax.Autocompleter('issue_parent_issue_id',
199 'parent_issue_candidates',
200 url,
201 { minChars: 3,
202 frequency: 0.5,
203 paramName: 'q',
204 updateElement: function(value) {
205 document.getElementById('issue_parent_issue_id').value = value.id;
206 }});
207 }
208
197 /* shows and hides ajax indicator */
209 /* shows and hides ajax indicator */
198 Ajax.Responders.register({
210 Ajax.Responders.register({
199 onCreate: function(){
211 onCreate: function(){
@@ -237,6 +237,13 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
237 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
237 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
238 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
238 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
239
239
240 div.issue div.subject div div { padding-left: 16px; }
241 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
242 div.issue div.subject>div>p { margin-top: 0.5em; }
243 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
244
245 #issue_tree table.issues { border: 0; }
246
240 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
247 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
241 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
248 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
242 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
249 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
@@ -588,8 +595,7 button.tab-right {
588 /***** Auto-complete *****/
595 /***** Auto-complete *****/
589 div.autocomplete {
596 div.autocomplete {
590 position:absolute;
597 position:absolute;
591 width:250px;
598 width:400px;
592 background-color:white;
593 margin:0;
599 margin:0;
594 padding:0;
600 padding:0;
595 }
601 }
@@ -598,23 +604,26 div.autocomplete ul {
598 margin:0;
604 margin:0;
599 padding:0;
605 padding:0;
600 }
606 }
601 div.autocomplete ul li.selected { background-color: #ffb;}
602 div.autocomplete ul li {
607 div.autocomplete ul li {
603 list-style-type:none;
608 list-style-type:none;
604 display:block;
609 display:block;
605 margin:0;
610 margin:-1px 0 0 0;
606 padding:2px;
611 padding:2px;
607 cursor:pointer;
612 cursor:pointer;
608 font-size: 90%;
613 font-size: 90%;
609 border-bottom: 1px solid #ccc;
614 border: 1px solid #ccc;
610 border-left: 1px solid #ccc;
615 border-left: 1px solid #ccc;
611 border-right: 1px solid #ccc;
616 border-right: 1px solid #ccc;
617 background-color:white;
612 }
618 }
619 div.autocomplete ul li.selected { background-color: #ffb;}
613 div.autocomplete ul li span.informal {
620 div.autocomplete ul li span.informal {
614 font-size: 80%;
621 font-size: 80%;
615 color: #aaa;
622 color: #aaa;
616 }
623 }
617
624
625 #parent_issue_candidates ul li {width: 500px;}
626
618 /***** Diff *****/
627 /***** Diff *****/
619 .diff_out { background: #fcc; }
628 .diff_out { background: #fcc; }
620 .diff_in { background: #cfc; }
629 .diff_in { background: #cfc; }
@@ -741,6 +750,12 background-image:url('../images/close_hl.png');
741 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
750 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
742 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
751 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
743 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
752 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
753
754 .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;}
755 .task_late.parent, .task_done.parent { height: 3px;}
756 .task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;}
757 .task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;}
758
744 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
759 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
745
760
746 /***** Icons *****/
761 /***** Icons *****/
@@ -19,27 +19,32 enumerations_004:
19 id: 4
19 id: 4
20 type: IssuePriority
20 type: IssuePriority
21 active: true
21 active: true
22 position: 1
22 enumerations_005:
23 enumerations_005:
23 name: Normal
24 name: Normal
24 id: 5
25 id: 5
25 type: IssuePriority
26 type: IssuePriority
26 is_default: true
27 is_default: true
27 active: true
28 active: true
29 position: 2
28 enumerations_006:
30 enumerations_006:
29 name: High
31 name: High
30 id: 6
32 id: 6
31 type: IssuePriority
33 type: IssuePriority
32 active: true
34 active: true
35 position: 3
33 enumerations_007:
36 enumerations_007:
34 name: Urgent
37 name: Urgent
35 id: 7
38 id: 7
36 type: IssuePriority
39 type: IssuePriority
37 active: true
40 active: true
41 position: 4
38 enumerations_008:
42 enumerations_008:
39 name: Immediate
43 name: Immediate
40 id: 8
44 id: 8
41 type: IssuePriority
45 type: IssuePriority
42 active: true
46 active: true
47 position: 5
43 enumerations_009:
48 enumerations_009:
44 name: Design
49 name: Design
45 id: 9
50 id: 9
@@ -15,6 +15,9 issues_001:
15 status_id: 1
15 status_id: 1
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
18 root_id: 1
19 lft: 1
20 rgt: 2
18 issues_002:
21 issues_002:
19 created_on: 2006-07-19 21:04:21 +02:00
22 created_on: 2006-07-19 21:04:21 +02:00
20 project_id: 1
23 project_id: 1
@@ -31,6 +34,9 issues_002:
31 status_id: 2
34 status_id: 2
32 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
35 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
33 due_date:
36 due_date:
37 root_id: 2
38 lft: 1
39 rgt: 2
34 issues_003:
40 issues_003:
35 created_on: 2006-07-19 21:07:27 +02:00
41 created_on: 2006-07-19 21:07:27 +02:00
36 project_id: 1
42 project_id: 1
@@ -47,6 +53,9 issues_003:
47 status_id: 1
53 status_id: 1
48 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
54 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
49 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
55 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
56 root_id: 3
57 lft: 1
58 rgt: 2
50 issues_004:
59 issues_004:
51 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
60 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
52 project_id: 2
61 project_id: 2
@@ -61,6 +70,9 issues_004:
61 assigned_to_id: 2
70 assigned_to_id: 2
62 author_id: 2
71 author_id: 2
63 status_id: 1
72 status_id: 1
73 root_id: 4
74 lft: 1
75 rgt: 2
64 issues_005:
76 issues_005:
65 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
77 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
66 project_id: 3
78 project_id: 3
@@ -75,6 +87,9 issues_005:
75 assigned_to_id:
87 assigned_to_id:
76 author_id: 2
88 author_id: 2
77 status_id: 1
89 status_id: 1
90 root_id: 5
91 lft: 1
92 rgt: 2
78 issues_006:
93 issues_006:
79 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
94 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
80 project_id: 5
95 project_id: 5
@@ -91,6 +106,9 issues_006:
91 status_id: 1
106 status_id: 1
92 start_date: <%= Date.today.to_s(:db) %>
107 start_date: <%= Date.today.to_s(:db) %>
93 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
108 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
109 root_id: 6
110 lft: 1
111 rgt: 2
94 issues_007:
112 issues_007:
95 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
113 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
96 project_id: 1
114 project_id: 1
@@ -108,6 +126,9 issues_007:
108 start_date: <%= 10.days.ago.to_s(:db) %>
126 start_date: <%= 10.days.ago.to_s(:db) %>
109 due_date: <%= Date.today.to_s(:db) %>
127 due_date: <%= Date.today.to_s(:db) %>
110 lock_version: 0
128 lock_version: 0
129 root_id: 7
130 lft: 1
131 rgt: 2
111 issues_008:
132 issues_008:
112 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
133 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
113 project_id: 1
134 project_id: 1
@@ -125,6 +146,9 issues_008:
125 start_date:
146 start_date:
126 due_date:
147 due_date:
127 lock_version: 0
148 lock_version: 0
149 root_id: 8
150 lft: 1
151 rgt: 2
128 issues_009:
152 issues_009:
129 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
153 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
130 project_id: 5
154 project_id: 5
@@ -141,6 +165,9 issues_009:
141 status_id: 1
165 status_id: 1
142 start_date: <%= Date.today.to_s(:db) %>
166 start_date: <%= Date.today.to_s(:db) %>
143 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
167 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
168 root_id: 9
169 lft: 1
170 rgt: 2
144 issues_010:
171 issues_010:
145 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
172 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
146 project_id: 5
173 project_id: 5
@@ -157,6 +184,9 issues_010:
157 status_id: 1
184 status_id: 1
158 start_date: <%= Date.today.to_s(:db) %>
185 start_date: <%= Date.today.to_s(:db) %>
159 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
186 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
187 root_id: 10
188 lft: 1
189 rgt: 2
160 issues_011:
190 issues_011:
161 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
191 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
162 project_id: 1
192 project_id: 1
@@ -173,6 +203,9 issues_011:
173 status_id: 5
203 status_id: 5
174 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
204 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
175 due_date:
205 due_date:
206 root_id: 11
207 lft: 1
208 rgt: 2
176 issues_012:
209 issues_012:
177 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
210 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
178 project_id: 1
211 project_id: 1
@@ -189,6 +222,9 issues_012:
189 status_id: 5
222 status_id: 5
190 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
223 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
191 due_date:
224 due_date:
225 root_id: 12
226 lft: 1
227 rgt: 2
192 issues_013:
228 issues_013:
193 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
229 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
194 project_id: 3
230 project_id: 3
@@ -203,3 +239,6 issues_013:
203 assigned_to_id:
239 assigned_to_id:
204 author_id: 2
240 author_id: 2
205 status_id: 1
241 status_id: 1
242 root_id: 13
243 lft: 1
244 rgt: 2
@@ -14,6 +14,7 roles_001:
14 - :add_issues
14 - :add_issues
15 - :edit_issues
15 - :edit_issues
16 - :manage_issue_relations
16 - :manage_issue_relations
17 - :manage_subtasks
17 - :add_issue_notes
18 - :add_issue_notes
18 - :move_issues
19 - :move_issues
19 - :delete_issues
20 - :delete_issues
@@ -66,6 +67,7 roles_002:
66 - :add_issues
67 - :add_issues
67 - :edit_issues
68 - :edit_issues
68 - :manage_issue_relations
69 - :manage_issue_relations
70 - :manage_subtasks
69 - :add_issue_notes
71 - :add_issue_notes
70 - :move_issues
72 - :move_issues
71 - :delete_issues
73 - :delete_issues
@@ -476,7 +476,7 class IssuesControllerTest < ActionController::TestCase
476 :subject => 'This is first issue',
476 :subject => 'This is first issue',
477 :priority_id => 5},
477 :priority_id => 5},
478 :continue => ''
478 :continue => ''
479 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
479 assert_redirected_to :controller => 'issues', :action => 'new', :issue => {:tracker_id => 3}
480 end
480 end
481
481
482 def test_post_new_without_custom_fields_param
482 def test_post_new_without_custom_fields_param
@@ -533,6 +533,20 class IssuesControllerTest < ActionController::TestCase
533 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
533 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
534 end
534 end
535
535
536 def test_post_new_subissue
537 @request.session[:user_id] = 2
538
539 assert_difference 'Issue.count' do
540 post :new, :project_id => 1,
541 :issue => {:tracker_id => 1,
542 :subject => 'This is a child issue',
543 :parent_issue_id => 2}
544 end
545 issue = Issue.find_by_subject('This is a child issue')
546 assert_not_nil issue
547 assert_equal Issue.find(2), issue.parent
548 end
549
536 def test_post_new_should_send_a_notification
550 def test_post_new_should_send_a_notification
537 ActionMailer::Base.deliveries.clear
551 ActionMailer::Base.deliveries.clear
538 @request.session[:user_id] = 2
552 @request.session[:user_id] = 2
@@ -1215,6 +1229,34 class IssuesControllerTest < ActionController::TestCase
1215 :class => 'icon-del disabled' }
1229 :class => 'icon-del disabled' }
1216 end
1230 end
1217
1231
1232 def test_auto_complete_routing
1233 assert_routing(
1234 {:method => :get, :path => '/issues/auto_complete'},
1235 :controller => 'issues', :action => 'auto_complete'
1236 )
1237 end
1238
1239 def test_auto_complete_should_not_be_case_sensitive
1240 get :auto_complete, :project_id => 'ecookbook', :q => 'ReCiPe'
1241 assert_response :success
1242 assert_not_nil assigns(:issues)
1243 assert assigns(:issues).detect {|issue| issue.subject.match /recipe/}
1244 end
1245
1246 def test_auto_complete_should_return_issue_with_given_id
1247 get :auto_complete, :project_id => 'subproject1', :q => '13'
1248 assert_response :success
1249 assert_not_nil assigns(:issues)
1250 assert assigns(:issues).include?(Issue.find(13))
1251 end
1252
1253 def test_destroy_routing
1254 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1255 {:controller => 'issues', :action => 'destroy', :id => '1'},
1256 {:method => :post, :path => '/issues/1/destroy'}
1257 )
1258 end
1259
1218 def test_destroy_issue_with_no_time_entries
1260 def test_destroy_issue_with_no_time_entries
1219 assert_nil TimeEntry.find_by_issue_id(2)
1261 assert_nil TimeEntry.find_by_issue_id(2)
1220 @request.session[:user_id] = 2
1262 @request.session[:user_id] = 2
@@ -329,7 +329,7 class IssueTest < ActiveSupport::TestCase
329
329
330 def test_move_to_another_project_with_same_category
330 def test_move_to_another_project_with_same_category
331 issue = Issue.find(1)
331 issue = Issue.find(1)
332 assert issue.move_to(Project.find(2))
332 assert issue.move_to_project(Project.find(2))
333 issue.reload
333 issue.reload
334 assert_equal 2, issue.project_id
334 assert_equal 2, issue.project_id
335 # Category changes
335 # Category changes
@@ -340,7 +340,7 class IssueTest < ActiveSupport::TestCase
340
340
341 def test_move_to_another_project_without_same_category
341 def test_move_to_another_project_without_same_category
342 issue = Issue.find(2)
342 issue = Issue.find(2)
343 assert issue.move_to(Project.find(2))
343 assert issue.move_to_project(Project.find(2))
344 issue.reload
344 issue.reload
345 assert_equal 2, issue.project_id
345 assert_equal 2, issue.project_id
346 # Category cleared
346 # Category cleared
@@ -350,7 +350,7 class IssueTest < ActiveSupport::TestCase
350 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
350 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
351 issue = Issue.find(1)
351 issue = Issue.find(1)
352 issue.update_attribute(:fixed_version_id, 1)
352 issue.update_attribute(:fixed_version_id, 1)
353 assert issue.move_to(Project.find(2))
353 assert issue.move_to_project(Project.find(2))
354 issue.reload
354 issue.reload
355 assert_equal 2, issue.project_id
355 assert_equal 2, issue.project_id
356 # Cleared fixed_version
356 # Cleared fixed_version
@@ -360,7 +360,7 class IssueTest < ActiveSupport::TestCase
360 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
360 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
361 issue = Issue.find(1)
361 issue = Issue.find(1)
362 issue.update_attribute(:fixed_version_id, 4)
362 issue.update_attribute(:fixed_version_id, 4)
363 assert issue.move_to(Project.find(5))
363 assert issue.move_to_project(Project.find(5))
364 issue.reload
364 issue.reload
365 assert_equal 5, issue.project_id
365 assert_equal 5, issue.project_id
366 # Keep fixed_version
366 # Keep fixed_version
@@ -370,7 +370,7 class IssueTest < ActiveSupport::TestCase
370 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
370 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
371 issue = Issue.find(1)
371 issue = Issue.find(1)
372 issue.update_attribute(:fixed_version_id, 1)
372 issue.update_attribute(:fixed_version_id, 1)
373 assert issue.move_to(Project.find(5))
373 assert issue.move_to_project(Project.find(5))
374 issue.reload
374 issue.reload
375 assert_equal 5, issue.project_id
375 assert_equal 5, issue.project_id
376 # Cleared fixed_version
376 # Cleared fixed_version
@@ -380,7 +380,7 class IssueTest < ActiveSupport::TestCase
380 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
380 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
381 issue = Issue.find(1)
381 issue = Issue.find(1)
382 issue.update_attribute(:fixed_version_id, 7)
382 issue.update_attribute(:fixed_version_id, 7)
383 assert issue.move_to(Project.find(2))
383 assert issue.move_to_project(Project.find(2))
384 issue.reload
384 issue.reload
385 assert_equal 2, issue.project_id
385 assert_equal 2, issue.project_id
386 # Keep fixed_version
386 # Keep fixed_version
@@ -392,7 +392,7 class IssueTest < ActiveSupport::TestCase
392 target = Project.find(2)
392 target = Project.find(2)
393 target.tracker_ids = [3]
393 target.tracker_ids = [3]
394 target.save
394 target.save
395 assert_equal false, issue.move_to(target)
395 assert_equal false, issue.move_to_project(target)
396 issue.reload
396 issue.reload
397 assert_equal 1, issue.project_id
397 assert_equal 1, issue.project_id
398 end
398 end
@@ -401,7 +401,7 class IssueTest < ActiveSupport::TestCase
401 issue = Issue.find(1)
401 issue = Issue.find(1)
402 copy = nil
402 copy = nil
403 assert_difference 'Issue.count' do
403 assert_difference 'Issue.count' do
404 copy = issue.move_to(issue.project, nil, :copy => true)
404 copy = issue.move_to_project(issue.project, nil, :copy => true)
405 end
405 end
406 assert_kind_of Issue, copy
406 assert_kind_of Issue, copy
407 assert_equal issue.project, copy.project
407 assert_equal issue.project, copy.project
@@ -412,8 +412,9 class IssueTest < ActiveSupport::TestCase
412 issue = Issue.find(1)
412 issue = Issue.find(1)
413 copy = nil
413 copy = nil
414 assert_difference 'Issue.count' do
414 assert_difference 'Issue.count' do
415 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
415 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
416 end
416 end
417 copy.reload
417 assert_kind_of Issue, copy
418 assert_kind_of Issue, copy
418 assert_equal Project.find(3), copy.project
419 assert_equal Project.find(3), copy.project
419 assert_equal Tracker.find(2), copy.tracker
420 assert_equal Tracker.find(2), copy.tracker
@@ -421,7 +422,7 class IssueTest < ActiveSupport::TestCase
421 assert_nil copy.custom_value_for(2)
422 assert_nil copy.custom_value_for(2)
422 end
423 end
423
424
424 context "#move_to" do
425 context "#move_to_project" do
425 context "as a copy" do
426 context "as a copy" do
426 setup do
427 setup do
427 @issue = Issue.find(1)
428 @issue = Issue.find(1)
@@ -429,24 +430,24 class IssueTest < ActiveSupport::TestCase
429 end
430 end
430
431
431 should "allow assigned_to changes" do
432 should "allow assigned_to changes" do
432 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
433 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
433 assert_equal 3, @copy.assigned_to_id
434 assert_equal 3, @copy.assigned_to_id
434 end
435 end
435
436
436 should "allow status changes" do
437 should "allow status changes" do
437 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
438 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
438 assert_equal 2, @copy.status_id
439 assert_equal 2, @copy.status_id
439 end
440 end
440
441
441 should "allow start date changes" do
442 should "allow start date changes" do
442 date = Date.today
443 date = Date.today
443 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
444 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
444 assert_equal date, @copy.start_date
445 assert_equal date, @copy.start_date
445 end
446 end
446
447
447 should "allow due date changes" do
448 should "allow due date changes" do
448 date = Date.today
449 date = Date.today
449 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
450 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
450
451
451 assert_equal date, @copy.due_date
452 assert_equal date, @copy.due_date
452 end
453 end
@@ -457,7 +458,7 class IssueTest < ActiveSupport::TestCase
457 issue = Issue.find(12)
458 issue = Issue.find(12)
458 assert issue.recipients.include?(issue.author.mail)
459 assert issue.recipients.include?(issue.author.mail)
459 # move the issue to a private project
460 # move the issue to a private project
460 copy = issue.move_to(Project.find(5), Tracker.find(2), :copy => true)
461 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
461 # author is not a member of project anymore
462 # author is not a member of project anymore
462 assert !copy.recipients.include?(copy.author.mail)
463 assert !copy.recipients.include?(copy.author.mail)
463 end
464 end
@@ -77,6 +77,13 module Redmine
77 @custom_field_values = nil
77 @custom_field_values = nil
78 end
78 end
79
79
80 def reset_custom_values!
81 @custom_field_values = nil
82 @custom_field_values_changed = true
83 values = custom_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
84 custom_values.each {|cv| cv.destroy unless custom_field_values.include?(cv)}
85 end
86
80 module ClassMethods
87 module ClassMethods
81 end
88 end
82 end
89 end
@@ -256,7 +256,7 module CollectiveIdea #:nodoc:
256 end
256 end
257
257
258 def leaf?
258 def leaf?
259 right - left == 1
259 new_record? || (right - left == 1)
260 end
260 end
261
261
262 # Returns true is this is a child node
262 # Returns true is this is a child node
General Comments 0
You need to be logged in to leave comments. Login now