@@ -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. |
|
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 |
|
|
114 | if new_project && issue.project_id != new_project.id | |
106 |
|
|
115 | # delete issue relations | |
107 |
|
|
116 | unless Setting.cross_project_issue_relations? | |
@@ -117,9 +126,13 class Issue < ActiveRecord::Base | |||||
117 |
|
|
126 | issue.fixed_version = nil | |
118 |
|
|
127 | end | |
119 |
|
|
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 |
|
|
132 | end | |
121 |
|
|
133 | if new_tracker | |
122 |
|
|
134 | issue.tracker = new_tracker | |
|
135 | issue.reset_custom_values! | |||
123 |
|
|
136 | end | |
124 |
|
|
137 | if options[:copy] | |
125 |
|
|
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 |
|
|
150 | unless options[:copy] | |
138 |
|
|
151 | # Manually update project_id on related time entries | |
139 |
|
|
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=" |
|
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 | <% if l_width > 0 %> |
|
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 | <% 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=" |
|
201 | <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="<%= css %> task_done"> </div> | |
199 | <% end %> |
|
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 | <%= 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: |
|
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 |
|
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 |
General Comments 0
You need to be logged in to leave comments.
Login now