##// END OF EJS Templates
Refactor: Moved the raw SQL finders from ReportsController to Issue....
Eric Davis -
r3248:b86b9b898e4d
parent child
Show More
@@ -1,200 +1,124
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ReportsController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_project, :authorize
21 21
22 22 def issue_report
23 23 @statuses = IssueStatus.find(:all, :order => 'position')
24 24
25 25 case params[:detail]
26 26 when "tracker"
27 27 @field = "tracker_id"
28 28 @rows = @project.trackers
29 29 @data = issues_by_tracker
30 30 @report_title = l(:field_tracker)
31 31 render :template => "reports/issue_report_details"
32 32 when "version"
33 33 @field = "fixed_version_id"
34 34 @rows = @project.shared_versions.sort
35 35 @data = issues_by_version
36 36 @report_title = l(:field_version)
37 37 render :template => "reports/issue_report_details"
38 38 when "priority"
39 39 @field = "priority_id"
40 40 @rows = IssuePriority.all
41 41 @data = issues_by_priority
42 42 @report_title = l(:field_priority)
43 43 render :template => "reports/issue_report_details"
44 44 when "category"
45 45 @field = "category_id"
46 46 @rows = @project.issue_categories
47 47 @data = issues_by_category
48 48 @report_title = l(:field_category)
49 49 render :template => "reports/issue_report_details"
50 50 when "assigned_to"
51 51 @field = "assigned_to_id"
52 52 @rows = @project.members.collect { |m| m.user }.sort
53 53 @data = issues_by_assigned_to
54 54 @report_title = l(:field_assigned_to)
55 55 render :template => "reports/issue_report_details"
56 56 when "author"
57 57 @field = "author_id"
58 58 @rows = @project.members.collect { |m| m.user }.sort
59 59 @data = issues_by_author
60 60 @report_title = l(:field_author)
61 61 render :template => "reports/issue_report_details"
62 62 when "subproject"
63 63 @field = "project_id"
64 64 @rows = @project.descendants.active
65 65 @data = issues_by_subproject
66 66 @report_title = l(:field_subproject)
67 67 render :template => "reports/issue_report_details"
68 68 else
69 69 @trackers = @project.trackers
70 70 @versions = @project.shared_versions.sort
71 71 @priorities = IssuePriority.all
72 72 @categories = @project.issue_categories
73 73 @assignees = @project.members.collect { |m| m.user }.sort
74 74 @authors = @project.members.collect { |m| m.user }.sort
75 75 @subprojects = @project.descendants.active
76 76 issues_by_tracker
77 77 issues_by_version
78 78 issues_by_priority
79 79 issues_by_category
80 80 issues_by_assigned_to
81 81 issues_by_author
82 82 issues_by_subproject
83 83
84 84 render :template => "reports/issue_report"
85 85 end
86 86 end
87 87
88 88 private
89 89 # Find project of id params[:id]
90 90 def find_project
91 91 @project = Project.find(params[:id])
92 92 rescue ActiveRecord::RecordNotFound
93 93 render_404
94 94 end
95 95
96 96 def issues_by_tracker
97 @issues_by_tracker ||=
98 ActiveRecord::Base.connection.select_all("select s.id as status_id,
99 s.is_closed as closed,
100 t.id as tracker_id,
101 count(i.id) as total
102 from
103 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
104 where
105 i.status_id=s.id
106 and i.tracker_id=t.id
107 and i.project_id=#{@project.id}
108 group by s.id, s.is_closed, t.id")
97 @issues_by_tracker ||= Issue.by_tracker(@project)
109 98 end
110 99
111 100 def issues_by_version
112 @issues_by_version ||=
113 ActiveRecord::Base.connection.select_all("select s.id as status_id,
114 s.is_closed as closed,
115 v.id as fixed_version_id,
116 count(i.id) as total
117 from
118 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
119 where
120 i.status_id=s.id
121 and i.fixed_version_id=v.id
122 and i.project_id=#{@project.id}
123 group by s.id, s.is_closed, v.id")
101 @issues_by_version ||= Issue.by_version(@project)
124 102 end
125 103
126 104 def issues_by_priority
127 @issues_by_priority ||=
128 ActiveRecord::Base.connection.select_all("select s.id as status_id,
129 s.is_closed as closed,
130 p.id as priority_id,
131 count(i.id) as total
132 from
133 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssuePriority.table_name} p
134 where
135 i.status_id=s.id
136 and i.priority_id=p.id
137 and i.project_id=#{@project.id}
138 group by s.id, s.is_closed, p.id")
105 @issues_by_priority ||= Issue.by_priority(@project)
139 106 end
140 107
141 108 def issues_by_category
142 @issues_by_category ||=
143 ActiveRecord::Base.connection.select_all("select s.id as status_id,
144 s.is_closed as closed,
145 c.id as category_id,
146 count(i.id) as total
147 from
148 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
149 where
150 i.status_id=s.id
151 and i.category_id=c.id
152 and i.project_id=#{@project.id}
153 group by s.id, s.is_closed, c.id")
109 @issues_by_category ||= Issue.by_category(@project)
154 110 end
155 111
156 112 def issues_by_assigned_to
157 @issues_by_assigned_to ||=
158 ActiveRecord::Base.connection.select_all("select s.id as status_id,
159 s.is_closed as closed,
160 a.id as assigned_to_id,
161 count(i.id) as total
162 from
163 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
164 where
165 i.status_id=s.id
166 and i.assigned_to_id=a.id
167 and i.project_id=#{@project.id}
168 group by s.id, s.is_closed, a.id")
113 @issues_by_assigned_to ||= Issue.by_assigned_to(@project)
169 114 end
170 115
171 116 def issues_by_author
172 @issues_by_author ||=
173 ActiveRecord::Base.connection.select_all("select s.id as status_id,
174 s.is_closed as closed,
175 a.id as author_id,
176 count(i.id) as total
177 from
178 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
179 where
180 i.status_id=s.id
181 and i.author_id=a.id
182 and i.project_id=#{@project.id}
183 group by s.id, s.is_closed, a.id")
117 @issues_by_author ||= Issue.by_author(@project)
184 118 end
185 119
186 120 def issues_by_subproject
187 @issues_by_subproject ||=
188 ActiveRecord::Base.connection.select_all("select s.id as status_id,
189 s.is_closed as closed,
190 i.project_id as project_id,
191 count(i.id) as total
192 from
193 #{Issue.table_name} i, #{IssueStatus.table_name} s
194 where
195 i.status_id=s.id
196 and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
197 group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
121 @issues_by_subproject ||= Issue.by_subproject(@project)
198 122 @issues_by_subproject ||= []
199 123 end
200 124 end
@@ -1,471 +1,571
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :tracker
21 21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27
28 28 has_many :journals, :as => :journalized, :dependent => :destroy
29 29 has_many :time_entries, :dependent => :delete_all
30 30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31 31
32 32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34 34
35 35 acts_as_attachable :after_remove => :attachment_removed
36 36 acts_as_customizable
37 37 acts_as_watchable
38 38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 39 :include => [:project, :journals],
40 40 # sort by id so that limited eager loading doesn't break with postgresql
41 41 :order_column => "#{table_name}.id"
42 42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 45
46 46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 47 :author_key => :author_id
48 48
49 49 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50 50
51 51 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
52 52 validates_length_of :subject, :maximum => 255
53 53 validates_inclusion_of :done_ratio, :in => 0..100
54 54 validates_numericality_of :estimated_hours, :allow_nil => true
55 55
56 56 named_scope :visible, lambda {|*args| { :include => :project,
57 57 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
58 58
59 59 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
60 60
61 61 before_save :update_done_ratio_from_issue_status
62 62 after_save :create_journal
63 63
64 64 # Returns true if usr or current user is allowed to view the issue
65 65 def visible?(usr=nil)
66 66 (usr || User.current).allowed_to?(:view_issues, self.project)
67 67 end
68 68
69 69 def after_initialize
70 70 if new_record?
71 71 # set default values for new records only
72 72 self.status ||= IssueStatus.default
73 73 self.priority ||= IssuePriority.default
74 74 end
75 75 end
76 76
77 77 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
78 78 def available_custom_fields
79 79 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
80 80 end
81 81
82 82 def copy_from(arg)
83 83 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
84 84 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
85 85 self.custom_values = issue.custom_values.collect {|v| v.clone}
86 86 self.status = issue.status
87 87 self
88 88 end
89 89
90 90 # Moves/copies an issue to a new project and tracker
91 91 # Returns the moved/copied issue on success, false on failure
92 92 def move_to(new_project, new_tracker = nil, options = {})
93 93 options ||= {}
94 94 issue = options[:copy] ? self.clone : self
95 95 transaction do
96 96 if new_project && issue.project_id != new_project.id
97 97 # delete issue relations
98 98 unless Setting.cross_project_issue_relations?
99 99 issue.relations_from.clear
100 100 issue.relations_to.clear
101 101 end
102 102 # issue is moved to another project
103 103 # reassign to the category with same name if any
104 104 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
105 105 issue.category = new_category
106 106 # Keep the fixed_version if it's still valid in the new_project
107 107 unless new_project.shared_versions.include?(issue.fixed_version)
108 108 issue.fixed_version = nil
109 109 end
110 110 issue.project = new_project
111 111 end
112 112 if new_tracker
113 113 issue.tracker = new_tracker
114 114 end
115 115 if options[:copy]
116 116 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
117 117 issue.status = if options[:attributes] && options[:attributes][:status_id]
118 118 IssueStatus.find_by_id(options[:attributes][:status_id])
119 119 else
120 120 self.status
121 121 end
122 122 end
123 123 # Allow bulk setting of attributes on the issue
124 124 if options[:attributes]
125 125 issue.attributes = options[:attributes]
126 126 end
127 127 if issue.save
128 128 unless options[:copy]
129 129 # Manually update project_id on related time entries
130 130 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
131 131 end
132 132 else
133 133 Issue.connection.rollback_db_transaction
134 134 return false
135 135 end
136 136 end
137 137 return issue
138 138 end
139 139
140 140 def priority_id=(pid)
141 141 self.priority = nil
142 142 write_attribute(:priority_id, pid)
143 143 end
144 144
145 145 def tracker_id=(tid)
146 146 self.tracker = nil
147 147 write_attribute(:tracker_id, tid)
148 148 result = write_attribute(:tracker_id, tid)
149 149 @custom_field_values = nil
150 150 result
151 151 end
152 152
153 153 # Overrides attributes= so that tracker_id gets assigned first
154 154 def attributes_with_tracker_first=(new_attributes, *args)
155 155 return if new_attributes.nil?
156 156 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
157 157 if new_tracker_id
158 158 self.tracker_id = new_tracker_id
159 159 end
160 160 send :attributes_without_tracker_first=, new_attributes, *args
161 161 end
162 162 alias_method_chain :attributes=, :tracker_first
163 163
164 164 def estimated_hours=(h)
165 165 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
166 166 end
167 167
168 168 SAFE_ATTRIBUTES = %w(
169 169 tracker_id
170 170 status_id
171 171 category_id
172 172 assigned_to_id
173 173 priority_id
174 174 fixed_version_id
175 175 subject
176 176 description
177 177 start_date
178 178 due_date
179 179 done_ratio
180 180 estimated_hours
181 181 custom_field_values
182 182 ) unless const_defined?(:SAFE_ATTRIBUTES)
183 183
184 184 # Safely sets attributes
185 185 # Should be called from controllers instead of #attributes=
186 186 # attr_accessible is too rough because we still want things like
187 187 # Issue.new(:project => foo) to work
188 188 # TODO: move workflow/permission checks from controllers to here
189 189 def safe_attributes=(attrs, user=User.current)
190 190 return if attrs.nil?
191 191 self.attributes = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
192 192 end
193 193
194 194 def done_ratio
195 195 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
196 196 status.default_done_ratio
197 197 else
198 198 read_attribute(:done_ratio)
199 199 end
200 200 end
201 201
202 202 def self.use_status_for_done_ratio?
203 203 Setting.issue_done_ratio == 'issue_status'
204 204 end
205 205
206 206 def self.use_field_for_done_ratio?
207 207 Setting.issue_done_ratio == 'issue_field'
208 208 end
209 209
210 210 def validate
211 211 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
212 212 errors.add :due_date, :not_a_date
213 213 end
214 214
215 215 if self.due_date and self.start_date and self.due_date < self.start_date
216 216 errors.add :due_date, :greater_than_start_date
217 217 end
218 218
219 219 if start_date && soonest_start && start_date < soonest_start
220 220 errors.add :start_date, :invalid
221 221 end
222 222
223 223 if fixed_version
224 224 if !assignable_versions.include?(fixed_version)
225 225 errors.add :fixed_version_id, :inclusion
226 226 elsif reopened? && fixed_version.closed?
227 227 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
228 228 end
229 229 end
230 230
231 231 # Checks that the issue can not be added/moved to a disabled tracker
232 232 if project && (tracker_id_changed? || project_id_changed?)
233 233 unless project.trackers.include?(tracker)
234 234 errors.add :tracker_id, :inclusion
235 235 end
236 236 end
237 237 end
238 238
239 239 def before_create
240 240 # default assignment based on category
241 241 if assigned_to.nil? && category && category.assigned_to
242 242 self.assigned_to = category.assigned_to
243 243 end
244 244 end
245 245
246 246 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
247 247 # even if the user turns off the setting later
248 248 def update_done_ratio_from_issue_status
249 249 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
250 250 self.done_ratio = status.default_done_ratio
251 251 end
252 252 end
253 253
254 254 def after_save
255 255 # Reload is needed in order to get the right status
256 256 reload
257 257
258 258 # Update start/due dates of following issues
259 259 relations_from.each(&:set_issue_to_dates)
260 260
261 261 # Close duplicates if the issue was closed
262 262 if @issue_before_change && !@issue_before_change.closed? && self.closed?
263 263 duplicates.each do |duplicate|
264 264 # Reload is need in case the duplicate was updated by a previous duplicate
265 265 duplicate.reload
266 266 # Don't re-close it if it's already closed
267 267 next if duplicate.closed?
268 268 # Same user and notes
269 269 duplicate.init_journal(@current_journal.user, @current_journal.notes)
270 270 duplicate.update_attribute :status, self.status
271 271 end
272 272 end
273 273 end
274 274
275 275 def init_journal(user, notes = "")
276 276 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
277 277 @issue_before_change = self.clone
278 278 @issue_before_change.status = self.status
279 279 @custom_values_before_change = {}
280 280 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
281 281 # Make sure updated_on is updated when adding a note.
282 282 updated_on_will_change!
283 283 @current_journal
284 284 end
285 285
286 286 # Return true if the issue is closed, otherwise false
287 287 def closed?
288 288 self.status.is_closed?
289 289 end
290 290
291 291 # Return true if the issue is being reopened
292 292 def reopened?
293 293 if !new_record? && status_id_changed?
294 294 status_was = IssueStatus.find_by_id(status_id_was)
295 295 status_new = IssueStatus.find_by_id(status_id)
296 296 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
297 297 return true
298 298 end
299 299 end
300 300 false
301 301 end
302 302
303 303 # Returns true if the issue is overdue
304 304 def overdue?
305 305 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
306 306 end
307 307
308 308 # Users the issue can be assigned to
309 309 def assignable_users
310 310 project.assignable_users
311 311 end
312 312
313 313 # Versions that the issue can be assigned to
314 314 def assignable_versions
315 315 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
316 316 end
317 317
318 318 # Returns true if this issue is blocked by another issue that is still open
319 319 def blocked?
320 320 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
321 321 end
322 322
323 323 # Returns an array of status that user is able to apply
324 324 def new_statuses_allowed_to(user)
325 325 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
326 326 statuses << status unless statuses.empty?
327 327 statuses = statuses.uniq.sort
328 328 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
329 329 end
330 330
331 331 # Returns the mail adresses of users that should be notified
332 332 def recipients
333 333 notified = project.notified_users
334 334 # Author and assignee are always notified unless they have been locked
335 335 notified << author if author && author.active?
336 336 notified << assigned_to if assigned_to && assigned_to.active?
337 337 notified.uniq!
338 338 # Remove users that can not view the issue
339 339 notified.reject! {|user| !visible?(user)}
340 340 notified.collect(&:mail)
341 341 end
342 342
343 343 # Returns the total number of hours spent on this issue.
344 344 #
345 345 # Example:
346 346 # spent_hours => 0
347 347 # spent_hours => 50
348 348 def spent_hours
349 349 @spent_hours ||= time_entries.sum(:hours) || 0
350 350 end
351 351
352 352 def relations
353 353 (relations_from + relations_to).sort
354 354 end
355 355
356 356 def all_dependent_issues
357 357 dependencies = []
358 358 relations_from.each do |relation|
359 359 dependencies << relation.issue_to
360 360 dependencies += relation.issue_to.all_dependent_issues
361 361 end
362 362 dependencies
363 363 end
364 364
365 365 # Returns an array of issues that duplicate this one
366 366 def duplicates
367 367 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
368 368 end
369 369
370 370 # Returns the due date or the target due date if any
371 371 # Used on gantt chart
372 372 def due_before
373 373 due_date || (fixed_version ? fixed_version.effective_date : nil)
374 374 end
375 375
376 376 # Returns the time scheduled for this issue.
377 377 #
378 378 # Example:
379 379 # Start Date: 2/26/09, End Date: 3/04/09
380 380 # duration => 6
381 381 def duration
382 382 (start_date && due_date) ? due_date - start_date : 0
383 383 end
384 384
385 385 def soonest_start
386 386 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
387 387 end
388 388
389 389 def to_s
390 390 "#{tracker} ##{id}: #{subject}"
391 391 end
392 392
393 393 # Returns a string of css classes that apply to the issue
394 394 def css_classes
395 395 s = "issue status-#{status.position} priority-#{priority.position}"
396 396 s << ' closed' if closed?
397 397 s << ' overdue' if overdue?
398 398 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
399 399 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
400 400 s
401 401 end
402 402
403 403 # Unassigns issues from +version+ if it's no longer shared with issue's project
404 404 def self.update_versions_from_sharing_change(version)
405 405 # Update issues assigned to the version
406 406 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
407 407 end
408 408
409 409 # Unassigns issues from versions that are no longer shared
410 410 # after +project+ was moved
411 411 def self.update_versions_from_hierarchy_change(project)
412 412 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
413 413 # Update issues of the moved projects and issues assigned to a version of a moved project
414 414 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
415 415 end
416 416
417 # Extracted from the ReportsController.
418 # TODO: refactor into a common factory or named scopes
419 def self.by_tracker(project)
420 ActiveRecord::Base.connection.select_all("select s.id as status_id,
421 s.is_closed as closed,
422 t.id as tracker_id,
423 count(i.id) as total
424 from
425 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
426 where
427 i.status_id=s.id
428 and i.tracker_id=t.id
429 and i.project_id=#{project.id}
430 group by s.id, s.is_closed, t.id")
431 end
432
433 def self.by_version(project)
434 ActiveRecord::Base.connection.select_all("select s.id as status_id,
435 s.is_closed as closed,
436 v.id as fixed_version_id,
437 count(i.id) as total
438 from
439 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
440 where
441 i.status_id=s.id
442 and i.fixed_version_id=v.id
443 and i.project_id=#{project.id}
444 group by s.id, s.is_closed, v.id")
445 end
446
447 def self.by_priority(project)
448 ActiveRecord::Base.connection.select_all("select s.id as status_id,
449 s.is_closed as closed,
450 p.id as priority_id,
451 count(i.id) as total
452 from
453 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssuePriority.table_name} p
454 where
455 i.status_id=s.id
456 and i.priority_id=p.id
457 and i.project_id=#{project.id}
458 group by s.id, s.is_closed, p.id")
459 end
460
461 def self.by_category(project)
462 ActiveRecord::Base.connection.select_all("select s.id as status_id,
463 s.is_closed as closed,
464 c.id as category_id,
465 count(i.id) as total
466 from
467 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
468 where
469 i.status_id=s.id
470 and i.category_id=c.id
471 and i.project_id=#{project.id}
472 group by s.id, s.is_closed, c.id")
473 end
474
475 def self.by_assigned_to(project)
476 ActiveRecord::Base.connection.select_all("select s.id as status_id,
477 s.is_closed as closed,
478 a.id as assigned_to_id,
479 count(i.id) as total
480 from
481 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
482 where
483 i.status_id=s.id
484 and i.assigned_to_id=a.id
485 and i.project_id=#{project.id}
486 group by s.id, s.is_closed, a.id")
487 end
488
489 def self.by_author(project)
490 ActiveRecord::Base.connection.select_all("select s.id as status_id,
491 s.is_closed as closed,
492 a.id as author_id,
493 count(i.id) as total
494 from
495 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
496 where
497 i.status_id=s.id
498 and i.author_id=a.id
499 and i.project_id=#{project.id}
500 group by s.id, s.is_closed, a.id")
501 end
502
503 def self.by_subproject(project)
504 ActiveRecord::Base.connection.select_all("select s.id as status_id,
505 s.is_closed as closed,
506 i.project_id as project_id,
507 count(i.id) as total
508 from
509 #{Issue.table_name} i, #{IssueStatus.table_name} s
510 where
511 i.status_id=s.id
512 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
513 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
514 end
515 # End ReportsController extraction
516
417 517 private
418 518
419 519 # Update issues so their versions are not pointing to a
420 520 # fixed_version that is not shared with the issue's project
421 521 def self.update_versions(conditions=nil)
422 522 # Only need to update issues with a fixed_version from
423 523 # a different project and that is not systemwide shared
424 524 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
425 525 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
426 526 " AND #{Version.table_name}.sharing <> 'system'",
427 527 conditions),
428 528 :include => [:project, :fixed_version]
429 529 ).each do |issue|
430 530 next if issue.project.nil? || issue.fixed_version.nil?
431 531 unless issue.project.shared_versions.include?(issue.fixed_version)
432 532 issue.init_journal(User.current)
433 533 issue.fixed_version = nil
434 534 issue.save
435 535 end
436 536 end
437 537 end
438 538
439 539 # Callback on attachment deletion
440 540 def attachment_removed(obj)
441 541 journal = init_journal(User.current)
442 542 journal.details << JournalDetail.new(:property => 'attachment',
443 543 :prop_key => obj.id,
444 544 :old_value => obj.filename)
445 545 journal.save
446 546 end
447 547
448 548 # Saves the changes in a Journal
449 549 # Called after_save
450 550 def create_journal
451 551 if @current_journal
452 552 # attributes changes
453 553 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
454 554 @current_journal.details << JournalDetail.new(:property => 'attr',
455 555 :prop_key => c,
456 556 :old_value => @issue_before_change.send(c),
457 557 :value => send(c)) unless send(c)==@issue_before_change.send(c)
458 558 }
459 559 # custom fields changes
460 560 custom_values.each {|c|
461 561 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
462 562 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
463 563 @current_journal.details << JournalDetail.new(:property => 'cf',
464 564 :prop_key => c.custom_field_id,
465 565 :old_value => @custom_values_before_change[c.custom_field_id],
466 566 :value => c.value)
467 567 }
468 568 @current_journal.save
469 569 end
470 570 end
471 571 end
General Comments 0
You need to be logged in to leave comments. Login now