@@ -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 |
@@ -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 | 22 | before_filter :find_issue, :only => [:show, :edit, :update, :reply] |
|
23 | 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 | 25 | before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu] |
|
26 | 26 | before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] |
|
27 | 27 | accept_key_auth :index, :show, :changes |
@@ -164,7 +164,8 class IssuesController < ApplicationController | |||
|
164 | 164 | call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) |
|
165 | 165 | respond_to do |format| |
|
166 | 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 | 169 | { :action => 'show', :id => @issue }) |
|
169 | 170 | } |
|
170 | 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 | 249 | # Bulk edit a set of issues |
|
249 | 250 | def bulk_edit |
|
251 | @issues.sort! | |
|
250 | 252 | if request.post? |
|
251 | 253 | attributes = (params[:issue] || {}).reject {|k,v| v.blank?} |
|
252 | 254 | attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'} |
@@ -254,6 +256,7 class IssuesController < ApplicationController | |||
|
254 | 256 | |
|
255 | 257 | unsaved_issue_ids = [] |
|
256 | 258 | @issues.each do |issue| |
|
259 | issue.reload | |
|
257 | 260 | journal = issue.init_journal(User.current, params[:notes]) |
|
258 | 261 | issue.safe_attributes = attributes |
|
259 | 262 | call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) |
@@ -271,6 +274,7 class IssuesController < ApplicationController | |||
|
271 | 274 | end |
|
272 | 275 | |
|
273 | 276 | def move |
|
277 | @issues.sort! | |
|
274 | 278 | @copy = params[:copy_options] && params[:copy_options][:copy] |
|
275 | 279 | @allowed_projects = [] |
|
276 | 280 | # find projects to which the user is allowed to move the issue |
@@ -289,6 +293,7 class IssuesController < ApplicationController | |||
|
289 | 293 | unsaved_issue_ids = [] |
|
290 | 294 | moved_issues = [] |
|
291 | 295 | @issues.each do |issue| |
|
296 | issue.reload | |
|
292 | 297 | changed_attributes = {} |
|
293 | 298 | [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute| |
|
294 | 299 | unless params[valid_attribute].blank? |
@@ -297,7 +302,7 class IssuesController < ApplicationController | |||
|
297 | 302 | end |
|
298 | 303 | issue.init_journal(User.current) |
|
299 | 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 | 306 | moved_issues << r |
|
302 | 307 | else |
|
303 | 308 | unsaved_issue_ids << issue.id |
@@ -456,6 +461,18 class IssuesController < ApplicationController | |||
|
456 | 461 | render :partial => 'common/preview' |
|
457 | 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 | 476 | private |
|
460 | 477 | def find_issue |
|
461 | 478 | @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) |
@@ -94,7 +94,7 class TimelogController < ApplicationController | |||
|
94 | 94 | elsif @issue.nil? |
|
95 | 95 | sql_condition = @project.project_condition(Setting.display_subprojects_issues?) |
|
96 | 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 | 98 | end |
|
99 | 99 | |
|
100 | 100 | sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours" |
@@ -166,7 +166,7 class TimelogController < ApplicationController | |||
|
166 | 166 | elsif @issue.nil? |
|
167 | 167 | cond << @project.project_condition(Setting.display_subprojects_issues?) |
|
168 | 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 | 170 | end |
|
171 | 171 | |
|
172 | 172 | retrieve_date_range |
@@ -176,7 +176,7 class TimelogController < ApplicationController | |||
|
176 | 176 | respond_to do |format| |
|
177 | 177 | format.html { |
|
178 | 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 | 180 | @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page'] |
|
181 | 181 | @entries = TimeEntry.find(:all, |
|
182 | 182 | :include => [:project, :activity, :user, {:issue => :tracker}], |
@@ -184,7 +184,7 class TimelogController < ApplicationController | |||
|
184 | 184 | :order => sort_clause, |
|
185 | 185 | :limit => @entry_pages.items_per_page, |
|
186 | 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 | 189 | render :layout => !request.xhr? |
|
190 | 190 | } |
@@ -30,6 +30,34 module IssuesHelper | |||
|
30 | 30 | "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" + |
|
31 | 31 | "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}" |
|
32 | 32 | end |
|
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 | |
|
33 | 61 | |
|
34 | 62 | def render_custom_fields_rows(issue) |
|
35 | 63 | return if issue.custom_field_values.empty? |
@@ -32,6 +32,7 class Issue < ActiveRecord::Base | |||
|
32 | 32 | has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all |
|
33 | 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 | 36 | acts_as_attachable :after_remove => :attachment_removed |
|
36 | 37 | acts_as_customizable |
|
37 | 38 | acts_as_watchable |
@@ -68,7 +69,9 class Issue < ActiveRecord::Base | |||
|
68 | 69 | |
|
69 | 70 | before_create :default_assign |
|
70 | 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 | 76 | # Returns true if usr or current user is allowed to view the issue |
|
74 | 77 | def visible?(usr=nil) |
@@ -90,60 +93,75 class Issue < ActiveRecord::Base | |||
|
90 | 93 | |
|
91 | 94 | def copy_from(arg) |
|
92 | 95 | issue = arg.is_a?(Issue) ? arg : Issue.find(arg) |
|
93 | self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on") | |
|
94 |
self.custom_values = issue.custom_values. |
|
|
96 | self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") | |
|
97 | self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} | |
|
95 | 98 | self.status = issue.status |
|
96 | 99 | self |
|
97 | 100 | end |
|
98 | 101 | |
|
99 | 102 | # Moves/copies an issue to a new project and tracker |
|
100 | 103 | # Returns the moved/copied issue on success, false on failure |
|
101 | def move_to(new_project, new_tracker = nil, options = {}) | |
|
102 | options ||= {} | |
|
103 | issue = options[:copy] ? self.clone : self | |
|
104 | def move_to_project(*args) | |
|
104 | 105 | ret = Issue.transaction do |
|
105 | if new_project && issue.project_id != new_project.id | |
|
106 | # delete issue relations | |
|
107 | unless Setting.cross_project_issue_relations? | |
|
108 | issue.relations_from.clear | |
|
109 | issue.relations_to.clear | |
|
110 | end | |
|
111 | # issue is moved to another project | |
|
112 | # reassign to the category with same name if any | |
|
113 | new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name) | |
|
114 | issue.category = new_category | |
|
115 | # Keep the fixed_version if it's still valid in the new_project | |
|
116 | unless new_project.shared_versions.include?(issue.fixed_version) | |
|
117 | issue.fixed_version = nil | |
|
118 | end | |
|
119 | issue.project = new_project | |
|
120 | end | |
|
121 | if new_tracker | |
|
122 | issue.tracker = new_tracker | |
|
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 | ||
|
114 | if new_project && issue.project_id != new_project.id | |
|
115 | # delete issue relations | |
|
116 | unless Setting.cross_project_issue_relations? | |
|
117 | issue.relations_from.clear | |
|
118 | issue.relations_to.clear | |
|
123 | 119 | end |
|
124 | if options[:copy] | |
|
125 | issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} | |
|
126 | issue.status = if options[:attributes] && options[:attributes][:status_id] | |
|
127 | IssueStatus.find_by_id(options[:attributes][:status_id]) | |
|
128 | else | |
|
129 | self.status | |
|
130 | end | |
|
120 | # issue is moved to another project | |
|
121 | # reassign to the category with same name if any | |
|
122 | new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name) | |
|
123 | issue.category = new_category | |
|
124 | # Keep the fixed_version if it's still valid in the new_project | |
|
125 | unless new_project.shared_versions.include?(issue.fixed_version) | |
|
126 | issue.fixed_version = nil | |
|
131 | 127 | end |
|
132 | # Allow bulk setting of attributes on the issue | |
|
133 | if options[:attributes] | |
|
134 | issue.attributes = options[:attributes] | |
|
128 | issue.project = new_project | |
|
129 | if issue.parent && issue.parent.project_id != issue.project_id | |
|
130 | issue.parent_issue_id = nil | |
|
135 | 131 | end |
|
136 | if issue.save | |
|
137 | unless options[:copy] | |
|
138 | # Manually update project_id on related time entries | |
|
139 | TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) | |
|
132 | end | |
|
133 | if new_tracker | |
|
134 | issue.tracker = new_tracker | |
|
135 | issue.reset_custom_values! | |
|
136 | end | |
|
137 | if options[:copy] | |
|
138 | issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} | |
|
139 | issue.status = if options[:attributes] && options[:attributes][:status_id] | |
|
140 | IssueStatus.find_by_id(options[:attributes][:status_id]) | |
|
141 | else | |
|
142 | self.status | |
|
143 | end | |
|
144 | end | |
|
145 | # Allow bulk setting of attributes on the issue | |
|
146 | if options[:attributes] | |
|
147 | issue.attributes = options[:attributes] | |
|
148 | end | |
|
149 | if issue.save | |
|
150 | unless options[:copy] | |
|
151 | # Manually update project_id on related time entries | |
|
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 | |
|
158 | end | |
|
140 | 159 | end |
|
141 | true | |
|
142 | else | |
|
143 | raise ActiveRecord::Rollback | |
|
144 | 160 | end |
|
161 | else | |
|
162 | return false | |
|
145 | 163 | end |
|
146 | ret ? issue : false | |
|
164 | issue | |
|
147 | 165 | end |
|
148 | 166 | |
|
149 | 167 | def priority_id=(pid) |
@@ -177,6 +195,7 class Issue < ActiveRecord::Base | |||
|
177 | 195 | SAFE_ATTRIBUTES = %w( |
|
178 | 196 | tracker_id |
|
179 | 197 | status_id |
|
198 | parent_issue_id | |
|
180 | 199 | category_id |
|
181 | 200 | assigned_to_id |
|
182 | 201 | priority_id |
@@ -203,6 +222,19 class Issue < ActiveRecord::Base | |||
|
203 | 222 | attrs.delete('status_id') |
|
204 | 223 | end |
|
205 | 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 | 238 | self.attributes = attrs |
|
207 | 239 | end |
|
208 | 240 | |
@@ -249,6 +281,22 class Issue < ActiveRecord::Base | |||
|
249 | 281 | errors.add :tracker_id, :inclusion |
|
250 | 282 | end |
|
251 | 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 | 300 | end |
|
253 | 301 | |
|
254 | 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 | 388 | notified.collect(&:mail) |
|
341 | 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 | 393 | # Example: |
|
346 | # spent_hours => 0 | |
|
347 | # spent_hours => 50 | |
|
394 | # spent_hours => 0.0 | |
|
395 | # spent_hours => 50.2 | |
|
348 | 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 | 398 | end |
|
351 | 399 | |
|
352 | 400 | def relations |
@@ -386,6 +434,16 class Issue < ActiveRecord::Base | |||
|
386 | 434 | @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min |
|
387 | 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 | 447 | def to_s |
|
390 | 448 | "#{tracker} ##{id}: #{subject}" |
|
391 | 449 | end |
@@ -442,6 +500,24 class Issue < ActiveRecord::Base | |||
|
442 | 500 | Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids]) |
|
443 | 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 | 521 | # Extracted from the ReportsController. |
|
446 | 522 | def self.by_tracker(project) |
|
447 | 523 | count_and_group_by(:project => project, |
@@ -495,6 +571,95 class Issue < ActiveRecord::Base | |||
|
495 | 571 | |
|
496 | 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 | 663 | # Update issues so their versions are not pointing to a |
|
499 | 664 | # fixed_version that is not shared with the issue's project |
|
500 | 665 | def self.update_versions(conditions=nil) |
@@ -562,7 +727,7 class Issue < ActiveRecord::Base | |||
|
562 | 727 | def create_journal |
|
563 | 728 | if @current_journal |
|
564 | 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 | 731 | @current_journal.details << JournalDetail.new(:property => 'attr', |
|
567 | 732 | :prop_key => c, |
|
568 | 733 | :old_value => @issue_before_change.send(c), |
@@ -48,6 +48,7 class IssueRelation < ActiveRecord::Base | |||
|
48 | 48 | errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id |
|
49 | 49 | errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? |
|
50 | 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 | 52 | end |
|
52 | 53 | end |
|
53 | 54 |
@@ -557,9 +557,12 class Project < ActiveRecord::Base | |||
|
557 | 557 | # value. Used to map the two togeather for issue relations. |
|
558 | 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 | 563 | new_issue = Issue.new |
|
562 | 564 | new_issue.copy_from(issue) |
|
565 | new_issue.project = self | |
|
563 | 566 | # Reassign fixed_versions by name, since names are unique per |
|
564 | 567 | # project and the versions for self are not yet saved |
|
565 | 568 | if issue.fixed_version |
@@ -570,6 +573,13 class Project < ActiveRecord::Base | |||
|
570 | 573 | if issue.category |
|
571 | 574 | new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first |
|
572 | 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 | 583 | self.issues << new_issue |
|
574 | 584 | issues_map[issue.id] = new_issue |
|
575 | 585 | end |
@@ -7,7 +7,7 | |||
|
7 | 7 | <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p> |
|
8 | 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 | 11 | <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p> |
|
12 | 12 | <% unless @project.issue_categories.empty? %> |
|
13 | 13 | <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> |
@@ -31,10 +31,10 | |||
|
31 | 31 | </div> |
|
32 | 32 | |
|
33 | 33 | <div class="splitcontentright"> |
|
34 | <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p> | |
|
35 | <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p> | |
|
36 | <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p> | |
|
37 | <% if Issue.use_field_for_done_ratio? %> | |
|
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, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p> | |
|
36 | <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p> | |
|
37 | <% if @issue.leaf? && Issue.use_field_for_done_ratio? %> | |
|
38 | 38 | <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p> |
|
39 | 39 | <% end %> |
|
40 | 40 | </div> |
@@ -5,6 +5,16 | |||
|
5 | 5 | :with => "Form.serialize('issue-form')" %> |
|
6 | 6 | |
|
7 | 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 | 18 | <p><%= f.text_area :description, |
|
9 | 19 | :cols => 60, |
|
10 | 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 | 79 | # Tasks subjects |
|
80 | 80 | # |
|
81 | 81 | top = headers_height + 8 |
|
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> | |
|
82 | @gantt.events.each do |i| | |
|
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 | 86 | <% if i.is_a? Issue %> |
|
85 | 87 | <%= h("#{i.project} -") unless @project && @project == i.project %> |
|
86 | 88 | <%= link_to_issue i %> |
@@ -189,15 +191,16 top = headers_height + 10 | |||
|
189 | 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 | 192 | d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width |
|
191 | 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=" |
|
|
196 | <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="<%= css %> task_todo"><div class="left"></div> <div class="right"></div></div> | |
|
194 | 197 | <% if l_width > 0 %> |
|
195 |
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class=" |
|
|
198 | <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="<%= css %> task_late"> </div> | |
|
196 | 199 | <% end %> |
|
197 | 200 | <% if d_width > 0 %> |
|
198 |
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class=" |
|
|
201 | <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="<%= css %> task_done"> </div> | |
|
199 | 202 | <% end %> |
|
200 |
<div style="top:<%= top %>px;left:<%= i_left + i_width + |
|
|
203 | <div style="top:<%= top %>px;left:<%= i_left + i_width + 8 %>px;background:#fff;" class="<%= css %>"> | |
|
201 | 204 | <%= i.status.name %> |
|
202 | 205 | <%= (i.done_ratio).to_i %>% |
|
203 | 206 | </div> |
@@ -4,7 +4,10 | |||
|
4 | 4 | |
|
5 | 5 | <div class="<%= @issue.css_classes %> details"> |
|
6 | 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 | 11 | <p class="author"> |
|
9 | 12 | <%= authoring @issue.created_on, @issue.author %>. |
|
10 | 13 | <% if @issue.created_on != @issue.updated_on %> |
@@ -56,6 +59,17 | |||
|
56 | 59 | |
|
57 | 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 | 73 | <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %> |
|
60 | 74 | <hr /> |
|
61 | 75 | <div id="relations"> |
@@ -113,4 +127,8 | |||
|
113 | 127 | <% content_for :header_tags do %> |
|
114 | 128 | <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %> |
|
115 | 129 | <%= stylesheet_link_tag 'scm' %> |
|
130 | <%= javascript_include_tag 'context_menu' %> | |
|
131 | <%= stylesheet_link_tag 'context_menu' %> | |
|
116 | 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 | 112 | greater_than_start_date: "must be greater than start date" |
|
113 | 113 | not_same_project: "doesn't belong to the same project" |
|
114 | 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 | 117 | actionview_instancetag_blank_option: Please select |
|
117 | 118 | |
@@ -277,6 +278,7 en: | |||
|
277 | 278 | field_content: Content |
|
278 | 279 | field_group_by: Group results by |
|
279 | 280 | field_sharing: Sharing |
|
281 | field_parent_issue: Parent task | |
|
280 | 282 | |
|
281 | 283 | setting_app_title: Application title |
|
282 | 284 | setting_app_subtitle: Application subtitle |
@@ -386,6 +388,7 en: | |||
|
386 | 388 | permission_delete_messages: Delete messages |
|
387 | 389 | permission_delete_own_messages: Delete own messages |
|
388 | 390 | permission_export_wiki_pages: Export wiki pages |
|
391 | permission_manage_subtasks: Manage subtasks | |
|
389 | 392 | |
|
390 | 393 | project_module_issue_tracking: Issue tracking |
|
391 | 394 | project_module_time_tracking: Time tracking |
@@ -750,6 +753,7 en: | |||
|
750 | 753 | label_missing_api_access_key: Missing an API access key |
|
751 | 754 | label_api_access_key_created_on: "API access key created {{value}} ago" |
|
752 | 755 | label_profile: Profile |
|
756 | label_subtask_plural: Subtasks | |
|
753 | 757 | |
|
754 | 758 | button_login: Login |
|
755 | 759 | button_submit: Submit |
@@ -136,6 +136,7 fr: | |||
|
136 | 136 | greater_than_start_date: "doit être postérieure à la date de début" |
|
137 | 137 | not_same_project: "n'appartient pas au même projet" |
|
138 | 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 | 141 | actionview_instancetag_blank_option: Choisir |
|
141 | 142 | |
@@ -299,6 +300,7 fr: | |||
|
299 | 300 | field_group_by: Grouper par |
|
300 | 301 | field_sharing: Partage |
|
301 | 302 | field_active: Actif |
|
303 | field_parent_issue: Tâche parente | |
|
302 | 304 | |
|
303 | 305 | setting_app_title: Titre de l'application |
|
304 | 306 | setting_app_subtitle: Sous-titre de l'application |
@@ -408,6 +410,7 fr: | |||
|
408 | 410 | permission_delete_own_messages: Supprimer ses propres messages |
|
409 | 411 | permission_export_wiki_pages: Exporter les pages |
|
410 | 412 | permission_manage_project_activities: Gérer les activités |
|
413 | permission_manage_subtasks: Gérer les sous-tâches | |
|
411 | 414 | |
|
412 | 415 | project_module_issue_tracking: Suivi des demandes |
|
413 | 416 | project_module_time_tracking: Suivi du temps passé |
@@ -765,6 +768,7 fr: | |||
|
765 | 768 | label_close_versions: Fermer les versions terminées |
|
766 | 769 | label_revision_id: Revision {{value}} |
|
767 | 770 | label_profile: Profil |
|
771 | label_subtask_pural: Sous-tâches | |
|
768 | 772 | |
|
769 | 773 | button_login: Connexion |
|
770 | 774 | button_submit: Soumettre |
@@ -47,13 +47,14 Redmine::AccessControl.map do |map| | |||
|
47 | 47 | map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member |
|
48 | 48 | # Issues |
|
49 | 49 | map.permission :view_issues, {:projects => :roadmap, |
|
50 | :issues => [:index, :changes, :show, :context_menu], | |
|
50 | :issues => [:index, :changes, :show, :context_menu, :auto_complete], | |
|
51 | 51 | :versions => [:show, :status_by], |
|
52 | 52 | :queries => :index, |
|
53 | 53 | :reports => [:issue_report, :issue_report_details]} |
|
54 | 54 | map.permission :add_issues, {:issues => [:new, :update_form]} |
|
55 | 55 | map.permission :edit_issues, {:issues => [:edit, :update, :reply, :bulk_edit, :update_form]} |
|
56 | 56 | map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} |
|
57 | map.permission :manage_subtasks, {} | |
|
57 | 58 | map.permission :add_issue_notes, {:issues => [:edit, :update, :reply]} |
|
58 | 59 | map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin |
|
59 | 60 | map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin |
@@ -53,6 +53,7 module Redmine | |||
|
53 | 53 | :add_issues, |
|
54 | 54 | :edit_issues, |
|
55 | 55 | :manage_issue_relations, |
|
56 | :manage_subtasks, | |
|
56 | 57 | :add_issue_notes, |
|
57 | 58 | :save_queries, |
|
58 | 59 | :view_gantt, |
@@ -52,8 +52,29 module Redmine | |||
|
52 | 52 | @date_to = (@date_from >> @months) - 1 |
|
53 | 53 | end |
|
54 | 54 | |
|
55 | ||
|
55 | 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 | 78 | end |
|
58 | 79 | |
|
59 | 80 | def params |
@@ -218,6 +239,36 module Redmine | |||
|
218 | 239 | imgl.format = format |
|
219 | 240 | imgl.to_blob |
|
220 | 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 | 272 | end |
|
222 | 273 | end |
|
223 | 274 | end |
@@ -194,6 +194,18 function randomKey(size) { | |||
|
194 | 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 | 209 | /* shows and hides ajax indicator */ |
|
198 | 210 | Ajax.Responders.register({ |
|
199 | 211 | onCreate: function(){ |
@@ -237,6 +237,13 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} | |||
|
237 | 237 | p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } |
|
238 | 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 | 247 | fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; } |
|
241 | 248 | fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; } |
|
242 | 249 | fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); } |
@@ -588,8 +595,7 button.tab-right { | |||
|
588 | 595 | /***** Auto-complete *****/ |
|
589 | 596 | div.autocomplete { |
|
590 | 597 | position:absolute; |
|
591 |
width: |
|
|
592 | background-color:white; | |
|
598 | width:400px; | |
|
593 | 599 | margin:0; |
|
594 | 600 | padding:0; |
|
595 | 601 | } |
@@ -598,23 +604,26 div.autocomplete ul { | |||
|
598 | 604 | margin:0; |
|
599 | 605 | padding:0; |
|
600 | 606 | } |
|
601 | div.autocomplete ul li.selected { background-color: #ffb;} | |
|
602 | 607 | div.autocomplete ul li { |
|
603 | 608 | list-style-type:none; |
|
604 | 609 | display:block; |
|
605 | margin:0; | |
|
610 | margin:-1px 0 0 0; | |
|
606 | 611 | padding:2px; |
|
607 | 612 | cursor:pointer; |
|
608 | 613 | font-size: 90%; |
|
609 |
border |
|
|
614 | border: 1px solid #ccc; | |
|
610 | 615 | border-left: 1px solid #ccc; |
|
611 | 616 | border-right: 1px solid #ccc; |
|
617 | background-color:white; | |
|
612 | 618 | } |
|
619 | div.autocomplete ul li.selected { background-color: #ffb;} | |
|
613 | 620 | div.autocomplete ul li span.informal { |
|
614 | 621 | font-size: 80%; |
|
615 | 622 | color: #aaa; |
|
616 | 623 | } |
|
617 | 624 | |
|
625 | #parent_issue_candidates ul li {width: 500px;} | |
|
626 | ||
|
618 | 627 | /***** Diff *****/ |
|
619 | 628 | .diff_out { background: #fcc; } |
|
620 | 629 | .diff_in { background: #cfc; } |
@@ -741,6 +750,12 background-image:url('../images/close_hl.png'); | |||
|
741 | 750 | .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } |
|
742 | 751 | .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; } |
|
743 | 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 | 759 | .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; } |
|
745 | 760 | |
|
746 | 761 | /***** Icons *****/ |
@@ -19,27 +19,32 enumerations_004: | |||
|
19 | 19 | id: 4 |
|
20 | 20 | type: IssuePriority |
|
21 | 21 | active: true |
|
22 | position: 1 | |
|
22 | 23 | enumerations_005: |
|
23 | 24 | name: Normal |
|
24 | 25 | id: 5 |
|
25 | 26 | type: IssuePriority |
|
26 | 27 | is_default: true |
|
27 | 28 | active: true |
|
29 | position: 2 | |
|
28 | 30 | enumerations_006: |
|
29 | 31 | name: High |
|
30 | 32 | id: 6 |
|
31 | 33 | type: IssuePriority |
|
32 | 34 | active: true |
|
35 | position: 3 | |
|
33 | 36 | enumerations_007: |
|
34 | 37 | name: Urgent |
|
35 | 38 | id: 7 |
|
36 | 39 | type: IssuePriority |
|
37 | 40 | active: true |
|
41 | position: 4 | |
|
38 | 42 | enumerations_008: |
|
39 | 43 | name: Immediate |
|
40 | 44 | id: 8 |
|
41 | 45 | type: IssuePriority |
|
42 | 46 | active: true |
|
47 | position: 5 | |
|
43 | 48 | enumerations_009: |
|
44 | 49 | name: Design |
|
45 | 50 | id: 9 |
@@ -15,6 +15,9 issues_001: | |||
|
15 | 15 | status_id: 1 |
|
16 | 16 | start_date: <%= 1.day.ago.to_date.to_s(:db) %> |
|
17 | 17 | due_date: <%= 10.day.from_now.to_date.to_s(:db) %> |
|
18 | root_id: 1 | |
|
19 | lft: 1 | |
|
20 | rgt: 2 | |
|
18 | 21 | issues_002: |
|
19 | 22 | created_on: 2006-07-19 21:04:21 +02:00 |
|
20 | 23 | project_id: 1 |
@@ -31,6 +34,9 issues_002: | |||
|
31 | 34 | status_id: 2 |
|
32 | 35 | start_date: <%= 2.day.ago.to_date.to_s(:db) %> |
|
33 | 36 | due_date: |
|
37 | root_id: 2 | |
|
38 | lft: 1 | |
|
39 | rgt: 2 | |
|
34 | 40 | issues_003: |
|
35 | 41 | created_on: 2006-07-19 21:07:27 +02:00 |
|
36 | 42 | project_id: 1 |
@@ -47,6 +53,9 issues_003: | |||
|
47 | 53 | status_id: 1 |
|
48 | 54 | start_date: <%= 1.day.from_now.to_date.to_s(:db) %> |
|
49 | 55 | due_date: <%= 40.day.ago.to_date.to_s(:db) %> |
|
56 | root_id: 3 | |
|
57 | lft: 1 | |
|
58 | rgt: 2 | |
|
50 | 59 | issues_004: |
|
51 | 60 | created_on: <%= 5.days.ago.to_date.to_s(:db) %> |
|
52 | 61 | project_id: 2 |
@@ -61,6 +70,9 issues_004: | |||
|
61 | 70 | assigned_to_id: 2 |
|
62 | 71 | author_id: 2 |
|
63 | 72 | status_id: 1 |
|
73 | root_id: 4 | |
|
74 | lft: 1 | |
|
75 | rgt: 2 | |
|
64 | 76 | issues_005: |
|
65 | 77 | created_on: <%= 5.days.ago.to_date.to_s(:db) %> |
|
66 | 78 | project_id: 3 |
@@ -75,6 +87,9 issues_005: | |||
|
75 | 87 | assigned_to_id: |
|
76 | 88 | author_id: 2 |
|
77 | 89 | status_id: 1 |
|
90 | root_id: 5 | |
|
91 | lft: 1 | |
|
92 | rgt: 2 | |
|
78 | 93 | issues_006: |
|
79 | 94 | created_on: <%= 1.minute.ago.to_date.to_s(:db) %> |
|
80 | 95 | project_id: 5 |
@@ -91,6 +106,9 issues_006: | |||
|
91 | 106 | status_id: 1 |
|
92 | 107 | start_date: <%= Date.today.to_s(:db) %> |
|
93 | 108 | due_date: <%= 1.days.from_now.to_date.to_s(:db) %> |
|
109 | root_id: 6 | |
|
110 | lft: 1 | |
|
111 | rgt: 2 | |
|
94 | 112 | issues_007: |
|
95 | 113 | created_on: <%= 10.days.ago.to_date.to_s(:db) %> |
|
96 | 114 | project_id: 1 |
@@ -108,6 +126,9 issues_007: | |||
|
108 | 126 | start_date: <%= 10.days.ago.to_s(:db) %> |
|
109 | 127 | due_date: <%= Date.today.to_s(:db) %> |
|
110 | 128 | lock_version: 0 |
|
129 | root_id: 7 | |
|
130 | lft: 1 | |
|
131 | rgt: 2 | |
|
111 | 132 | issues_008: |
|
112 | 133 | created_on: <%= 10.days.ago.to_date.to_s(:db) %> |
|
113 | 134 | project_id: 1 |
@@ -125,6 +146,9 issues_008: | |||
|
125 | 146 | start_date: |
|
126 | 147 | due_date: |
|
127 | 148 | lock_version: 0 |
|
149 | root_id: 8 | |
|
150 | lft: 1 | |
|
151 | rgt: 2 | |
|
128 | 152 | issues_009: |
|
129 | 153 | created_on: <%= 1.minute.ago.to_date.to_s(:db) %> |
|
130 | 154 | project_id: 5 |
@@ -141,6 +165,9 issues_009: | |||
|
141 | 165 | status_id: 1 |
|
142 | 166 | start_date: <%= Date.today.to_s(:db) %> |
|
143 | 167 | due_date: <%= 1.days.from_now.to_date.to_s(:db) %> |
|
168 | root_id: 9 | |
|
169 | lft: 1 | |
|
170 | rgt: 2 | |
|
144 | 171 | issues_010: |
|
145 | 172 | created_on: <%= 1.minute.ago.to_date.to_s(:db) %> |
|
146 | 173 | project_id: 5 |
@@ -157,6 +184,9 issues_010: | |||
|
157 | 184 | status_id: 1 |
|
158 | 185 | start_date: <%= Date.today.to_s(:db) %> |
|
159 | 186 | due_date: <%= 1.days.from_now.to_date.to_s(:db) %> |
|
187 | root_id: 10 | |
|
188 | lft: 1 | |
|
189 | rgt: 2 | |
|
160 | 190 | issues_011: |
|
161 | 191 | created_on: <%= 3.days.ago.to_date.to_s(:db) %> |
|
162 | 192 | project_id: 1 |
@@ -173,6 +203,9 issues_011: | |||
|
173 | 203 | status_id: 5 |
|
174 | 204 | start_date: <%= 1.day.ago.to_date.to_s(:db) %> |
|
175 | 205 | due_date: |
|
206 | root_id: 11 | |
|
207 | lft: 1 | |
|
208 | rgt: 2 | |
|
176 | 209 | issues_012: |
|
177 | 210 | created_on: <%= 3.days.ago.to_date.to_s(:db) %> |
|
178 | 211 | project_id: 1 |
@@ -189,6 +222,9 issues_012: | |||
|
189 | 222 | status_id: 5 |
|
190 | 223 | start_date: <%= 1.day.ago.to_date.to_s(:db) %> |
|
191 | 224 | due_date: |
|
225 | root_id: 12 | |
|
226 | lft: 1 | |
|
227 | rgt: 2 | |
|
192 | 228 | issues_013: |
|
193 | 229 | created_on: <%= 5.days.ago.to_date.to_s(:db) %> |
|
194 | 230 | project_id: 3 |
@@ -203,3 +239,6 issues_013: | |||
|
203 | 239 | assigned_to_id: |
|
204 | 240 | author_id: 2 |
|
205 | 241 | status_id: 1 |
|
242 | root_id: 13 | |
|
243 | lft: 1 | |
|
244 | rgt: 2 |
@@ -14,6 +14,7 roles_001: | |||
|
14 | 14 | - :add_issues |
|
15 | 15 | - :edit_issues |
|
16 | 16 | - :manage_issue_relations |
|
17 | - :manage_subtasks | |
|
17 | 18 | - :add_issue_notes |
|
18 | 19 | - :move_issues |
|
19 | 20 | - :delete_issues |
@@ -66,6 +67,7 roles_002: | |||
|
66 | 67 | - :add_issues |
|
67 | 68 | - :edit_issues |
|
68 | 69 | - :manage_issue_relations |
|
70 | - :manage_subtasks | |
|
69 | 71 | - :add_issue_notes |
|
70 | 72 | - :move_issues |
|
71 | 73 | - :delete_issues |
@@ -476,7 +476,7 class IssuesControllerTest < ActionController::TestCase | |||
|
476 | 476 | :subject => 'This is first issue', |
|
477 | 477 | :priority_id => 5}, |
|
478 | 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 | 480 | end |
|
481 | 481 | |
|
482 | 482 | def test_post_new_without_custom_fields_param |
@@ -533,6 +533,20 class IssuesControllerTest < ActionController::TestCase | |||
|
533 | 533 | assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail) |
|
534 | 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 | 550 | def test_post_new_should_send_a_notification |
|
537 | 551 | ActionMailer::Base.deliveries.clear |
|
538 | 552 | @request.session[:user_id] = 2 |
@@ -1214,6 +1228,34 class IssuesControllerTest < ActionController::TestCase | |||
|
1214 | 1228 | :attributes => { :href => '#', |
|
1215 | 1229 | :class => 'icon-del disabled' } |
|
1216 | 1230 | end |
|
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 | |
|
1217 | 1259 | |
|
1218 | 1260 | def test_destroy_issue_with_no_time_entries |
|
1219 | 1261 | assert_nil TimeEntry.find_by_issue_id(2) |
@@ -329,7 +329,7 class IssueTest < ActiveSupport::TestCase | |||
|
329 | 329 | |
|
330 | 330 | def test_move_to_another_project_with_same_category |
|
331 | 331 | issue = Issue.find(1) |
|
332 | assert issue.move_to(Project.find(2)) | |
|
332 | assert issue.move_to_project(Project.find(2)) | |
|
333 | 333 | issue.reload |
|
334 | 334 | assert_equal 2, issue.project_id |
|
335 | 335 | # Category changes |
@@ -340,7 +340,7 class IssueTest < ActiveSupport::TestCase | |||
|
340 | 340 | |
|
341 | 341 | def test_move_to_another_project_without_same_category |
|
342 | 342 | issue = Issue.find(2) |
|
343 | assert issue.move_to(Project.find(2)) | |
|
343 | assert issue.move_to_project(Project.find(2)) | |
|
344 | 344 | issue.reload |
|
345 | 345 | assert_equal 2, issue.project_id |
|
346 | 346 | # Category cleared |
@@ -350,7 +350,7 class IssueTest < ActiveSupport::TestCase | |||
|
350 | 350 | def test_move_to_another_project_should_clear_fixed_version_when_not_shared |
|
351 | 351 | issue = Issue.find(1) |
|
352 | 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 | 354 | issue.reload |
|
355 | 355 | assert_equal 2, issue.project_id |
|
356 | 356 | # Cleared fixed_version |
@@ -360,7 +360,7 class IssueTest < ActiveSupport::TestCase | |||
|
360 | 360 | def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project |
|
361 | 361 | issue = Issue.find(1) |
|
362 | 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 | 364 | issue.reload |
|
365 | 365 | assert_equal 5, issue.project_id |
|
366 | 366 | # Keep fixed_version |
@@ -370,7 +370,7 class IssueTest < ActiveSupport::TestCase | |||
|
370 | 370 | def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project |
|
371 | 371 | issue = Issue.find(1) |
|
372 | 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 | 374 | issue.reload |
|
375 | 375 | assert_equal 5, issue.project_id |
|
376 | 376 | # Cleared fixed_version |
@@ -380,7 +380,7 class IssueTest < ActiveSupport::TestCase | |||
|
380 | 380 | def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide |
|
381 | 381 | issue = Issue.find(1) |
|
382 | 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 | 384 | issue.reload |
|
385 | 385 | assert_equal 2, issue.project_id |
|
386 | 386 | # Keep fixed_version |
@@ -392,7 +392,7 class IssueTest < ActiveSupport::TestCase | |||
|
392 | 392 | target = Project.find(2) |
|
393 | 393 | target.tracker_ids = [3] |
|
394 | 394 | target.save |
|
395 | assert_equal false, issue.move_to(target) | |
|
395 | assert_equal false, issue.move_to_project(target) | |
|
396 | 396 | issue.reload |
|
397 | 397 | assert_equal 1, issue.project_id |
|
398 | 398 | end |
@@ -401,7 +401,7 class IssueTest < ActiveSupport::TestCase | |||
|
401 | 401 | issue = Issue.find(1) |
|
402 | 402 | copy = nil |
|
403 | 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 | 405 | end |
|
406 | 406 | assert_kind_of Issue, copy |
|
407 | 407 | assert_equal issue.project, copy.project |
@@ -412,8 +412,9 class IssueTest < ActiveSupport::TestCase | |||
|
412 | 412 | issue = Issue.find(1) |
|
413 | 413 | copy = nil |
|
414 | 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 | 416 | end |
|
417 | copy.reload | |
|
417 | 418 | assert_kind_of Issue, copy |
|
418 | 419 | assert_equal Project.find(3), copy.project |
|
419 | 420 | assert_equal Tracker.find(2), copy.tracker |
@@ -421,7 +422,7 class IssueTest < ActiveSupport::TestCase | |||
|
421 | 422 | assert_nil copy.custom_value_for(2) |
|
422 | 423 | end |
|
423 | 424 | |
|
424 | context "#move_to" do | |
|
425 | context "#move_to_project" do | |
|
425 | 426 | context "as a copy" do |
|
426 | 427 | setup do |
|
427 | 428 | @issue = Issue.find(1) |
@@ -429,24 +430,24 class IssueTest < ActiveSupport::TestCase | |||
|
429 | 430 | end |
|
430 | 431 | |
|
431 | 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 | 434 | assert_equal 3, @copy.assigned_to_id |
|
434 | 435 | end |
|
435 | 436 | |
|
436 | 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 | 439 | assert_equal 2, @copy.status_id |
|
439 | 440 | end |
|
440 | 441 | |
|
441 | 442 | should "allow start date changes" do |
|
442 | 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 | 445 | assert_equal date, @copy.start_date |
|
445 | 446 | end |
|
446 | 447 | |
|
447 | 448 | should "allow due date changes" do |
|
448 | 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 | 452 | assert_equal date, @copy.due_date |
|
452 | 453 | end |
@@ -457,7 +458,7 class IssueTest < ActiveSupport::TestCase | |||
|
457 | 458 | issue = Issue.find(12) |
|
458 | 459 | assert issue.recipients.include?(issue.author.mail) |
|
459 | 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 | 462 | # author is not a member of project anymore |
|
462 | 463 | assert !copy.recipients.include?(copy.author.mail) |
|
463 | 464 | end |
@@ -77,6 +77,13 module Redmine | |||
|
77 | 77 | @custom_field_values = nil |
|
78 | 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 | 87 | module ClassMethods |
|
81 | 88 | end |
|
82 | 89 | end |
General Comments 0
You need to be logged in to leave comments.
Login now