##// END OF EJS Templates
Refactor: Extract Query#sortable_columns from the controller....
Eric Davis -
r3490:6e6e260ceae6
parent child
Show More
@@ -1,588 +1,588
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20 default_search_scope :issues
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, :auto_complete]
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
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 include ProjectsHelper
33 include ProjectsHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 include QueriesHelper
43 include QueriesHelper
44 helper :sort
44 helper :sort
45 include SortHelper
45 include SortHelper
46 include IssuesHelper
46 include IssuesHelper
47 helper :timelog
47 helper :timelog
48 include Redmine::Export::PDF
48 include Redmine::Export::PDF
49
49
50 verify :method => [:post, :delete],
50 verify :method => [:post, :delete],
51 :only => :destroy,
51 :only => :destroy,
52 :render => { :nothing => true, :status => :method_not_allowed }
52 :render => { :nothing => true, :status => :method_not_allowed }
53
53
54 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
54 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
55
55
56 def index
56 def index
57 retrieve_query
57 retrieve_query
58 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
59 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
59 sort_update(@query.sortable_columns)
60
60
61 if @query.valid?
61 if @query.valid?
62 limit = case params[:format]
62 limit = case params[:format]
63 when 'csv', 'pdf'
63 when 'csv', 'pdf'
64 Setting.issues_export_limit.to_i
64 Setting.issues_export_limit.to_i
65 when 'atom'
65 when 'atom'
66 Setting.feeds_limit.to_i
66 Setting.feeds_limit.to_i
67 else
67 else
68 per_page_option
68 per_page_option
69 end
69 end
70
70
71 @issue_count = @query.issue_count
71 @issue_count = @query.issue_count
72 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
72 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
73 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
73 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
74 :order => sort_clause,
74 :order => sort_clause,
75 :offset => @issue_pages.current.offset,
75 :offset => @issue_pages.current.offset,
76 :limit => limit)
76 :limit => limit)
77 @issue_count_by_group = @query.issue_count_by_group
77 @issue_count_by_group = @query.issue_count_by_group
78
78
79 respond_to do |format|
79 respond_to do |format|
80 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
80 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
81 format.xml { render :layout => false }
81 format.xml { render :layout => false }
82 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
82 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
83 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
83 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
84 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
84 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
85 end
85 end
86 else
86 else
87 # Send html if the query is not valid
87 # Send html if the query is not valid
88 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
88 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
89 end
89 end
90 rescue ActiveRecord::RecordNotFound
90 rescue ActiveRecord::RecordNotFound
91 render_404
91 render_404
92 end
92 end
93
93
94 def changes
94 def changes
95 retrieve_query
95 retrieve_query
96 sort_init 'id', 'desc'
96 sort_init 'id', 'desc'
97 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
97 sort_update(@query.sortable_columns)
98
98
99 if @query.valid?
99 if @query.valid?
100 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
100 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
101 :limit => 25)
101 :limit => 25)
102 end
102 end
103 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
103 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
104 render :layout => false, :content_type => 'application/atom+xml'
104 render :layout => false, :content_type => 'application/atom+xml'
105 rescue ActiveRecord::RecordNotFound
105 rescue ActiveRecord::RecordNotFound
106 render_404
106 render_404
107 end
107 end
108
108
109 def show
109 def show
110 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
110 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
111 @journals.each_with_index {|j,i| j.indice = i+1}
111 @journals.each_with_index {|j,i| j.indice = i+1}
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
113 @changesets = @issue.changesets.visible.all
113 @changesets = @issue.changesets.visible.all
114 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
115 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
116 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
116 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
117 @priorities = IssuePriority.all
117 @priorities = IssuePriority.all
118 @time_entry = TimeEntry.new
118 @time_entry = TimeEntry.new
119 respond_to do |format|
119 respond_to do |format|
120 format.html { render :template => 'issues/show.rhtml' }
120 format.html { render :template => 'issues/show.rhtml' }
121 format.xml { render :layout => false }
121 format.xml { render :layout => false }
122 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
122 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
123 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
123 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
124 end
124 end
125 end
125 end
126
126
127 # Add a new issue
127 # Add a new issue
128 # The new issue will be created from an existing one if copy_from parameter is given
128 # The new issue will be created from an existing one if copy_from parameter is given
129 def new
129 def new
130 @issue = Issue.new
130 @issue = Issue.new
131 @issue.copy_from(params[:copy_from]) if params[:copy_from]
131 @issue.copy_from(params[:copy_from]) if params[:copy_from]
132 @issue.project = @project
132 @issue.project = @project
133 # Tracker must be set before custom field values
133 # Tracker must be set before custom field values
134 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
134 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
135 if @issue.tracker.nil?
135 if @issue.tracker.nil?
136 render_error l(:error_no_tracker_in_project)
136 render_error l(:error_no_tracker_in_project)
137 return
137 return
138 end
138 end
139 if params[:issue].is_a?(Hash)
139 if params[:issue].is_a?(Hash)
140 @issue.safe_attributes = params[:issue]
140 @issue.safe_attributes = params[:issue]
141 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
141 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
142 end
142 end
143 @issue.author = User.current
143 @issue.author = User.current
144
144
145 default_status = IssueStatus.default
145 default_status = IssueStatus.default
146 unless default_status
146 unless default_status
147 render_error l(:error_no_default_issue_status)
147 render_error l(:error_no_default_issue_status)
148 return
148 return
149 end
149 end
150 @issue.status = default_status
150 @issue.status = default_status
151 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
151 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
152
152
153 if request.get? || request.xhr?
153 if request.get? || request.xhr?
154 @issue.start_date ||= Date.today
154 @issue.start_date ||= Date.today
155 else
155 else
156 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
156 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
157 # Check that the user is allowed to apply the requested status
157 # Check that the user is allowed to apply the requested status
158 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
158 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
159 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
159 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
160 if @issue.save
160 if @issue.save
161 attachments = Attachment.attach_files(@issue, params[:attachments])
161 attachments = Attachment.attach_files(@issue, params[:attachments])
162 render_attachment_warning_if_needed(@issue)
162 render_attachment_warning_if_needed(@issue)
163 flash[:notice] = l(:notice_successful_create)
163 flash[:notice] = l(:notice_successful_create)
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', :issue => {: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 :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
169 { :action => 'show', :id => @issue })
169 { :action => 'show', :id => @issue })
170 }
170 }
171 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) }
172 end
172 end
173 return
173 return
174 else
174 else
175 respond_to do |format|
175 respond_to do |format|
176 format.html { }
176 format.html { }
177 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
177 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
178 end
178 end
179 end
179 end
180 end
180 end
181 @priorities = IssuePriority.all
181 @priorities = IssuePriority.all
182 render :layout => !request.xhr?
182 render :layout => !request.xhr?
183 end
183 end
184
184
185 # Attributes that can be updated on workflow transition (without :edit permission)
185 # Attributes that can be updated on workflow transition (without :edit permission)
186 # TODO: make it configurable (at least per role)
186 # TODO: make it configurable (at least per role)
187 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
187 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
188
188
189 def edit
189 def edit
190 update_issue_from_params
190 update_issue_from_params
191
191
192 @journal = @issue.current_journal
192 @journal = @issue.current_journal
193
193
194 respond_to do |format|
194 respond_to do |format|
195 format.html { }
195 format.html { }
196 format.xml { }
196 format.xml { }
197 end
197 end
198 end
198 end
199
199
200 def update
200 def update
201 update_issue_from_params
201 update_issue_from_params
202
202
203 if @issue.save_issue_with_child_records(params, @time_entry)
203 if @issue.save_issue_with_child_records(params, @time_entry)
204 render_attachment_warning_if_needed(@issue)
204 render_attachment_warning_if_needed(@issue)
205 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
205 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
206
206
207 respond_to do |format|
207 respond_to do |format|
208 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
208 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
209 format.xml { head :ok }
209 format.xml { head :ok }
210 end
210 end
211 else
211 else
212 render_attachment_warning_if_needed(@issue)
212 render_attachment_warning_if_needed(@issue)
213 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
213 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
214 @journal = @issue.current_journal
214 @journal = @issue.current_journal
215
215
216 respond_to do |format|
216 respond_to do |format|
217 format.html { render :action => 'edit' }
217 format.html { render :action => 'edit' }
218 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
218 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
219 end
219 end
220 end
220 end
221
221
222 rescue ActiveRecord::StaleObjectError
222 rescue ActiveRecord::StaleObjectError
223 # Optimistic locking exception
223 # Optimistic locking exception
224 flash.now[:error] = l(:notice_locking_conflict)
224 flash.now[:error] = l(:notice_locking_conflict)
225 # Remove the previously added attachments if issue was not updated
225 # Remove the previously added attachments if issue was not updated
226 attachments[:files].each(&:destroy) if attachments[:files]
226 attachments[:files].each(&:destroy) if attachments[:files]
227 end
227 end
228
228
229 def reply
229 def reply
230 journal = Journal.find(params[:journal_id]) if params[:journal_id]
230 journal = Journal.find(params[:journal_id]) if params[:journal_id]
231 if journal
231 if journal
232 user = journal.user
232 user = journal.user
233 text = journal.notes
233 text = journal.notes
234 else
234 else
235 user = @issue.author
235 user = @issue.author
236 text = @issue.description
236 text = @issue.description
237 end
237 end
238 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
238 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
239 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
239 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
240 render(:update) { |page|
240 render(:update) { |page|
241 page.<< "$('notes').value = \"#{content}\";"
241 page.<< "$('notes').value = \"#{content}\";"
242 page.show 'update'
242 page.show 'update'
243 page << "Form.Element.focus('notes');"
243 page << "Form.Element.focus('notes');"
244 page << "Element.scrollTo('update');"
244 page << "Element.scrollTo('update');"
245 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
245 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
246 }
246 }
247 end
247 end
248
248
249 # Bulk edit a set of issues
249 # Bulk edit a set of issues
250 def bulk_edit
250 def bulk_edit
251 @issues.sort!
251 @issues.sort!
252 if request.post?
252 if request.post?
253 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
253 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
254 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
254 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
255 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
255 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
256
256
257 unsaved_issue_ids = []
257 unsaved_issue_ids = []
258 @issues.each do |issue|
258 @issues.each do |issue|
259 issue.reload
259 issue.reload
260 journal = issue.init_journal(User.current, params[:notes])
260 journal = issue.init_journal(User.current, params[:notes])
261 issue.safe_attributes = attributes
261 issue.safe_attributes = attributes
262 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 })
263 unless issue.save
263 unless issue.save
264 # Keep unsaved issue ids to display them in flash error
264 # Keep unsaved issue ids to display them in flash error
265 unsaved_issue_ids << issue.id
265 unsaved_issue_ids << issue.id
266 end
266 end
267 end
267 end
268 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
268 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
269 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
269 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
270 return
270 return
271 end
271 end
272 @available_statuses = Workflow.available_statuses(@project)
272 @available_statuses = Workflow.available_statuses(@project)
273 @custom_fields = @project.all_issue_custom_fields
273 @custom_fields = @project.all_issue_custom_fields
274 end
274 end
275
275
276 def move
276 def move
277 @issues.sort!
277 @issues.sort!
278 @copy = params[:copy_options] && params[:copy_options][:copy]
278 @copy = params[:copy_options] && params[:copy_options][:copy]
279 @allowed_projects = []
279 @allowed_projects = []
280 # 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
281 if User.current.admin?
281 if User.current.admin?
282 # admin is allowed to move issues to any active (visible) project
282 # admin is allowed to move issues to any active (visible) project
283 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
283 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
284 else
284 else
285 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
285 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
286 end
286 end
287 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
287 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
288 @target_project ||= @project
288 @target_project ||= @project
289 @trackers = @target_project.trackers
289 @trackers = @target_project.trackers
290 @available_statuses = Workflow.available_statuses(@project)
290 @available_statuses = Workflow.available_statuses(@project)
291 if request.post?
291 if request.post?
292 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
292 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
293 unsaved_issue_ids = []
293 unsaved_issue_ids = []
294 moved_issues = []
294 moved_issues = []
295 @issues.each do |issue|
295 @issues.each do |issue|
296 issue.reload
296 issue.reload
297 changed_attributes = {}
297 changed_attributes = {}
298 [: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|
299 unless params[valid_attribute].blank?
299 unless params[valid_attribute].blank?
300 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
300 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
301 end
301 end
302 end
302 end
303 issue.init_journal(User.current)
303 issue.init_journal(User.current)
304 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 })
305 if r = issue.move_to_project(@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})
306 moved_issues << r
306 moved_issues << r
307 else
307 else
308 unsaved_issue_ids << issue.id
308 unsaved_issue_ids << issue.id
309 end
309 end
310 end
310 end
311 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
311 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
312
312
313 if params[:follow]
313 if params[:follow]
314 if @issues.size == 1 && moved_issues.size == 1
314 if @issues.size == 1 && moved_issues.size == 1
315 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
315 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
316 else
316 else
317 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
317 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
318 end
318 end
319 else
319 else
320 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
320 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
321 end
321 end
322 return
322 return
323 end
323 end
324 render :layout => false if request.xhr?
324 render :layout => false if request.xhr?
325 end
325 end
326
326
327 def destroy
327 def destroy
328 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
328 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
329 if @hours > 0
329 if @hours > 0
330 case params[:todo]
330 case params[:todo]
331 when 'destroy'
331 when 'destroy'
332 # nothing to do
332 # nothing to do
333 when 'nullify'
333 when 'nullify'
334 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
334 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
335 when 'reassign'
335 when 'reassign'
336 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
336 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
337 if reassign_to.nil?
337 if reassign_to.nil?
338 flash.now[:error] = l(:error_issue_not_found_in_project)
338 flash.now[:error] = l(:error_issue_not_found_in_project)
339 return
339 return
340 else
340 else
341 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
341 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
342 end
342 end
343 else
343 else
344 unless params[:format] == 'xml'
344 unless params[:format] == 'xml'
345 # display the destroy form if it's a user request
345 # display the destroy form if it's a user request
346 return
346 return
347 end
347 end
348 end
348 end
349 end
349 end
350 @issues.each(&:destroy)
350 @issues.each(&:destroy)
351 respond_to do |format|
351 respond_to do |format|
352 format.html { redirect_to :action => 'index', :project_id => @project }
352 format.html { redirect_to :action => 'index', :project_id => @project }
353 format.xml { head :ok }
353 format.xml { head :ok }
354 end
354 end
355 end
355 end
356
356
357 def gantt
357 def gantt
358 @gantt = Redmine::Helpers::Gantt.new(params)
358 @gantt = Redmine::Helpers::Gantt.new(params)
359 retrieve_query
359 retrieve_query
360 @query.group_by = nil
360 @query.group_by = nil
361 if @query.valid?
361 if @query.valid?
362 events = []
362 events = []
363 # Issues that have start and due dates
363 # Issues that have start and due dates
364 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
364 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
365 :order => "start_date, due_date",
365 :order => "start_date, due_date",
366 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
366 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
367 )
367 )
368 # Issues that don't have a due date but that are assigned to a version with a date
368 # Issues that don't have a due date but that are assigned to a version with a date
369 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
369 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
370 :order => "start_date, effective_date",
370 :order => "start_date, effective_date",
371 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
371 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
372 )
372 )
373 # Versions
373 # Versions
374 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
374 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
375
375
376 @gantt.events = events
376 @gantt.events = events
377 end
377 end
378
378
379 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
379 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
380
380
381 respond_to do |format|
381 respond_to do |format|
382 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
382 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
383 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
383 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
384 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
384 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
385 end
385 end
386 end
386 end
387
387
388 def calendar
388 def calendar
389 if params[:year] and params[:year].to_i > 1900
389 if params[:year] and params[:year].to_i > 1900
390 @year = params[:year].to_i
390 @year = params[:year].to_i
391 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
391 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
392 @month = params[:month].to_i
392 @month = params[:month].to_i
393 end
393 end
394 end
394 end
395 @year ||= Date.today.year
395 @year ||= Date.today.year
396 @month ||= Date.today.month
396 @month ||= Date.today.month
397
397
398 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
398 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
399 retrieve_query
399 retrieve_query
400 @query.group_by = nil
400 @query.group_by = nil
401 if @query.valid?
401 if @query.valid?
402 events = []
402 events = []
403 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
403 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
404 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
404 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
405 )
405 )
406 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
406 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
407
407
408 @calendar.events = events
408 @calendar.events = events
409 end
409 end
410
410
411 render :layout => false if request.xhr?
411 render :layout => false if request.xhr?
412 end
412 end
413
413
414 def context_menu
414 def context_menu
415 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
415 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
416 if (@issues.size == 1)
416 if (@issues.size == 1)
417 @issue = @issues.first
417 @issue = @issues.first
418 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
418 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
419 end
419 end
420 projects = @issues.collect(&:project).compact.uniq
420 projects = @issues.collect(&:project).compact.uniq
421 @project = projects.first if projects.size == 1
421 @project = projects.first if projects.size == 1
422
422
423 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
423 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
424 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
424 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
425 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
425 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
426 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
426 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
427 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
427 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
428 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
428 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
429 }
429 }
430 if @project
430 if @project
431 @assignables = @project.assignable_users
431 @assignables = @project.assignable_users
432 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
432 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
433 @trackers = @project.trackers
433 @trackers = @project.trackers
434 end
434 end
435
435
436 @priorities = IssuePriority.all.reverse
436 @priorities = IssuePriority.all.reverse
437 @statuses = IssueStatus.find(:all, :order => 'position')
437 @statuses = IssueStatus.find(:all, :order => 'position')
438 @back = params[:back_url] || request.env['HTTP_REFERER']
438 @back = params[:back_url] || request.env['HTTP_REFERER']
439
439
440 render :layout => false
440 render :layout => false
441 end
441 end
442
442
443 def update_form
443 def update_form
444 if params[:id].blank?
444 if params[:id].blank?
445 @issue = Issue.new
445 @issue = Issue.new
446 @issue.project = @project
446 @issue.project = @project
447 else
447 else
448 @issue = @project.issues.visible.find(params[:id])
448 @issue = @project.issues.visible.find(params[:id])
449 end
449 end
450 @issue.attributes = params[:issue]
450 @issue.attributes = params[:issue]
451 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
451 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
452 @priorities = IssuePriority.all
452 @priorities = IssuePriority.all
453
453
454 render :partial => 'attributes'
454 render :partial => 'attributes'
455 end
455 end
456
456
457 def preview
457 def preview
458 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
458 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
459 @attachements = @issue.attachments if @issue
459 @attachements = @issue.attachments if @issue
460 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
460 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
461 render :partial => 'common/preview'
461 render :partial => 'common/preview'
462 end
462 end
463
463
464 def auto_complete
464 def auto_complete
465 @issues = []
465 @issues = []
466 q = params[:q].to_s
466 q = params[:q].to_s
467 if q.match(/^\d+$/)
467 if q.match(/^\d+$/)
468 @issues << @project.issues.visible.find_by_id(q.to_i)
468 @issues << @project.issues.visible.find_by_id(q.to_i)
469 end
469 end
470 unless q.blank?
470 unless q.blank?
471 @issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
471 @issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
472 end
472 end
473 render :layout => false
473 render :layout => false
474 end
474 end
475
475
476 private
476 private
477 def find_issue
477 def find_issue
478 @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])
479 @project = @issue.project
479 @project = @issue.project
480 rescue ActiveRecord::RecordNotFound
480 rescue ActiveRecord::RecordNotFound
481 render_404
481 render_404
482 end
482 end
483
483
484 # Filter for bulk operations
484 # Filter for bulk operations
485 def find_issues
485 def find_issues
486 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
486 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
487 raise ActiveRecord::RecordNotFound if @issues.empty?
487 raise ActiveRecord::RecordNotFound if @issues.empty?
488 projects = @issues.collect(&:project).compact.uniq
488 projects = @issues.collect(&:project).compact.uniq
489 if projects.size == 1
489 if projects.size == 1
490 @project = projects.first
490 @project = projects.first
491 else
491 else
492 # TODO: let users bulk edit/move/destroy issues from different projects
492 # TODO: let users bulk edit/move/destroy issues from different projects
493 render_error 'Can not bulk edit/move/destroy issues from different projects'
493 render_error 'Can not bulk edit/move/destroy issues from different projects'
494 return false
494 return false
495 end
495 end
496 rescue ActiveRecord::RecordNotFound
496 rescue ActiveRecord::RecordNotFound
497 render_404
497 render_404
498 end
498 end
499
499
500 def find_project
500 def find_project
501 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
501 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
502 @project = Project.find(project_id)
502 @project = Project.find(project_id)
503 rescue ActiveRecord::RecordNotFound
503 rescue ActiveRecord::RecordNotFound
504 render_404
504 render_404
505 end
505 end
506
506
507 def find_optional_project
507 def find_optional_project
508 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
508 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
509 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
509 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
510 allowed ? true : deny_access
510 allowed ? true : deny_access
511 rescue ActiveRecord::RecordNotFound
511 rescue ActiveRecord::RecordNotFound
512 render_404
512 render_404
513 end
513 end
514
514
515 # Retrieve query from session or build a new query
515 # Retrieve query from session or build a new query
516 def retrieve_query
516 def retrieve_query
517 if !params[:query_id].blank?
517 if !params[:query_id].blank?
518 cond = "project_id IS NULL"
518 cond = "project_id IS NULL"
519 cond << " OR project_id = #{@project.id}" if @project
519 cond << " OR project_id = #{@project.id}" if @project
520 @query = Query.find(params[:query_id], :conditions => cond)
520 @query = Query.find(params[:query_id], :conditions => cond)
521 @query.project = @project
521 @query.project = @project
522 session[:query] = {:id => @query.id, :project_id => @query.project_id}
522 session[:query] = {:id => @query.id, :project_id => @query.project_id}
523 sort_clear
523 sort_clear
524 else
524 else
525 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
525 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
526 # Give it a name, required to be valid
526 # Give it a name, required to be valid
527 @query = Query.new(:name => "_")
527 @query = Query.new(:name => "_")
528 @query.project = @project
528 @query.project = @project
529 if params[:fields] and params[:fields].is_a? Array
529 if params[:fields] and params[:fields].is_a? Array
530 params[:fields].each do |field|
530 params[:fields].each do |field|
531 @query.add_filter(field, params[:operators][field], params[:values][field])
531 @query.add_filter(field, params[:operators][field], params[:values][field])
532 end
532 end
533 else
533 else
534 @query.available_filters.keys.each do |field|
534 @query.available_filters.keys.each do |field|
535 @query.add_short_filter(field, params[field]) if params[field]
535 @query.add_short_filter(field, params[field]) if params[field]
536 end
536 end
537 end
537 end
538 @query.group_by = params[:group_by]
538 @query.group_by = params[:group_by]
539 @query.column_names = params[:query] && params[:query][:column_names]
539 @query.column_names = params[:query] && params[:query][:column_names]
540 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
540 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
541 else
541 else
542 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
542 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
543 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
543 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
544 @query.project = @project
544 @query.project = @project
545 end
545 end
546 end
546 end
547 end
547 end
548
548
549 # Rescues an invalid query statement. Just in case...
549 # Rescues an invalid query statement. Just in case...
550 def query_statement_invalid(exception)
550 def query_statement_invalid(exception)
551 logger.error "Query::StatementInvalid: #{exception.message}" if logger
551 logger.error "Query::StatementInvalid: #{exception.message}" if logger
552 session.delete(:query)
552 session.delete(:query)
553 sort_clear
553 sort_clear
554 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
554 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
555 end
555 end
556
556
557 # Used by #edit and #update to set some common instance variables
557 # Used by #edit and #update to set some common instance variables
558 # from the params
558 # from the params
559 # TODO: Refactor, not everything in here is needed by #edit
559 # TODO: Refactor, not everything in here is needed by #edit
560 def update_issue_from_params
560 def update_issue_from_params
561 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
561 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
562 @priorities = IssuePriority.all
562 @priorities = IssuePriority.all
563 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
563 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
564 @time_entry = TimeEntry.new
564 @time_entry = TimeEntry.new
565
565
566 @notes = params[:notes]
566 @notes = params[:notes]
567 @issue.init_journal(User.current, @notes)
567 @issue.init_journal(User.current, @notes)
568 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
568 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
569 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
569 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
570 attrs = params[:issue].dup
570 attrs = params[:issue].dup
571 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
571 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
572 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
572 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
573 @issue.safe_attributes = attrs
573 @issue.safe_attributes = attrs
574 end
574 end
575
575
576 end
576 end
577
577
578 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
578 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
579 if unsaved_issue_ids.empty?
579 if unsaved_issue_ids.empty?
580 flash[:notice] = l(:notice_successful_update) unless issues.empty?
580 flash[:notice] = l(:notice_successful_update) unless issues.empty?
581 else
581 else
582 flash[:error] = l(:notice_failed_to_save_issues,
582 flash[:error] = l(:notice_failed_to_save_issues,
583 :count => unsaved_issue_ids.size,
583 :count => unsaved_issue_ids.size,
584 :total => issues.size,
584 :total => issues.size,
585 :ids => '#' + unsaved_issue_ids.join(', #'))
585 :ids => '#' + unsaved_issue_ids.join(', #'))
586 end
586 end
587 end
587 end
588 end
588 end
@@ -1,563 +1,571
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 end
30 end
31
31
32 def caption
32 def caption
33 l("field_#{name}")
33 l("field_#{name}")
34 end
34 end
35
35
36 # Returns true if the column is sortable, otherwise false
36 # Returns true if the column is sortable, otherwise false
37 def sortable?
37 def sortable?
38 !sortable.nil?
38 !sortable.nil?
39 end
39 end
40
40
41 def value(issue)
41 def value(issue)
42 issue.send name
42 issue.send name
43 end
43 end
44 end
44 end
45
45
46 class QueryCustomFieldColumn < QueryColumn
46 class QueryCustomFieldColumn < QueryColumn
47
47
48 def initialize(custom_field)
48 def initialize(custom_field)
49 self.name = "cf_#{custom_field.id}".to_sym
49 self.name = "cf_#{custom_field.id}".to_sym
50 self.sortable = custom_field.order_statement || false
50 self.sortable = custom_field.order_statement || false
51 if %w(list date bool int).include?(custom_field.field_format)
51 if %w(list date bool int).include?(custom_field.field_format)
52 self.groupable = custom_field.order_statement
52 self.groupable = custom_field.order_statement
53 end
53 end
54 self.groupable ||= false
54 self.groupable ||= false
55 @cf = custom_field
55 @cf = custom_field
56 end
56 end
57
57
58 def caption
58 def caption
59 @cf.name
59 @cf.name
60 end
60 end
61
61
62 def custom_field
62 def custom_field
63 @cf
63 @cf
64 end
64 end
65
65
66 def value(issue)
66 def value(issue)
67 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
67 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv && @cf.cast_value(cv.value)
68 cv && @cf.cast_value(cv.value)
69 end
69 end
70 end
70 end
71
71
72 class Query < ActiveRecord::Base
72 class Query < ActiveRecord::Base
73 class StatementInvalid < ::ActiveRecord::StatementInvalid
73 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 end
74 end
75
75
76 belongs_to :project
76 belongs_to :project
77 belongs_to :user
77 belongs_to :user
78 serialize :filters
78 serialize :filters
79 serialize :column_names
79 serialize :column_names
80 serialize :sort_criteria, Array
80 serialize :sort_criteria, Array
81
81
82 attr_protected :project_id, :user_id
82 attr_protected :project_id, :user_id
83
83
84 validates_presence_of :name, :on => :save
84 validates_presence_of :name, :on => :save
85 validates_length_of :name, :maximum => 255
85 validates_length_of :name, :maximum => 255
86
86
87 @@operators = { "=" => :label_equals,
87 @@operators = { "=" => :label_equals,
88 "!" => :label_not_equals,
88 "!" => :label_not_equals,
89 "o" => :label_open_issues,
89 "o" => :label_open_issues,
90 "c" => :label_closed_issues,
90 "c" => :label_closed_issues,
91 "!*" => :label_none,
91 "!*" => :label_none,
92 "*" => :label_all,
92 "*" => :label_all,
93 ">=" => :label_greater_or_equal,
93 ">=" => :label_greater_or_equal,
94 "<=" => :label_less_or_equal,
94 "<=" => :label_less_or_equal,
95 "<t+" => :label_in_less_than,
95 "<t+" => :label_in_less_than,
96 ">t+" => :label_in_more_than,
96 ">t+" => :label_in_more_than,
97 "t+" => :label_in,
97 "t+" => :label_in,
98 "t" => :label_today,
98 "t" => :label_today,
99 "w" => :label_this_week,
99 "w" => :label_this_week,
100 ">t-" => :label_less_than_ago,
100 ">t-" => :label_less_than_ago,
101 "<t-" => :label_more_than_ago,
101 "<t-" => :label_more_than_ago,
102 "t-" => :label_ago,
102 "t-" => :label_ago,
103 "~" => :label_contains,
103 "~" => :label_contains,
104 "!~" => :label_not_contains }
104 "!~" => :label_not_contains }
105
105
106 cattr_reader :operators
106 cattr_reader :operators
107
107
108 @@operators_by_filter_type = { :list => [ "=", "!" ],
108 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 :list_status => [ "o", "=", "!", "c", "*" ],
109 :list_status => [ "o", "=", "!", "c", "*" ],
110 :list_optional => [ "=", "!", "!*", "*" ],
110 :list_optional => [ "=", "!", "!*", "*" ],
111 :list_subprojects => [ "*", "!*", "=" ],
111 :list_subprojects => [ "*", "!*", "=" ],
112 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
112 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
113 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 :string => [ "=", "~", "!", "!~" ],
114 :string => [ "=", "~", "!", "!~" ],
115 :text => [ "~", "!~" ],
115 :text => [ "~", "!~" ],
116 :integer => [ "=", ">=", "<=", "!*", "*" ] }
116 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117
117
118 cattr_reader :operators_by_filter_type
118 cattr_reader :operators_by_filter_type
119
119
120 @@available_columns = [
120 @@available_columns = [
121 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
121 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
122 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
123 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
123 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
124 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
124 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
125 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
125 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
126 QueryColumn.new(:author),
126 QueryColumn.new(:author),
127 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
127 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
128 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
128 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
129 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
129 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
130 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
130 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
131 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
131 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
132 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
132 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
133 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
133 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
134 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
134 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
135 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
135 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
136 ]
136 ]
137 cattr_reader :available_columns
137 cattr_reader :available_columns
138
138
139 def initialize(attributes = nil)
139 def initialize(attributes = nil)
140 super attributes
140 super attributes
141 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
141 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
142 end
142 end
143
143
144 def after_initialize
144 def after_initialize
145 # Store the fact that project is nil (used in #editable_by?)
145 # Store the fact that project is nil (used in #editable_by?)
146 @is_for_all = project.nil?
146 @is_for_all = project.nil?
147 end
147 end
148
148
149 def validate
149 def validate
150 filters.each_key do |field|
150 filters.each_key do |field|
151 errors.add label_for(field), :blank unless
151 errors.add label_for(field), :blank unless
152 # filter requires one or more values
152 # filter requires one or more values
153 (values_for(field) and !values_for(field).first.blank?) or
153 (values_for(field) and !values_for(field).first.blank?) or
154 # filter doesn't require any value
154 # filter doesn't require any value
155 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
155 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
156 end if filters
156 end if filters
157 end
157 end
158
158
159 def editable_by?(user)
159 def editable_by?(user)
160 return false unless user
160 return false unless user
161 # Admin can edit them all and regular users can edit their private queries
161 # Admin can edit them all and regular users can edit their private queries
162 return true if user.admin? || (!is_public && self.user_id == user.id)
162 return true if user.admin? || (!is_public && self.user_id == user.id)
163 # Members can not edit public queries that are for all project (only admin is allowed to)
163 # Members can not edit public queries that are for all project (only admin is allowed to)
164 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
164 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
165 end
165 end
166
166
167 def available_filters
167 def available_filters
168 return @available_filters if @available_filters
168 return @available_filters if @available_filters
169
169
170 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
170 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
171
171
172 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
172 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
173 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
173 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
174 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
174 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
175 "subject" => { :type => :text, :order => 8 },
175 "subject" => { :type => :text, :order => 8 },
176 "created_on" => { :type => :date_past, :order => 9 },
176 "created_on" => { :type => :date_past, :order => 9 },
177 "updated_on" => { :type => :date_past, :order => 10 },
177 "updated_on" => { :type => :date_past, :order => 10 },
178 "start_date" => { :type => :date, :order => 11 },
178 "start_date" => { :type => :date, :order => 11 },
179 "due_date" => { :type => :date, :order => 12 },
179 "due_date" => { :type => :date, :order => 12 },
180 "estimated_hours" => { :type => :integer, :order => 13 },
180 "estimated_hours" => { :type => :integer, :order => 13 },
181 "done_ratio" => { :type => :integer, :order => 14 }}
181 "done_ratio" => { :type => :integer, :order => 14 }}
182
182
183 user_values = []
183 user_values = []
184 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
184 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
185 if project
185 if project
186 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
186 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
187 else
187 else
188 # members of the user's projects
188 # members of the user's projects
189 # OPTIMIZE: Is selecting from users per project (N+1)
189 # OPTIMIZE: Is selecting from users per project (N+1)
190 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
190 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
191 end
191 end
192 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
192 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
193 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
193 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
194
194
195 if User.current.logged?
195 if User.current.logged?
196 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
196 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
197 end
197 end
198
198
199 if project
199 if project
200 # project specific filters
200 # project specific filters
201 unless @project.issue_categories.empty?
201 unless @project.issue_categories.empty?
202 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
202 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
203 end
203 end
204 unless @project.shared_versions.empty?
204 unless @project.shared_versions.empty?
205 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
205 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
206 end
206 end
207 unless @project.descendants.active.empty?
207 unless @project.descendants.active.empty?
208 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
208 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
209 end
209 end
210 add_custom_fields_filters(@project.all_issue_custom_fields)
210 add_custom_fields_filters(@project.all_issue_custom_fields)
211 else
211 else
212 # global filters for cross project issue list
212 # global filters for cross project issue list
213 system_shared_versions = Version.visible.find_all_by_sharing('system')
213 system_shared_versions = Version.visible.find_all_by_sharing('system')
214 unless system_shared_versions.empty?
214 unless system_shared_versions.empty?
215 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
215 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
216 end
216 end
217 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
217 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
218 end
218 end
219 @available_filters
219 @available_filters
220 end
220 end
221
221
222 def add_filter(field, operator, values)
222 def add_filter(field, operator, values)
223 # values must be an array
223 # values must be an array
224 return unless values and values.is_a? Array # and !values.first.empty?
224 return unless values and values.is_a? Array # and !values.first.empty?
225 # check if field is defined as an available filter
225 # check if field is defined as an available filter
226 if available_filters.has_key? field
226 if available_filters.has_key? field
227 filter_options = available_filters[field]
227 filter_options = available_filters[field]
228 # check if operator is allowed for that filter
228 # check if operator is allowed for that filter
229 #if @@operators_by_filter_type[filter_options[:type]].include? operator
229 #if @@operators_by_filter_type[filter_options[:type]].include? operator
230 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
230 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
231 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
231 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
232 #end
232 #end
233 filters[field] = {:operator => operator, :values => values }
233 filters[field] = {:operator => operator, :values => values }
234 end
234 end
235 end
235 end
236
236
237 def add_short_filter(field, expression)
237 def add_short_filter(field, expression)
238 return unless expression
238 return unless expression
239 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
239 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
240 add_filter field, (parms[0] || "="), [parms[1] || ""]
240 add_filter field, (parms[0] || "="), [parms[1] || ""]
241 end
241 end
242
242
243 def has_filter?(field)
243 def has_filter?(field)
244 filters and filters[field]
244 filters and filters[field]
245 end
245 end
246
246
247 def operator_for(field)
247 def operator_for(field)
248 has_filter?(field) ? filters[field][:operator] : nil
248 has_filter?(field) ? filters[field][:operator] : nil
249 end
249 end
250
250
251 def values_for(field)
251 def values_for(field)
252 has_filter?(field) ? filters[field][:values] : nil
252 has_filter?(field) ? filters[field][:values] : nil
253 end
253 end
254
254
255 def label_for(field)
255 def label_for(field)
256 label = available_filters[field][:name] if available_filters.has_key?(field)
256 label = available_filters[field][:name] if available_filters.has_key?(field)
257 label ||= field.gsub(/\_id$/, "")
257 label ||= field.gsub(/\_id$/, "")
258 end
258 end
259
259
260 def available_columns
260 def available_columns
261 return @available_columns if @available_columns
261 return @available_columns if @available_columns
262 @available_columns = Query.available_columns
262 @available_columns = Query.available_columns
263 @available_columns += (project ?
263 @available_columns += (project ?
264 project.all_issue_custom_fields :
264 project.all_issue_custom_fields :
265 IssueCustomField.find(:all)
265 IssueCustomField.find(:all)
266 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
266 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
267 end
267 end
268
268
269 # Returns an array of columns that can be used to group the results
269 # Returns an array of columns that can be used to group the results
270 def groupable_columns
270 def groupable_columns
271 available_columns.select {|c| c.groupable}
271 available_columns.select {|c| c.groupable}
272 end
272 end
273
274 # Returns a Hash of columns and the key for sorting
275 def sortable_columns
276 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
277 h[column.name.to_s] = column.sortable
278 h
279 })
280 end
273
281
274 def columns
282 def columns
275 if has_default_columns?
283 if has_default_columns?
276 available_columns.select do |c|
284 available_columns.select do |c|
277 # Adds the project column by default for cross-project lists
285 # Adds the project column by default for cross-project lists
278 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
286 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
279 end
287 end
280 else
288 else
281 # preserve the column_names order
289 # preserve the column_names order
282 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
290 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
283 end
291 end
284 end
292 end
285
293
286 def column_names=(names)
294 def column_names=(names)
287 if names
295 if names
288 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
296 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
289 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
297 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
290 # Set column_names to nil if default columns
298 # Set column_names to nil if default columns
291 if names.map(&:to_s) == Setting.issue_list_default_columns
299 if names.map(&:to_s) == Setting.issue_list_default_columns
292 names = nil
300 names = nil
293 end
301 end
294 end
302 end
295 write_attribute(:column_names, names)
303 write_attribute(:column_names, names)
296 end
304 end
297
305
298 def has_column?(column)
306 def has_column?(column)
299 column_names && column_names.include?(column.name)
307 column_names && column_names.include?(column.name)
300 end
308 end
301
309
302 def has_default_columns?
310 def has_default_columns?
303 column_names.nil? || column_names.empty?
311 column_names.nil? || column_names.empty?
304 end
312 end
305
313
306 def sort_criteria=(arg)
314 def sort_criteria=(arg)
307 c = []
315 c = []
308 if arg.is_a?(Hash)
316 if arg.is_a?(Hash)
309 arg = arg.keys.sort.collect {|k| arg[k]}
317 arg = arg.keys.sort.collect {|k| arg[k]}
310 end
318 end
311 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
319 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
312 write_attribute(:sort_criteria, c)
320 write_attribute(:sort_criteria, c)
313 end
321 end
314
322
315 def sort_criteria
323 def sort_criteria
316 read_attribute(:sort_criteria) || []
324 read_attribute(:sort_criteria) || []
317 end
325 end
318
326
319 def sort_criteria_key(arg)
327 def sort_criteria_key(arg)
320 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
328 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
321 end
329 end
322
330
323 def sort_criteria_order(arg)
331 def sort_criteria_order(arg)
324 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
332 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
325 end
333 end
326
334
327 # Returns the SQL sort order that should be prepended for grouping
335 # Returns the SQL sort order that should be prepended for grouping
328 def group_by_sort_order
336 def group_by_sort_order
329 if grouped? && (column = group_by_column)
337 if grouped? && (column = group_by_column)
330 column.sortable.is_a?(Array) ?
338 column.sortable.is_a?(Array) ?
331 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
339 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
332 "#{column.sortable} #{column.default_order}"
340 "#{column.sortable} #{column.default_order}"
333 end
341 end
334 end
342 end
335
343
336 # Returns true if the query is a grouped query
344 # Returns true if the query is a grouped query
337 def grouped?
345 def grouped?
338 !group_by.blank?
346 !group_by.blank?
339 end
347 end
340
348
341 def group_by_column
349 def group_by_column
342 groupable_columns.detect {|c| c.name.to_s == group_by}
350 groupable_columns.detect {|c| c.name.to_s == group_by}
343 end
351 end
344
352
345 def group_by_statement
353 def group_by_statement
346 group_by_column.groupable
354 group_by_column.groupable
347 end
355 end
348
356
349 def project_statement
357 def project_statement
350 project_clauses = []
358 project_clauses = []
351 if project && !@project.descendants.active.empty?
359 if project && !@project.descendants.active.empty?
352 ids = [project.id]
360 ids = [project.id]
353 if has_filter?("subproject_id")
361 if has_filter?("subproject_id")
354 case operator_for("subproject_id")
362 case operator_for("subproject_id")
355 when '='
363 when '='
356 # include the selected subprojects
364 # include the selected subprojects
357 ids += values_for("subproject_id").each(&:to_i)
365 ids += values_for("subproject_id").each(&:to_i)
358 when '!*'
366 when '!*'
359 # main project only
367 # main project only
360 else
368 else
361 # all subprojects
369 # all subprojects
362 ids += project.descendants.collect(&:id)
370 ids += project.descendants.collect(&:id)
363 end
371 end
364 elsif Setting.display_subprojects_issues?
372 elsif Setting.display_subprojects_issues?
365 ids += project.descendants.collect(&:id)
373 ids += project.descendants.collect(&:id)
366 end
374 end
367 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
375 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
368 elsif project
376 elsif project
369 project_clauses << "#{Project.table_name}.id = %d" % project.id
377 project_clauses << "#{Project.table_name}.id = %d" % project.id
370 end
378 end
371 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
379 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
372 project_clauses.join(' AND ')
380 project_clauses.join(' AND ')
373 end
381 end
374
382
375 def statement
383 def statement
376 # filters clauses
384 # filters clauses
377 filters_clauses = []
385 filters_clauses = []
378 filters.each_key do |field|
386 filters.each_key do |field|
379 next if field == "subproject_id"
387 next if field == "subproject_id"
380 v = values_for(field).clone
388 v = values_for(field).clone
381 next unless v and !v.empty?
389 next unless v and !v.empty?
382 operator = operator_for(field)
390 operator = operator_for(field)
383
391
384 # "me" value subsitution
392 # "me" value subsitution
385 if %w(assigned_to_id author_id watcher_id).include?(field)
393 if %w(assigned_to_id author_id watcher_id).include?(field)
386 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
394 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
387 end
395 end
388
396
389 sql = ''
397 sql = ''
390 if field =~ /^cf_(\d+)$/
398 if field =~ /^cf_(\d+)$/
391 # custom field
399 # custom field
392 db_table = CustomValue.table_name
400 db_table = CustomValue.table_name
393 db_field = 'value'
401 db_field = 'value'
394 is_custom_filter = true
402 is_custom_filter = true
395 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
403 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
396 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
404 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
397 elsif field == 'watcher_id'
405 elsif field == 'watcher_id'
398 db_table = Watcher.table_name
406 db_table = Watcher.table_name
399 db_field = 'user_id'
407 db_field = 'user_id'
400 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
408 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
401 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
409 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
402 else
410 else
403 # regular field
411 # regular field
404 db_table = Issue.table_name
412 db_table = Issue.table_name
405 db_field = field
413 db_field = field
406 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
414 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
407 end
415 end
408 filters_clauses << sql
416 filters_clauses << sql
409
417
410 end if filters and valid?
418 end if filters and valid?
411
419
412 (filters_clauses << project_statement).join(' AND ')
420 (filters_clauses << project_statement).join(' AND ')
413 end
421 end
414
422
415 # Returns the issue count
423 # Returns the issue count
416 def issue_count
424 def issue_count
417 Issue.count(:include => [:status, :project], :conditions => statement)
425 Issue.count(:include => [:status, :project], :conditions => statement)
418 rescue ::ActiveRecord::StatementInvalid => e
426 rescue ::ActiveRecord::StatementInvalid => e
419 raise StatementInvalid.new(e.message)
427 raise StatementInvalid.new(e.message)
420 end
428 end
421
429
422 # Returns the issue count by group or nil if query is not grouped
430 # Returns the issue count by group or nil if query is not grouped
423 def issue_count_by_group
431 def issue_count_by_group
424 r = nil
432 r = nil
425 if grouped?
433 if grouped?
426 begin
434 begin
427 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
435 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
428 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
436 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
429 rescue ActiveRecord::RecordNotFound
437 rescue ActiveRecord::RecordNotFound
430 r = {nil => issue_count}
438 r = {nil => issue_count}
431 end
439 end
432 c = group_by_column
440 c = group_by_column
433 if c.is_a?(QueryCustomFieldColumn)
441 if c.is_a?(QueryCustomFieldColumn)
434 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
442 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
435 end
443 end
436 end
444 end
437 r
445 r
438 rescue ::ActiveRecord::StatementInvalid => e
446 rescue ::ActiveRecord::StatementInvalid => e
439 raise StatementInvalid.new(e.message)
447 raise StatementInvalid.new(e.message)
440 end
448 end
441
449
442 # Returns the issues
450 # Returns the issues
443 # Valid options are :order, :offset, :limit, :include, :conditions
451 # Valid options are :order, :offset, :limit, :include, :conditions
444 def issues(options={})
452 def issues(options={})
445 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
453 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
446 order_option = nil if order_option.blank?
454 order_option = nil if order_option.blank?
447
455
448 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
456 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
449 :conditions => Query.merge_conditions(statement, options[:conditions]),
457 :conditions => Query.merge_conditions(statement, options[:conditions]),
450 :order => order_option,
458 :order => order_option,
451 :limit => options[:limit],
459 :limit => options[:limit],
452 :offset => options[:offset]
460 :offset => options[:offset]
453 rescue ::ActiveRecord::StatementInvalid => e
461 rescue ::ActiveRecord::StatementInvalid => e
454 raise StatementInvalid.new(e.message)
462 raise StatementInvalid.new(e.message)
455 end
463 end
456
464
457 # Returns the journals
465 # Returns the journals
458 # Valid options are :order, :offset, :limit
466 # Valid options are :order, :offset, :limit
459 def journals(options={})
467 def journals(options={})
460 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
468 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
461 :conditions => statement,
469 :conditions => statement,
462 :order => options[:order],
470 :order => options[:order],
463 :limit => options[:limit],
471 :limit => options[:limit],
464 :offset => options[:offset]
472 :offset => options[:offset]
465 rescue ::ActiveRecord::StatementInvalid => e
473 rescue ::ActiveRecord::StatementInvalid => e
466 raise StatementInvalid.new(e.message)
474 raise StatementInvalid.new(e.message)
467 end
475 end
468
476
469 # Returns the versions
477 # Returns the versions
470 # Valid options are :conditions
478 # Valid options are :conditions
471 def versions(options={})
479 def versions(options={})
472 Version.find :all, :include => :project,
480 Version.find :all, :include => :project,
473 :conditions => Query.merge_conditions(project_statement, options[:conditions])
481 :conditions => Query.merge_conditions(project_statement, options[:conditions])
474 rescue ::ActiveRecord::StatementInvalid => e
482 rescue ::ActiveRecord::StatementInvalid => e
475 raise StatementInvalid.new(e.message)
483 raise StatementInvalid.new(e.message)
476 end
484 end
477
485
478 private
486 private
479
487
480 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
488 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
481 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
489 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
482 sql = ''
490 sql = ''
483 case operator
491 case operator
484 when "="
492 when "="
485 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
493 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
486 when "!"
494 when "!"
487 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
495 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
488 when "!*"
496 when "!*"
489 sql = "#{db_table}.#{db_field} IS NULL"
497 sql = "#{db_table}.#{db_field} IS NULL"
490 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
498 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
491 when "*"
499 when "*"
492 sql = "#{db_table}.#{db_field} IS NOT NULL"
500 sql = "#{db_table}.#{db_field} IS NOT NULL"
493 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
501 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
494 when ">="
502 when ">="
495 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
503 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
496 when "<="
504 when "<="
497 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
505 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
498 when "o"
506 when "o"
499 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
507 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
500 when "c"
508 when "c"
501 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
509 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
502 when ">t-"
510 when ">t-"
503 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
511 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
504 when "<t-"
512 when "<t-"
505 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
513 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
506 when "t-"
514 when "t-"
507 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
515 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
508 when ">t+"
516 when ">t+"
509 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
517 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
510 when "<t+"
518 when "<t+"
511 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
519 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
512 when "t+"
520 when "t+"
513 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
521 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
514 when "t"
522 when "t"
515 sql = date_range_clause(db_table, db_field, 0, 0)
523 sql = date_range_clause(db_table, db_field, 0, 0)
516 when "w"
524 when "w"
517 from = l(:general_first_day_of_week) == '7' ?
525 from = l(:general_first_day_of_week) == '7' ?
518 # week starts on sunday
526 # week starts on sunday
519 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
527 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
520 # week starts on monday (Rails default)
528 # week starts on monday (Rails default)
521 Time.now.at_beginning_of_week
529 Time.now.at_beginning_of_week
522 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
530 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
523 when "~"
531 when "~"
524 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
532 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
525 when "!~"
533 when "!~"
526 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
534 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
527 end
535 end
528
536
529 return sql
537 return sql
530 end
538 end
531
539
532 def add_custom_fields_filters(custom_fields)
540 def add_custom_fields_filters(custom_fields)
533 @available_filters ||= {}
541 @available_filters ||= {}
534
542
535 custom_fields.select(&:is_filter?).each do |field|
543 custom_fields.select(&:is_filter?).each do |field|
536 case field.field_format
544 case field.field_format
537 when "text"
545 when "text"
538 options = { :type => :text, :order => 20 }
546 options = { :type => :text, :order => 20 }
539 when "list"
547 when "list"
540 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
548 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
541 when "date"
549 when "date"
542 options = { :type => :date, :order => 20 }
550 options = { :type => :date, :order => 20 }
543 when "bool"
551 when "bool"
544 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
552 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
545 else
553 else
546 options = { :type => :string, :order => 20 }
554 options = { :type => :string, :order => 20 }
547 end
555 end
548 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
556 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
549 end
557 end
550 end
558 end
551
559
552 # Returns a SQL clause for a date or datetime field.
560 # Returns a SQL clause for a date or datetime field.
553 def date_range_clause(table, field, from, to)
561 def date_range_clause(table, field, from, to)
554 s = []
562 s = []
555 if from
563 if from
556 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
564 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
557 end
565 end
558 if to
566 if to
559 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
567 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
560 end
568 end
561 s.join(' AND ')
569 s.join(' AND ')
562 end
570 end
563 end
571 end
General Comments 0
You need to be logged in to leave comments. Login now