##// END OF EJS Templates
Optimize associations loading on the issue list....
Jean-Philippe Lang -
r2958:d84bd8edd8af
parent child
Show More
@@ -1,526 +1,526
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, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :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 helper :journals
29 helper :journals
30 helper :projects
30 helper :projects
31 include ProjectsHelper
31 include ProjectsHelper
32 helper :custom_fields
32 helper :custom_fields
33 include CustomFieldsHelper
33 include CustomFieldsHelper
34 helper :issue_relations
34 helper :issue_relations
35 include IssueRelationsHelper
35 include IssueRelationsHelper
36 helper :watchers
36 helper :watchers
37 include WatchersHelper
37 include WatchersHelper
38 helper :attachments
38 helper :attachments
39 include AttachmentsHelper
39 include AttachmentsHelper
40 helper :queries
40 helper :queries
41 helper :sort
41 helper :sort
42 include SortHelper
42 include SortHelper
43 include IssuesHelper
43 include IssuesHelper
44 helper :timelog
44 helper :timelog
45 include Redmine::Export::PDF
45 include Redmine::Export::PDF
46
46
47 verify :method => :post,
47 verify :method => :post,
48 :only => :destroy,
48 :only => :destroy,
49 :render => { :nothing => true, :status => :method_not_allowed }
49 :render => { :nothing => true, :status => :method_not_allowed }
50
50
51 def index
51 def index
52 retrieve_query
52 retrieve_query
53 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
53 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
54 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
54 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
55
55
56 if @query.valid?
56 if @query.valid?
57 limit = per_page_option
57 limit = per_page_option
58 respond_to do |format|
58 respond_to do |format|
59 format.html { }
59 format.html { }
60 format.atom { limit = Setting.feeds_limit.to_i }
60 format.atom { limit = Setting.feeds_limit.to_i }
61 format.csv { limit = Setting.issues_export_limit.to_i }
61 format.csv { limit = Setting.issues_export_limit.to_i }
62 format.pdf { limit = Setting.issues_export_limit.to_i }
62 format.pdf { limit = Setting.issues_export_limit.to_i }
63 end
63 end
64 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
64 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
65 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
65 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
66 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
66 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
67 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
67 :include => ([:status, :project, :priority] + @query.include_options),
68 :conditions => @query.statement,
68 :conditions => @query.statement,
69 :limit => limit,
69 :limit => limit,
70 :offset => @issue_pages.current.offset
70 :offset => @issue_pages.current.offset
71 respond_to do |format|
71 respond_to do |format|
72 format.html {
72 format.html {
73 if @query.grouped?
73 if @query.grouped?
74 # Retrieve the issue count by group
74 # Retrieve the issue count by group
75 @issue_count_by_group = begin
75 @issue_count_by_group = begin
76 Issue.count(:group => @query.group_by_statement, :include => [:status, :project], :conditions => @query.statement)
76 Issue.count(:group => @query.group_by_statement, :include => [:status, :project], :conditions => @query.statement)
77 # Rails will raise an (unexpected) error if there's only a nil group value
77 # Rails will raise an (unexpected) error if there's only a nil group value
78 rescue ActiveRecord::RecordNotFound
78 rescue ActiveRecord::RecordNotFound
79 {nil => @issue_count}
79 {nil => @issue_count}
80 end
80 end
81 end
81 end
82 render :template => 'issues/index.rhtml', :layout => !request.xhr?
82 render :template => 'issues/index.rhtml', :layout => !request.xhr?
83 }
83 }
84 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
84 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
85 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
85 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
86 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
86 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
87 end
87 end
88 else
88 else
89 # Send html if the query is not valid
89 # Send html if the query is not valid
90 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
90 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
91 end
91 end
92 rescue ActiveRecord::RecordNotFound
92 rescue ActiveRecord::RecordNotFound
93 render_404
93 render_404
94 end
94 end
95
95
96 def changes
96 def changes
97 retrieve_query
97 retrieve_query
98 sort_init 'id', 'desc'
98 sort_init 'id', 'desc'
99 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
99 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
100
100
101 if @query.valid?
101 if @query.valid?
102 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
102 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
103 :conditions => @query.statement,
103 :conditions => @query.statement,
104 :limit => 25,
104 :limit => 25,
105 :order => "#{Journal.table_name}.created_on DESC"
105 :order => "#{Journal.table_name}.created_on DESC"
106 end
106 end
107 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
107 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
108 render :layout => false, :content_type => 'application/atom+xml'
108 render :layout => false, :content_type => 'application/atom+xml'
109 rescue ActiveRecord::RecordNotFound
109 rescue ActiveRecord::RecordNotFound
110 render_404
110 render_404
111 end
111 end
112
112
113 def show
113 def show
114 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
114 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
115 @journals.each_with_index {|j,i| j.indice = i+1}
115 @journals.each_with_index {|j,i| j.indice = i+1}
116 @journals.reverse! if User.current.wants_comments_in_reverse_order?
116 @journals.reverse! if User.current.wants_comments_in_reverse_order?
117 @changesets = @issue.changesets
117 @changesets = @issue.changesets
118 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
118 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
119 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
120 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
121 @priorities = IssuePriority.all
121 @priorities = IssuePriority.all
122 @time_entry = TimeEntry.new
122 @time_entry = TimeEntry.new
123 respond_to do |format|
123 respond_to do |format|
124 format.html { render :template => 'issues/show.rhtml' }
124 format.html { render :template => 'issues/show.rhtml' }
125 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
125 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
126 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
126 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
127 end
127 end
128 end
128 end
129
129
130 # Add a new issue
130 # Add a new issue
131 # The new issue will be created from an existing one if copy_from parameter is given
131 # The new issue will be created from an existing one if copy_from parameter is given
132 def new
132 def new
133 @issue = Issue.new
133 @issue = Issue.new
134 @issue.copy_from(params[:copy_from]) if params[:copy_from]
134 @issue.copy_from(params[:copy_from]) if params[:copy_from]
135 @issue.project = @project
135 @issue.project = @project
136 # Tracker must be set before custom field values
136 # Tracker must be set before custom field values
137 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
137 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
138 if @issue.tracker.nil?
138 if @issue.tracker.nil?
139 render_error l(:error_no_tracker_in_project)
139 render_error l(:error_no_tracker_in_project)
140 return
140 return
141 end
141 end
142 if params[:issue].is_a?(Hash)
142 if params[:issue].is_a?(Hash)
143 @issue.attributes = params[:issue]
143 @issue.attributes = params[:issue]
144 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
144 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
145 end
145 end
146 @issue.author = User.current
146 @issue.author = User.current
147
147
148 default_status = IssueStatus.default
148 default_status = IssueStatus.default
149 unless default_status
149 unless default_status
150 render_error l(:error_no_default_issue_status)
150 render_error l(:error_no_default_issue_status)
151 return
151 return
152 end
152 end
153 @issue.status = default_status
153 @issue.status = default_status
154 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
154 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
155
155
156 if request.get? || request.xhr?
156 if request.get? || request.xhr?
157 @issue.start_date ||= Date.today
157 @issue.start_date ||= Date.today
158 else
158 else
159 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
159 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
160 # Check that the user is allowed to apply the requested status
160 # Check that the user is allowed to apply the requested status
161 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
161 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
162 if @issue.save
162 if @issue.save
163 attach_files(@issue, params[:attachments])
163 attach_files(@issue, params[:attachments])
164 flash[:notice] = l(:notice_successful_create)
164 flash[:notice] = l(:notice_successful_create)
165 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
165 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
166 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
166 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
167 { :action => 'show', :id => @issue })
167 { :action => 'show', :id => @issue })
168 return
168 return
169 end
169 end
170 end
170 end
171 @priorities = IssuePriority.all
171 @priorities = IssuePriority.all
172 render :layout => !request.xhr?
172 render :layout => !request.xhr?
173 end
173 end
174
174
175 # Attributes that can be updated on workflow transition (without :edit permission)
175 # Attributes that can be updated on workflow transition (without :edit permission)
176 # TODO: make it configurable (at least per role)
176 # TODO: make it configurable (at least per role)
177 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
177 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
178
178
179 def edit
179 def edit
180 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
180 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
181 @priorities = IssuePriority.all
181 @priorities = IssuePriority.all
182 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
182 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
183 @time_entry = TimeEntry.new
183 @time_entry = TimeEntry.new
184
184
185 @notes = params[:notes]
185 @notes = params[:notes]
186 journal = @issue.init_journal(User.current, @notes)
186 journal = @issue.init_journal(User.current, @notes)
187 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
187 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
188 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
188 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
189 attrs = params[:issue].dup
189 attrs = params[:issue].dup
190 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
190 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
191 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
191 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
192 @issue.attributes = attrs
192 @issue.attributes = attrs
193 end
193 end
194
194
195 if request.post?
195 if request.post?
196 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
196 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
197 @time_entry.attributes = params[:time_entry]
197 @time_entry.attributes = params[:time_entry]
198 attachments = attach_files(@issue, params[:attachments])
198 attachments = attach_files(@issue, params[:attachments])
199 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
199 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
200
200
201 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
201 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
202
202
203 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
203 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
204 # Log spend time
204 # Log spend time
205 if User.current.allowed_to?(:log_time, @project)
205 if User.current.allowed_to?(:log_time, @project)
206 @time_entry.save
206 @time_entry.save
207 end
207 end
208 if !journal.new_record?
208 if !journal.new_record?
209 # Only send notification if something was actually changed
209 # Only send notification if something was actually changed
210 flash[:notice] = l(:notice_successful_update)
210 flash[:notice] = l(:notice_successful_update)
211 end
211 end
212 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
212 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
213 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
213 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
214 end
214 end
215 end
215 end
216 rescue ActiveRecord::StaleObjectError
216 rescue ActiveRecord::StaleObjectError
217 # Optimistic locking exception
217 # Optimistic locking exception
218 flash.now[:error] = l(:notice_locking_conflict)
218 flash.now[:error] = l(:notice_locking_conflict)
219 # Remove the previously added attachments if issue was not updated
219 # Remove the previously added attachments if issue was not updated
220 attachments.each(&:destroy)
220 attachments.each(&:destroy)
221 end
221 end
222
222
223 def reply
223 def reply
224 journal = Journal.find(params[:journal_id]) if params[:journal_id]
224 journal = Journal.find(params[:journal_id]) if params[:journal_id]
225 if journal
225 if journal
226 user = journal.user
226 user = journal.user
227 text = journal.notes
227 text = journal.notes
228 else
228 else
229 user = @issue.author
229 user = @issue.author
230 text = @issue.description
230 text = @issue.description
231 end
231 end
232 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
232 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
233 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
233 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
234 render(:update) { |page|
234 render(:update) { |page|
235 page.<< "$('notes').value = \"#{content}\";"
235 page.<< "$('notes').value = \"#{content}\";"
236 page.show 'update'
236 page.show 'update'
237 page << "Form.Element.focus('notes');"
237 page << "Form.Element.focus('notes');"
238 page << "Element.scrollTo('update');"
238 page << "Element.scrollTo('update');"
239 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
239 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
240 }
240 }
241 end
241 end
242
242
243 # Bulk edit a set of issues
243 # Bulk edit a set of issues
244 def bulk_edit
244 def bulk_edit
245 if request.post?
245 if request.post?
246 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
246 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
247 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
247 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
248 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
248 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
249 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
249 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
250 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
250 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
251 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
251 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
252
252
253 unsaved_issue_ids = []
253 unsaved_issue_ids = []
254 @issues.each do |issue|
254 @issues.each do |issue|
255 journal = issue.init_journal(User.current, params[:notes])
255 journal = issue.init_journal(User.current, params[:notes])
256 issue.priority = priority if priority
256 issue.priority = priority if priority
257 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
257 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
258 issue.category = category if category || params[:category_id] == 'none'
258 issue.category = category if category || params[:category_id] == 'none'
259 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
259 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
260 issue.start_date = params[:start_date] unless params[:start_date].blank?
260 issue.start_date = params[:start_date] unless params[:start_date].blank?
261 issue.due_date = params[:due_date] unless params[:due_date].blank?
261 issue.due_date = params[:due_date] unless params[:due_date].blank?
262 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
262 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
263 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
263 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
264 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
264 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
265 # Don't save any change to the issue if the user is not authorized to apply the requested status
265 # Don't save any change to the issue if the user is not authorized to apply the requested status
266 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
266 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
267 # Keep unsaved issue ids to display them in flash error
267 # Keep unsaved issue ids to display them in flash error
268 unsaved_issue_ids << issue.id
268 unsaved_issue_ids << issue.id
269 end
269 end
270 end
270 end
271 if unsaved_issue_ids.empty?
271 if unsaved_issue_ids.empty?
272 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
272 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
273 else
273 else
274 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
274 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
275 :total => @issues.size,
275 :total => @issues.size,
276 :ids => '#' + unsaved_issue_ids.join(', #'))
276 :ids => '#' + unsaved_issue_ids.join(', #'))
277 end
277 end
278 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
278 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
279 return
279 return
280 end
280 end
281 # Find potential statuses the user could be allowed to switch issues to
281 # Find potential statuses the user could be allowed to switch issues to
282 @available_statuses = Workflow.find(:all, :include => :new_status,
282 @available_statuses = Workflow.find(:all, :include => :new_status,
283 :conditions => {:role_id => User.current.roles_for_project(@project).collect(&:id)}).collect(&:new_status).compact.uniq.sort
283 :conditions => {:role_id => User.current.roles_for_project(@project).collect(&:id)}).collect(&:new_status).compact.uniq.sort
284 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
284 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
285 end
285 end
286
286
287 def move
287 def move
288 @allowed_projects = []
288 @allowed_projects = []
289 # find projects to which the user is allowed to move the issue
289 # find projects to which the user is allowed to move the issue
290 if User.current.admin?
290 if User.current.admin?
291 # admin is allowed to move issues to any active (visible) project
291 # admin is allowed to move issues to any active (visible) project
292 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
292 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
293 else
293 else
294 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
294 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
295 end
295 end
296 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
296 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
297 @target_project ||= @project
297 @target_project ||= @project
298 @trackers = @target_project.trackers
298 @trackers = @target_project.trackers
299 if request.post?
299 if request.post?
300 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
300 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
301 unsaved_issue_ids = []
301 unsaved_issue_ids = []
302 moved_issues = []
302 moved_issues = []
303 @issues.each do |issue|
303 @issues.each do |issue|
304 issue.init_journal(User.current)
304 issue.init_journal(User.current)
305 if r = issue.move_to(@target_project, new_tracker, params[:copy_options])
305 if r = issue.move_to(@target_project, new_tracker, params[:copy_options])
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 if unsaved_issue_ids.empty?
311 if unsaved_issue_ids.empty?
312 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
312 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
313 else
313 else
314 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
314 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
315 :total => @issues.size,
315 :total => @issues.size,
316 :ids => '#' + unsaved_issue_ids.join(', #'))
316 :ids => '#' + unsaved_issue_ids.join(', #'))
317 end
317 end
318 if params[:follow]
318 if params[:follow]
319 if @issues.size == 1 && moved_issues.size == 1
319 if @issues.size == 1 && moved_issues.size == 1
320 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
320 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
321 else
321 else
322 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
322 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
323 end
323 end
324 else
324 else
325 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
325 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
326 end
326 end
327 return
327 return
328 end
328 end
329 render :layout => false if request.xhr?
329 render :layout => false if request.xhr?
330 end
330 end
331
331
332 def destroy
332 def destroy
333 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
333 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
334 if @hours > 0
334 if @hours > 0
335 case params[:todo]
335 case params[:todo]
336 when 'destroy'
336 when 'destroy'
337 # nothing to do
337 # nothing to do
338 when 'nullify'
338 when 'nullify'
339 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
339 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
340 when 'reassign'
340 when 'reassign'
341 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
341 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
342 if reassign_to.nil?
342 if reassign_to.nil?
343 flash.now[:error] = l(:error_issue_not_found_in_project)
343 flash.now[:error] = l(:error_issue_not_found_in_project)
344 return
344 return
345 else
345 else
346 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
346 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
347 end
347 end
348 else
348 else
349 # display the destroy form
349 # display the destroy form
350 return
350 return
351 end
351 end
352 end
352 end
353 @issues.each(&:destroy)
353 @issues.each(&:destroy)
354 redirect_to :action => 'index', :project_id => @project
354 redirect_to :action => 'index', :project_id => @project
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 if @query.valid?
360 if @query.valid?
361 events = []
361 events = []
362 # Issues that have start and due dates
362 # Issues that have start and due dates
363 events += Issue.find(:all,
363 events += Issue.find(:all,
364 :order => "start_date, due_date",
364 :order => "start_date, due_date",
365 :include => [:tracker, :status, :assigned_to, :priority, :project],
365 :include => [:tracker, :status, :assigned_to, :priority, :project],
366 :conditions => ["(#{@query.statement}) AND (((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 => ["(#{@query.statement}) AND (((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 += Issue.find(:all,
369 events += Issue.find(:all,
370 :order => "start_date, effective_date",
370 :order => "start_date, effective_date",
371 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
371 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
372 :conditions => ["(#{@query.statement}) AND (((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 :conditions => ["(#{@query.statement}) AND (((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]
373 )
373 )
374 # Versions
374 # Versions
375 events += Version.find(:all, :include => :project,
375 events += Version.find(:all, :include => :project,
376 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
376 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
377
377
378 @gantt.events = events
378 @gantt.events = events
379 end
379 end
380
380
381 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
381 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
382
382
383 respond_to do |format|
383 respond_to do |format|
384 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
384 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
385 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
385 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
386 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
386 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
387 end
387 end
388 end
388 end
389
389
390 def calendar
390 def calendar
391 if params[:year] and params[:year].to_i > 1900
391 if params[:year] and params[:year].to_i > 1900
392 @year = params[:year].to_i
392 @year = params[:year].to_i
393 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
393 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
394 @month = params[:month].to_i
394 @month = params[:month].to_i
395 end
395 end
396 end
396 end
397 @year ||= Date.today.year
397 @year ||= Date.today.year
398 @month ||= Date.today.month
398 @month ||= Date.today.month
399
399
400 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
400 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
401 retrieve_query
401 retrieve_query
402 if @query.valid?
402 if @query.valid?
403 events = []
403 events = []
404 events += Issue.find(:all,
404 events += Issue.find(:all,
405 :include => [:tracker, :status, :assigned_to, :priority, :project],
405 :include => [:tracker, :status, :assigned_to, :priority, :project],
406 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
406 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
407 )
407 )
408 events += Version.find(:all, :include => :project,
408 events += Version.find(:all, :include => :project,
409 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
409 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
410
410
411 @calendar.events = events
411 @calendar.events = events
412 end
412 end
413
413
414 render :layout => false if request.xhr?
414 render :layout => false if request.xhr?
415 end
415 end
416
416
417 def context_menu
417 def context_menu
418 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
418 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
419 if (@issues.size == 1)
419 if (@issues.size == 1)
420 @issue = @issues.first
420 @issue = @issues.first
421 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
421 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
422 end
422 end
423 projects = @issues.collect(&:project).compact.uniq
423 projects = @issues.collect(&:project).compact.uniq
424 @project = projects.first if projects.size == 1
424 @project = projects.first if projects.size == 1
425
425
426 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
426 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
427 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
427 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
428 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
428 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
429 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
429 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
430 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
430 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
431 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
431 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
432 }
432 }
433 if @project
433 if @project
434 @assignables = @project.assignable_users
434 @assignables = @project.assignable_users
435 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
435 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
436 end
436 end
437
437
438 @priorities = IssuePriority.all.reverse
438 @priorities = IssuePriority.all.reverse
439 @statuses = IssueStatus.find(:all, :order => 'position')
439 @statuses = IssueStatus.find(:all, :order => 'position')
440 @back = params[:back_url] || request.env['HTTP_REFERER']
440 @back = params[:back_url] || request.env['HTTP_REFERER']
441
441
442 render :layout => false
442 render :layout => false
443 end
443 end
444
444
445 def update_form
445 def update_form
446 @issue = Issue.new(params[:issue])
446 @issue = Issue.new(params[:issue])
447 render :action => :new, :layout => false
447 render :action => :new, :layout => false
448 end
448 end
449
449
450 def preview
450 def preview
451 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
451 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
452 @attachements = @issue.attachments if @issue
452 @attachements = @issue.attachments if @issue
453 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
453 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
454 render :partial => 'common/preview'
454 render :partial => 'common/preview'
455 end
455 end
456
456
457 private
457 private
458 def find_issue
458 def find_issue
459 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
459 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
460 @project = @issue.project
460 @project = @issue.project
461 rescue ActiveRecord::RecordNotFound
461 rescue ActiveRecord::RecordNotFound
462 render_404
462 render_404
463 end
463 end
464
464
465 # Filter for bulk operations
465 # Filter for bulk operations
466 def find_issues
466 def find_issues
467 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
467 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
468 raise ActiveRecord::RecordNotFound if @issues.empty?
468 raise ActiveRecord::RecordNotFound if @issues.empty?
469 projects = @issues.collect(&:project).compact.uniq
469 projects = @issues.collect(&:project).compact.uniq
470 if projects.size == 1
470 if projects.size == 1
471 @project = projects.first
471 @project = projects.first
472 else
472 else
473 # TODO: let users bulk edit/move/destroy issues from different projects
473 # TODO: let users bulk edit/move/destroy issues from different projects
474 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
474 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
475 end
475 end
476 rescue ActiveRecord::RecordNotFound
476 rescue ActiveRecord::RecordNotFound
477 render_404
477 render_404
478 end
478 end
479
479
480 def find_project
480 def find_project
481 @project = Project.find(params[:project_id])
481 @project = Project.find(params[:project_id])
482 rescue ActiveRecord::RecordNotFound
482 rescue ActiveRecord::RecordNotFound
483 render_404
483 render_404
484 end
484 end
485
485
486 def find_optional_project
486 def find_optional_project
487 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
487 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
488 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
488 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
489 allowed ? true : deny_access
489 allowed ? true : deny_access
490 rescue ActiveRecord::RecordNotFound
490 rescue ActiveRecord::RecordNotFound
491 render_404
491 render_404
492 end
492 end
493
493
494 # Retrieve query from session or build a new query
494 # Retrieve query from session or build a new query
495 def retrieve_query
495 def retrieve_query
496 if !params[:query_id].blank?
496 if !params[:query_id].blank?
497 cond = "project_id IS NULL"
497 cond = "project_id IS NULL"
498 cond << " OR project_id = #{@project.id}" if @project
498 cond << " OR project_id = #{@project.id}" if @project
499 @query = Query.find(params[:query_id], :conditions => cond)
499 @query = Query.find(params[:query_id], :conditions => cond)
500 @query.project = @project
500 @query.project = @project
501 session[:query] = {:id => @query.id, :project_id => @query.project_id}
501 session[:query] = {:id => @query.id, :project_id => @query.project_id}
502 sort_clear
502 sort_clear
503 else
503 else
504 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
504 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
505 # Give it a name, required to be valid
505 # Give it a name, required to be valid
506 @query = Query.new(:name => "_")
506 @query = Query.new(:name => "_")
507 @query.project = @project
507 @query.project = @project
508 if params[:fields] and params[:fields].is_a? Array
508 if params[:fields] and params[:fields].is_a? Array
509 params[:fields].each do |field|
509 params[:fields].each do |field|
510 @query.add_filter(field, params[:operators][field], params[:values][field])
510 @query.add_filter(field, params[:operators][field], params[:values][field])
511 end
511 end
512 else
512 else
513 @query.available_filters.keys.each do |field|
513 @query.available_filters.keys.each do |field|
514 @query.add_short_filter(field, params[field]) if params[field]
514 @query.add_short_filter(field, params[field]) if params[field]
515 end
515 end
516 end
516 end
517 @query.group_by = params[:group_by]
517 @query.group_by = params[:group_by]
518 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
518 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
519 else
519 else
520 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
520 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
521 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
521 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
522 @query.project = @project
522 @query.project = @project
523 end
523 end
524 end
524 end
525 end
525 end
526 end
526 end
@@ -1,477 +1,483
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, :include_options
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 self.include_options = options[:include]
30 end
31 end
31
32
32 def caption
33 def caption
33 l("field_#{name}")
34 l("field_#{name}")
34 end
35 end
35
36
36 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
37 def sortable?
38 def sortable?
38 !sortable.nil?
39 !sortable.nil?
39 end
40 end
40 end
41 end
41
42
42 class QueryCustomFieldColumn < QueryColumn
43 class QueryCustomFieldColumn < QueryColumn
43
44
44 def initialize(custom_field)
45 def initialize(custom_field)
45 self.name = "cf_#{custom_field.id}".to_sym
46 self.name = "cf_#{custom_field.id}".to_sym
46 self.sortable = custom_field.order_statement || false
47 self.sortable = custom_field.order_statement || false
47 if %w(list date bool int).include?(custom_field.field_format)
48 if %w(list date bool int).include?(custom_field.field_format)
48 self.groupable = custom_field.order_statement
49 self.groupable = custom_field.order_statement
49 end
50 end
50 self.groupable ||= false
51 self.groupable ||= false
52 self.include_options = :custom_values
51 @cf = custom_field
53 @cf = custom_field
52 end
54 end
53
55
54 def caption
56 def caption
55 @cf.name
57 @cf.name
56 end
58 end
57
59
58 def custom_field
60 def custom_field
59 @cf
61 @cf
60 end
62 end
61 end
63 end
62
64
63 class Query < ActiveRecord::Base
65 class Query < ActiveRecord::Base
64 belongs_to :project
66 belongs_to :project
65 belongs_to :user
67 belongs_to :user
66 serialize :filters
68 serialize :filters
67 serialize :column_names
69 serialize :column_names
68 serialize :sort_criteria, Array
70 serialize :sort_criteria, Array
69
71
70 attr_protected :project_id, :user_id
72 attr_protected :project_id, :user_id
71
73
72 validates_presence_of :name, :on => :save
74 validates_presence_of :name, :on => :save
73 validates_length_of :name, :maximum => 255
75 validates_length_of :name, :maximum => 255
74
76
75 @@operators = { "=" => :label_equals,
77 @@operators = { "=" => :label_equals,
76 "!" => :label_not_equals,
78 "!" => :label_not_equals,
77 "o" => :label_open_issues,
79 "o" => :label_open_issues,
78 "c" => :label_closed_issues,
80 "c" => :label_closed_issues,
79 "!*" => :label_none,
81 "!*" => :label_none,
80 "*" => :label_all,
82 "*" => :label_all,
81 ">=" => :label_greater_or_equal,
83 ">=" => :label_greater_or_equal,
82 "<=" => :label_less_or_equal,
84 "<=" => :label_less_or_equal,
83 "<t+" => :label_in_less_than,
85 "<t+" => :label_in_less_than,
84 ">t+" => :label_in_more_than,
86 ">t+" => :label_in_more_than,
85 "t+" => :label_in,
87 "t+" => :label_in,
86 "t" => :label_today,
88 "t" => :label_today,
87 "w" => :label_this_week,
89 "w" => :label_this_week,
88 ">t-" => :label_less_than_ago,
90 ">t-" => :label_less_than_ago,
89 "<t-" => :label_more_than_ago,
91 "<t-" => :label_more_than_ago,
90 "t-" => :label_ago,
92 "t-" => :label_ago,
91 "~" => :label_contains,
93 "~" => :label_contains,
92 "!~" => :label_not_contains }
94 "!~" => :label_not_contains }
93
95
94 cattr_reader :operators
96 cattr_reader :operators
95
97
96 @@operators_by_filter_type = { :list => [ "=", "!" ],
98 @@operators_by_filter_type = { :list => [ "=", "!" ],
97 :list_status => [ "o", "=", "!", "c", "*" ],
99 :list_status => [ "o", "=", "!", "c", "*" ],
98 :list_optional => [ "=", "!", "!*", "*" ],
100 :list_optional => [ "=", "!", "!*", "*" ],
99 :list_subprojects => [ "*", "!*", "=" ],
101 :list_subprojects => [ "*", "!*", "=" ],
100 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
102 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
101 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
103 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
102 :string => [ "=", "~", "!", "!~" ],
104 :string => [ "=", "~", "!", "!~" ],
103 :text => [ "~", "!~" ],
105 :text => [ "~", "!~" ],
104 :integer => [ "=", ">=", "<=", "!*", "*" ] }
106 :integer => [ "=", ">=", "<=", "!*", "*" ] }
105
107
106 cattr_reader :operators_by_filter_type
108 cattr_reader :operators_by_filter_type
107
109
108 @@available_columns = [
110 @@available_columns = [
109 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
111 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
110 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
112 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true, :include => :tracker),
111 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
113 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
112 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
114 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
113 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
115 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
114 QueryColumn.new(:author),
116 QueryColumn.new(:author),
115 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
117 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true, :include => :assigned_to),
116 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
118 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
117 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
119 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true, :include => :category),
118 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
120 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true, :include => :fixed_version),
119 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
121 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
120 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
122 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
121 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
123 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
122 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
124 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
123 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
125 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
124 ]
126 ]
125 cattr_reader :available_columns
127 cattr_reader :available_columns
126
128
127 def initialize(attributes = nil)
129 def initialize(attributes = nil)
128 super attributes
130 super attributes
129 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
131 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
130 end
132 end
131
133
132 def after_initialize
134 def after_initialize
133 # Store the fact that project is nil (used in #editable_by?)
135 # Store the fact that project is nil (used in #editable_by?)
134 @is_for_all = project.nil?
136 @is_for_all = project.nil?
135 end
137 end
136
138
137 def validate
139 def validate
138 filters.each_key do |field|
140 filters.each_key do |field|
139 errors.add label_for(field), :blank unless
141 errors.add label_for(field), :blank unless
140 # filter requires one or more values
142 # filter requires one or more values
141 (values_for(field) and !values_for(field).first.blank?) or
143 (values_for(field) and !values_for(field).first.blank?) or
142 # filter doesn't require any value
144 # filter doesn't require any value
143 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
145 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
144 end if filters
146 end if filters
145 end
147 end
146
148
147 def editable_by?(user)
149 def editable_by?(user)
148 return false unless user
150 return false unless user
149 # Admin can edit them all and regular users can edit their private queries
151 # Admin can edit them all and regular users can edit their private queries
150 return true if user.admin? || (!is_public && self.user_id == user.id)
152 return true if user.admin? || (!is_public && self.user_id == user.id)
151 # Members can not edit public queries that are for all project (only admin is allowed to)
153 # Members can not edit public queries that are for all project (only admin is allowed to)
152 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
154 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
153 end
155 end
154
156
155 def available_filters
157 def available_filters
156 return @available_filters if @available_filters
158 return @available_filters if @available_filters
157
159
158 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
160 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
159
161
160 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
162 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
161 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
163 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
162 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
164 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
163 "subject" => { :type => :text, :order => 8 },
165 "subject" => { :type => :text, :order => 8 },
164 "created_on" => { :type => :date_past, :order => 9 },
166 "created_on" => { :type => :date_past, :order => 9 },
165 "updated_on" => { :type => :date_past, :order => 10 },
167 "updated_on" => { :type => :date_past, :order => 10 },
166 "start_date" => { :type => :date, :order => 11 },
168 "start_date" => { :type => :date, :order => 11 },
167 "due_date" => { :type => :date, :order => 12 },
169 "due_date" => { :type => :date, :order => 12 },
168 "estimated_hours" => { :type => :integer, :order => 13 },
170 "estimated_hours" => { :type => :integer, :order => 13 },
169 "done_ratio" => { :type => :integer, :order => 14 }}
171 "done_ratio" => { :type => :integer, :order => 14 }}
170
172
171 user_values = []
173 user_values = []
172 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
174 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
173 if project
175 if project
174 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
176 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
175 else
177 else
176 # members of the user's projects
178 # members of the user's projects
177 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
179 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
178 end
180 end
179 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
181 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
180 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
182 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
181
183
182 if User.current.logged?
184 if User.current.logged?
183 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
185 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
184 end
186 end
185
187
186 if project
188 if project
187 # project specific filters
189 # project specific filters
188 unless @project.issue_categories.empty?
190 unless @project.issue_categories.empty?
189 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
191 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
190 end
192 end
191 unless @project.versions.empty?
193 unless @project.versions.empty?
192 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
194 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
193 end
195 end
194 unless @project.descendants.active.empty?
196 unless @project.descendants.active.empty?
195 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
197 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
196 end
198 end
197 add_custom_fields_filters(@project.all_issue_custom_fields)
199 add_custom_fields_filters(@project.all_issue_custom_fields)
198 else
200 else
199 # global filters for cross project issue list
201 # global filters for cross project issue list
200 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
202 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
201 end
203 end
202 @available_filters
204 @available_filters
203 end
205 end
204
206
205 def add_filter(field, operator, values)
207 def add_filter(field, operator, values)
206 # values must be an array
208 # values must be an array
207 return unless values and values.is_a? Array # and !values.first.empty?
209 return unless values and values.is_a? Array # and !values.first.empty?
208 # check if field is defined as an available filter
210 # check if field is defined as an available filter
209 if available_filters.has_key? field
211 if available_filters.has_key? field
210 filter_options = available_filters[field]
212 filter_options = available_filters[field]
211 # check if operator is allowed for that filter
213 # check if operator is allowed for that filter
212 #if @@operators_by_filter_type[filter_options[:type]].include? operator
214 #if @@operators_by_filter_type[filter_options[:type]].include? operator
213 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
215 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
214 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
216 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
215 #end
217 #end
216 filters[field] = {:operator => operator, :values => values }
218 filters[field] = {:operator => operator, :values => values }
217 end
219 end
218 end
220 end
219
221
220 def add_short_filter(field, expression)
222 def add_short_filter(field, expression)
221 return unless expression
223 return unless expression
222 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
224 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
223 add_filter field, (parms[0] || "="), [parms[1] || ""]
225 add_filter field, (parms[0] || "="), [parms[1] || ""]
224 end
226 end
225
227
226 def has_filter?(field)
228 def has_filter?(field)
227 filters and filters[field]
229 filters and filters[field]
228 end
230 end
229
231
230 def operator_for(field)
232 def operator_for(field)
231 has_filter?(field) ? filters[field][:operator] : nil
233 has_filter?(field) ? filters[field][:operator] : nil
232 end
234 end
233
235
234 def values_for(field)
236 def values_for(field)
235 has_filter?(field) ? filters[field][:values] : nil
237 has_filter?(field) ? filters[field][:values] : nil
236 end
238 end
237
239
238 def label_for(field)
240 def label_for(field)
239 label = available_filters[field][:name] if available_filters.has_key?(field)
241 label = available_filters[field][:name] if available_filters.has_key?(field)
240 label ||= field.gsub(/\_id$/, "")
242 label ||= field.gsub(/\_id$/, "")
241 end
243 end
242
244
243 def available_columns
245 def available_columns
244 return @available_columns if @available_columns
246 return @available_columns if @available_columns
245 @available_columns = Query.available_columns
247 @available_columns = Query.available_columns
246 @available_columns += (project ?
248 @available_columns += (project ?
247 project.all_issue_custom_fields :
249 project.all_issue_custom_fields :
248 IssueCustomField.find(:all)
250 IssueCustomField.find(:all)
249 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
251 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
250 end
252 end
251
253
252 # Returns an array of columns that can be used to group the results
254 # Returns an array of columns that can be used to group the results
253 def groupable_columns
255 def groupable_columns
254 available_columns.select {|c| c.groupable}
256 available_columns.select {|c| c.groupable}
255 end
257 end
256
258
257 def columns
259 def columns
258 if has_default_columns?
260 if has_default_columns?
259 available_columns.select do |c|
261 available_columns.select do |c|
260 # Adds the project column by default for cross-project lists
262 # Adds the project column by default for cross-project lists
261 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
263 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
262 end
264 end
263 else
265 else
264 # preserve the column_names order
266 # preserve the column_names order
265 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
267 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
266 end
268 end
267 end
269 end
268
270
269 def column_names=(names)
271 def column_names=(names)
270 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
272 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
271 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
273 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
272 write_attribute(:column_names, names)
274 write_attribute(:column_names, names)
273 end
275 end
274
276
275 def has_column?(column)
277 def has_column?(column)
276 column_names && column_names.include?(column.name)
278 column_names && column_names.include?(column.name)
277 end
279 end
278
280
279 def has_default_columns?
281 def has_default_columns?
280 column_names.nil? || column_names.empty?
282 column_names.nil? || column_names.empty?
281 end
283 end
282
284
283 def sort_criteria=(arg)
285 def sort_criteria=(arg)
284 c = []
286 c = []
285 if arg.is_a?(Hash)
287 if arg.is_a?(Hash)
286 arg = arg.keys.sort.collect {|k| arg[k]}
288 arg = arg.keys.sort.collect {|k| arg[k]}
287 end
289 end
288 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
290 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
289 write_attribute(:sort_criteria, c)
291 write_attribute(:sort_criteria, c)
290 end
292 end
291
293
292 def sort_criteria
294 def sort_criteria
293 read_attribute(:sort_criteria) || []
295 read_attribute(:sort_criteria) || []
294 end
296 end
295
297
296 def sort_criteria_key(arg)
298 def sort_criteria_key(arg)
297 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
299 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
298 end
300 end
299
301
300 def sort_criteria_order(arg)
302 def sort_criteria_order(arg)
301 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
303 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
302 end
304 end
303
305
304 # Returns the SQL sort order that should be prepended for grouping
306 # Returns the SQL sort order that should be prepended for grouping
305 def group_by_sort_order
307 def group_by_sort_order
306 if grouped? && (column = group_by_column)
308 if grouped? && (column = group_by_column)
307 column.sortable.is_a?(Array) ?
309 column.sortable.is_a?(Array) ?
308 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
310 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
309 "#{column.sortable} #{column.default_order}"
311 "#{column.sortable} #{column.default_order}"
310 end
312 end
311 end
313 end
312
314
313 # Returns true if the query is a grouped query
315 # Returns true if the query is a grouped query
314 def grouped?
316 def grouped?
315 !group_by.blank?
317 !group_by.blank?
316 end
318 end
317
319
318 def group_by_column
320 def group_by_column
319 groupable_columns.detect {|c| c.name.to_s == group_by}
321 groupable_columns.detect {|c| c.name.to_s == group_by}
320 end
322 end
321
323
322 def group_by_statement
324 def group_by_statement
323 group_by_column.groupable
325 group_by_column.groupable
324 end
326 end
325
327
328 def include_options
329 (columns << group_by_column).collect {|column| column && column.include_options}.flatten.compact.uniq
330 end
331
326 def project_statement
332 def project_statement
327 project_clauses = []
333 project_clauses = []
328 if project && !@project.descendants.active.empty?
334 if project && !@project.descendants.active.empty?
329 ids = [project.id]
335 ids = [project.id]
330 if has_filter?("subproject_id")
336 if has_filter?("subproject_id")
331 case operator_for("subproject_id")
337 case operator_for("subproject_id")
332 when '='
338 when '='
333 # include the selected subprojects
339 # include the selected subprojects
334 ids += values_for("subproject_id").each(&:to_i)
340 ids += values_for("subproject_id").each(&:to_i)
335 when '!*'
341 when '!*'
336 # main project only
342 # main project only
337 else
343 else
338 # all subprojects
344 # all subprojects
339 ids += project.descendants.collect(&:id)
345 ids += project.descendants.collect(&:id)
340 end
346 end
341 elsif Setting.display_subprojects_issues?
347 elsif Setting.display_subprojects_issues?
342 ids += project.descendants.collect(&:id)
348 ids += project.descendants.collect(&:id)
343 end
349 end
344 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
350 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
345 elsif project
351 elsif project
346 project_clauses << "#{Project.table_name}.id = %d" % project.id
352 project_clauses << "#{Project.table_name}.id = %d" % project.id
347 end
353 end
348 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
354 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
349 project_clauses.join(' AND ')
355 project_clauses.join(' AND ')
350 end
356 end
351
357
352 def statement
358 def statement
353 # filters clauses
359 # filters clauses
354 filters_clauses = []
360 filters_clauses = []
355 filters.each_key do |field|
361 filters.each_key do |field|
356 next if field == "subproject_id"
362 next if field == "subproject_id"
357 v = values_for(field).clone
363 v = values_for(field).clone
358 next unless v and !v.empty?
364 next unless v and !v.empty?
359 operator = operator_for(field)
365 operator = operator_for(field)
360
366
361 # "me" value subsitution
367 # "me" value subsitution
362 if %w(assigned_to_id author_id watcher_id).include?(field)
368 if %w(assigned_to_id author_id watcher_id).include?(field)
363 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
369 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
364 end
370 end
365
371
366 sql = ''
372 sql = ''
367 if field =~ /^cf_(\d+)$/
373 if field =~ /^cf_(\d+)$/
368 # custom field
374 # custom field
369 db_table = CustomValue.table_name
375 db_table = CustomValue.table_name
370 db_field = 'value'
376 db_field = 'value'
371 is_custom_filter = true
377 is_custom_filter = true
372 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 "
378 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 "
373 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
379 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
374 elsif field == 'watcher_id'
380 elsif field == 'watcher_id'
375 db_table = Watcher.table_name
381 db_table = Watcher.table_name
376 db_field = 'user_id'
382 db_field = 'user_id'
377 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
383 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
378 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
384 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
379 else
385 else
380 # regular field
386 # regular field
381 db_table = Issue.table_name
387 db_table = Issue.table_name
382 db_field = field
388 db_field = field
383 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
389 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
384 end
390 end
385 filters_clauses << sql
391 filters_clauses << sql
386
392
387 end if filters and valid?
393 end if filters and valid?
388
394
389 (filters_clauses << project_statement).join(' AND ')
395 (filters_clauses << project_statement).join(' AND ')
390 end
396 end
391
397
392 private
398 private
393
399
394 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
400 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
395 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
401 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
396 sql = ''
402 sql = ''
397 case operator
403 case operator
398 when "="
404 when "="
399 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
405 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
400 when "!"
406 when "!"
401 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
407 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
402 when "!*"
408 when "!*"
403 sql = "#{db_table}.#{db_field} IS NULL"
409 sql = "#{db_table}.#{db_field} IS NULL"
404 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
410 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
405 when "*"
411 when "*"
406 sql = "#{db_table}.#{db_field} IS NOT NULL"
412 sql = "#{db_table}.#{db_field} IS NOT NULL"
407 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
413 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
408 when ">="
414 when ">="
409 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
415 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
410 when "<="
416 when "<="
411 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
417 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
412 when "o"
418 when "o"
413 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
419 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
414 when "c"
420 when "c"
415 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
421 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
416 when ">t-"
422 when ">t-"
417 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
423 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
418 when "<t-"
424 when "<t-"
419 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
425 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
420 when "t-"
426 when "t-"
421 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
427 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
422 when ">t+"
428 when ">t+"
423 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
429 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
424 when "<t+"
430 when "<t+"
425 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
431 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
426 when "t+"
432 when "t+"
427 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
433 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
428 when "t"
434 when "t"
429 sql = date_range_clause(db_table, db_field, 0, 0)
435 sql = date_range_clause(db_table, db_field, 0, 0)
430 when "w"
436 when "w"
431 from = l(:general_first_day_of_week) == '7' ?
437 from = l(:general_first_day_of_week) == '7' ?
432 # week starts on sunday
438 # week starts on sunday
433 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
439 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
434 # week starts on monday (Rails default)
440 # week starts on monday (Rails default)
435 Time.now.at_beginning_of_week
441 Time.now.at_beginning_of_week
436 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
442 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
437 when "~"
443 when "~"
438 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
444 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
439 when "!~"
445 when "!~"
440 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
446 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
441 end
447 end
442
448
443 return sql
449 return sql
444 end
450 end
445
451
446 def add_custom_fields_filters(custom_fields)
452 def add_custom_fields_filters(custom_fields)
447 @available_filters ||= {}
453 @available_filters ||= {}
448
454
449 custom_fields.select(&:is_filter?).each do |field|
455 custom_fields.select(&:is_filter?).each do |field|
450 case field.field_format
456 case field.field_format
451 when "text"
457 when "text"
452 options = { :type => :text, :order => 20 }
458 options = { :type => :text, :order => 20 }
453 when "list"
459 when "list"
454 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
460 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
455 when "date"
461 when "date"
456 options = { :type => :date, :order => 20 }
462 options = { :type => :date, :order => 20 }
457 when "bool"
463 when "bool"
458 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
464 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
459 else
465 else
460 options = { :type => :string, :order => 20 }
466 options = { :type => :string, :order => 20 }
461 end
467 end
462 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
468 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
463 end
469 end
464 end
470 end
465
471
466 # Returns a SQL clause for a date or datetime field.
472 # Returns a SQL clause for a date or datetime field.
467 def date_range_clause(table, field, from, to)
473 def date_range_clause(table, field, from, to)
468 s = []
474 s = []
469 if from
475 if from
470 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
476 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
471 end
477 end
472 if to
478 if to
473 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
479 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
474 end
480 end
475 s.join(' AND ')
481 s.join(' AND ')
476 end
482 end
477 end
483 end
@@ -1,306 +1,315
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 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22
22
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 query = Query.new(:project => nil, :name => '_')
24 query = Query.new(:project => nil, :name => '_')
25 assert query.available_filters.has_key?('cf_1')
25 assert query.available_filters.has_key?('cf_1')
26 assert !query.available_filters.has_key?('cf_3')
26 assert !query.available_filters.has_key?('cf_3')
27 end
27 end
28
28
29 def find_issues_with_query(query)
29 def find_issues_with_query(query)
30 Issue.find :all,
30 Issue.find :all,
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
32 :conditions => query.statement
32 :conditions => query.statement
33 end
33 end
34
34
35 def test_query_with_multiple_custom_fields
35 def test_query_with_multiple_custom_fields
36 query = Query.find(1)
36 query = Query.find(1)
37 assert query.valid?
37 assert query.valid?
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
39 issues = find_issues_with_query(query)
39 issues = find_issues_with_query(query)
40 assert_equal 1, issues.length
40 assert_equal 1, issues.length
41 assert_equal Issue.find(3), issues.first
41 assert_equal Issue.find(3), issues.first
42 end
42 end
43
43
44 def test_operator_none
44 def test_operator_none
45 query = Query.new(:project => Project.find(1), :name => '_')
45 query = Query.new(:project => Project.find(1), :name => '_')
46 query.add_filter('fixed_version_id', '!*', [''])
46 query.add_filter('fixed_version_id', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
50 find_issues_with_query(query)
50 find_issues_with_query(query)
51 end
51 end
52
52
53 def test_operator_none_for_integer
53 def test_operator_none_for_integer
54 query = Query.new(:project => Project.find(1), :name => '_')
54 query = Query.new(:project => Project.find(1), :name => '_')
55 query.add_filter('estimated_hours', '!*', [''])
55 query.add_filter('estimated_hours', '!*', [''])
56 issues = find_issues_with_query(query)
56 issues = find_issues_with_query(query)
57 assert !issues.empty?
57 assert !issues.empty?
58 assert issues.all? {|i| !i.estimated_hours}
58 assert issues.all? {|i| !i.estimated_hours}
59 end
59 end
60
60
61 def test_operator_all
61 def test_operator_all
62 query = Query.new(:project => Project.find(1), :name => '_')
62 query = Query.new(:project => Project.find(1), :name => '_')
63 query.add_filter('fixed_version_id', '*', [''])
63 query.add_filter('fixed_version_id', '*', [''])
64 query.add_filter('cf_1', '*', [''])
64 query.add_filter('cf_1', '*', [''])
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
67 find_issues_with_query(query)
67 find_issues_with_query(query)
68 end
68 end
69
69
70 def test_operator_greater_than
70 def test_operator_greater_than
71 query = Query.new(:project => Project.find(1), :name => '_')
71 query = Query.new(:project => Project.find(1), :name => '_')
72 query.add_filter('done_ratio', '>=', ['40'])
72 query.add_filter('done_ratio', '>=', ['40'])
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
74 find_issues_with_query(query)
74 find_issues_with_query(query)
75 end
75 end
76
76
77 def test_operator_in_more_than
77 def test_operator_in_more_than
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
79 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('due_date', '>t+', ['15'])
80 query.add_filter('due_date', '>t+', ['15'])
81 issues = find_issues_with_query(query)
81 issues = find_issues_with_query(query)
82 assert !issues.empty?
82 assert !issues.empty?
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
84 end
84 end
85
85
86 def test_operator_in_less_than
86 def test_operator_in_less_than
87 query = Query.new(:project => Project.find(1), :name => '_')
87 query = Query.new(:project => Project.find(1), :name => '_')
88 query.add_filter('due_date', '<t+', ['15'])
88 query.add_filter('due_date', '<t+', ['15'])
89 issues = find_issues_with_query(query)
89 issues = find_issues_with_query(query)
90 assert !issues.empty?
90 assert !issues.empty?
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
92 end
92 end
93
93
94 def test_operator_less_than_ago
94 def test_operator_less_than_ago
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
96 query = Query.new(:project => Project.find(1), :name => '_')
96 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('due_date', '>t-', ['3'])
97 query.add_filter('due_date', '>t-', ['3'])
98 issues = find_issues_with_query(query)
98 issues = find_issues_with_query(query)
99 assert !issues.empty?
99 assert !issues.empty?
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
101 end
101 end
102
102
103 def test_operator_more_than_ago
103 def test_operator_more_than_ago
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
105 query = Query.new(:project => Project.find(1), :name => '_')
105 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('due_date', '<t-', ['10'])
106 query.add_filter('due_date', '<t-', ['10'])
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
108 issues = find_issues_with_query(query)
108 issues = find_issues_with_query(query)
109 assert !issues.empty?
109 assert !issues.empty?
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
111 end
111 end
112
112
113 def test_operator_in
113 def test_operator_in
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
115 query = Query.new(:project => Project.find(1), :name => '_')
115 query = Query.new(:project => Project.find(1), :name => '_')
116 query.add_filter('due_date', 't+', ['2'])
116 query.add_filter('due_date', 't+', ['2'])
117 issues = find_issues_with_query(query)
117 issues = find_issues_with_query(query)
118 assert !issues.empty?
118 assert !issues.empty?
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
120 end
120 end
121
121
122 def test_operator_ago
122 def test_operator_ago
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
124 query = Query.new(:project => Project.find(1), :name => '_')
124 query = Query.new(:project => Project.find(1), :name => '_')
125 query.add_filter('due_date', 't-', ['3'])
125 query.add_filter('due_date', 't-', ['3'])
126 issues = find_issues_with_query(query)
126 issues = find_issues_with_query(query)
127 assert !issues.empty?
127 assert !issues.empty?
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
129 end
129 end
130
130
131 def test_operator_today
131 def test_operator_today
132 query = Query.new(:project => Project.find(1), :name => '_')
132 query = Query.new(:project => Project.find(1), :name => '_')
133 query.add_filter('due_date', 't', [''])
133 query.add_filter('due_date', 't', [''])
134 issues = find_issues_with_query(query)
134 issues = find_issues_with_query(query)
135 assert !issues.empty?
135 assert !issues.empty?
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
137 end
137 end
138
138
139 def test_operator_this_week_on_date
139 def test_operator_this_week_on_date
140 query = Query.new(:project => Project.find(1), :name => '_')
140 query = Query.new(:project => Project.find(1), :name => '_')
141 query.add_filter('due_date', 'w', [''])
141 query.add_filter('due_date', 'w', [''])
142 find_issues_with_query(query)
142 find_issues_with_query(query)
143 end
143 end
144
144
145 def test_operator_this_week_on_datetime
145 def test_operator_this_week_on_datetime
146 query = Query.new(:project => Project.find(1), :name => '_')
146 query = Query.new(:project => Project.find(1), :name => '_')
147 query.add_filter('created_on', 'w', [''])
147 query.add_filter('created_on', 'w', [''])
148 find_issues_with_query(query)
148 find_issues_with_query(query)
149 end
149 end
150
150
151 def test_operator_contains
151 def test_operator_contains
152 query = Query.new(:project => Project.find(1), :name => '_')
152 query = Query.new(:project => Project.find(1), :name => '_')
153 query.add_filter('subject', '~', ['uNable'])
153 query.add_filter('subject', '~', ['uNable'])
154 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
154 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
155 result = find_issues_with_query(query)
155 result = find_issues_with_query(query)
156 assert result.empty?
156 assert result.empty?
157 result.each {|issue| assert issue.subject.downcase.include?('unable') }
157 result.each {|issue| assert issue.subject.downcase.include?('unable') }
158 end
158 end
159
159
160 def test_operator_does_not_contains
160 def test_operator_does_not_contains
161 query = Query.new(:project => Project.find(1), :name => '_')
161 query = Query.new(:project => Project.find(1), :name => '_')
162 query.add_filter('subject', '!~', ['uNable'])
162 query.add_filter('subject', '!~', ['uNable'])
163 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
163 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
164 find_issues_with_query(query)
164 find_issues_with_query(query)
165 end
165 end
166
166
167 def test_filter_watched_issues
167 def test_filter_watched_issues
168 User.current = User.find(1)
168 User.current = User.find(1)
169 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
169 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
170 result = find_issues_with_query(query)
170 result = find_issues_with_query(query)
171 assert_not_nil result
171 assert_not_nil result
172 assert !result.empty?
172 assert !result.empty?
173 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
173 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
174 User.current = nil
174 User.current = nil
175 end
175 end
176
176
177 def test_filter_unwatched_issues
177 def test_filter_unwatched_issues
178 User.current = User.find(1)
178 User.current = User.find(1)
179 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
179 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
180 result = find_issues_with_query(query)
180 result = find_issues_with_query(query)
181 assert_not_nil result
181 assert_not_nil result
182 assert !result.empty?
182 assert !result.empty?
183 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
183 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
184 User.current = nil
184 User.current = nil
185 end
185 end
186
186
187 def test_default_columns
187 def test_default_columns
188 q = Query.new
188 q = Query.new
189 assert !q.columns.empty?
189 assert !q.columns.empty?
190 end
190 end
191
191
192 def test_set_column_names
192 def test_set_column_names
193 q = Query.new
193 q = Query.new
194 q.column_names = ['tracker', :subject, '', 'unknonw_column']
194 q.column_names = ['tracker', :subject, '', 'unknonw_column']
195 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
195 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
196 c = q.columns.first
196 c = q.columns.first
197 assert q.has_column?(c)
197 assert q.has_column?(c)
198 end
198 end
199
199
200 def test_groupable_columns_should_include_custom_fields
200 def test_groupable_columns_should_include_custom_fields
201 q = Query.new
201 q = Query.new
202 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
202 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
203 end
203 end
204
204
205 def test_include_options
206 q = Query.new
207 q.column_names = %w(subject tracker)
208 assert_equal [:tracker], q.include_options
209
210 q.group_by = 'category'
211 assert_equal [:tracker, :category], q.include_options
212 end
213
205 def test_default_sort
214 def test_default_sort
206 q = Query.new
215 q = Query.new
207 assert_equal [], q.sort_criteria
216 assert_equal [], q.sort_criteria
208 end
217 end
209
218
210 def test_set_sort_criteria_with_hash
219 def test_set_sort_criteria_with_hash
211 q = Query.new
220 q = Query.new
212 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
221 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
213 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
222 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
214 end
223 end
215
224
216 def test_set_sort_criteria_with_array
225 def test_set_sort_criteria_with_array
217 q = Query.new
226 q = Query.new
218 q.sort_criteria = [['priority', 'desc'], 'tracker']
227 q.sort_criteria = [['priority', 'desc'], 'tracker']
219 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
228 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
220 end
229 end
221
230
222 def test_create_query_with_sort
231 def test_create_query_with_sort
223 q = Query.new(:name => 'Sorted')
232 q = Query.new(:name => 'Sorted')
224 q.sort_criteria = [['priority', 'desc'], 'tracker']
233 q.sort_criteria = [['priority', 'desc'], 'tracker']
225 assert q.save
234 assert q.save
226 q.reload
235 q.reload
227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
236 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
228 end
237 end
229
238
230 def test_sort_by_string_custom_field_asc
239 def test_sort_by_string_custom_field_asc
231 q = Query.new
240 q = Query.new
232 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
241 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
233 assert c
242 assert c
234 assert c.sortable
243 assert c.sortable
235 issues = Issue.find :all,
244 issues = Issue.find :all,
236 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
245 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
237 :conditions => q.statement,
246 :conditions => q.statement,
238 :order => "#{c.sortable} ASC"
247 :order => "#{c.sortable} ASC"
239 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
248 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
240 assert !values.empty?
249 assert !values.empty?
241 assert_equal values.sort, values
250 assert_equal values.sort, values
242 end
251 end
243
252
244 def test_sort_by_string_custom_field_desc
253 def test_sort_by_string_custom_field_desc
245 q = Query.new
254 q = Query.new
246 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
255 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
247 assert c
256 assert c
248 assert c.sortable
257 assert c.sortable
249 issues = Issue.find :all,
258 issues = Issue.find :all,
250 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
259 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
251 :conditions => q.statement,
260 :conditions => q.statement,
252 :order => "#{c.sortable} DESC"
261 :order => "#{c.sortable} DESC"
253 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
262 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
254 assert !values.empty?
263 assert !values.empty?
255 assert_equal values.sort.reverse, values
264 assert_equal values.sort.reverse, values
256 end
265 end
257
266
258 def test_sort_by_float_custom_field_asc
267 def test_sort_by_float_custom_field_asc
259 q = Query.new
268 q = Query.new
260 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
269 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
261 assert c
270 assert c
262 assert c.sortable
271 assert c.sortable
263 issues = Issue.find :all,
272 issues = Issue.find :all,
264 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
273 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
265 :conditions => q.statement,
274 :conditions => q.statement,
266 :order => "#{c.sortable} ASC"
275 :order => "#{c.sortable} ASC"
267 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
276 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
268 assert !values.empty?
277 assert !values.empty?
269 assert_equal values.sort, values
278 assert_equal values.sort, values
270 end
279 end
271
280
272 def test_label_for
281 def test_label_for
273 q = Query.new
282 q = Query.new
274 assert_equal 'assigned_to', q.label_for('assigned_to_id')
283 assert_equal 'assigned_to', q.label_for('assigned_to_id')
275 end
284 end
276
285
277 def test_editable_by
286 def test_editable_by
278 admin = User.find(1)
287 admin = User.find(1)
279 manager = User.find(2)
288 manager = User.find(2)
280 developer = User.find(3)
289 developer = User.find(3)
281
290
282 # Public query on project 1
291 # Public query on project 1
283 q = Query.find(1)
292 q = Query.find(1)
284 assert q.editable_by?(admin)
293 assert q.editable_by?(admin)
285 assert q.editable_by?(manager)
294 assert q.editable_by?(manager)
286 assert !q.editable_by?(developer)
295 assert !q.editable_by?(developer)
287
296
288 # Private query on project 1
297 # Private query on project 1
289 q = Query.find(2)
298 q = Query.find(2)
290 assert q.editable_by?(admin)
299 assert q.editable_by?(admin)
291 assert !q.editable_by?(manager)
300 assert !q.editable_by?(manager)
292 assert q.editable_by?(developer)
301 assert q.editable_by?(developer)
293
302
294 # Private query for all projects
303 # Private query for all projects
295 q = Query.find(3)
304 q = Query.find(3)
296 assert q.editable_by?(admin)
305 assert q.editable_by?(admin)
297 assert !q.editable_by?(manager)
306 assert !q.editable_by?(manager)
298 assert q.editable_by?(developer)
307 assert q.editable_by?(developer)
299
308
300 # Public query for all projects
309 # Public query for all projects
301 q = Query.find(4)
310 q = Query.find(4)
302 assert q.editable_by?(admin)
311 assert q.editable_by?(admin)
303 assert !q.editable_by?(manager)
312 assert !q.editable_by?(manager)
304 assert !q.editable_by?(developer)
313 assert !q.editable_by?(developer)
305 end
314 end
306 end
315 end
General Comments 0
You need to be logged in to leave comments. Login now