##// END OF EJS Templates
Allow issue grouping by custom field (#2679)....
Jean-Philippe Lang -
r2957:71bc44a89dce
parent child
Show More
@@ -1,526 +1,526
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
28
28
29 helper :journals
29 helper :journals
30 helper :projects
30 helper :projects
31 include ProjectsHelper
31 include ProjectsHelper
32 helper :custom_fields
32 helper :custom_fields
33 include CustomFieldsHelper
33 include CustomFieldsHelper
34 helper :issue_relations
34 helper :issue_relations
35 include IssueRelationsHelper
35 include IssueRelationsHelper
36 helper :watchers
36 helper :watchers
37 include WatchersHelper
37 include WatchersHelper
38 helper :attachments
38 helper :attachments
39 include AttachmentsHelper
39 include AttachmentsHelper
40 helper :queries
40 helper :queries
41 helper :sort
41 helper :sort
42 include SortHelper
42 include SortHelper
43 include IssuesHelper
43 include IssuesHelper
44 helper :timelog
44 helper :timelog
45 include Redmine::Export::PDF
45 include Redmine::Export::PDF
46
46
47 verify :method => :post,
47 verify :method => :post,
48 :only => :destroy,
48 :only => :destroy,
49 :render => { :nothing => true, :status => :method_not_allowed }
49 :render => { :nothing => true, :status => :method_not_allowed }
50
50
51 def index
51 def index
52 retrieve_query
52 retrieve_query
53 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
53 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
54 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
54 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
55
55
56 if @query.valid?
56 if @query.valid?
57 limit = per_page_option
57 limit = per_page_option
58 respond_to do |format|
58 respond_to do |format|
59 format.html { }
59 format.html { }
60 format.atom { limit = Setting.feeds_limit.to_i }
60 format.atom { limit = Setting.feeds_limit.to_i }
61 format.csv { limit = Setting.issues_export_limit.to_i }
61 format.csv { limit = Setting.issues_export_limit.to_i }
62 format.pdf { limit = Setting.issues_export_limit.to_i }
62 format.pdf { limit = Setting.issues_export_limit.to_i }
63 end
63 end
64 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
64 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
65 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
65 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
66 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
66 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
67 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
67 :include => [ :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, :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,61 +1,70
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 module QueriesHelper
18 module QueriesHelper
19
19
20 def operators_for_select(filter_type)
20 def operators_for_select(filter_type)
21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
22 end
22 end
23
23
24 def column_header(column)
24 def column_header(column)
25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
26 :default_order => column.default_order) :
26 :default_order => column.default_order) :
27 content_tag('th', column.caption)
27 content_tag('th', column.caption)
28 end
28 end
29
29
30 def column_value(column, issue)
31 if column.is_a?(QueryCustomFieldColumn)
32 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
33 show_value(cv)
34 else
35 value = issue.send(column.name)
36 end
37 end
38
30 def column_content(column, issue)
39 def column_content(column, issue)
31 if column.is_a?(QueryCustomFieldColumn)
40 if column.is_a?(QueryCustomFieldColumn)
32 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
41 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
33 show_value(cv)
42 show_value(cv)
34 else
43 else
35 value = issue.send(column.name)
44 value = issue.send(column.name)
36 if value.is_a?(Date)
45 if value.is_a?(Date)
37 format_date(value)
46 format_date(value)
38 elsif value.is_a?(Time)
47 elsif value.is_a?(Time)
39 format_time(value)
48 format_time(value)
40 else
49 else
41 case column.name
50 case column.name
42 when :subject
51 when :subject
43 h((!@project.nil? && @project != issue.project) ? "#{issue.project.name} - " : '') +
52 h((!@project.nil? && @project != issue.project) ? "#{issue.project.name} - " : '') +
44 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
53 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
45 when :project
54 when :project
46 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
55 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
47 when :assigned_to
56 when :assigned_to
48 link_to_user value
57 link_to_user value
49 when :author
58 when :author
50 link_to_user value
59 link_to_user value
51 when :done_ratio
60 when :done_ratio
52 progress_bar(value, :width => '80px')
61 progress_bar(value, :width => '80px')
53 when :fixed_version
62 when :fixed_version
54 link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
63 link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
55 else
64 else
56 h(value)
65 h(value)
57 end
66 end
58 end
67 end
59 end
68 end
60 end
69 end
61 end
70 end
@@ -1,466 +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
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
27 self.groupable = name.to_s
28 end
26 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
27 end
30 end
28
31
29 def caption
32 def caption
30 l("field_#{name}")
33 l("field_#{name}")
31 end
34 end
32
35
33 # Returns true if the column is sortable, otherwise false
36 # Returns true if the column is sortable, otherwise false
34 def sortable?
37 def sortable?
35 !sortable.nil?
38 !sortable.nil?
36 end
39 end
37 end
40 end
38
41
39 class QueryCustomFieldColumn < QueryColumn
42 class QueryCustomFieldColumn < QueryColumn
40
43
41 def initialize(custom_field)
44 def initialize(custom_field)
42 self.name = "cf_#{custom_field.id}".to_sym
45 self.name = "cf_#{custom_field.id}".to_sym
43 self.sortable = custom_field.order_statement || false
46 self.sortable = custom_field.order_statement || false
47 if %w(list date bool int).include?(custom_field.field_format)
48 self.groupable = custom_field.order_statement
49 end
50 self.groupable ||= false
44 @cf = custom_field
51 @cf = custom_field
45 end
52 end
46
53
47 def caption
54 def caption
48 @cf.name
55 @cf.name
49 end
56 end
50
57
51 def custom_field
58 def custom_field
52 @cf
59 @cf
53 end
60 end
54 end
61 end
55
62
56 class Query < ActiveRecord::Base
63 class Query < ActiveRecord::Base
57 belongs_to :project
64 belongs_to :project
58 belongs_to :user
65 belongs_to :user
59 serialize :filters
66 serialize :filters
60 serialize :column_names
67 serialize :column_names
61 serialize :sort_criteria, Array
68 serialize :sort_criteria, Array
62
69
63 attr_protected :project_id, :user_id
70 attr_protected :project_id, :user_id
64
71
65 validates_presence_of :name, :on => :save
72 validates_presence_of :name, :on => :save
66 validates_length_of :name, :maximum => 255
73 validates_length_of :name, :maximum => 255
67
74
68 @@operators = { "=" => :label_equals,
75 @@operators = { "=" => :label_equals,
69 "!" => :label_not_equals,
76 "!" => :label_not_equals,
70 "o" => :label_open_issues,
77 "o" => :label_open_issues,
71 "c" => :label_closed_issues,
78 "c" => :label_closed_issues,
72 "!*" => :label_none,
79 "!*" => :label_none,
73 "*" => :label_all,
80 "*" => :label_all,
74 ">=" => :label_greater_or_equal,
81 ">=" => :label_greater_or_equal,
75 "<=" => :label_less_or_equal,
82 "<=" => :label_less_or_equal,
76 "<t+" => :label_in_less_than,
83 "<t+" => :label_in_less_than,
77 ">t+" => :label_in_more_than,
84 ">t+" => :label_in_more_than,
78 "t+" => :label_in,
85 "t+" => :label_in,
79 "t" => :label_today,
86 "t" => :label_today,
80 "w" => :label_this_week,
87 "w" => :label_this_week,
81 ">t-" => :label_less_than_ago,
88 ">t-" => :label_less_than_ago,
82 "<t-" => :label_more_than_ago,
89 "<t-" => :label_more_than_ago,
83 "t-" => :label_ago,
90 "t-" => :label_ago,
84 "~" => :label_contains,
91 "~" => :label_contains,
85 "!~" => :label_not_contains }
92 "!~" => :label_not_contains }
86
93
87 cattr_reader :operators
94 cattr_reader :operators
88
95
89 @@operators_by_filter_type = { :list => [ "=", "!" ],
96 @@operators_by_filter_type = { :list => [ "=", "!" ],
90 :list_status => [ "o", "=", "!", "c", "*" ],
97 :list_status => [ "o", "=", "!", "c", "*" ],
91 :list_optional => [ "=", "!", "!*", "*" ],
98 :list_optional => [ "=", "!", "!*", "*" ],
92 :list_subprojects => [ "*", "!*", "=" ],
99 :list_subprojects => [ "*", "!*", "=" ],
93 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
100 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
94 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
101 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
95 :string => [ "=", "~", "!", "!~" ],
102 :string => [ "=", "~", "!", "!~" ],
96 :text => [ "~", "!~" ],
103 :text => [ "~", "!~" ],
97 :integer => [ "=", ">=", "<=", "!*", "*" ] }
104 :integer => [ "=", ">=", "<=", "!*", "*" ] }
98
105
99 cattr_reader :operators_by_filter_type
106 cattr_reader :operators_by_filter_type
100
107
101 @@available_columns = [
108 @@available_columns = [
102 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
109 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
103 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
110 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
104 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
111 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
105 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),
106 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
113 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
107 QueryColumn.new(:author),
114 QueryColumn.new(:author),
108 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
115 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
109 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'),
110 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
117 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
111 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
118 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
112 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
119 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
113 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
120 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
114 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
121 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
115 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),
116 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'),
117 ]
124 ]
118 cattr_reader :available_columns
125 cattr_reader :available_columns
119
126
120 def initialize(attributes = nil)
127 def initialize(attributes = nil)
121 super attributes
128 super attributes
122 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
129 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
123 end
130 end
124
131
125 def after_initialize
132 def after_initialize
126 # Store the fact that project is nil (used in #editable_by?)
133 # Store the fact that project is nil (used in #editable_by?)
127 @is_for_all = project.nil?
134 @is_for_all = project.nil?
128 end
135 end
129
136
130 def validate
137 def validate
131 filters.each_key do |field|
138 filters.each_key do |field|
132 errors.add label_for(field), :blank unless
139 errors.add label_for(field), :blank unless
133 # filter requires one or more values
140 # filter requires one or more values
134 (values_for(field) and !values_for(field).first.blank?) or
141 (values_for(field) and !values_for(field).first.blank?) or
135 # filter doesn't require any value
142 # filter doesn't require any value
136 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
143 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
137 end if filters
144 end if filters
138 end
145 end
139
146
140 def editable_by?(user)
147 def editable_by?(user)
141 return false unless user
148 return false unless user
142 # 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
143 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)
144 # 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)
145 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)
146 end
153 end
147
154
148 def available_filters
155 def available_filters
149 return @available_filters if @available_filters
156 return @available_filters if @available_filters
150
157
151 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
152
159
153 @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] } },
154 "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] } },
155 "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] } },
156 "subject" => { :type => :text, :order => 8 },
163 "subject" => { :type => :text, :order => 8 },
157 "created_on" => { :type => :date_past, :order => 9 },
164 "created_on" => { :type => :date_past, :order => 9 },
158 "updated_on" => { :type => :date_past, :order => 10 },
165 "updated_on" => { :type => :date_past, :order => 10 },
159 "start_date" => { :type => :date, :order => 11 },
166 "start_date" => { :type => :date, :order => 11 },
160 "due_date" => { :type => :date, :order => 12 },
167 "due_date" => { :type => :date, :order => 12 },
161 "estimated_hours" => { :type => :integer, :order => 13 },
168 "estimated_hours" => { :type => :integer, :order => 13 },
162 "done_ratio" => { :type => :integer, :order => 14 }}
169 "done_ratio" => { :type => :integer, :order => 14 }}
163
170
164 user_values = []
171 user_values = []
165 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
172 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
166 if project
173 if project
167 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] }
168 else
175 else
169 # members of the user's projects
176 # members of the user's projects
170 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] }
171 end
178 end
172 @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?
173 @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?
174
181
175 if User.current.logged?
182 if User.current.logged?
176 @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"]] }
177 end
184 end
178
185
179 if project
186 if project
180 # project specific filters
187 # project specific filters
181 unless @project.issue_categories.empty?
188 unless @project.issue_categories.empty?
182 @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] } }
183 end
190 end
184 unless @project.versions.empty?
191 unless @project.versions.empty?
185 @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] } }
186 end
193 end
187 unless @project.descendants.active.empty?
194 unless @project.descendants.active.empty?
188 @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] } }
189 end
196 end
190 add_custom_fields_filters(@project.all_issue_custom_fields)
197 add_custom_fields_filters(@project.all_issue_custom_fields)
191 else
198 else
192 # global filters for cross project issue list
199 # global filters for cross project issue list
193 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}))
194 end
201 end
195 @available_filters
202 @available_filters
196 end
203 end
197
204
198 def add_filter(field, operator, values)
205 def add_filter(field, operator, values)
199 # values must be an array
206 # values must be an array
200 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?
201 # check if field is defined as an available filter
208 # check if field is defined as an available filter
202 if available_filters.has_key? field
209 if available_filters.has_key? field
203 filter_options = available_filters[field]
210 filter_options = available_filters[field]
204 # check if operator is allowed for that filter
211 # check if operator is allowed for that filter
205 #if @@operators_by_filter_type[filter_options[:type]].include? operator
212 #if @@operators_by_filter_type[filter_options[:type]].include? operator
206 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
213 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
207 # 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
208 #end
215 #end
209 filters[field] = {:operator => operator, :values => values }
216 filters[field] = {:operator => operator, :values => values }
210 end
217 end
211 end
218 end
212
219
213 def add_short_filter(field, expression)
220 def add_short_filter(field, expression)
214 return unless expression
221 return unless expression
215 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
222 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
216 add_filter field, (parms[0] || "="), [parms[1] || ""]
223 add_filter field, (parms[0] || "="), [parms[1] || ""]
217 end
224 end
218
225
219 def has_filter?(field)
226 def has_filter?(field)
220 filters and filters[field]
227 filters and filters[field]
221 end
228 end
222
229
223 def operator_for(field)
230 def operator_for(field)
224 has_filter?(field) ? filters[field][:operator] : nil
231 has_filter?(field) ? filters[field][:operator] : nil
225 end
232 end
226
233
227 def values_for(field)
234 def values_for(field)
228 has_filter?(field) ? filters[field][:values] : nil
235 has_filter?(field) ? filters[field][:values] : nil
229 end
236 end
230
237
231 def label_for(field)
238 def label_for(field)
232 label = available_filters[field][:name] if available_filters.has_key?(field)
239 label = available_filters[field][:name] if available_filters.has_key?(field)
233 label ||= field.gsub(/\_id$/, "")
240 label ||= field.gsub(/\_id$/, "")
234 end
241 end
235
242
236 def available_columns
243 def available_columns
237 return @available_columns if @available_columns
244 return @available_columns if @available_columns
238 @available_columns = Query.available_columns
245 @available_columns = Query.available_columns
239 @available_columns += (project ?
246 @available_columns += (project ?
240 project.all_issue_custom_fields :
247 project.all_issue_custom_fields :
241 IssueCustomField.find(:all)
248 IssueCustomField.find(:all)
242 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
249 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
243 end
250 end
244
251
245 # 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
246 def groupable_columns
253 def groupable_columns
247 available_columns.select {|c| c.groupable}
254 available_columns.select {|c| c.groupable}
248 end
255 end
249
256
250 def columns
257 def columns
251 if has_default_columns?
258 if has_default_columns?
252 available_columns.select do |c|
259 available_columns.select do |c|
253 # Adds the project column by default for cross-project lists
260 # Adds the project column by default for cross-project lists
254 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?)
255 end
262 end
256 else
263 else
257 # preserve the column_names order
264 # preserve the column_names order
258 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
259 end
266 end
260 end
267 end
261
268
262 def column_names=(names)
269 def column_names=(names)
263 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
264 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
265 write_attribute(:column_names, names)
272 write_attribute(:column_names, names)
266 end
273 end
267
274
268 def has_column?(column)
275 def has_column?(column)
269 column_names && column_names.include?(column.name)
276 column_names && column_names.include?(column.name)
270 end
277 end
271
278
272 def has_default_columns?
279 def has_default_columns?
273 column_names.nil? || column_names.empty?
280 column_names.nil? || column_names.empty?
274 end
281 end
275
282
276 def sort_criteria=(arg)
283 def sort_criteria=(arg)
277 c = []
284 c = []
278 if arg.is_a?(Hash)
285 if arg.is_a?(Hash)
279 arg = arg.keys.sort.collect {|k| arg[k]}
286 arg = arg.keys.sort.collect {|k| arg[k]}
280 end
287 end
281 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']}
282 write_attribute(:sort_criteria, c)
289 write_attribute(:sort_criteria, c)
283 end
290 end
284
291
285 def sort_criteria
292 def sort_criteria
286 read_attribute(:sort_criteria) || []
293 read_attribute(:sort_criteria) || []
287 end
294 end
288
295
289 def sort_criteria_key(arg)
296 def sort_criteria_key(arg)
290 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
297 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
291 end
298 end
292
299
293 def sort_criteria_order(arg)
300 def sort_criteria_order(arg)
294 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
301 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
295 end
302 end
296
303
297 # Returns the SQL sort order that should be prepended for grouping
304 # Returns the SQL sort order that should be prepended for grouping
298 def group_by_sort_order
305 def group_by_sort_order
299 if grouped? && (column = group_by_column)
306 if grouped? && (column = group_by_column)
300 column.sortable.is_a?(Array) ?
307 column.sortable.is_a?(Array) ?
301 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
308 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
302 "#{column.sortable} #{column.default_order}"
309 "#{column.sortable} #{column.default_order}"
303 end
310 end
304 end
311 end
305
312
306 # Returns true if the query is a grouped query
313 # Returns true if the query is a grouped query
307 def grouped?
314 def grouped?
308 !group_by.blank?
315 !group_by.blank?
309 end
316 end
310
317
311 def group_by_column
318 def group_by_column
312 groupable_columns.detect {|c| c.name.to_s == group_by}
319 groupable_columns.detect {|c| c.name.to_s == group_by}
313 end
320 end
314
321
322 def group_by_statement
323 group_by_column.groupable
324 end
325
315 def project_statement
326 def project_statement
316 project_clauses = []
327 project_clauses = []
317 if project && !@project.descendants.active.empty?
328 if project && !@project.descendants.active.empty?
318 ids = [project.id]
329 ids = [project.id]
319 if has_filter?("subproject_id")
330 if has_filter?("subproject_id")
320 case operator_for("subproject_id")
331 case operator_for("subproject_id")
321 when '='
332 when '='
322 # include the selected subprojects
333 # include the selected subprojects
323 ids += values_for("subproject_id").each(&:to_i)
334 ids += values_for("subproject_id").each(&:to_i)
324 when '!*'
335 when '!*'
325 # main project only
336 # main project only
326 else
337 else
327 # all subprojects
338 # all subprojects
328 ids += project.descendants.collect(&:id)
339 ids += project.descendants.collect(&:id)
329 end
340 end
330 elsif Setting.display_subprojects_issues?
341 elsif Setting.display_subprojects_issues?
331 ids += project.descendants.collect(&:id)
342 ids += project.descendants.collect(&:id)
332 end
343 end
333 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
344 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
334 elsif project
345 elsif project
335 project_clauses << "#{Project.table_name}.id = %d" % project.id
346 project_clauses << "#{Project.table_name}.id = %d" % project.id
336 end
347 end
337 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
348 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
338 project_clauses.join(' AND ')
349 project_clauses.join(' AND ')
339 end
350 end
340
351
341 def statement
352 def statement
342 # filters clauses
353 # filters clauses
343 filters_clauses = []
354 filters_clauses = []
344 filters.each_key do |field|
355 filters.each_key do |field|
345 next if field == "subproject_id"
356 next if field == "subproject_id"
346 v = values_for(field).clone
357 v = values_for(field).clone
347 next unless v and !v.empty?
358 next unless v and !v.empty?
348 operator = operator_for(field)
359 operator = operator_for(field)
349
360
350 # "me" value subsitution
361 # "me" value subsitution
351 if %w(assigned_to_id author_id watcher_id).include?(field)
362 if %w(assigned_to_id author_id watcher_id).include?(field)
352 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")
353 end
364 end
354
365
355 sql = ''
366 sql = ''
356 if field =~ /^cf_(\d+)$/
367 if field =~ /^cf_(\d+)$/
357 # custom field
368 # custom field
358 db_table = CustomValue.table_name
369 db_table = CustomValue.table_name
359 db_field = 'value'
370 db_field = 'value'
360 is_custom_filter = true
371 is_custom_filter = true
361 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 "
362 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) + ')'
363 elsif field == 'watcher_id'
374 elsif field == 'watcher_id'
364 db_table = Watcher.table_name
375 db_table = Watcher.table_name
365 db_field = 'user_id'
376 db_field = 'user_id'
366 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 "
367 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
378 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
368 else
379 else
369 # regular field
380 # regular field
370 db_table = Issue.table_name
381 db_table = Issue.table_name
371 db_field = field
382 db_field = field
372 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
383 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
373 end
384 end
374 filters_clauses << sql
385 filters_clauses << sql
375
386
376 end if filters and valid?
387 end if filters and valid?
377
388
378 (filters_clauses << project_statement).join(' AND ')
389 (filters_clauses << project_statement).join(' AND ')
379 end
390 end
380
391
381 private
392 private
382
393
383 # 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+
384 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)
385 sql = ''
396 sql = ''
386 case operator
397 case operator
387 when "="
398 when "="
388 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(",") + ")"
389 when "!"
400 when "!"
390 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(",") + "))"
391 when "!*"
402 when "!*"
392 sql = "#{db_table}.#{db_field} IS NULL"
403 sql = "#{db_table}.#{db_field} IS NULL"
393 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
404 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
394 when "*"
405 when "*"
395 sql = "#{db_table}.#{db_field} IS NOT NULL"
406 sql = "#{db_table}.#{db_field} IS NOT NULL"
396 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
407 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
397 when ">="
408 when ">="
398 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
409 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
399 when "<="
410 when "<="
400 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
411 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
401 when "o"
412 when "o"
402 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"
403 when "c"
414 when "c"
404 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"
405 when ">t-"
416 when ">t-"
406 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)
407 when "<t-"
418 when "<t-"
408 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)
409 when "t-"
420 when "t-"
410 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)
411 when ">t+"
422 when ">t+"
412 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)
413 when "<t+"
424 when "<t+"
414 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)
415 when "t+"
426 when "t+"
416 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)
417 when "t"
428 when "t"
418 sql = date_range_clause(db_table, db_field, 0, 0)
429 sql = date_range_clause(db_table, db_field, 0, 0)
419 when "w"
430 when "w"
420 from = l(:general_first_day_of_week) == '7' ?
431 from = l(:general_first_day_of_week) == '7' ?
421 # week starts on sunday
432 # week starts on sunday
422 ((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) :
423 # week starts on monday (Rails default)
434 # week starts on monday (Rails default)
424 Time.now.at_beginning_of_week
435 Time.now.at_beginning_of_week
425 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)]
426 when "~"
437 when "~"
427 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)}%'"
428 when "!~"
439 when "!~"
429 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)}%'"
430 end
441 end
431
442
432 return sql
443 return sql
433 end
444 end
434
445
435 def add_custom_fields_filters(custom_fields)
446 def add_custom_fields_filters(custom_fields)
436 @available_filters ||= {}
447 @available_filters ||= {}
437
448
438 custom_fields.select(&:is_filter?).each do |field|
449 custom_fields.select(&:is_filter?).each do |field|
439 case field.field_format
450 case field.field_format
440 when "text"
451 when "text"
441 options = { :type => :text, :order => 20 }
452 options = { :type => :text, :order => 20 }
442 when "list"
453 when "list"
443 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
454 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
444 when "date"
455 when "date"
445 options = { :type => :date, :order => 20 }
456 options = { :type => :date, :order => 20 }
446 when "bool"
457 when "bool"
447 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 }
448 else
459 else
449 options = { :type => :string, :order => 20 }
460 options = { :type => :string, :order => 20 }
450 end
461 end
451 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
462 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
452 end
463 end
453 end
464 end
454
465
455 # Returns a SQL clause for a date or datetime field.
466 # Returns a SQL clause for a date or datetime field.
456 def date_range_clause(table, field, from, to)
467 def date_range_clause(table, field, from, to)
457 s = []
468 s = []
458 if from
469 if from
459 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)])
460 end
471 end
461 if to
472 if to
462 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)])
463 end
474 end
464 s.join(' AND ')
475 s.join(' AND ')
465 end
476 end
466 end
477 end
@@ -1,34 +1,34
1 <% form_tag({}) do -%>
1 <% form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 <table class="list issues">
3 <table class="list issues">
4 <thead><tr>
4 <thead><tr>
5 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
5 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
7 </th>
7 </th>
8 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
8 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
9 <% query.columns.each do |column| %>
9 <% query.columns.each do |column| %>
10 <%= column_header(column) %>
10 <%= column_header(column) %>
11 <% end %>
11 <% end %>
12 </tr></thead>
12 </tr></thead>
13 <% group = false %>
13 <% previous_group = false %>
14 <tbody>
14 <tbody>
15 <% issues.each do |issue| -%>
15 <% issues.each do |issue| -%>
16 <% if @query.grouped? && issue.send(@query.group_by) != group %>
16 <% if @query.grouped? && (group = column_value(@query.group_by_column, issue) || '') != previous_group %>
17 <% group = issue.send(@query.group_by) %>
18 <% reset_cycle %>
17 <% reset_cycle %>
19 <tr class="group open">
18 <tr class="group open">
20 <td colspan="<%= query.columns.size + 2 %>">
19 <td colspan="<%= query.columns.size + 2 %>">
21 <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
20 <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
22 <%= group.blank? ? 'None' : group %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
21 <%= group.blank? ? 'None' : group %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
23 </td>
22 </td>
24 </tr>
23 </tr>
24 <% previous_group = group %>
25 <% end %>
25 <% end %>
26 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
26 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
27 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
27 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
28 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
28 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
29 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
29 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
30 </tr>
30 </tr>
31 <% end -%>
31 <% end -%>
32 </tbody>
32 </tbody>
33 </table>
33 </table>
34 <% end -%>
34 <% end -%>
@@ -1,137 +1,156
1 ---
1 ---
2 queries_001:
2 queries_001:
3 id: 1
3 id: 1
4 project_id: 1
4 project_id: 1
5 is_public: true
5 is_public: true
6 name: Multiple custom fields query
6 name: Multiple custom fields query
7 filters: |
7 filters: |
8 ---
8 ---
9 cf_1:
9 cf_1:
10 :values:
10 :values:
11 - MySQL
11 - MySQL
12 :operator: "="
12 :operator: "="
13 status_id:
13 status_id:
14 :values:
14 :values:
15 - "1"
15 - "1"
16 :operator: o
16 :operator: o
17 cf_2:
17 cf_2:
18 :values:
18 :values:
19 - "125"
19 - "125"
20 :operator: "="
20 :operator: "="
21
21
22 user_id: 1
22 user_id: 1
23 column_names:
23 column_names:
24 queries_002:
24 queries_002:
25 id: 2
25 id: 2
26 project_id: 1
26 project_id: 1
27 is_public: false
27 is_public: false
28 name: Private query for cookbook
28 name: Private query for cookbook
29 filters: |
29 filters: |
30 ---
30 ---
31 tracker_id:
31 tracker_id:
32 :values:
32 :values:
33 - "3"
33 - "3"
34 :operator: "="
34 :operator: "="
35 status_id:
35 status_id:
36 :values:
36 :values:
37 - "1"
37 - "1"
38 :operator: o
38 :operator: o
39
39
40 user_id: 3
40 user_id: 3
41 column_names:
41 column_names:
42 queries_003:
42 queries_003:
43 id: 3
43 id: 3
44 project_id:
44 project_id:
45 is_public: false
45 is_public: false
46 name: Private query for all projects
46 name: Private query for all projects
47 filters: |
47 filters: |
48 ---
48 ---
49 tracker_id:
49 tracker_id:
50 :values:
50 :values:
51 - "3"
51 - "3"
52 :operator: "="
52 :operator: "="
53
53
54 user_id: 3
54 user_id: 3
55 column_names:
55 column_names:
56 queries_004:
56 queries_004:
57 id: 4
57 id: 4
58 project_id:
58 project_id:
59 is_public: true
59 is_public: true
60 name: Public query for all projects
60 name: Public query for all projects
61 filters: |
61 filters: |
62 ---
62 ---
63 tracker_id:
63 tracker_id:
64 :values:
64 :values:
65 - "3"
65 - "3"
66 :operator: "="
66 :operator: "="
67
67
68 user_id: 2
68 user_id: 2
69 column_names:
69 column_names:
70 queries_005:
70 queries_005:
71 id: 5
71 id: 5
72 project_id:
72 project_id:
73 is_public: true
73 is_public: true
74 name: Open issues by priority and tracker
74 name: Open issues by priority and tracker
75 filters: |
75 filters: |
76 ---
76 ---
77 status_id:
77 status_id:
78 :values:
78 :values:
79 - "1"
79 - "1"
80 :operator: o
80 :operator: o
81
81
82 user_id: 1
82 user_id: 1
83 column_names:
83 column_names:
84 sort_criteria: |
84 sort_criteria: |
85 ---
85 ---
86 - - priority
86 - - priority
87 - desc
87 - desc
88 - - tracker
88 - - tracker
89 - asc
89 - asc
90 queries_006:
90 queries_006:
91 id: 6
91 id: 6
92 project_id:
92 project_id:
93 is_public: true
93 is_public: true
94 name: Open issues grouped by tracker
94 name: Open issues grouped by tracker
95 filters: |
95 filters: |
96 ---
96 ---
97 status_id:
97 status_id:
98 :values:
98 :values:
99 - "1"
99 - "1"
100 :operator: o
100 :operator: o
101
101
102 user_id: 1
102 user_id: 1
103 column_names:
103 column_names:
104 group_by: tracker
104 group_by: tracker
105 sort_criteria: |
105 sort_criteria: |
106 ---
106 ---
107 - - priority
107 - - priority
108 - desc
108 - desc
109 queries_007:
109 queries_007:
110 id: 7
110 id: 7
111 project_id: 2
111 project_id: 2
112 is_public: true
112 is_public: true
113 name: Public query for project 2
113 name: Public query for project 2
114 filters: |
114 filters: |
115 ---
115 ---
116 tracker_id:
116 tracker_id:
117 :values:
117 :values:
118 - "3"
118 - "3"
119 :operator: "="
119 :operator: "="
120
120
121 user_id: 2
121 user_id: 2
122 column_names:
122 column_names:
123 queries_008:
123 queries_008:
124 id: 8
124 id: 8
125 project_id: 2
125 project_id: 2
126 is_public: false
126 is_public: false
127 name: Private query for project 2
127 name: Private query for project 2
128 filters: |
128 filters: |
129 ---
129 ---
130 tracker_id:
130 tracker_id:
131 :values:
131 :values:
132 - "3"
132 - "3"
133 :operator: "="
133 :operator: "="
134
134
135 user_id: 2
135 user_id: 2
136 column_names:
136 column_names:
137 queries_009:
138 id: 9
139 project_id:
140 is_public: true
141 name: Open issues grouped by list custom field
142 filters: |
143 ---
144 status_id:
145 :values:
146 - "1"
147 :operator: o
148
149 user_id: 1
150 column_names:
151 group_by: cf_1
152 sort_criteria: |
153 ---
154 - - priority
155 - desc
137
156
@@ -1,1139 +1,1153
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 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < ActionController::TestCase
24 class IssuesControllerTest < ActionController::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :member_roles,
29 :member_roles,
30 :issues,
30 :issues,
31 :issue_statuses,
31 :issue_statuses,
32 :versions,
32 :versions,
33 :trackers,
33 :trackers,
34 :projects_trackers,
34 :projects_trackers,
35 :issue_categories,
35 :issue_categories,
36 :enabled_modules,
36 :enabled_modules,
37 :enumerations,
37 :enumerations,
38 :attachments,
38 :attachments,
39 :workflows,
39 :workflows,
40 :custom_fields,
40 :custom_fields,
41 :custom_values,
41 :custom_values,
42 :custom_fields_trackers,
42 :custom_fields_trackers,
43 :time_entries,
43 :time_entries,
44 :journals,
44 :journals,
45 :journal_details,
45 :journal_details,
46 :queries
46 :queries
47
47
48 def setup
48 def setup
49 @controller = IssuesController.new
49 @controller = IssuesController.new
50 @request = ActionController::TestRequest.new
50 @request = ActionController::TestRequest.new
51 @response = ActionController::TestResponse.new
51 @response = ActionController::TestResponse.new
52 User.current = nil
52 User.current = nil
53 end
53 end
54
54
55 def test_index_routing
55 def test_index_routing
56 assert_routing(
56 assert_routing(
57 {:method => :get, :path => '/issues'},
57 {:method => :get, :path => '/issues'},
58 :controller => 'issues', :action => 'index'
58 :controller => 'issues', :action => 'index'
59 )
59 )
60 end
60 end
61
61
62 def test_index
62 def test_index
63 Setting.default_language = 'en'
63 Setting.default_language = 'en'
64
64
65 get :index
65 get :index
66 assert_response :success
66 assert_response :success
67 assert_template 'index.rhtml'
67 assert_template 'index.rhtml'
68 assert_not_nil assigns(:issues)
68 assert_not_nil assigns(:issues)
69 assert_nil assigns(:project)
69 assert_nil assigns(:project)
70 assert_tag :tag => 'a', :content => /Can't print recipes/
70 assert_tag :tag => 'a', :content => /Can't print recipes/
71 assert_tag :tag => 'a', :content => /Subproject issue/
71 assert_tag :tag => 'a', :content => /Subproject issue/
72 # private projects hidden
72 # private projects hidden
73 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
73 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
74 assert_no_tag :tag => 'a', :content => /Issue on project 2/
74 assert_no_tag :tag => 'a', :content => /Issue on project 2/
75 # project column
75 # project column
76 assert_tag :tag => 'th', :content => /Project/
76 assert_tag :tag => 'th', :content => /Project/
77 end
77 end
78
78
79 def test_index_should_not_list_issues_when_module_disabled
79 def test_index_should_not_list_issues_when_module_disabled
80 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
80 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
81 get :index
81 get :index
82 assert_response :success
82 assert_response :success
83 assert_template 'index.rhtml'
83 assert_template 'index.rhtml'
84 assert_not_nil assigns(:issues)
84 assert_not_nil assigns(:issues)
85 assert_nil assigns(:project)
85 assert_nil assigns(:project)
86 assert_no_tag :tag => 'a', :content => /Can't print recipes/
86 assert_no_tag :tag => 'a', :content => /Can't print recipes/
87 assert_tag :tag => 'a', :content => /Subproject issue/
87 assert_tag :tag => 'a', :content => /Subproject issue/
88 end
88 end
89
89
90 def test_index_with_project_routing
90 def test_index_with_project_routing
91 assert_routing(
91 assert_routing(
92 {:method => :get, :path => '/projects/23/issues'},
92 {:method => :get, :path => '/projects/23/issues'},
93 :controller => 'issues', :action => 'index', :project_id => '23'
93 :controller => 'issues', :action => 'index', :project_id => '23'
94 )
94 )
95 end
95 end
96
96
97 def test_index_should_not_list_issues_when_module_disabled
97 def test_index_should_not_list_issues_when_module_disabled
98 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
98 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
99 get :index
99 get :index
100 assert_response :success
100 assert_response :success
101 assert_template 'index.rhtml'
101 assert_template 'index.rhtml'
102 assert_not_nil assigns(:issues)
102 assert_not_nil assigns(:issues)
103 assert_nil assigns(:project)
103 assert_nil assigns(:project)
104 assert_no_tag :tag => 'a', :content => /Can't print recipes/
104 assert_no_tag :tag => 'a', :content => /Can't print recipes/
105 assert_tag :tag => 'a', :content => /Subproject issue/
105 assert_tag :tag => 'a', :content => /Subproject issue/
106 end
106 end
107
107
108 def test_index_with_project_routing
108 def test_index_with_project_routing
109 assert_routing(
109 assert_routing(
110 {:method => :get, :path => 'projects/23/issues'},
110 {:method => :get, :path => 'projects/23/issues'},
111 :controller => 'issues', :action => 'index', :project_id => '23'
111 :controller => 'issues', :action => 'index', :project_id => '23'
112 )
112 )
113 end
113 end
114
114
115 def test_index_with_project
115 def test_index_with_project
116 Setting.display_subprojects_issues = 0
116 Setting.display_subprojects_issues = 0
117 get :index, :project_id => 1
117 get :index, :project_id => 1
118 assert_response :success
118 assert_response :success
119 assert_template 'index.rhtml'
119 assert_template 'index.rhtml'
120 assert_not_nil assigns(:issues)
120 assert_not_nil assigns(:issues)
121 assert_tag :tag => 'a', :content => /Can't print recipes/
121 assert_tag :tag => 'a', :content => /Can't print recipes/
122 assert_no_tag :tag => 'a', :content => /Subproject issue/
122 assert_no_tag :tag => 'a', :content => /Subproject issue/
123 end
123 end
124
124
125 def test_index_with_project_and_subprojects
125 def test_index_with_project_and_subprojects
126 Setting.display_subprojects_issues = 1
126 Setting.display_subprojects_issues = 1
127 get :index, :project_id => 1
127 get :index, :project_id => 1
128 assert_response :success
128 assert_response :success
129 assert_template 'index.rhtml'
129 assert_template 'index.rhtml'
130 assert_not_nil assigns(:issues)
130 assert_not_nil assigns(:issues)
131 assert_tag :tag => 'a', :content => /Can't print recipes/
131 assert_tag :tag => 'a', :content => /Can't print recipes/
132 assert_tag :tag => 'a', :content => /Subproject issue/
132 assert_tag :tag => 'a', :content => /Subproject issue/
133 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
133 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
134 end
134 end
135
135
136 def test_index_with_project_and_subprojects_should_show_private_subprojects
136 def test_index_with_project_and_subprojects_should_show_private_subprojects
137 @request.session[:user_id] = 2
137 @request.session[:user_id] = 2
138 Setting.display_subprojects_issues = 1
138 Setting.display_subprojects_issues = 1
139 get :index, :project_id => 1
139 get :index, :project_id => 1
140 assert_response :success
140 assert_response :success
141 assert_template 'index.rhtml'
141 assert_template 'index.rhtml'
142 assert_not_nil assigns(:issues)
142 assert_not_nil assigns(:issues)
143 assert_tag :tag => 'a', :content => /Can't print recipes/
143 assert_tag :tag => 'a', :content => /Can't print recipes/
144 assert_tag :tag => 'a', :content => /Subproject issue/
144 assert_tag :tag => 'a', :content => /Subproject issue/
145 assert_tag :tag => 'a', :content => /Issue of a private subproject/
145 assert_tag :tag => 'a', :content => /Issue of a private subproject/
146 end
146 end
147
147
148 def test_index_with_project_routing_formatted
148 def test_index_with_project_routing_formatted
149 assert_routing(
149 assert_routing(
150 {:method => :get, :path => 'projects/23/issues.pdf'},
150 {:method => :get, :path => 'projects/23/issues.pdf'},
151 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
151 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
152 )
152 )
153 assert_routing(
153 assert_routing(
154 {:method => :get, :path => 'projects/23/issues.atom'},
154 {:method => :get, :path => 'projects/23/issues.atom'},
155 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
155 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
156 )
156 )
157 end
157 end
158
158
159 def test_index_with_project_and_filter
159 def test_index_with_project_and_filter
160 get :index, :project_id => 1, :set_filter => 1
160 get :index, :project_id => 1, :set_filter => 1
161 assert_response :success
161 assert_response :success
162 assert_template 'index.rhtml'
162 assert_template 'index.rhtml'
163 assert_not_nil assigns(:issues)
163 assert_not_nil assigns(:issues)
164 end
164 end
165
165
166 def test_index_with_query
166 def test_index_with_query
167 get :index, :project_id => 1, :query_id => 5
167 get :index, :project_id => 1, :query_id => 5
168 assert_response :success
168 assert_response :success
169 assert_template 'index.rhtml'
169 assert_template 'index.rhtml'
170 assert_not_nil assigns(:issues)
170 assert_not_nil assigns(:issues)
171 assert_nil assigns(:issue_count_by_group)
171 assert_nil assigns(:issue_count_by_group)
172 end
172 end
173
173
174 def test_index_with_grouped_query
174 def test_index_with_query_grouped_by_tracker
175 get :index, :project_id => 1, :query_id => 6
175 get :index, :project_id => 1, :query_id => 6
176 assert_response :success
176 assert_response :success
177 assert_template 'index.rhtml'
177 assert_template 'index.rhtml'
178 assert_not_nil assigns(:issues)
178 assert_not_nil assigns(:issues)
179 assert_not_nil assigns(:issue_count_by_group)
179 count_by_group = assigns(:issue_count_by_group)
180 assert_kind_of Hash, count_by_group
181 assert_kind_of Tracker, count_by_group.keys.first
182 assert_not_nil count_by_group[Tracker.find(1)]
183 end
184
185 def test_index_with_query_grouped_by_list_custom_field
186 get :index, :project_id => 1, :query_id => 9
187 assert_response :success
188 assert_template 'index.rhtml'
189 assert_not_nil assigns(:issues)
190 count_by_group = assigns(:issue_count_by_group)
191 assert_kind_of Hash, count_by_group
192 assert_kind_of String, count_by_group.keys.first
193 assert_not_nil count_by_group['MySQL']
180 end
194 end
181
195
182 def test_index_csv_with_project
196 def test_index_csv_with_project
183 Setting.default_language = 'en'
197 Setting.default_language = 'en'
184
198
185 get :index, :format => 'csv'
199 get :index, :format => 'csv'
186 assert_response :success
200 assert_response :success
187 assert_not_nil assigns(:issues)
201 assert_not_nil assigns(:issues)
188 assert_equal 'text/csv', @response.content_type
202 assert_equal 'text/csv', @response.content_type
189 assert @response.body.starts_with?("#,")
203 assert @response.body.starts_with?("#,")
190
204
191 get :index, :project_id => 1, :format => 'csv'
205 get :index, :project_id => 1, :format => 'csv'
192 assert_response :success
206 assert_response :success
193 assert_not_nil assigns(:issues)
207 assert_not_nil assigns(:issues)
194 assert_equal 'text/csv', @response.content_type
208 assert_equal 'text/csv', @response.content_type
195 end
209 end
196
210
197 def test_index_formatted
211 def test_index_formatted
198 assert_routing(
212 assert_routing(
199 {:method => :get, :path => 'issues.pdf'},
213 {:method => :get, :path => 'issues.pdf'},
200 :controller => 'issues', :action => 'index', :format => 'pdf'
214 :controller => 'issues', :action => 'index', :format => 'pdf'
201 )
215 )
202 assert_routing(
216 assert_routing(
203 {:method => :get, :path => 'issues.atom'},
217 {:method => :get, :path => 'issues.atom'},
204 :controller => 'issues', :action => 'index', :format => 'atom'
218 :controller => 'issues', :action => 'index', :format => 'atom'
205 )
219 )
206 end
220 end
207
221
208 def test_index_pdf
222 def test_index_pdf
209 get :index, :format => 'pdf'
223 get :index, :format => 'pdf'
210 assert_response :success
224 assert_response :success
211 assert_not_nil assigns(:issues)
225 assert_not_nil assigns(:issues)
212 assert_equal 'application/pdf', @response.content_type
226 assert_equal 'application/pdf', @response.content_type
213
227
214 get :index, :project_id => 1, :format => 'pdf'
228 get :index, :project_id => 1, :format => 'pdf'
215 assert_response :success
229 assert_response :success
216 assert_not_nil assigns(:issues)
230 assert_not_nil assigns(:issues)
217 assert_equal 'application/pdf', @response.content_type
231 assert_equal 'application/pdf', @response.content_type
218
232
219 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
233 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
220 assert_response :success
234 assert_response :success
221 assert_not_nil assigns(:issues)
235 assert_not_nil assigns(:issues)
222 assert_equal 'application/pdf', @response.content_type
236 assert_equal 'application/pdf', @response.content_type
223 end
237 end
224
238
225 def test_index_sort
239 def test_index_sort
226 get :index, :sort => 'tracker,id:desc'
240 get :index, :sort => 'tracker,id:desc'
227 assert_response :success
241 assert_response :success
228
242
229 sort_params = @request.session['issues_index_sort']
243 sort_params = @request.session['issues_index_sort']
230 assert sort_params.is_a?(String)
244 assert sort_params.is_a?(String)
231 assert_equal 'tracker,id:desc', sort_params
245 assert_equal 'tracker,id:desc', sort_params
232
246
233 issues = assigns(:issues)
247 issues = assigns(:issues)
234 assert_not_nil issues
248 assert_not_nil issues
235 assert !issues.empty?
249 assert !issues.empty?
236 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
250 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
237 end
251 end
238
252
239 def test_gantt
253 def test_gantt
240 get :gantt, :project_id => 1
254 get :gantt, :project_id => 1
241 assert_response :success
255 assert_response :success
242 assert_template 'gantt.rhtml'
256 assert_template 'gantt.rhtml'
243 assert_not_nil assigns(:gantt)
257 assert_not_nil assigns(:gantt)
244 events = assigns(:gantt).events
258 events = assigns(:gantt).events
245 assert_not_nil events
259 assert_not_nil events
246 # Issue with start and due dates
260 # Issue with start and due dates
247 i = Issue.find(1)
261 i = Issue.find(1)
248 assert_not_nil i.due_date
262 assert_not_nil i.due_date
249 assert events.include?(Issue.find(1))
263 assert events.include?(Issue.find(1))
250 # Issue with without due date but targeted to a version with date
264 # Issue with without due date but targeted to a version with date
251 i = Issue.find(2)
265 i = Issue.find(2)
252 assert_nil i.due_date
266 assert_nil i.due_date
253 assert events.include?(i)
267 assert events.include?(i)
254 end
268 end
255
269
256 def test_cross_project_gantt
270 def test_cross_project_gantt
257 get :gantt
271 get :gantt
258 assert_response :success
272 assert_response :success
259 assert_template 'gantt.rhtml'
273 assert_template 'gantt.rhtml'
260 assert_not_nil assigns(:gantt)
274 assert_not_nil assigns(:gantt)
261 events = assigns(:gantt).events
275 events = assigns(:gantt).events
262 assert_not_nil events
276 assert_not_nil events
263 end
277 end
264
278
265 def test_gantt_export_to_pdf
279 def test_gantt_export_to_pdf
266 get :gantt, :project_id => 1, :format => 'pdf'
280 get :gantt, :project_id => 1, :format => 'pdf'
267 assert_response :success
281 assert_response :success
268 assert_equal 'application/pdf', @response.content_type
282 assert_equal 'application/pdf', @response.content_type
269 assert @response.body.starts_with?('%PDF')
283 assert @response.body.starts_with?('%PDF')
270 assert_not_nil assigns(:gantt)
284 assert_not_nil assigns(:gantt)
271 end
285 end
272
286
273 def test_cross_project_gantt_export_to_pdf
287 def test_cross_project_gantt_export_to_pdf
274 get :gantt, :format => 'pdf'
288 get :gantt, :format => 'pdf'
275 assert_response :success
289 assert_response :success
276 assert_equal 'application/pdf', @response.content_type
290 assert_equal 'application/pdf', @response.content_type
277 assert @response.body.starts_with?('%PDF')
291 assert @response.body.starts_with?('%PDF')
278 assert_not_nil assigns(:gantt)
292 assert_not_nil assigns(:gantt)
279 end
293 end
280
294
281 if Object.const_defined?(:Magick)
295 if Object.const_defined?(:Magick)
282 def test_gantt_image
296 def test_gantt_image
283 get :gantt, :project_id => 1, :format => 'png'
297 get :gantt, :project_id => 1, :format => 'png'
284 assert_response :success
298 assert_response :success
285 assert_equal 'image/png', @response.content_type
299 assert_equal 'image/png', @response.content_type
286 end
300 end
287 else
301 else
288 puts "RMagick not installed. Skipping tests !!!"
302 puts "RMagick not installed. Skipping tests !!!"
289 end
303 end
290
304
291 def test_calendar
305 def test_calendar
292 get :calendar, :project_id => 1
306 get :calendar, :project_id => 1
293 assert_response :success
307 assert_response :success
294 assert_template 'calendar'
308 assert_template 'calendar'
295 assert_not_nil assigns(:calendar)
309 assert_not_nil assigns(:calendar)
296 end
310 end
297
311
298 def test_cross_project_calendar
312 def test_cross_project_calendar
299 get :calendar
313 get :calendar
300 assert_response :success
314 assert_response :success
301 assert_template 'calendar'
315 assert_template 'calendar'
302 assert_not_nil assigns(:calendar)
316 assert_not_nil assigns(:calendar)
303 end
317 end
304
318
305 def test_changes
319 def test_changes
306 get :changes, :project_id => 1
320 get :changes, :project_id => 1
307 assert_response :success
321 assert_response :success
308 assert_not_nil assigns(:journals)
322 assert_not_nil assigns(:journals)
309 assert_equal 'application/atom+xml', @response.content_type
323 assert_equal 'application/atom+xml', @response.content_type
310 end
324 end
311
325
312 def test_show_routing
326 def test_show_routing
313 assert_routing(
327 assert_routing(
314 {:method => :get, :path => '/issues/64'},
328 {:method => :get, :path => '/issues/64'},
315 :controller => 'issues', :action => 'show', :id => '64'
329 :controller => 'issues', :action => 'show', :id => '64'
316 )
330 )
317 end
331 end
318
332
319 def test_show_routing_formatted
333 def test_show_routing_formatted
320 assert_routing(
334 assert_routing(
321 {:method => :get, :path => '/issues/2332.pdf'},
335 {:method => :get, :path => '/issues/2332.pdf'},
322 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
336 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
323 )
337 )
324 assert_routing(
338 assert_routing(
325 {:method => :get, :path => '/issues/23123.atom'},
339 {:method => :get, :path => '/issues/23123.atom'},
326 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
340 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
327 )
341 )
328 end
342 end
329
343
330 def test_show_by_anonymous
344 def test_show_by_anonymous
331 get :show, :id => 1
345 get :show, :id => 1
332 assert_response :success
346 assert_response :success
333 assert_template 'show.rhtml'
347 assert_template 'show.rhtml'
334 assert_not_nil assigns(:issue)
348 assert_not_nil assigns(:issue)
335 assert_equal Issue.find(1), assigns(:issue)
349 assert_equal Issue.find(1), assigns(:issue)
336
350
337 # anonymous role is allowed to add a note
351 # anonymous role is allowed to add a note
338 assert_tag :tag => 'form',
352 assert_tag :tag => 'form',
339 :descendant => { :tag => 'fieldset',
353 :descendant => { :tag => 'fieldset',
340 :child => { :tag => 'legend',
354 :child => { :tag => 'legend',
341 :content => /Notes/ } }
355 :content => /Notes/ } }
342 end
356 end
343
357
344 def test_show_by_manager
358 def test_show_by_manager
345 @request.session[:user_id] = 2
359 @request.session[:user_id] = 2
346 get :show, :id => 1
360 get :show, :id => 1
347 assert_response :success
361 assert_response :success
348
362
349 assert_tag :tag => 'form',
363 assert_tag :tag => 'form',
350 :descendant => { :tag => 'fieldset',
364 :descendant => { :tag => 'fieldset',
351 :child => { :tag => 'legend',
365 :child => { :tag => 'legend',
352 :content => /Change properties/ } },
366 :content => /Change properties/ } },
353 :descendant => { :tag => 'fieldset',
367 :descendant => { :tag => 'fieldset',
354 :child => { :tag => 'legend',
368 :child => { :tag => 'legend',
355 :content => /Log time/ } },
369 :content => /Log time/ } },
356 :descendant => { :tag => 'fieldset',
370 :descendant => { :tag => 'fieldset',
357 :child => { :tag => 'legend',
371 :child => { :tag => 'legend',
358 :content => /Notes/ } }
372 :content => /Notes/ } }
359 end
373 end
360
374
361 def test_show_should_deny_anonymous_access_without_permission
375 def test_show_should_deny_anonymous_access_without_permission
362 Role.anonymous.remove_permission!(:view_issues)
376 Role.anonymous.remove_permission!(:view_issues)
363 get :show, :id => 1
377 get :show, :id => 1
364 assert_response :redirect
378 assert_response :redirect
365 end
379 end
366
380
367 def test_show_should_deny_non_member_access_without_permission
381 def test_show_should_deny_non_member_access_without_permission
368 Role.non_member.remove_permission!(:view_issues)
382 Role.non_member.remove_permission!(:view_issues)
369 @request.session[:user_id] = 9
383 @request.session[:user_id] = 9
370 get :show, :id => 1
384 get :show, :id => 1
371 assert_response 403
385 assert_response 403
372 end
386 end
373
387
374 def test_show_should_deny_member_access_without_permission
388 def test_show_should_deny_member_access_without_permission
375 Role.find(1).remove_permission!(:view_issues)
389 Role.find(1).remove_permission!(:view_issues)
376 @request.session[:user_id] = 2
390 @request.session[:user_id] = 2
377 get :show, :id => 1
391 get :show, :id => 1
378 assert_response 403
392 assert_response 403
379 end
393 end
380
394
381 def test_show_should_not_disclose_relations_to_invisible_issues
395 def test_show_should_not_disclose_relations_to_invisible_issues
382 Setting.cross_project_issue_relations = '1'
396 Setting.cross_project_issue_relations = '1'
383 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
397 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
384 # Relation to a private project issue
398 # Relation to a private project issue
385 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
399 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
386
400
387 get :show, :id => 1
401 get :show, :id => 1
388 assert_response :success
402 assert_response :success
389
403
390 assert_tag :div, :attributes => { :id => 'relations' },
404 assert_tag :div, :attributes => { :id => 'relations' },
391 :descendant => { :tag => 'a', :content => /#2$/ }
405 :descendant => { :tag => 'a', :content => /#2$/ }
392 assert_no_tag :div, :attributes => { :id => 'relations' },
406 assert_no_tag :div, :attributes => { :id => 'relations' },
393 :descendant => { :tag => 'a', :content => /#4$/ }
407 :descendant => { :tag => 'a', :content => /#4$/ }
394 end
408 end
395
409
396 def test_show_atom
410 def test_show_atom
397 get :show, :id => 2, :format => 'atom'
411 get :show, :id => 2, :format => 'atom'
398 assert_response :success
412 assert_response :success
399 assert_template 'changes.rxml'
413 assert_template 'changes.rxml'
400 # Inline image
414 # Inline image
401 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
415 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
402 end
416 end
403
417
404 def test_new_routing
418 def test_new_routing
405 assert_routing(
419 assert_routing(
406 {:method => :get, :path => '/projects/1/issues/new'},
420 {:method => :get, :path => '/projects/1/issues/new'},
407 :controller => 'issues', :action => 'new', :project_id => '1'
421 :controller => 'issues', :action => 'new', :project_id => '1'
408 )
422 )
409 assert_recognizes(
423 assert_recognizes(
410 {:controller => 'issues', :action => 'new', :project_id => '1'},
424 {:controller => 'issues', :action => 'new', :project_id => '1'},
411 {:method => :post, :path => '/projects/1/issues'}
425 {:method => :post, :path => '/projects/1/issues'}
412 )
426 )
413 end
427 end
414
428
415 def test_show_export_to_pdf
429 def test_show_export_to_pdf
416 get :show, :id => 3, :format => 'pdf'
430 get :show, :id => 3, :format => 'pdf'
417 assert_response :success
431 assert_response :success
418 assert_equal 'application/pdf', @response.content_type
432 assert_equal 'application/pdf', @response.content_type
419 assert @response.body.starts_with?('%PDF')
433 assert @response.body.starts_with?('%PDF')
420 assert_not_nil assigns(:issue)
434 assert_not_nil assigns(:issue)
421 end
435 end
422
436
423 def test_get_new
437 def test_get_new
424 @request.session[:user_id] = 2
438 @request.session[:user_id] = 2
425 get :new, :project_id => 1, :tracker_id => 1
439 get :new, :project_id => 1, :tracker_id => 1
426 assert_response :success
440 assert_response :success
427 assert_template 'new'
441 assert_template 'new'
428
442
429 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
443 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
430 :value => 'Default string' }
444 :value => 'Default string' }
431 end
445 end
432
446
433 def test_get_new_without_tracker_id
447 def test_get_new_without_tracker_id
434 @request.session[:user_id] = 2
448 @request.session[:user_id] = 2
435 get :new, :project_id => 1
449 get :new, :project_id => 1
436 assert_response :success
450 assert_response :success
437 assert_template 'new'
451 assert_template 'new'
438
452
439 issue = assigns(:issue)
453 issue = assigns(:issue)
440 assert_not_nil issue
454 assert_not_nil issue
441 assert_equal Project.find(1).trackers.first, issue.tracker
455 assert_equal Project.find(1).trackers.first, issue.tracker
442 end
456 end
443
457
444 def test_get_new_with_no_default_status_should_display_an_error
458 def test_get_new_with_no_default_status_should_display_an_error
445 @request.session[:user_id] = 2
459 @request.session[:user_id] = 2
446 IssueStatus.delete_all
460 IssueStatus.delete_all
447
461
448 get :new, :project_id => 1
462 get :new, :project_id => 1
449 assert_response 500
463 assert_response 500
450 assert_not_nil flash[:error]
464 assert_not_nil flash[:error]
451 assert_tag :tag => 'div', :attributes => { :class => /error/ },
465 assert_tag :tag => 'div', :attributes => { :class => /error/ },
452 :content => /No default issue/
466 :content => /No default issue/
453 end
467 end
454
468
455 def test_get_new_with_no_tracker_should_display_an_error
469 def test_get_new_with_no_tracker_should_display_an_error
456 @request.session[:user_id] = 2
470 @request.session[:user_id] = 2
457 Tracker.delete_all
471 Tracker.delete_all
458
472
459 get :new, :project_id => 1
473 get :new, :project_id => 1
460 assert_response 500
474 assert_response 500
461 assert_not_nil flash[:error]
475 assert_not_nil flash[:error]
462 assert_tag :tag => 'div', :attributes => { :class => /error/ },
476 assert_tag :tag => 'div', :attributes => { :class => /error/ },
463 :content => /No tracker/
477 :content => /No tracker/
464 end
478 end
465
479
466 def test_update_new_form
480 def test_update_new_form
467 @request.session[:user_id] = 2
481 @request.session[:user_id] = 2
468 xhr :post, :new, :project_id => 1,
482 xhr :post, :new, :project_id => 1,
469 :issue => {:tracker_id => 2,
483 :issue => {:tracker_id => 2,
470 :subject => 'This is the test_new issue',
484 :subject => 'This is the test_new issue',
471 :description => 'This is the description',
485 :description => 'This is the description',
472 :priority_id => 5}
486 :priority_id => 5}
473 assert_response :success
487 assert_response :success
474 assert_template 'new'
488 assert_template 'new'
475 end
489 end
476
490
477 def test_post_new
491 def test_post_new
478 @request.session[:user_id] = 2
492 @request.session[:user_id] = 2
479 assert_difference 'Issue.count' do
493 assert_difference 'Issue.count' do
480 post :new, :project_id => 1,
494 post :new, :project_id => 1,
481 :issue => {:tracker_id => 3,
495 :issue => {:tracker_id => 3,
482 :subject => 'This is the test_new issue',
496 :subject => 'This is the test_new issue',
483 :description => 'This is the description',
497 :description => 'This is the description',
484 :priority_id => 5,
498 :priority_id => 5,
485 :estimated_hours => '',
499 :estimated_hours => '',
486 :custom_field_values => {'2' => 'Value for field 2'}}
500 :custom_field_values => {'2' => 'Value for field 2'}}
487 end
501 end
488 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
502 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
489
503
490 issue = Issue.find_by_subject('This is the test_new issue')
504 issue = Issue.find_by_subject('This is the test_new issue')
491 assert_not_nil issue
505 assert_not_nil issue
492 assert_equal 2, issue.author_id
506 assert_equal 2, issue.author_id
493 assert_equal 3, issue.tracker_id
507 assert_equal 3, issue.tracker_id
494 assert_nil issue.estimated_hours
508 assert_nil issue.estimated_hours
495 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
509 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
496 assert_not_nil v
510 assert_not_nil v
497 assert_equal 'Value for field 2', v.value
511 assert_equal 'Value for field 2', v.value
498 end
512 end
499
513
500 def test_post_new_and_continue
514 def test_post_new_and_continue
501 @request.session[:user_id] = 2
515 @request.session[:user_id] = 2
502 post :new, :project_id => 1,
516 post :new, :project_id => 1,
503 :issue => {:tracker_id => 3,
517 :issue => {:tracker_id => 3,
504 :subject => 'This is first issue',
518 :subject => 'This is first issue',
505 :priority_id => 5},
519 :priority_id => 5},
506 :continue => ''
520 :continue => ''
507 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
521 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
508 end
522 end
509
523
510 def test_post_new_without_custom_fields_param
524 def test_post_new_without_custom_fields_param
511 @request.session[:user_id] = 2
525 @request.session[:user_id] = 2
512 assert_difference 'Issue.count' do
526 assert_difference 'Issue.count' do
513 post :new, :project_id => 1,
527 post :new, :project_id => 1,
514 :issue => {:tracker_id => 1,
528 :issue => {:tracker_id => 1,
515 :subject => 'This is the test_new issue',
529 :subject => 'This is the test_new issue',
516 :description => 'This is the description',
530 :description => 'This is the description',
517 :priority_id => 5}
531 :priority_id => 5}
518 end
532 end
519 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
533 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
520 end
534 end
521
535
522 def test_post_new_with_required_custom_field_and_without_custom_fields_param
536 def test_post_new_with_required_custom_field_and_without_custom_fields_param
523 field = IssueCustomField.find_by_name('Database')
537 field = IssueCustomField.find_by_name('Database')
524 field.update_attribute(:is_required, true)
538 field.update_attribute(:is_required, true)
525
539
526 @request.session[:user_id] = 2
540 @request.session[:user_id] = 2
527 post :new, :project_id => 1,
541 post :new, :project_id => 1,
528 :issue => {:tracker_id => 1,
542 :issue => {:tracker_id => 1,
529 :subject => 'This is the test_new issue',
543 :subject => 'This is the test_new issue',
530 :description => 'This is the description',
544 :description => 'This is the description',
531 :priority_id => 5}
545 :priority_id => 5}
532 assert_response :success
546 assert_response :success
533 assert_template 'new'
547 assert_template 'new'
534 issue = assigns(:issue)
548 issue = assigns(:issue)
535 assert_not_nil issue
549 assert_not_nil issue
536 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
550 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
537 end
551 end
538
552
539 def test_post_new_with_watchers
553 def test_post_new_with_watchers
540 @request.session[:user_id] = 2
554 @request.session[:user_id] = 2
541 ActionMailer::Base.deliveries.clear
555 ActionMailer::Base.deliveries.clear
542
556
543 assert_difference 'Watcher.count', 2 do
557 assert_difference 'Watcher.count', 2 do
544 post :new, :project_id => 1,
558 post :new, :project_id => 1,
545 :issue => {:tracker_id => 1,
559 :issue => {:tracker_id => 1,
546 :subject => 'This is a new issue with watchers',
560 :subject => 'This is a new issue with watchers',
547 :description => 'This is the description',
561 :description => 'This is the description',
548 :priority_id => 5,
562 :priority_id => 5,
549 :watcher_user_ids => ['2', '3']}
563 :watcher_user_ids => ['2', '3']}
550 end
564 end
551 issue = Issue.find_by_subject('This is a new issue with watchers')
565 issue = Issue.find_by_subject('This is a new issue with watchers')
552 assert_not_nil issue
566 assert_not_nil issue
553 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
567 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
554
568
555 # Watchers added
569 # Watchers added
556 assert_equal [2, 3], issue.watcher_user_ids.sort
570 assert_equal [2, 3], issue.watcher_user_ids.sort
557 assert issue.watched_by?(User.find(3))
571 assert issue.watched_by?(User.find(3))
558 # Watchers notified
572 # Watchers notified
559 mail = ActionMailer::Base.deliveries.last
573 mail = ActionMailer::Base.deliveries.last
560 assert_kind_of TMail::Mail, mail
574 assert_kind_of TMail::Mail, mail
561 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
575 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
562 end
576 end
563
577
564 def test_post_new_should_send_a_notification
578 def test_post_new_should_send_a_notification
565 ActionMailer::Base.deliveries.clear
579 ActionMailer::Base.deliveries.clear
566 @request.session[:user_id] = 2
580 @request.session[:user_id] = 2
567 assert_difference 'Issue.count' do
581 assert_difference 'Issue.count' do
568 post :new, :project_id => 1,
582 post :new, :project_id => 1,
569 :issue => {:tracker_id => 3,
583 :issue => {:tracker_id => 3,
570 :subject => 'This is the test_new issue',
584 :subject => 'This is the test_new issue',
571 :description => 'This is the description',
585 :description => 'This is the description',
572 :priority_id => 5,
586 :priority_id => 5,
573 :estimated_hours => '',
587 :estimated_hours => '',
574 :custom_field_values => {'2' => 'Value for field 2'}}
588 :custom_field_values => {'2' => 'Value for field 2'}}
575 end
589 end
576 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
590 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
577
591
578 assert_equal 1, ActionMailer::Base.deliveries.size
592 assert_equal 1, ActionMailer::Base.deliveries.size
579 end
593 end
580
594
581 def test_post_should_preserve_fields_values_on_validation_failure
595 def test_post_should_preserve_fields_values_on_validation_failure
582 @request.session[:user_id] = 2
596 @request.session[:user_id] = 2
583 post :new, :project_id => 1,
597 post :new, :project_id => 1,
584 :issue => {:tracker_id => 1,
598 :issue => {:tracker_id => 1,
585 # empty subject
599 # empty subject
586 :subject => '',
600 :subject => '',
587 :description => 'This is a description',
601 :description => 'This is a description',
588 :priority_id => 6,
602 :priority_id => 6,
589 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
603 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
590 assert_response :success
604 assert_response :success
591 assert_template 'new'
605 assert_template 'new'
592
606
593 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
607 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
594 :content => 'This is a description'
608 :content => 'This is a description'
595 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
609 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
596 :child => { :tag => 'option', :attributes => { :selected => 'selected',
610 :child => { :tag => 'option', :attributes => { :selected => 'selected',
597 :value => '6' },
611 :value => '6' },
598 :content => 'High' }
612 :content => 'High' }
599 # Custom fields
613 # Custom fields
600 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
614 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
601 :child => { :tag => 'option', :attributes => { :selected => 'selected',
615 :child => { :tag => 'option', :attributes => { :selected => 'selected',
602 :value => 'Oracle' },
616 :value => 'Oracle' },
603 :content => 'Oracle' }
617 :content => 'Oracle' }
604 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
618 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
605 :value => 'Value for field 2'}
619 :value => 'Value for field 2'}
606 end
620 end
607
621
608 def test_copy_routing
622 def test_copy_routing
609 assert_routing(
623 assert_routing(
610 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
624 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
611 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
625 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
612 )
626 )
613 end
627 end
614
628
615 def test_copy_issue
629 def test_copy_issue
616 @request.session[:user_id] = 2
630 @request.session[:user_id] = 2
617 get :new, :project_id => 1, :copy_from => 1
631 get :new, :project_id => 1, :copy_from => 1
618 assert_template 'new'
632 assert_template 'new'
619 assert_not_nil assigns(:issue)
633 assert_not_nil assigns(:issue)
620 orig = Issue.find(1)
634 orig = Issue.find(1)
621 assert_equal orig.subject, assigns(:issue).subject
635 assert_equal orig.subject, assigns(:issue).subject
622 end
636 end
623
637
624 def test_edit_routing
638 def test_edit_routing
625 assert_routing(
639 assert_routing(
626 {:method => :get, :path => '/issues/1/edit'},
640 {:method => :get, :path => '/issues/1/edit'},
627 :controller => 'issues', :action => 'edit', :id => '1'
641 :controller => 'issues', :action => 'edit', :id => '1'
628 )
642 )
629 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
643 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
630 {:controller => 'issues', :action => 'edit', :id => '1'},
644 {:controller => 'issues', :action => 'edit', :id => '1'},
631 {:method => :post, :path => '/issues/1/edit'}
645 {:method => :post, :path => '/issues/1/edit'}
632 )
646 )
633 end
647 end
634
648
635 def test_get_edit
649 def test_get_edit
636 @request.session[:user_id] = 2
650 @request.session[:user_id] = 2
637 get :edit, :id => 1
651 get :edit, :id => 1
638 assert_response :success
652 assert_response :success
639 assert_template 'edit'
653 assert_template 'edit'
640 assert_not_nil assigns(:issue)
654 assert_not_nil assigns(:issue)
641 assert_equal Issue.find(1), assigns(:issue)
655 assert_equal Issue.find(1), assigns(:issue)
642 end
656 end
643
657
644 def test_get_edit_with_params
658 def test_get_edit_with_params
645 @request.session[:user_id] = 2
659 @request.session[:user_id] = 2
646 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
660 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
647 assert_response :success
661 assert_response :success
648 assert_template 'edit'
662 assert_template 'edit'
649
663
650 issue = assigns(:issue)
664 issue = assigns(:issue)
651 assert_not_nil issue
665 assert_not_nil issue
652
666
653 assert_equal 5, issue.status_id
667 assert_equal 5, issue.status_id
654 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
668 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
655 :child => { :tag => 'option',
669 :child => { :tag => 'option',
656 :content => 'Closed',
670 :content => 'Closed',
657 :attributes => { :selected => 'selected' } }
671 :attributes => { :selected => 'selected' } }
658
672
659 assert_equal 7, issue.priority_id
673 assert_equal 7, issue.priority_id
660 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
674 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
661 :child => { :tag => 'option',
675 :child => { :tag => 'option',
662 :content => 'Urgent',
676 :content => 'Urgent',
663 :attributes => { :selected => 'selected' } }
677 :attributes => { :selected => 'selected' } }
664 end
678 end
665
679
666 def test_reply_routing
680 def test_reply_routing
667 assert_routing(
681 assert_routing(
668 {:method => :post, :path => '/issues/1/quoted'},
682 {:method => :post, :path => '/issues/1/quoted'},
669 :controller => 'issues', :action => 'reply', :id => '1'
683 :controller => 'issues', :action => 'reply', :id => '1'
670 )
684 )
671 end
685 end
672
686
673 def test_reply_to_issue
687 def test_reply_to_issue
674 @request.session[:user_id] = 2
688 @request.session[:user_id] = 2
675 get :reply, :id => 1
689 get :reply, :id => 1
676 assert_response :success
690 assert_response :success
677 assert_select_rjs :show, "update"
691 assert_select_rjs :show, "update"
678 end
692 end
679
693
680 def test_reply_to_note
694 def test_reply_to_note
681 @request.session[:user_id] = 2
695 @request.session[:user_id] = 2
682 get :reply, :id => 1, :journal_id => 2
696 get :reply, :id => 1, :journal_id => 2
683 assert_response :success
697 assert_response :success
684 assert_select_rjs :show, "update"
698 assert_select_rjs :show, "update"
685 end
699 end
686
700
687 def test_post_edit_without_custom_fields_param
701 def test_post_edit_without_custom_fields_param
688 @request.session[:user_id] = 2
702 @request.session[:user_id] = 2
689 ActionMailer::Base.deliveries.clear
703 ActionMailer::Base.deliveries.clear
690
704
691 issue = Issue.find(1)
705 issue = Issue.find(1)
692 assert_equal '125', issue.custom_value_for(2).value
706 assert_equal '125', issue.custom_value_for(2).value
693 old_subject = issue.subject
707 old_subject = issue.subject
694 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
708 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
695
709
696 assert_difference('Journal.count') do
710 assert_difference('Journal.count') do
697 assert_difference('JournalDetail.count', 2) do
711 assert_difference('JournalDetail.count', 2) do
698 post :edit, :id => 1, :issue => {:subject => new_subject,
712 post :edit, :id => 1, :issue => {:subject => new_subject,
699 :priority_id => '6',
713 :priority_id => '6',
700 :category_id => '1' # no change
714 :category_id => '1' # no change
701 }
715 }
702 end
716 end
703 end
717 end
704 assert_redirected_to :action => 'show', :id => '1'
718 assert_redirected_to :action => 'show', :id => '1'
705 issue.reload
719 issue.reload
706 assert_equal new_subject, issue.subject
720 assert_equal new_subject, issue.subject
707 # Make sure custom fields were not cleared
721 # Make sure custom fields were not cleared
708 assert_equal '125', issue.custom_value_for(2).value
722 assert_equal '125', issue.custom_value_for(2).value
709
723
710 mail = ActionMailer::Base.deliveries.last
724 mail = ActionMailer::Base.deliveries.last
711 assert_kind_of TMail::Mail, mail
725 assert_kind_of TMail::Mail, mail
712 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
726 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
713 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
727 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
714 end
728 end
715
729
716 def test_post_edit_with_custom_field_change
730 def test_post_edit_with_custom_field_change
717 @request.session[:user_id] = 2
731 @request.session[:user_id] = 2
718 issue = Issue.find(1)
732 issue = Issue.find(1)
719 assert_equal '125', issue.custom_value_for(2).value
733 assert_equal '125', issue.custom_value_for(2).value
720
734
721 assert_difference('Journal.count') do
735 assert_difference('Journal.count') do
722 assert_difference('JournalDetail.count', 3) do
736 assert_difference('JournalDetail.count', 3) do
723 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
737 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
724 :priority_id => '6',
738 :priority_id => '6',
725 :category_id => '1', # no change
739 :category_id => '1', # no change
726 :custom_field_values => { '2' => 'New custom value' }
740 :custom_field_values => { '2' => 'New custom value' }
727 }
741 }
728 end
742 end
729 end
743 end
730 assert_redirected_to :action => 'show', :id => '1'
744 assert_redirected_to :action => 'show', :id => '1'
731 issue.reload
745 issue.reload
732 assert_equal 'New custom value', issue.custom_value_for(2).value
746 assert_equal 'New custom value', issue.custom_value_for(2).value
733
747
734 mail = ActionMailer::Base.deliveries.last
748 mail = ActionMailer::Base.deliveries.last
735 assert_kind_of TMail::Mail, mail
749 assert_kind_of TMail::Mail, mail
736 assert mail.body.include?("Searchable field changed from 125 to New custom value")
750 assert mail.body.include?("Searchable field changed from 125 to New custom value")
737 end
751 end
738
752
739 def test_post_edit_with_status_and_assignee_change
753 def test_post_edit_with_status_and_assignee_change
740 issue = Issue.find(1)
754 issue = Issue.find(1)
741 assert_equal 1, issue.status_id
755 assert_equal 1, issue.status_id
742 @request.session[:user_id] = 2
756 @request.session[:user_id] = 2
743 assert_difference('TimeEntry.count', 0) do
757 assert_difference('TimeEntry.count', 0) do
744 post :edit,
758 post :edit,
745 :id => 1,
759 :id => 1,
746 :issue => { :status_id => 2, :assigned_to_id => 3 },
760 :issue => { :status_id => 2, :assigned_to_id => 3 },
747 :notes => 'Assigned to dlopper',
761 :notes => 'Assigned to dlopper',
748 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
762 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
749 end
763 end
750 assert_redirected_to :action => 'show', :id => '1'
764 assert_redirected_to :action => 'show', :id => '1'
751 issue.reload
765 issue.reload
752 assert_equal 2, issue.status_id
766 assert_equal 2, issue.status_id
753 j = issue.journals.find(:first, :order => 'id DESC')
767 j = issue.journals.find(:first, :order => 'id DESC')
754 assert_equal 'Assigned to dlopper', j.notes
768 assert_equal 'Assigned to dlopper', j.notes
755 assert_equal 2, j.details.size
769 assert_equal 2, j.details.size
756
770
757 mail = ActionMailer::Base.deliveries.last
771 mail = ActionMailer::Base.deliveries.last
758 assert mail.body.include?("Status changed from New to Assigned")
772 assert mail.body.include?("Status changed from New to Assigned")
759 # subject should contain the new status
773 # subject should contain the new status
760 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
774 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
761 end
775 end
762
776
763 def test_post_edit_with_note_only
777 def test_post_edit_with_note_only
764 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
778 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
765 # anonymous user
779 # anonymous user
766 post :edit,
780 post :edit,
767 :id => 1,
781 :id => 1,
768 :notes => notes
782 :notes => notes
769 assert_redirected_to :action => 'show', :id => '1'
783 assert_redirected_to :action => 'show', :id => '1'
770 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
784 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
771 assert_equal notes, j.notes
785 assert_equal notes, j.notes
772 assert_equal 0, j.details.size
786 assert_equal 0, j.details.size
773 assert_equal User.anonymous, j.user
787 assert_equal User.anonymous, j.user
774
788
775 mail = ActionMailer::Base.deliveries.last
789 mail = ActionMailer::Base.deliveries.last
776 assert mail.body.include?(notes)
790 assert mail.body.include?(notes)
777 end
791 end
778
792
779 def test_post_edit_with_note_and_spent_time
793 def test_post_edit_with_note_and_spent_time
780 @request.session[:user_id] = 2
794 @request.session[:user_id] = 2
781 spent_hours_before = Issue.find(1).spent_hours
795 spent_hours_before = Issue.find(1).spent_hours
782 assert_difference('TimeEntry.count') do
796 assert_difference('TimeEntry.count') do
783 post :edit,
797 post :edit,
784 :id => 1,
798 :id => 1,
785 :notes => '2.5 hours added',
799 :notes => '2.5 hours added',
786 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
800 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
787 end
801 end
788 assert_redirected_to :action => 'show', :id => '1'
802 assert_redirected_to :action => 'show', :id => '1'
789
803
790 issue = Issue.find(1)
804 issue = Issue.find(1)
791
805
792 j = issue.journals.find(:first, :order => 'id DESC')
806 j = issue.journals.find(:first, :order => 'id DESC')
793 assert_equal '2.5 hours added', j.notes
807 assert_equal '2.5 hours added', j.notes
794 assert_equal 0, j.details.size
808 assert_equal 0, j.details.size
795
809
796 t = issue.time_entries.find(:first, :order => 'id DESC')
810 t = issue.time_entries.find(:first, :order => 'id DESC')
797 assert_not_nil t
811 assert_not_nil t
798 assert_equal 2.5, t.hours
812 assert_equal 2.5, t.hours
799 assert_equal spent_hours_before + 2.5, issue.spent_hours
813 assert_equal spent_hours_before + 2.5, issue.spent_hours
800 end
814 end
801
815
802 def test_post_edit_with_attachment_only
816 def test_post_edit_with_attachment_only
803 set_tmp_attachments_directory
817 set_tmp_attachments_directory
804
818
805 # Delete all fixtured journals, a race condition can occur causing the wrong
819 # Delete all fixtured journals, a race condition can occur causing the wrong
806 # journal to get fetched in the next find.
820 # journal to get fetched in the next find.
807 Journal.delete_all
821 Journal.delete_all
808
822
809 # anonymous user
823 # anonymous user
810 post :edit,
824 post :edit,
811 :id => 1,
825 :id => 1,
812 :notes => '',
826 :notes => '',
813 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
827 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
814 assert_redirected_to :action => 'show', :id => '1'
828 assert_redirected_to :action => 'show', :id => '1'
815 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
829 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
816 assert j.notes.blank?
830 assert j.notes.blank?
817 assert_equal 1, j.details.size
831 assert_equal 1, j.details.size
818 assert_equal 'testfile.txt', j.details.first.value
832 assert_equal 'testfile.txt', j.details.first.value
819 assert_equal User.anonymous, j.user
833 assert_equal User.anonymous, j.user
820
834
821 mail = ActionMailer::Base.deliveries.last
835 mail = ActionMailer::Base.deliveries.last
822 assert mail.body.include?('testfile.txt')
836 assert mail.body.include?('testfile.txt')
823 end
837 end
824
838
825 def test_post_edit_with_no_change
839 def test_post_edit_with_no_change
826 issue = Issue.find(1)
840 issue = Issue.find(1)
827 issue.journals.clear
841 issue.journals.clear
828 ActionMailer::Base.deliveries.clear
842 ActionMailer::Base.deliveries.clear
829
843
830 post :edit,
844 post :edit,
831 :id => 1,
845 :id => 1,
832 :notes => ''
846 :notes => ''
833 assert_redirected_to :action => 'show', :id => '1'
847 assert_redirected_to :action => 'show', :id => '1'
834
848
835 issue.reload
849 issue.reload
836 assert issue.journals.empty?
850 assert issue.journals.empty?
837 # No email should be sent
851 # No email should be sent
838 assert ActionMailer::Base.deliveries.empty?
852 assert ActionMailer::Base.deliveries.empty?
839 end
853 end
840
854
841 def test_post_edit_should_send_a_notification
855 def test_post_edit_should_send_a_notification
842 @request.session[:user_id] = 2
856 @request.session[:user_id] = 2
843 ActionMailer::Base.deliveries.clear
857 ActionMailer::Base.deliveries.clear
844 issue = Issue.find(1)
858 issue = Issue.find(1)
845 old_subject = issue.subject
859 old_subject = issue.subject
846 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
860 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
847
861
848 post :edit, :id => 1, :issue => {:subject => new_subject,
862 post :edit, :id => 1, :issue => {:subject => new_subject,
849 :priority_id => '6',
863 :priority_id => '6',
850 :category_id => '1' # no change
864 :category_id => '1' # no change
851 }
865 }
852 assert_equal 1, ActionMailer::Base.deliveries.size
866 assert_equal 1, ActionMailer::Base.deliveries.size
853 end
867 end
854
868
855 def test_post_edit_with_invalid_spent_time
869 def test_post_edit_with_invalid_spent_time
856 @request.session[:user_id] = 2
870 @request.session[:user_id] = 2
857 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
871 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
858
872
859 assert_no_difference('Journal.count') do
873 assert_no_difference('Journal.count') do
860 post :edit,
874 post :edit,
861 :id => 1,
875 :id => 1,
862 :notes => notes,
876 :notes => notes,
863 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
877 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
864 end
878 end
865 assert_response :success
879 assert_response :success
866 assert_template 'edit'
880 assert_template 'edit'
867
881
868 assert_tag :textarea, :attributes => { :name => 'notes' },
882 assert_tag :textarea, :attributes => { :name => 'notes' },
869 :content => notes
883 :content => notes
870 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
884 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
871 end
885 end
872
886
873 def test_get_bulk_edit
887 def test_get_bulk_edit
874 @request.session[:user_id] = 2
888 @request.session[:user_id] = 2
875 get :bulk_edit, :ids => [1, 2]
889 get :bulk_edit, :ids => [1, 2]
876 assert_response :success
890 assert_response :success
877 assert_template 'bulk_edit'
891 assert_template 'bulk_edit'
878 end
892 end
879
893
880 def test_bulk_edit
894 def test_bulk_edit
881 @request.session[:user_id] = 2
895 @request.session[:user_id] = 2
882 # update issues priority
896 # update issues priority
883 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
897 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
884 :assigned_to_id => '',
898 :assigned_to_id => '',
885 :custom_field_values => {'2' => ''},
899 :custom_field_values => {'2' => ''},
886 :notes => 'Bulk editing'
900 :notes => 'Bulk editing'
887 assert_response 302
901 assert_response 302
888 # check that the issues were updated
902 # check that the issues were updated
889 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
903 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
890
904
891 issue = Issue.find(1)
905 issue = Issue.find(1)
892 journal = issue.journals.find(:first, :order => 'created_on DESC')
906 journal = issue.journals.find(:first, :order => 'created_on DESC')
893 assert_equal '125', issue.custom_value_for(2).value
907 assert_equal '125', issue.custom_value_for(2).value
894 assert_equal 'Bulk editing', journal.notes
908 assert_equal 'Bulk editing', journal.notes
895 assert_equal 1, journal.details.size
909 assert_equal 1, journal.details.size
896 end
910 end
897
911
898 def test_bullk_edit_should_send_a_notification
912 def test_bullk_edit_should_send_a_notification
899 @request.session[:user_id] = 2
913 @request.session[:user_id] = 2
900 ActionMailer::Base.deliveries.clear
914 ActionMailer::Base.deliveries.clear
901 post(:bulk_edit,
915 post(:bulk_edit,
902 {
916 {
903 :ids => [1, 2],
917 :ids => [1, 2],
904 :priority_id => 7,
918 :priority_id => 7,
905 :assigned_to_id => '',
919 :assigned_to_id => '',
906 :custom_field_values => {'2' => ''},
920 :custom_field_values => {'2' => ''},
907 :notes => 'Bulk editing'
921 :notes => 'Bulk editing'
908 })
922 })
909
923
910 assert_response 302
924 assert_response 302
911 assert_equal 2, ActionMailer::Base.deliveries.size
925 assert_equal 2, ActionMailer::Base.deliveries.size
912 end
926 end
913
927
914 def test_bulk_edit_status
928 def test_bulk_edit_status
915 @request.session[:user_id] = 2
929 @request.session[:user_id] = 2
916 # update issues priority
930 # update issues priority
917 post :bulk_edit, :ids => [1, 2], :priority_id => '',
931 post :bulk_edit, :ids => [1, 2], :priority_id => '',
918 :assigned_to_id => '',
932 :assigned_to_id => '',
919 :status_id => '5',
933 :status_id => '5',
920 :notes => 'Bulk editing status'
934 :notes => 'Bulk editing status'
921 assert_response 302
935 assert_response 302
922 issue = Issue.find(1)
936 issue = Issue.find(1)
923 assert issue.closed?
937 assert issue.closed?
924 end
938 end
925
939
926 def test_bulk_edit_custom_field
940 def test_bulk_edit_custom_field
927 @request.session[:user_id] = 2
941 @request.session[:user_id] = 2
928 # update issues priority
942 # update issues priority
929 post :bulk_edit, :ids => [1, 2], :priority_id => '',
943 post :bulk_edit, :ids => [1, 2], :priority_id => '',
930 :assigned_to_id => '',
944 :assigned_to_id => '',
931 :custom_field_values => {'2' => '777'},
945 :custom_field_values => {'2' => '777'},
932 :notes => 'Bulk editing custom field'
946 :notes => 'Bulk editing custom field'
933 assert_response 302
947 assert_response 302
934
948
935 issue = Issue.find(1)
949 issue = Issue.find(1)
936 journal = issue.journals.find(:first, :order => 'created_on DESC')
950 journal = issue.journals.find(:first, :order => 'created_on DESC')
937 assert_equal '777', issue.custom_value_for(2).value
951 assert_equal '777', issue.custom_value_for(2).value
938 assert_equal 1, journal.details.size
952 assert_equal 1, journal.details.size
939 assert_equal '125', journal.details.first.old_value
953 assert_equal '125', journal.details.first.old_value
940 assert_equal '777', journal.details.first.value
954 assert_equal '777', journal.details.first.value
941 end
955 end
942
956
943 def test_bulk_unassign
957 def test_bulk_unassign
944 assert_not_nil Issue.find(2).assigned_to
958 assert_not_nil Issue.find(2).assigned_to
945 @request.session[:user_id] = 2
959 @request.session[:user_id] = 2
946 # unassign issues
960 # unassign issues
947 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
961 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
948 assert_response 302
962 assert_response 302
949 # check that the issues were updated
963 # check that the issues were updated
950 assert_nil Issue.find(2).assigned_to
964 assert_nil Issue.find(2).assigned_to
951 end
965 end
952
966
953 def test_move_routing
967 def test_move_routing
954 assert_routing(
968 assert_routing(
955 {:method => :get, :path => '/issues/1/move'},
969 {:method => :get, :path => '/issues/1/move'},
956 :controller => 'issues', :action => 'move', :id => '1'
970 :controller => 'issues', :action => 'move', :id => '1'
957 )
971 )
958 assert_recognizes(
972 assert_recognizes(
959 {:controller => 'issues', :action => 'move', :id => '1'},
973 {:controller => 'issues', :action => 'move', :id => '1'},
960 {:method => :post, :path => '/issues/1/move'}
974 {:method => :post, :path => '/issues/1/move'}
961 )
975 )
962 end
976 end
963
977
964 def test_move_one_issue_to_another_project
978 def test_move_one_issue_to_another_project
965 @request.session[:user_id] = 2
979 @request.session[:user_id] = 2
966 post :move, :id => 1, :new_project_id => 2
980 post :move, :id => 1, :new_project_id => 2
967 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
981 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
968 assert_equal 2, Issue.find(1).project_id
982 assert_equal 2, Issue.find(1).project_id
969 end
983 end
970
984
971 def test_move_one_issue_to_another_project_should_follow_when_needed
985 def test_move_one_issue_to_another_project_should_follow_when_needed
972 @request.session[:user_id] = 2
986 @request.session[:user_id] = 2
973 post :move, :id => 1, :new_project_id => 2, :follow => '1'
987 post :move, :id => 1, :new_project_id => 2, :follow => '1'
974 assert_redirected_to '/issues/1'
988 assert_redirected_to '/issues/1'
975 end
989 end
976
990
977 def test_bulk_move_to_another_project
991 def test_bulk_move_to_another_project
978 @request.session[:user_id] = 2
992 @request.session[:user_id] = 2
979 post :move, :ids => [1, 2], :new_project_id => 2
993 post :move, :ids => [1, 2], :new_project_id => 2
980 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
994 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
981 # Issues moved to project 2
995 # Issues moved to project 2
982 assert_equal 2, Issue.find(1).project_id
996 assert_equal 2, Issue.find(1).project_id
983 assert_equal 2, Issue.find(2).project_id
997 assert_equal 2, Issue.find(2).project_id
984 # No tracker change
998 # No tracker change
985 assert_equal 1, Issue.find(1).tracker_id
999 assert_equal 1, Issue.find(1).tracker_id
986 assert_equal 2, Issue.find(2).tracker_id
1000 assert_equal 2, Issue.find(2).tracker_id
987 end
1001 end
988
1002
989 def test_bulk_move_to_another_tracker
1003 def test_bulk_move_to_another_tracker
990 @request.session[:user_id] = 2
1004 @request.session[:user_id] = 2
991 post :move, :ids => [1, 2], :new_tracker_id => 2
1005 post :move, :ids => [1, 2], :new_tracker_id => 2
992 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1006 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
993 assert_equal 2, Issue.find(1).tracker_id
1007 assert_equal 2, Issue.find(1).tracker_id
994 assert_equal 2, Issue.find(2).tracker_id
1008 assert_equal 2, Issue.find(2).tracker_id
995 end
1009 end
996
1010
997 def test_bulk_copy_to_another_project
1011 def test_bulk_copy_to_another_project
998 @request.session[:user_id] = 2
1012 @request.session[:user_id] = 2
999 assert_difference 'Issue.count', 2 do
1013 assert_difference 'Issue.count', 2 do
1000 assert_no_difference 'Project.find(1).issues.count' do
1014 assert_no_difference 'Project.find(1).issues.count' do
1001 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1015 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1002 end
1016 end
1003 end
1017 end
1004 assert_redirected_to 'projects/ecookbook/issues'
1018 assert_redirected_to 'projects/ecookbook/issues'
1005 end
1019 end
1006
1020
1007 def test_copy_to_another_project_should_follow_when_needed
1021 def test_copy_to_another_project_should_follow_when_needed
1008 @request.session[:user_id] = 2
1022 @request.session[:user_id] = 2
1009 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1023 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1010 issue = Issue.first(:order => 'id DESC')
1024 issue = Issue.first(:order => 'id DESC')
1011 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1025 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1012 end
1026 end
1013
1027
1014 def test_context_menu_one_issue
1028 def test_context_menu_one_issue
1015 @request.session[:user_id] = 2
1029 @request.session[:user_id] = 2
1016 get :context_menu, :ids => [1]
1030 get :context_menu, :ids => [1]
1017 assert_response :success
1031 assert_response :success
1018 assert_template 'context_menu'
1032 assert_template 'context_menu'
1019 assert_tag :tag => 'a', :content => 'Edit',
1033 assert_tag :tag => 'a', :content => 'Edit',
1020 :attributes => { :href => '/issues/1/edit',
1034 :attributes => { :href => '/issues/1/edit',
1021 :class => 'icon-edit' }
1035 :class => 'icon-edit' }
1022 assert_tag :tag => 'a', :content => 'Closed',
1036 assert_tag :tag => 'a', :content => 'Closed',
1023 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1037 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1024 :class => '' }
1038 :class => '' }
1025 assert_tag :tag => 'a', :content => 'Immediate',
1039 assert_tag :tag => 'a', :content => 'Immediate',
1026 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1040 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1027 :class => '' }
1041 :class => '' }
1028 assert_tag :tag => 'a', :content => 'Dave Lopper',
1042 assert_tag :tag => 'a', :content => 'Dave Lopper',
1029 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1043 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1030 :class => '' }
1044 :class => '' }
1031 assert_tag :tag => 'a', :content => 'Copy',
1045 assert_tag :tag => 'a', :content => 'Copy',
1032 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1046 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1033 :class => 'icon-copy' }
1047 :class => 'icon-copy' }
1034 assert_tag :tag => 'a', :content => 'Move',
1048 assert_tag :tag => 'a', :content => 'Move',
1035 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1049 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1036 :class => 'icon-move' }
1050 :class => 'icon-move' }
1037 assert_tag :tag => 'a', :content => 'Delete',
1051 assert_tag :tag => 'a', :content => 'Delete',
1038 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1052 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1039 :class => 'icon-del' }
1053 :class => 'icon-del' }
1040 end
1054 end
1041
1055
1042 def test_context_menu_one_issue_by_anonymous
1056 def test_context_menu_one_issue_by_anonymous
1043 get :context_menu, :ids => [1]
1057 get :context_menu, :ids => [1]
1044 assert_response :success
1058 assert_response :success
1045 assert_template 'context_menu'
1059 assert_template 'context_menu'
1046 assert_tag :tag => 'a', :content => 'Delete',
1060 assert_tag :tag => 'a', :content => 'Delete',
1047 :attributes => { :href => '#',
1061 :attributes => { :href => '#',
1048 :class => 'icon-del disabled' }
1062 :class => 'icon-del disabled' }
1049 end
1063 end
1050
1064
1051 def test_context_menu_multiple_issues_of_same_project
1065 def test_context_menu_multiple_issues_of_same_project
1052 @request.session[:user_id] = 2
1066 @request.session[:user_id] = 2
1053 get :context_menu, :ids => [1, 2]
1067 get :context_menu, :ids => [1, 2]
1054 assert_response :success
1068 assert_response :success
1055 assert_template 'context_menu'
1069 assert_template 'context_menu'
1056 assert_tag :tag => 'a', :content => 'Edit',
1070 assert_tag :tag => 'a', :content => 'Edit',
1057 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1071 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1058 :class => 'icon-edit' }
1072 :class => 'icon-edit' }
1059 assert_tag :tag => 'a', :content => 'Immediate',
1073 assert_tag :tag => 'a', :content => 'Immediate',
1060 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1074 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1061 :class => '' }
1075 :class => '' }
1062 assert_tag :tag => 'a', :content => 'Dave Lopper',
1076 assert_tag :tag => 'a', :content => 'Dave Lopper',
1063 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1077 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1064 :class => '' }
1078 :class => '' }
1065 assert_tag :tag => 'a', :content => 'Move',
1079 assert_tag :tag => 'a', :content => 'Move',
1066 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1080 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1067 :class => 'icon-move' }
1081 :class => 'icon-move' }
1068 assert_tag :tag => 'a', :content => 'Delete',
1082 assert_tag :tag => 'a', :content => 'Delete',
1069 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1083 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1070 :class => 'icon-del' }
1084 :class => 'icon-del' }
1071 end
1085 end
1072
1086
1073 def test_context_menu_multiple_issues_of_different_project
1087 def test_context_menu_multiple_issues_of_different_project
1074 @request.session[:user_id] = 2
1088 @request.session[:user_id] = 2
1075 get :context_menu, :ids => [1, 2, 4]
1089 get :context_menu, :ids => [1, 2, 4]
1076 assert_response :success
1090 assert_response :success
1077 assert_template 'context_menu'
1091 assert_template 'context_menu'
1078 assert_tag :tag => 'a', :content => 'Delete',
1092 assert_tag :tag => 'a', :content => 'Delete',
1079 :attributes => { :href => '#',
1093 :attributes => { :href => '#',
1080 :class => 'icon-del disabled' }
1094 :class => 'icon-del disabled' }
1081 end
1095 end
1082
1096
1083 def test_destroy_routing
1097 def test_destroy_routing
1084 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1098 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1085 {:controller => 'issues', :action => 'destroy', :id => '1'},
1099 {:controller => 'issues', :action => 'destroy', :id => '1'},
1086 {:method => :post, :path => '/issues/1/destroy'}
1100 {:method => :post, :path => '/issues/1/destroy'}
1087 )
1101 )
1088 end
1102 end
1089
1103
1090 def test_destroy_issue_with_no_time_entries
1104 def test_destroy_issue_with_no_time_entries
1091 assert_nil TimeEntry.find_by_issue_id(2)
1105 assert_nil TimeEntry.find_by_issue_id(2)
1092 @request.session[:user_id] = 2
1106 @request.session[:user_id] = 2
1093 post :destroy, :id => 2
1107 post :destroy, :id => 2
1094 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1108 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1095 assert_nil Issue.find_by_id(2)
1109 assert_nil Issue.find_by_id(2)
1096 end
1110 end
1097
1111
1098 def test_destroy_issues_with_time_entries
1112 def test_destroy_issues_with_time_entries
1099 @request.session[:user_id] = 2
1113 @request.session[:user_id] = 2
1100 post :destroy, :ids => [1, 3]
1114 post :destroy, :ids => [1, 3]
1101 assert_response :success
1115 assert_response :success
1102 assert_template 'destroy'
1116 assert_template 'destroy'
1103 assert_not_nil assigns(:hours)
1117 assert_not_nil assigns(:hours)
1104 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1118 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1105 end
1119 end
1106
1120
1107 def test_destroy_issues_and_destroy_time_entries
1121 def test_destroy_issues_and_destroy_time_entries
1108 @request.session[:user_id] = 2
1122 @request.session[:user_id] = 2
1109 post :destroy, :ids => [1, 3], :todo => 'destroy'
1123 post :destroy, :ids => [1, 3], :todo => 'destroy'
1110 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1124 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1111 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1125 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1112 assert_nil TimeEntry.find_by_id([1, 2])
1126 assert_nil TimeEntry.find_by_id([1, 2])
1113 end
1127 end
1114
1128
1115 def test_destroy_issues_and_assign_time_entries_to_project
1129 def test_destroy_issues_and_assign_time_entries_to_project
1116 @request.session[:user_id] = 2
1130 @request.session[:user_id] = 2
1117 post :destroy, :ids => [1, 3], :todo => 'nullify'
1131 post :destroy, :ids => [1, 3], :todo => 'nullify'
1118 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1132 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1119 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1133 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1120 assert_nil TimeEntry.find(1).issue_id
1134 assert_nil TimeEntry.find(1).issue_id
1121 assert_nil TimeEntry.find(2).issue_id
1135 assert_nil TimeEntry.find(2).issue_id
1122 end
1136 end
1123
1137
1124 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1138 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1125 @request.session[:user_id] = 2
1139 @request.session[:user_id] = 2
1126 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1140 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1127 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1141 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1128 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1142 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1129 assert_equal 2, TimeEntry.find(1).issue_id
1143 assert_equal 2, TimeEntry.find(1).issue_id
1130 assert_equal 2, TimeEntry.find(2).issue_id
1144 assert_equal 2, TimeEntry.find(2).issue_id
1131 end
1145 end
1132
1146
1133 def test_default_search_scope
1147 def test_default_search_scope
1134 get :index
1148 get :index
1135 assert_tag :div, :attributes => {:id => 'quick-search'},
1149 assert_tag :div, :attributes => {:id => 'quick-search'},
1136 :child => {:tag => 'form',
1150 :child => {:tag => 'form',
1137 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1151 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1138 end
1152 end
1139 end
1153 end
@@ -1,301 +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
201 q = Query.new
202 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
203 end
204
200 def test_default_sort
205 def test_default_sort
201 q = Query.new
206 q = Query.new
202 assert_equal [], q.sort_criteria
207 assert_equal [], q.sort_criteria
203 end
208 end
204
209
205 def test_set_sort_criteria_with_hash
210 def test_set_sort_criteria_with_hash
206 q = Query.new
211 q = Query.new
207 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
212 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
208 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
213 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
209 end
214 end
210
215
211 def test_set_sort_criteria_with_array
216 def test_set_sort_criteria_with_array
212 q = Query.new
217 q = Query.new
213 q.sort_criteria = [['priority', 'desc'], 'tracker']
218 q.sort_criteria = [['priority', 'desc'], 'tracker']
214 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
219 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
215 end
220 end
216
221
217 def test_create_query_with_sort
222 def test_create_query_with_sort
218 q = Query.new(:name => 'Sorted')
223 q = Query.new(:name => 'Sorted')
219 q.sort_criteria = [['priority', 'desc'], 'tracker']
224 q.sort_criteria = [['priority', 'desc'], 'tracker']
220 assert q.save
225 assert q.save
221 q.reload
226 q.reload
222 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
223 end
228 end
224
229
225 def test_sort_by_string_custom_field_asc
230 def test_sort_by_string_custom_field_asc
226 q = Query.new
231 q = Query.new
227 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' }
228 assert c
233 assert c
229 assert c.sortable
234 assert c.sortable
230 issues = Issue.find :all,
235 issues = Issue.find :all,
231 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
236 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
232 :conditions => q.statement,
237 :conditions => q.statement,
233 :order => "#{c.sortable} ASC"
238 :order => "#{c.sortable} ASC"
234 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}
235 assert !values.empty?
240 assert !values.empty?
236 assert_equal values.sort, values
241 assert_equal values.sort, values
237 end
242 end
238
243
239 def test_sort_by_string_custom_field_desc
244 def test_sort_by_string_custom_field_desc
240 q = Query.new
245 q = Query.new
241 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' }
242 assert c
247 assert c
243 assert c.sortable
248 assert c.sortable
244 issues = Issue.find :all,
249 issues = Issue.find :all,
245 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
250 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
246 :conditions => q.statement,
251 :conditions => q.statement,
247 :order => "#{c.sortable} DESC"
252 :order => "#{c.sortable} DESC"
248 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}
249 assert !values.empty?
254 assert !values.empty?
250 assert_equal values.sort.reverse, values
255 assert_equal values.sort.reverse, values
251 end
256 end
252
257
253 def test_sort_by_float_custom_field_asc
258 def test_sort_by_float_custom_field_asc
254 q = Query.new
259 q = Query.new
255 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' }
256 assert c
261 assert c
257 assert c.sortable
262 assert c.sortable
258 issues = Issue.find :all,
263 issues = Issue.find :all,
259 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
264 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
260 :conditions => q.statement,
265 :conditions => q.statement,
261 :order => "#{c.sortable} ASC"
266 :order => "#{c.sortable} ASC"
262 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
263 assert !values.empty?
268 assert !values.empty?
264 assert_equal values.sort, values
269 assert_equal values.sort, values
265 end
270 end
266
271
267 def test_label_for
272 def test_label_for
268 q = Query.new
273 q = Query.new
269 assert_equal 'assigned_to', q.label_for('assigned_to_id')
274 assert_equal 'assigned_to', q.label_for('assigned_to_id')
270 end
275 end
271
276
272 def test_editable_by
277 def test_editable_by
273 admin = User.find(1)
278 admin = User.find(1)
274 manager = User.find(2)
279 manager = User.find(2)
275 developer = User.find(3)
280 developer = User.find(3)
276
281
277 # Public query on project 1
282 # Public query on project 1
278 q = Query.find(1)
283 q = Query.find(1)
279 assert q.editable_by?(admin)
284 assert q.editable_by?(admin)
280 assert q.editable_by?(manager)
285 assert q.editable_by?(manager)
281 assert !q.editable_by?(developer)
286 assert !q.editable_by?(developer)
282
287
283 # Private query on project 1
288 # Private query on project 1
284 q = Query.find(2)
289 q = Query.find(2)
285 assert q.editable_by?(admin)
290 assert q.editable_by?(admin)
286 assert !q.editable_by?(manager)
291 assert !q.editable_by?(manager)
287 assert q.editable_by?(developer)
292 assert q.editable_by?(developer)
288
293
289 # Private query for all projects
294 # Private query for all projects
290 q = Query.find(3)
295 q = Query.find(3)
291 assert q.editable_by?(admin)
296 assert q.editable_by?(admin)
292 assert !q.editable_by?(manager)
297 assert !q.editable_by?(manager)
293 assert q.editable_by?(developer)
298 assert q.editable_by?(developer)
294
299
295 # Public query for all projects
300 # Public query for all projects
296 q = Query.find(4)
301 q = Query.find(4)
297 assert q.editable_by?(admin)
302 assert q.editable_by?(admin)
298 assert !q.editable_by?(manager)
303 assert !q.editable_by?(manager)
299 assert !q.editable_by?(developer)
304 assert !q.editable_by?(developer)
300 end
305 end
301 end
306 end
General Comments 0
You need to be logged in to leave comments. Login now