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