##// END OF EJS Templates
Reverts r3072 (#4302: error raised when sorting on an association not included as column)....
Jean-Philippe Lang -
r2983:4af8765f1582
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 => ([:status, :project, :priority] + @query.include_options),
67 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
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,483 +1,477
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, :include_options
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 self.include_options = options[:include]
31 end
30 end
32
31
33 def caption
32 def caption
34 l("field_#{name}")
33 l("field_#{name}")
35 end
34 end
36
35
37 # Returns true if the column is sortable, otherwise false
36 # Returns true if the column is sortable, otherwise false
38 def sortable?
37 def sortable?
39 !sortable.nil?
38 !sortable.nil?
40 end
39 end
41 end
40 end
42
41
43 class QueryCustomFieldColumn < QueryColumn
42 class QueryCustomFieldColumn < QueryColumn
44
43
45 def initialize(custom_field)
44 def initialize(custom_field)
46 self.name = "cf_#{custom_field.id}".to_sym
45 self.name = "cf_#{custom_field.id}".to_sym
47 self.sortable = custom_field.order_statement || false
46 self.sortable = custom_field.order_statement || false
48 if %w(list date bool int).include?(custom_field.field_format)
47 if %w(list date bool int).include?(custom_field.field_format)
49 self.groupable = custom_field.order_statement
48 self.groupable = custom_field.order_statement
50 end
49 end
51 self.groupable ||= false
50 self.groupable ||= false
52 self.include_options = :custom_values
53 @cf = custom_field
51 @cf = custom_field
54 end
52 end
55
53
56 def caption
54 def caption
57 @cf.name
55 @cf.name
58 end
56 end
59
57
60 def custom_field
58 def custom_field
61 @cf
59 @cf
62 end
60 end
63 end
61 end
64
62
65 class Query < ActiveRecord::Base
63 class Query < ActiveRecord::Base
66 belongs_to :project
64 belongs_to :project
67 belongs_to :user
65 belongs_to :user
68 serialize :filters
66 serialize :filters
69 serialize :column_names
67 serialize :column_names
70 serialize :sort_criteria, Array
68 serialize :sort_criteria, Array
71
69
72 attr_protected :project_id, :user_id
70 attr_protected :project_id, :user_id
73
71
74 validates_presence_of :name, :on => :save
72 validates_presence_of :name, :on => :save
75 validates_length_of :name, :maximum => 255
73 validates_length_of :name, :maximum => 255
76
74
77 @@operators = { "=" => :label_equals,
75 @@operators = { "=" => :label_equals,
78 "!" => :label_not_equals,
76 "!" => :label_not_equals,
79 "o" => :label_open_issues,
77 "o" => :label_open_issues,
80 "c" => :label_closed_issues,
78 "c" => :label_closed_issues,
81 "!*" => :label_none,
79 "!*" => :label_none,
82 "*" => :label_all,
80 "*" => :label_all,
83 ">=" => :label_greater_or_equal,
81 ">=" => :label_greater_or_equal,
84 "<=" => :label_less_or_equal,
82 "<=" => :label_less_or_equal,
85 "<t+" => :label_in_less_than,
83 "<t+" => :label_in_less_than,
86 ">t+" => :label_in_more_than,
84 ">t+" => :label_in_more_than,
87 "t+" => :label_in,
85 "t+" => :label_in,
88 "t" => :label_today,
86 "t" => :label_today,
89 "w" => :label_this_week,
87 "w" => :label_this_week,
90 ">t-" => :label_less_than_ago,
88 ">t-" => :label_less_than_ago,
91 "<t-" => :label_more_than_ago,
89 "<t-" => :label_more_than_ago,
92 "t-" => :label_ago,
90 "t-" => :label_ago,
93 "~" => :label_contains,
91 "~" => :label_contains,
94 "!~" => :label_not_contains }
92 "!~" => :label_not_contains }
95
93
96 cattr_reader :operators
94 cattr_reader :operators
97
95
98 @@operators_by_filter_type = { :list => [ "=", "!" ],
96 @@operators_by_filter_type = { :list => [ "=", "!" ],
99 :list_status => [ "o", "=", "!", "c", "*" ],
97 :list_status => [ "o", "=", "!", "c", "*" ],
100 :list_optional => [ "=", "!", "!*", "*" ],
98 :list_optional => [ "=", "!", "!*", "*" ],
101 :list_subprojects => [ "*", "!*", "=" ],
99 :list_subprojects => [ "*", "!*", "=" ],
102 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
100 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
103 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
101 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
104 :string => [ "=", "~", "!", "!~" ],
102 :string => [ "=", "~", "!", "!~" ],
105 :text => [ "~", "!~" ],
103 :text => [ "~", "!~" ],
106 :integer => [ "=", ">=", "<=", "!*", "*" ] }
104 :integer => [ "=", ">=", "<=", "!*", "*" ] }
107
105
108 cattr_reader :operators_by_filter_type
106 cattr_reader :operators_by_filter_type
109
107
110 @@available_columns = [
108 @@available_columns = [
111 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
109 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
112 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true, :include => :tracker),
110 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
113 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
111 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
114 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
112 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
115 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
113 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
116 QueryColumn.new(:author),
114 QueryColumn.new(:author),
117 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true, :include => :assigned_to),
115 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
118 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
116 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
119 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true, :include => :category),
117 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :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),
118 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
121 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
119 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
122 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
120 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
123 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
121 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
124 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
122 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
125 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
123 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
126 ]
124 ]
127 cattr_reader :available_columns
125 cattr_reader :available_columns
128
126
129 def initialize(attributes = nil)
127 def initialize(attributes = nil)
130 super attributes
128 super attributes
131 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
129 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
132 end
130 end
133
131
134 def after_initialize
132 def after_initialize
135 # Store the fact that project is nil (used in #editable_by?)
133 # Store the fact that project is nil (used in #editable_by?)
136 @is_for_all = project.nil?
134 @is_for_all = project.nil?
137 end
135 end
138
136
139 def validate
137 def validate
140 filters.each_key do |field|
138 filters.each_key do |field|
141 errors.add label_for(field), :blank unless
139 errors.add label_for(field), :blank unless
142 # filter requires one or more values
140 # filter requires one or more values
143 (values_for(field) and !values_for(field).first.blank?) or
141 (values_for(field) and !values_for(field).first.blank?) or
144 # filter doesn't require any value
142 # filter doesn't require any value
145 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
143 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
146 end if filters
144 end if filters
147 end
145 end
148
146
149 def editable_by?(user)
147 def editable_by?(user)
150 return false unless user
148 return false unless user
151 # Admin can edit them all and regular users can edit their private queries
149 # Admin can edit them all and regular users can edit their private queries
152 return true if user.admin? || (!is_public && self.user_id == user.id)
150 return true if user.admin? || (!is_public && self.user_id == user.id)
153 # Members can not edit public queries that are for all project (only admin is allowed to)
151 # Members can not edit public queries that are for all project (only admin is allowed to)
154 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
152 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
155 end
153 end
156
154
157 def available_filters
155 def available_filters
158 return @available_filters if @available_filters
156 return @available_filters if @available_filters
159
157
160 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
158 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
161
159
162 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
160 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').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] } },
161 "tracker_id" => { :type => :list, :order => 2, :values => trackers.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] } },
162 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
165 "subject" => { :type => :text, :order => 8 },
163 "subject" => { :type => :text, :order => 8 },
166 "created_on" => { :type => :date_past, :order => 9 },
164 "created_on" => { :type => :date_past, :order => 9 },
167 "updated_on" => { :type => :date_past, :order => 10 },
165 "updated_on" => { :type => :date_past, :order => 10 },
168 "start_date" => { :type => :date, :order => 11 },
166 "start_date" => { :type => :date, :order => 11 },
169 "due_date" => { :type => :date, :order => 12 },
167 "due_date" => { :type => :date, :order => 12 },
170 "estimated_hours" => { :type => :integer, :order => 13 },
168 "estimated_hours" => { :type => :integer, :order => 13 },
171 "done_ratio" => { :type => :integer, :order => 14 }}
169 "done_ratio" => { :type => :integer, :order => 14 }}
172
170
173 user_values = []
171 user_values = []
174 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
172 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
175 if project
173 if project
176 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
174 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
177 else
175 else
178 # members of the user's projects
176 # members of the user's projects
179 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
177 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
180 end
178 end
181 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
179 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
182 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
180 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
183
181
184 if User.current.logged?
182 if User.current.logged?
185 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
183 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
186 end
184 end
187
185
188 if project
186 if project
189 # project specific filters
187 # project specific filters
190 unless @project.issue_categories.empty?
188 unless @project.issue_categories.empty?
191 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
189 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
192 end
190 end
193 unless @project.versions.empty?
191 unless @project.versions.empty?
194 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
192 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
195 end
193 end
196 unless @project.descendants.active.empty?
194 unless @project.descendants.active.empty?
197 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
195 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
198 end
196 end
199 add_custom_fields_filters(@project.all_issue_custom_fields)
197 add_custom_fields_filters(@project.all_issue_custom_fields)
200 else
198 else
201 # global filters for cross project issue list
199 # global filters for cross project issue list
202 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
200 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
203 end
201 end
204 @available_filters
202 @available_filters
205 end
203 end
206
204
207 def add_filter(field, operator, values)
205 def add_filter(field, operator, values)
208 # values must be an array
206 # values must be an array
209 return unless values and values.is_a? Array # and !values.first.empty?
207 return unless values and values.is_a? Array # and !values.first.empty?
210 # check if field is defined as an available filter
208 # check if field is defined as an available filter
211 if available_filters.has_key? field
209 if available_filters.has_key? field
212 filter_options = available_filters[field]
210 filter_options = available_filters[field]
213 # check if operator is allowed for that filter
211 # check if operator is allowed for that filter
214 #if @@operators_by_filter_type[filter_options[:type]].include? operator
212 #if @@operators_by_filter_type[filter_options[:type]].include? operator
215 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
213 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
216 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
214 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
217 #end
215 #end
218 filters[field] = {:operator => operator, :values => values }
216 filters[field] = {:operator => operator, :values => values }
219 end
217 end
220 end
218 end
221
219
222 def add_short_filter(field, expression)
220 def add_short_filter(field, expression)
223 return unless expression
221 return unless expression
224 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
222 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
225 add_filter field, (parms[0] || "="), [parms[1] || ""]
223 add_filter field, (parms[0] || "="), [parms[1] || ""]
226 end
224 end
227
225
228 def has_filter?(field)
226 def has_filter?(field)
229 filters and filters[field]
227 filters and filters[field]
230 end
228 end
231
229
232 def operator_for(field)
230 def operator_for(field)
233 has_filter?(field) ? filters[field][:operator] : nil
231 has_filter?(field) ? filters[field][:operator] : nil
234 end
232 end
235
233
236 def values_for(field)
234 def values_for(field)
237 has_filter?(field) ? filters[field][:values] : nil
235 has_filter?(field) ? filters[field][:values] : nil
238 end
236 end
239
237
240 def label_for(field)
238 def label_for(field)
241 label = available_filters[field][:name] if available_filters.has_key?(field)
239 label = available_filters[field][:name] if available_filters.has_key?(field)
242 label ||= field.gsub(/\_id$/, "")
240 label ||= field.gsub(/\_id$/, "")
243 end
241 end
244
242
245 def available_columns
243 def available_columns
246 return @available_columns if @available_columns
244 return @available_columns if @available_columns
247 @available_columns = Query.available_columns
245 @available_columns = Query.available_columns
248 @available_columns += (project ?
246 @available_columns += (project ?
249 project.all_issue_custom_fields :
247 project.all_issue_custom_fields :
250 IssueCustomField.find(:all)
248 IssueCustomField.find(:all)
251 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
249 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
252 end
250 end
253
251
254 # Returns an array of columns that can be used to group the results
252 # Returns an array of columns that can be used to group the results
255 def groupable_columns
253 def groupable_columns
256 available_columns.select {|c| c.groupable}
254 available_columns.select {|c| c.groupable}
257 end
255 end
258
256
259 def columns
257 def columns
260 if has_default_columns?
258 if has_default_columns?
261 available_columns.select do |c|
259 available_columns.select do |c|
262 # Adds the project column by default for cross-project lists
260 # Adds the project column by default for cross-project lists
263 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
261 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
264 end
262 end
265 else
263 else
266 # preserve the column_names order
264 # preserve the column_names order
267 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
265 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
268 end
266 end
269 end
267 end
270
268
271 def column_names=(names)
269 def column_names=(names)
272 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
270 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
273 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
271 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
274 write_attribute(:column_names, names)
272 write_attribute(:column_names, names)
275 end
273 end
276
274
277 def has_column?(column)
275 def has_column?(column)
278 column_names && column_names.include?(column.name)
276 column_names && column_names.include?(column.name)
279 end
277 end
280
278
281 def has_default_columns?
279 def has_default_columns?
282 column_names.nil? || column_names.empty?
280 column_names.nil? || column_names.empty?
283 end
281 end
284
282
285 def sort_criteria=(arg)
283 def sort_criteria=(arg)
286 c = []
284 c = []
287 if arg.is_a?(Hash)
285 if arg.is_a?(Hash)
288 arg = arg.keys.sort.collect {|k| arg[k]}
286 arg = arg.keys.sort.collect {|k| arg[k]}
289 end
287 end
290 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
288 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
291 write_attribute(:sort_criteria, c)
289 write_attribute(:sort_criteria, c)
292 end
290 end
293
291
294 def sort_criteria
292 def sort_criteria
295 read_attribute(:sort_criteria) || []
293 read_attribute(:sort_criteria) || []
296 end
294 end
297
295
298 def sort_criteria_key(arg)
296 def sort_criteria_key(arg)
299 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
297 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
300 end
298 end
301
299
302 def sort_criteria_order(arg)
300 def sort_criteria_order(arg)
303 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
301 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
304 end
302 end
305
303
306 # Returns the SQL sort order that should be prepended for grouping
304 # Returns the SQL sort order that should be prepended for grouping
307 def group_by_sort_order
305 def group_by_sort_order
308 if grouped? && (column = group_by_column)
306 if grouped? && (column = group_by_column)
309 column.sortable.is_a?(Array) ?
307 column.sortable.is_a?(Array) ?
310 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
308 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
311 "#{column.sortable} #{column.default_order}"
309 "#{column.sortable} #{column.default_order}"
312 end
310 end
313 end
311 end
314
312
315 # Returns true if the query is a grouped query
313 # Returns true if the query is a grouped query
316 def grouped?
314 def grouped?
317 !group_by.blank?
315 !group_by.blank?
318 end
316 end
319
317
320 def group_by_column
318 def group_by_column
321 groupable_columns.detect {|c| c.name.to_s == group_by}
319 groupable_columns.detect {|c| c.name.to_s == group_by}
322 end
320 end
323
321
324 def group_by_statement
322 def group_by_statement
325 group_by_column.groupable
323 group_by_column.groupable
326 end
324 end
327
328 def include_options
329 (columns << group_by_column).collect {|column| column && column.include_options}.flatten.compact.uniq
330 end
331
325
332 def project_statement
326 def project_statement
333 project_clauses = []
327 project_clauses = []
334 if project && !@project.descendants.active.empty?
328 if project && !@project.descendants.active.empty?
335 ids = [project.id]
329 ids = [project.id]
336 if has_filter?("subproject_id")
330 if has_filter?("subproject_id")
337 case operator_for("subproject_id")
331 case operator_for("subproject_id")
338 when '='
332 when '='
339 # include the selected subprojects
333 # include the selected subprojects
340 ids += values_for("subproject_id").each(&:to_i)
334 ids += values_for("subproject_id").each(&:to_i)
341 when '!*'
335 when '!*'
342 # main project only
336 # main project only
343 else
337 else
344 # all subprojects
338 # all subprojects
345 ids += project.descendants.collect(&:id)
339 ids += project.descendants.collect(&:id)
346 end
340 end
347 elsif Setting.display_subprojects_issues?
341 elsif Setting.display_subprojects_issues?
348 ids += project.descendants.collect(&:id)
342 ids += project.descendants.collect(&:id)
349 end
343 end
350 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
344 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
351 elsif project
345 elsif project
352 project_clauses << "#{Project.table_name}.id = %d" % project.id
346 project_clauses << "#{Project.table_name}.id = %d" % project.id
353 end
347 end
354 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
348 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
355 project_clauses.join(' AND ')
349 project_clauses.join(' AND ')
356 end
350 end
357
351
358 def statement
352 def statement
359 # filters clauses
353 # filters clauses
360 filters_clauses = []
354 filters_clauses = []
361 filters.each_key do |field|
355 filters.each_key do |field|
362 next if field == "subproject_id"
356 next if field == "subproject_id"
363 v = values_for(field).clone
357 v = values_for(field).clone
364 next unless v and !v.empty?
358 next unless v and !v.empty?
365 operator = operator_for(field)
359 operator = operator_for(field)
366
360
367 # "me" value subsitution
361 # "me" value subsitution
368 if %w(assigned_to_id author_id watcher_id).include?(field)
362 if %w(assigned_to_id author_id watcher_id).include?(field)
369 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
363 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
370 end
364 end
371
365
372 sql = ''
366 sql = ''
373 if field =~ /^cf_(\d+)$/
367 if field =~ /^cf_(\d+)$/
374 # custom field
368 # custom field
375 db_table = CustomValue.table_name
369 db_table = CustomValue.table_name
376 db_field = 'value'
370 db_field = 'value'
377 is_custom_filter = true
371 is_custom_filter = true
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 "
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 "
379 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
373 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
380 elsif field == 'watcher_id'
374 elsif field == 'watcher_id'
381 db_table = Watcher.table_name
375 db_table = Watcher.table_name
382 db_field = 'user_id'
376 db_field = 'user_id'
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 "
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 "
384 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
378 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
385 else
379 else
386 # regular field
380 # regular field
387 db_table = Issue.table_name
381 db_table = Issue.table_name
388 db_field = field
382 db_field = field
389 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
383 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
390 end
384 end
391 filters_clauses << sql
385 filters_clauses << sql
392
386
393 end if filters and valid?
387 end if filters and valid?
394
388
395 (filters_clauses << project_statement).join(' AND ')
389 (filters_clauses << project_statement).join(' AND ')
396 end
390 end
397
391
398 private
392 private
399
393
400 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
394 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
401 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
395 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
402 sql = ''
396 sql = ''
403 case operator
397 case operator
404 when "="
398 when "="
405 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
399 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
406 when "!"
400 when "!"
407 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
401 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
408 when "!*"
402 when "!*"
409 sql = "#{db_table}.#{db_field} IS NULL"
403 sql = "#{db_table}.#{db_field} IS NULL"
410 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
404 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
411 when "*"
405 when "*"
412 sql = "#{db_table}.#{db_field} IS NOT NULL"
406 sql = "#{db_table}.#{db_field} IS NOT NULL"
413 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
407 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
414 when ">="
408 when ">="
415 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
409 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
416 when "<="
410 when "<="
417 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
411 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
418 when "o"
412 when "o"
419 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
413 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
420 when "c"
414 when "c"
421 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
415 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
422 when ">t-"
416 when ">t-"
423 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
417 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
424 when "<t-"
418 when "<t-"
425 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
419 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
426 when "t-"
420 when "t-"
427 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
421 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
428 when ">t+"
422 when ">t+"
429 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
423 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
430 when "<t+"
424 when "<t+"
431 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
425 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
432 when "t+"
426 when "t+"
433 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)
434 when "t"
428 when "t"
435 sql = date_range_clause(db_table, db_field, 0, 0)
429 sql = date_range_clause(db_table, db_field, 0, 0)
436 when "w"
430 when "w"
437 from = l(:general_first_day_of_week) == '7' ?
431 from = l(:general_first_day_of_week) == '7' ?
438 # week starts on sunday
432 # week starts on sunday
439 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
433 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
440 # week starts on monday (Rails default)
434 # week starts on monday (Rails default)
441 Time.now.at_beginning_of_week
435 Time.now.at_beginning_of_week
442 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
436 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
443 when "~"
437 when "~"
444 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
438 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
445 when "!~"
439 when "!~"
446 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
440 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
447 end
441 end
448
442
449 return sql
443 return sql
450 end
444 end
451
445
452 def add_custom_fields_filters(custom_fields)
446 def add_custom_fields_filters(custom_fields)
453 @available_filters ||= {}
447 @available_filters ||= {}
454
448
455 custom_fields.select(&:is_filter?).each do |field|
449 custom_fields.select(&:is_filter?).each do |field|
456 case field.field_format
450 case field.field_format
457 when "text"
451 when "text"
458 options = { :type => :text, :order => 20 }
452 options = { :type => :text, :order => 20 }
459 when "list"
453 when "list"
460 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
454 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
461 when "date"
455 when "date"
462 options = { :type => :date, :order => 20 }
456 options = { :type => :date, :order => 20 }
463 when "bool"
457 when "bool"
464 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
458 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
465 else
459 else
466 options = { :type => :string, :order => 20 }
460 options = { :type => :string, :order => 20 }
467 end
461 end
468 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
462 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
469 end
463 end
470 end
464 end
471
465
472 # Returns a SQL clause for a date or datetime field.
466 # Returns a SQL clause for a date or datetime field.
473 def date_range_clause(table, field, from, to)
467 def date_range_clause(table, field, from, to)
474 s = []
468 s = []
475 if from
469 if from
476 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
470 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
477 end
471 end
478 if to
472 if to
479 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
473 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
480 end
474 end
481 s.join(' AND ')
475 s.join(' AND ')
482 end
476 end
483 end
477 end
@@ -1,315 +1,306
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
214 def test_default_sort
205 def test_default_sort
215 q = Query.new
206 q = Query.new
216 assert_equal [], q.sort_criteria
207 assert_equal [], q.sort_criteria
217 end
208 end
218
209
219 def test_set_sort_criteria_with_hash
210 def test_set_sort_criteria_with_hash
220 q = Query.new
211 q = Query.new
221 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
212 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
222 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
213 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
223 end
214 end
224
215
225 def test_set_sort_criteria_with_array
216 def test_set_sort_criteria_with_array
226 q = Query.new
217 q = Query.new
227 q.sort_criteria = [['priority', 'desc'], 'tracker']
218 q.sort_criteria = [['priority', 'desc'], 'tracker']
228 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
219 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
229 end
220 end
230
221
231 def test_create_query_with_sort
222 def test_create_query_with_sort
232 q = Query.new(:name => 'Sorted')
223 q = Query.new(:name => 'Sorted')
233 q.sort_criteria = [['priority', 'desc'], 'tracker']
224 q.sort_criteria = [['priority', 'desc'], 'tracker']
234 assert q.save
225 assert q.save
235 q.reload
226 q.reload
236 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
237 end
228 end
238
229
239 def test_sort_by_string_custom_field_asc
230 def test_sort_by_string_custom_field_asc
240 q = Query.new
231 q = Query.new
241 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
232 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
242 assert c
233 assert c
243 assert c.sortable
234 assert c.sortable
244 issues = Issue.find :all,
235 issues = Issue.find :all,
245 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
236 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
246 :conditions => q.statement,
237 :conditions => q.statement,
247 :order => "#{c.sortable} ASC"
238 :order => "#{c.sortable} ASC"
248 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
239 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
249 assert !values.empty?
240 assert !values.empty?
250 assert_equal values.sort, values
241 assert_equal values.sort, values
251 end
242 end
252
243
253 def test_sort_by_string_custom_field_desc
244 def test_sort_by_string_custom_field_desc
254 q = Query.new
245 q = Query.new
255 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
246 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
256 assert c
247 assert c
257 assert c.sortable
248 assert c.sortable
258 issues = Issue.find :all,
249 issues = Issue.find :all,
259 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
250 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
260 :conditions => q.statement,
251 :conditions => q.statement,
261 :order => "#{c.sortable} DESC"
252 :order => "#{c.sortable} DESC"
262 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
253 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
263 assert !values.empty?
254 assert !values.empty?
264 assert_equal values.sort.reverse, values
255 assert_equal values.sort.reverse, values
265 end
256 end
266
257
267 def test_sort_by_float_custom_field_asc
258 def test_sort_by_float_custom_field_asc
268 q = Query.new
259 q = Query.new
269 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
260 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
270 assert c
261 assert c
271 assert c.sortable
262 assert c.sortable
272 issues = Issue.find :all,
263 issues = Issue.find :all,
273 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
264 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
274 :conditions => q.statement,
265 :conditions => q.statement,
275 :order => "#{c.sortable} ASC"
266 :order => "#{c.sortable} ASC"
276 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
267 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
277 assert !values.empty?
268 assert !values.empty?
278 assert_equal values.sort, values
269 assert_equal values.sort, values
279 end
270 end
280
271
281 def test_label_for
272 def test_label_for
282 q = Query.new
273 q = Query.new
283 assert_equal 'assigned_to', q.label_for('assigned_to_id')
274 assert_equal 'assigned_to', q.label_for('assigned_to_id')
284 end
275 end
285
276
286 def test_editable_by
277 def test_editable_by
287 admin = User.find(1)
278 admin = User.find(1)
288 manager = User.find(2)
279 manager = User.find(2)
289 developer = User.find(3)
280 developer = User.find(3)
290
281
291 # Public query on project 1
282 # Public query on project 1
292 q = Query.find(1)
283 q = Query.find(1)
293 assert q.editable_by?(admin)
284 assert q.editable_by?(admin)
294 assert q.editable_by?(manager)
285 assert q.editable_by?(manager)
295 assert !q.editable_by?(developer)
286 assert !q.editable_by?(developer)
296
287
297 # Private query on project 1
288 # Private query on project 1
298 q = Query.find(2)
289 q = Query.find(2)
299 assert q.editable_by?(admin)
290 assert q.editable_by?(admin)
300 assert !q.editable_by?(manager)
291 assert !q.editable_by?(manager)
301 assert q.editable_by?(developer)
292 assert q.editable_by?(developer)
302
293
303 # Private query for all projects
294 # Private query for all projects
304 q = Query.find(3)
295 q = Query.find(3)
305 assert q.editable_by?(admin)
296 assert q.editable_by?(admin)
306 assert !q.editable_by?(manager)
297 assert !q.editable_by?(manager)
307 assert q.editable_by?(developer)
298 assert q.editable_by?(developer)
308
299
309 # Public query for all projects
300 # Public query for all projects
310 q = Query.find(4)
301 q = Query.find(4)
311 assert q.editable_by?(admin)
302 assert q.editable_by?(admin)
312 assert !q.editable_by?(manager)
303 assert !q.editable_by?(manager)
313 assert !q.editable_by?(developer)
304 assert !q.editable_by?(developer)
314 end
305 end
315 end
306 end
General Comments 0
You need to be logged in to leave comments. Login now