##// END OF EJS Templates
Allows project to be changed from the regular issue update action (#4769, #9803)....
Jean-Philippe Lang -
r8411:81cf6b234397
parent child
Show More
@@ -1,351 +1,361
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => [:new, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 25 before_filter :find_project, :only => [:new, :create]
26 26 before_filter :authorize, :except => [:index]
27 27 before_filter :find_optional_project, :only => [:index]
28 28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 30 accept_rss_auth :index, :show
31 31 accept_api_auth :index, :show, :create, :update, :destroy
32 32
33 33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34 34
35 35 helper :journals
36 36 helper :projects
37 37 include ProjectsHelper
38 38 helper :custom_fields
39 39 include CustomFieldsHelper
40 40 helper :issue_relations
41 41 include IssueRelationsHelper
42 42 helper :watchers
43 43 include WatchersHelper
44 44 helper :attachments
45 45 include AttachmentsHelper
46 46 helper :queries
47 47 include QueriesHelper
48 48 helper :repositories
49 49 include RepositoriesHelper
50 50 helper :sort
51 51 include SortHelper
52 52 include IssuesHelper
53 53 helper :timelog
54 54 helper :gantt
55 55 include Redmine::Export::PDF
56 56
57 57 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 58 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 59 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60 60
61 61 def index
62 62 retrieve_query
63 63 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 64 sort_update(@query.sortable_columns)
65 65
66 66 if @query.valid?
67 67 case params[:format]
68 68 when 'csv', 'pdf'
69 69 @limit = Setting.issues_export_limit.to_i
70 70 when 'atom'
71 71 @limit = Setting.feeds_limit.to_i
72 72 when 'xml', 'json'
73 73 @offset, @limit = api_offset_and_limit
74 74 else
75 75 @limit = per_page_option
76 76 end
77 77
78 78 @issue_count = @query.issue_count
79 79 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
80 80 @offset ||= @issue_pages.current.offset
81 81 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
82 82 :order => sort_clause,
83 83 :offset => @offset,
84 84 :limit => @limit)
85 85 @issue_count_by_group = @query.issue_count_by_group
86 86
87 87 respond_to do |format|
88 88 format.html { render :template => 'issues/index', :layout => !request.xhr? }
89 89 format.api {
90 90 Issue.load_relations(@issues) if include_in_api_response?('relations')
91 91 }
92 92 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
93 93 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
94 94 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
95 95 end
96 96 else
97 97 respond_to do |format|
98 98 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
99 99 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
100 100 format.api { render_validation_errors(@query) }
101 101 end
102 102 end
103 103 rescue ActiveRecord::RecordNotFound
104 104 render_404
105 105 end
106 106
107 107 def show
108 108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 109 @journals.each_with_index {|j,i| j.indice = i+1}
110 110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111 111
112 112 if User.current.allowed_to?(:view_changesets, @project)
113 113 @changesets = @issue.changesets.visible.all
114 114 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 115 end
116 116
117 117 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
118 118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 119 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 120 @priorities = IssuePriority.active
121 121 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
122 122 respond_to do |format|
123 123 format.html {
124 124 retrieve_previous_and_next_issue_ids
125 125 render :template => 'issues/show'
126 126 }
127 127 format.api
128 128 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
129 129 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
130 130 end
131 131 end
132 132
133 133 # Add a new issue
134 134 # The new issue will be created from an existing one if copy_from parameter is given
135 135 def new
136 136 respond_to do |format|
137 137 format.html { render :action => 'new', :layout => !request.xhr? }
138 format.js { render :partial => 'attributes' }
138 format.js {
139 render(:update) { |page|
140 if params[:project_change]
141 page.replace_html 'all_attributes', :partial => 'form'
142 else
143 page.replace_html 'attributes', :partial => 'attributes'
144 end
145 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
146 page << "if ($('log_time')) {Element.#{m}('log_time');}"
147 }
148 }
139 149 end
140 150 end
141 151
142 152 def create
143 153 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
144 154 if @issue.save
145 155 attachments = Attachment.attach_files(@issue, params[:attachments])
146 156 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
147 157 respond_to do |format|
148 158 format.html {
149 159 render_attachment_warning_if_needed(@issue)
150 160 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
151 161 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
152 162 { :action => 'show', :id => @issue })
153 163 }
154 164 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
155 165 end
156 166 return
157 167 else
158 168 respond_to do |format|
159 169 format.html { render :action => 'new' }
160 170 format.api { render_validation_errors(@issue) }
161 171 end
162 172 end
163 173 end
164 174
165 175 def edit
166 176 update_issue_from_params
167 177
168 178 @journal = @issue.current_journal
169 179
170 180 respond_to do |format|
171 181 format.html { }
172 182 format.xml { }
173 183 end
174 184 end
175 185
176 186 def update
177 187 update_issue_from_params
178 188
179 189 if @issue.save_issue_with_child_records(params, @time_entry)
180 190 render_attachment_warning_if_needed(@issue)
181 191 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
182 192
183 193 respond_to do |format|
184 194 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
185 195 format.api { head :ok }
186 196 end
187 197 else
188 198 render_attachment_warning_if_needed(@issue)
189 199 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
190 200 @journal = @issue.current_journal
191 201
192 202 respond_to do |format|
193 203 format.html { render :action => 'edit' }
194 204 format.api { render_validation_errors(@issue) }
195 205 end
196 206 end
197 207 end
198 208
199 209 # Bulk edit a set of issues
200 210 def bulk_edit
201 211 @issues.sort!
202 212 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
203 213 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
204 214 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
205 215 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
206 216 end
207 217
208 218 def bulk_update
209 219 @issues.sort!
210 220 attributes = parse_params_for_bulk_issue_attributes(params)
211 221
212 222 unsaved_issue_ids = []
213 223 @issues.each do |issue|
214 224 issue.reload
215 225 journal = issue.init_journal(User.current, params[:notes])
216 226 issue.safe_attributes = attributes
217 227 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
218 228 unless issue.save
219 229 # Keep unsaved issue ids to display them in flash error
220 230 unsaved_issue_ids << issue.id
221 231 end
222 232 end
223 233 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
224 234 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
225 235 end
226 236
227 237 verify :method => :delete, :only => :destroy, :render => { :nothing => true, :status => :method_not_allowed }
228 238 def destroy
229 239 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
230 240 if @hours > 0
231 241 case params[:todo]
232 242 when 'destroy'
233 243 # nothing to do
234 244 when 'nullify'
235 245 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
236 246 when 'reassign'
237 247 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
238 248 if reassign_to.nil?
239 249 flash.now[:error] = l(:error_issue_not_found_in_project)
240 250 return
241 251 else
242 252 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
243 253 end
244 254 else
245 255 # display the destroy form if it's a user request
246 256 return unless api_request?
247 257 end
248 258 end
249 259 @issues.each do |issue|
250 260 begin
251 261 issue.reload.destroy
252 262 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
253 263 # nothing to do, issue was already deleted (eg. by a parent)
254 264 end
255 265 end
256 266 respond_to do |format|
257 267 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
258 268 format.api { head :ok }
259 269 end
260 270 end
261 271
262 272 private
263 273 def find_issue
264 274 # Issue.visible.find(...) can not be used to redirect user to the login form
265 275 # if the issue actually exists but requires authentication
266 276 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
267 277 unless @issue.visible?
268 278 deny_access
269 279 return
270 280 end
271 281 @project = @issue.project
272 282 rescue ActiveRecord::RecordNotFound
273 283 render_404
274 284 end
275 285
276 286 def find_project
277 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
287 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
278 288 @project = Project.find(project_id)
279 289 rescue ActiveRecord::RecordNotFound
280 290 render_404
281 291 end
282 292
283 293 def retrieve_previous_and_next_issue_ids
284 294 retrieve_query_from_session
285 295 if @query
286 296 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
287 297 sort_update(@query.sortable_columns, 'issues_index_sort')
288 298 limit = 500
289 299 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1))
290 300 if (idx = issue_ids.index(@issue.id)) && idx < limit
291 301 @prev_issue_id = issue_ids[idx - 1] if idx > 0
292 302 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
293 303 end
294 304 end
295 305 end
296 306
297 307 # Used by #edit and #update to set some common instance variables
298 308 # from the params
299 309 # TODO: Refactor, not everything in here is needed by #edit
300 310 def update_issue_from_params
301 311 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
302 312 @priorities = IssuePriority.active
303 313 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
304 314 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
305 315 @time_entry.attributes = params[:time_entry]
306 316
307 317 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
308 318 @issue.init_journal(User.current, @notes)
309 319 @issue.safe_attributes = params[:issue]
310 320 end
311 321
312 322 # TODO: Refactor, lots of extra code in here
313 323 # TODO: Changing tracker on an existing issue should not trigger this
314 324 def build_new_issue_from_params
315 325 if params[:id].blank?
316 326 @issue = Issue.new
317 327 @issue.copy_from(params[:copy_from]) if params[:copy_from]
318 328 @issue.project = @project
319 329 else
320 330 @issue = @project.issues.visible.find(params[:id])
321 331 end
322 332
323 333 @issue.project = @project
324 334 @issue.author = User.current
325 335 # Tracker must be set before custom field values
326 336 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
327 337 if @issue.tracker.nil?
328 338 render_error l(:error_no_tracker_in_project)
329 339 return false
330 340 end
331 341 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
332 342 @issue.safe_attributes = params[:issue]
333 343
334 344 @priorities = IssuePriority.active
335 345 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
336 346 end
337 347
338 348 def check_for_default_issue_status
339 349 if IssueStatus.default.nil?
340 350 render_error l(:error_no_default_issue_status)
341 351 return false
342 352 end
343 353 end
344 354
345 355 def parse_params_for_bulk_issue_attributes(params)
346 356 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
347 357 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
348 358 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
349 359 attributes
350 360 end
351 361 end
@@ -1,982 +1,1008
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 61 validate :validate_issue
62 62
63 63 named_scope :visible, lambda {|*args| { :include => :project,
64 64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 65
66 66 named_scope :open, lambda {|*args|
67 67 is_closed = args.size > 0 ? !args.first : false
68 68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 69 }
70 70
71 71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75 75
76 76 before_create :default_assign
77 77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 80 after_destroy :update_parent_attributes
81 81
82 82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 83 def self.visible_condition(user, options={})
84 84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 85 case role.issues_visibility
86 86 when 'all'
87 87 nil
88 88 when 'default'
89 89 user_ids = [user.id] + user.groups.map(&:id)
90 90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
91 91 when 'own'
92 92 user_ids = [user.id] + user.groups.map(&:id)
93 93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 94 else
95 95 '1=0'
96 96 end
97 97 end
98 98 end
99 99
100 100 # Returns true if usr or current user is allowed to view the issue
101 101 def visible?(usr=nil)
102 102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 103 case role.issues_visibility
104 104 when 'all'
105 105 true
106 106 when 'default'
107 107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 108 when 'own'
109 109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 110 else
111 111 false
112 112 end
113 113 end
114 114 end
115 115
116 116 def initialize(attributes=nil, *args)
117 117 super
118 118 if new_record?
119 119 # set default values for new records only
120 120 self.status ||= IssueStatus.default
121 121 self.priority ||= IssuePriority.default
122 122 end
123 123 end
124 124
125 125 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
126 126 def available_custom_fields
127 127 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
128 128 end
129 129
130 130 def copy_from(arg)
131 131 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
132 132 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
133 133 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
134 134 self.status = issue.status
135 135 self.author = User.current
136 136 self
137 137 end
138 138
139 139 # Moves/copies an issue to a new project and tracker
140 140 # Returns the moved/copied issue on success, false on failure
141 141 def move_to_project(new_project, new_tracker=nil, options={})
142 142 if options[:copy]
143 143 issue = self.class.new.copy_from(self)
144 144 else
145 145 issue = self
146 146 end
147 147
148 148 issue.init_journal(User.current, options[:notes])
149 149
150 issue.project = new_project
150 # Preserve previous behaviour
151 # #move_to_project doesn't change tracker automatically
152 issue.send :project=, new_project, true
151 153 if new_tracker
152 154 issue.tracker = new_tracker
153 155 end
154 156 # Allow bulk setting of attributes on the issue
155 157 if options[:attributes]
156 158 issue.attributes = options[:attributes]
157 159 end
158 160
159 161 issue.save ? issue : false
160 162 end
161 163
162 164 def status_id=(sid)
163 165 self.status = nil
164 166 write_attribute(:status_id, sid)
165 167 end
166 168
167 169 def priority_id=(pid)
168 170 self.priority = nil
169 171 write_attribute(:priority_id, pid)
170 172 end
171 173
174 def category_id=(cid)
175 self.category = nil
176 write_attribute(:category_id, cid)
177 end
178
179 def fixed_version_id=(vid)
180 self.fixed_version = nil
181 write_attribute(:fixed_version_id, vid)
182 end
183
172 184 def tracker_id=(tid)
173 185 self.tracker = nil
174 186 result = write_attribute(:tracker_id, tid)
175 187 @custom_field_values = nil
176 188 result
177 189 end
178 190
179 191 def project_id=(project_id)
180 192 if project_id.to_s != self.project_id.to_s
181 193 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
182 194 end
183 195 end
184 196
185 def project=(project)
197 def project=(project, keep_tracker=false)
186 198 project_was = self.project
187 199 write_attribute(:project_id, project ? project.id : nil)
188 200 association_instance_set('project', project)
189 201 if project_was && project && project_was != project
202 unless keep_tracker || project.trackers.include?(tracker)
203 self.tracker = project.trackers.first
204 end
190 205 # Reassign to the category with same name if any
191 206 if category
192 207 self.category = project.issue_categories.find_by_name(category.name)
193 208 end
194 209 # Keep the fixed_version if it's still valid in the new_project
195 210 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
196 211 self.fixed_version = nil
197 212 end
198 213 if parent && parent.project_id != project_id
199 214 self.parent_issue_id = nil
200 215 end
201 216 @custom_field_values = nil
202 217 end
203 218 end
204 219
205 220 def description=(arg)
206 221 if arg.is_a?(String)
207 222 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
208 223 end
209 224 write_attribute(:description, arg)
210 225 end
211 226
212 227 # Overrides attributes= so that project and tracker get assigned first
213 228 def attributes_with_project_and_tracker_first=(new_attributes, *args)
214 229 return if new_attributes.nil?
215 230 attrs = new_attributes.dup
216 231 attrs.stringify_keys!
217 232
218 233 %w(project project_id tracker tracker_id).each do |attr|
219 234 if attrs.has_key?(attr)
220 235 send "#{attr}=", attrs.delete(attr)
221 236 end
222 237 end
223 238 send :attributes_without_project_and_tracker_first=, attrs, *args
224 239 end
225 240 # Do not redefine alias chain on reload (see #4838)
226 241 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
227 242
228 243 def estimated_hours=(h)
229 244 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
230 245 end
231 246
247 safe_attributes 'project_id',
248 :if => lambda {|issue, user|
249 projects = Issue.allowed_target_projects_on_move(user)
250 projects.include?(issue.project) && projects.size > 1
251 }
252
232 253 safe_attributes 'tracker_id',
233 254 'status_id',
234 255 'category_id',
235 256 'assigned_to_id',
236 257 'priority_id',
237 258 'fixed_version_id',
238 259 'subject',
239 260 'description',
240 261 'start_date',
241 262 'due_date',
242 263 'done_ratio',
243 264 'estimated_hours',
244 265 'custom_field_values',
245 266 'custom_fields',
246 267 'lock_version',
247 268 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
248 269
249 270 safe_attributes 'status_id',
250 271 'assigned_to_id',
251 272 'fixed_version_id',
252 273 'done_ratio',
253 274 'lock_version',
254 275 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
255 276
256 277 safe_attributes 'watcher_user_ids',
257 278 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
258 279
259 280 safe_attributes 'is_private',
260 281 :if => lambda {|issue, user|
261 282 user.allowed_to?(:set_issues_private, issue.project) ||
262 283 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
263 284 }
264 285
265 286 safe_attributes 'parent_issue_id',
266 287 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
267 288 user.allowed_to?(:manage_subtasks, issue.project)}
268 289
269 290 # Safely sets attributes
270 291 # Should be called from controllers instead of #attributes=
271 292 # attr_accessible is too rough because we still want things like
272 293 # Issue.new(:project => foo) to work
273 294 # TODO: move workflow/permission checks from controllers to here
274 295 def safe_attributes=(attrs, user=User.current)
275 296 return unless attrs.is_a?(Hash)
276 297
277 298 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
278 299 attrs = delete_unsafe_attributes(attrs, user)
279 300 return if attrs.empty?
280 301
281 # Tracker must be set before since new_statuses_allowed_to depends on it.
302 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
303 if p = attrs.delete('project_id')
304 self.project_id = p
305 end
306
282 307 if t = attrs.delete('tracker_id')
283 308 self.tracker_id = t
284 309 end
285 310
286 311 if attrs['status_id']
287 312 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
288 313 attrs.delete('status_id')
289 314 end
290 315 end
291 316
292 317 unless leaf?
293 318 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
294 319 end
295 320
296 321 if attrs['parent_issue_id'].present?
297 322 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
298 323 end
299 324
300 325 # mass-assignment security bypass
301 326 self.send :attributes=, attrs, false
302 327 end
303 328
304 329 def done_ratio
305 330 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
306 331 status.default_done_ratio
307 332 else
308 333 read_attribute(:done_ratio)
309 334 end
310 335 end
311 336
312 337 def self.use_status_for_done_ratio?
313 338 Setting.issue_done_ratio == 'issue_status'
314 339 end
315 340
316 341 def self.use_field_for_done_ratio?
317 342 Setting.issue_done_ratio == 'issue_field'
318 343 end
319 344
320 345 def validate_issue
321 346 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
322 347 errors.add :due_date, :not_a_date
323 348 end
324 349
325 350 if self.due_date and self.start_date and self.due_date < self.start_date
326 351 errors.add :due_date, :greater_than_start_date
327 352 end
328 353
329 354 if start_date && soonest_start && start_date < soonest_start
330 355 errors.add :start_date, :invalid
331 356 end
332 357
333 358 if fixed_version
334 359 if !assignable_versions.include?(fixed_version)
335 360 errors.add :fixed_version_id, :inclusion
336 361 elsif reopened? && fixed_version.closed?
337 362 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
338 363 end
339 364 end
340 365
341 366 # Checks that the issue can not be added/moved to a disabled tracker
342 367 if project && (tracker_id_changed? || project_id_changed?)
343 368 unless project.trackers.include?(tracker)
344 369 errors.add :tracker_id, :inclusion
345 370 end
346 371 end
347 372
348 373 # Checks parent issue assignment
349 374 if @parent_issue
350 375 if @parent_issue.project_id != project_id
351 376 errors.add :parent_issue_id, :not_same_project
352 377 elsif !new_record?
353 378 # moving an existing issue
354 379 if @parent_issue.root_id != root_id
355 380 # we can always move to another tree
356 381 elsif move_possible?(@parent_issue)
357 382 # move accepted inside tree
358 383 else
359 384 errors.add :parent_issue_id, :not_a_valid_parent
360 385 end
361 386 end
362 387 end
363 388 end
364 389
365 390 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
366 391 # even if the user turns off the setting later
367 392 def update_done_ratio_from_issue_status
368 393 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
369 394 self.done_ratio = status.default_done_ratio
370 395 end
371 396 end
372 397
373 398 def init_journal(user, notes = "")
374 399 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
375 400 if new_record?
376 401 @current_journal.notify = false
377 402 else
378 403 @attributes_before_change = attributes.dup
379 404 @custom_values_before_change = {}
380 405 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
381 406 end
382 407 # Make sure updated_on is updated when adding a note.
383 408 updated_on_will_change!
384 409 @current_journal
385 410 end
386 411
387 412 # Return true if the issue is closed, otherwise false
388 413 def closed?
389 414 self.status.is_closed?
390 415 end
391 416
392 417 # Return true if the issue is being reopened
393 418 def reopened?
394 419 if !new_record? && status_id_changed?
395 420 status_was = IssueStatus.find_by_id(status_id_was)
396 421 status_new = IssueStatus.find_by_id(status_id)
397 422 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
398 423 return true
399 424 end
400 425 end
401 426 false
402 427 end
403 428
404 429 # Return true if the issue is being closed
405 430 def closing?
406 431 if !new_record? && status_id_changed?
407 432 status_was = IssueStatus.find_by_id(status_id_was)
408 433 status_new = IssueStatus.find_by_id(status_id)
409 434 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
410 435 return true
411 436 end
412 437 end
413 438 false
414 439 end
415 440
416 441 # Returns true if the issue is overdue
417 442 def overdue?
418 443 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
419 444 end
420 445
421 446 # Is the amount of work done less than it should for the due date
422 447 def behind_schedule?
423 448 return false if start_date.nil? || due_date.nil?
424 449 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
425 450 return done_date <= Date.today
426 451 end
427 452
428 453 # Does this issue have children?
429 454 def children?
430 455 !leaf?
431 456 end
432 457
433 458 # Users the issue can be assigned to
434 459 def assignable_users
435 460 users = project.assignable_users
436 461 users << author if author
437 462 users << assigned_to if assigned_to
438 463 users.uniq.sort
439 464 end
440 465
441 466 # Versions that the issue can be assigned to
442 467 def assignable_versions
443 468 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
444 469 end
445 470
446 471 # Returns true if this issue is blocked by another issue that is still open
447 472 def blocked?
448 473 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
449 474 end
450 475
451 476 # Returns an array of status that user is able to apply
452 477 def new_statuses_allowed_to(user, include_default=false)
453 478 statuses = status.find_new_statuses_allowed_to(
454 479 user.roles_for_project(project),
455 480 tracker,
456 481 author == user,
457 482 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
458 483 )
459 484 statuses << status unless statuses.empty?
460 485 statuses << IssueStatus.default if include_default
461 486 statuses = statuses.uniq.sort
462 487 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
463 488 end
464 489
465 490 # Returns the mail adresses of users that should be notified
466 491 def recipients
467 492 notified = project.notified_users
468 493 # Author and assignee are always notified unless they have been
469 494 # locked or don't want to be notified
470 495 notified << author if author && author.active? && author.notify_about?(self)
471 496 if assigned_to
472 497 if assigned_to.is_a?(Group)
473 498 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
474 499 else
475 500 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
476 501 end
477 502 end
478 503 notified.uniq!
479 504 # Remove users that can not view the issue
480 505 notified.reject! {|user| !visible?(user)}
481 506 notified.collect(&:mail)
482 507 end
483 508
484 509 # Returns the number of hours spent on this issue
485 510 def spent_hours
486 511 @spent_hours ||= time_entries.sum(:hours) || 0
487 512 end
488 513
489 514 # Returns the total number of hours spent on this issue and its descendants
490 515 #
491 516 # Example:
492 517 # spent_hours => 0.0
493 518 # spent_hours => 50.2
494 519 def total_spent_hours
495 520 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
496 521 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
497 522 end
498 523
499 524 def relations
500 525 @relations ||= (relations_from + relations_to).sort
501 526 end
502 527
503 528 # Preloads relations for a collection of issues
504 529 def self.load_relations(issues)
505 530 if issues.any?
506 531 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
507 532 issues.each do |issue|
508 533 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
509 534 end
510 535 end
511 536 end
512 537
513 538 # Preloads visible spent time for a collection of issues
514 539 def self.load_visible_spent_hours(issues, user=User.current)
515 540 if issues.any?
516 541 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
517 542 issues.each do |issue|
518 543 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
519 544 end
520 545 end
521 546 end
522 547
523 548 # Finds an issue relation given its id.
524 549 def find_relation(relation_id)
525 550 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
526 551 end
527 552
528 553 def all_dependent_issues(except=[])
529 554 except << self
530 555 dependencies = []
531 556 relations_from.each do |relation|
532 557 if relation.issue_to && !except.include?(relation.issue_to)
533 558 dependencies << relation.issue_to
534 559 dependencies += relation.issue_to.all_dependent_issues(except)
535 560 end
536 561 end
537 562 dependencies
538 563 end
539 564
540 565 # Returns an array of issues that duplicate this one
541 566 def duplicates
542 567 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
543 568 end
544 569
545 570 # Returns the due date or the target due date if any
546 571 # Used on gantt chart
547 572 def due_before
548 573 due_date || (fixed_version ? fixed_version.effective_date : nil)
549 574 end
550 575
551 576 # Returns the time scheduled for this issue.
552 577 #
553 578 # Example:
554 579 # Start Date: 2/26/09, End Date: 3/04/09
555 580 # duration => 6
556 581 def duration
557 582 (start_date && due_date) ? due_date - start_date : 0
558 583 end
559 584
560 585 def soonest_start
561 586 @soonest_start ||= (
562 587 relations_to.collect{|relation| relation.successor_soonest_start} +
563 588 ancestors.collect(&:soonest_start)
564 589 ).compact.max
565 590 end
566 591
567 592 def reschedule_after(date)
568 593 return if date.nil?
569 594 if leaf?
570 595 if start_date.nil? || start_date < date
571 596 self.start_date, self.due_date = date, date + duration
572 597 save
573 598 end
574 599 else
575 600 leaves.each do |leaf|
576 601 leaf.reschedule_after(date)
577 602 end
578 603 end
579 604 end
580 605
581 606 def <=>(issue)
582 607 if issue.nil?
583 608 -1
584 609 elsif root_id != issue.root_id
585 610 (root_id || 0) <=> (issue.root_id || 0)
586 611 else
587 612 (lft || 0) <=> (issue.lft || 0)
588 613 end
589 614 end
590 615
591 616 def to_s
592 617 "#{tracker} ##{id}: #{subject}"
593 618 end
594 619
595 620 # Returns a string of css classes that apply to the issue
596 621 def css_classes
597 622 s = "issue status-#{status.position} priority-#{priority.position}"
598 623 s << ' closed' if closed?
599 624 s << ' overdue' if overdue?
600 625 s << ' child' if child?
601 626 s << ' parent' unless leaf?
602 627 s << ' private' if is_private?
603 628 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
604 629 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
605 630 s
606 631 end
607 632
608 633 # Saves an issue, time_entry, attachments, and a journal from the parameters
609 634 # Returns false if save fails
610 635 def save_issue_with_child_records(params, existing_time_entry=nil)
611 636 Issue.transaction do
612 637 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
613 638 @time_entry = existing_time_entry || TimeEntry.new
614 639 @time_entry.project = project
615 640 @time_entry.issue = self
616 641 @time_entry.user = User.current
617 642 @time_entry.spent_on = User.current.today
618 643 @time_entry.attributes = params[:time_entry]
619 644 self.time_entries << @time_entry
620 645 end
621 646
622 647 if valid?
623 648 attachments = Attachment.attach_files(self, params[:attachments])
624 649 # TODO: Rename hook
625 650 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
626 651 begin
627 652 if save
628 653 # TODO: Rename hook
629 654 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
630 655 else
631 656 raise ActiveRecord::Rollback
632 657 end
633 658 rescue ActiveRecord::StaleObjectError
634 659 attachments[:files].each(&:destroy)
635 660 errors.add :base, l(:notice_locking_conflict)
636 661 raise ActiveRecord::Rollback
637 662 end
638 663 end
639 664 end
640 665 end
641 666
642 667 # Unassigns issues from +version+ if it's no longer shared with issue's project
643 668 def self.update_versions_from_sharing_change(version)
644 669 # Update issues assigned to the version
645 670 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
646 671 end
647 672
648 673 # Unassigns issues from versions that are no longer shared
649 674 # after +project+ was moved
650 675 def self.update_versions_from_hierarchy_change(project)
651 676 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
652 677 # Update issues of the moved projects and issues assigned to a version of a moved project
653 678 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
654 679 end
655 680
656 681 def parent_issue_id=(arg)
657 682 parent_issue_id = arg.blank? ? nil : arg.to_i
658 683 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
659 684 @parent_issue.id
660 685 else
661 686 @parent_issue = nil
662 687 nil
663 688 end
664 689 end
665 690
666 691 def parent_issue_id
667 692 if instance_variable_defined? :@parent_issue
668 693 @parent_issue.nil? ? nil : @parent_issue.id
669 694 else
670 695 parent_id
671 696 end
672 697 end
673 698
674 699 # Extracted from the ReportsController.
675 700 def self.by_tracker(project)
676 701 count_and_group_by(:project => project,
677 702 :field => 'tracker_id',
678 703 :joins => Tracker.table_name)
679 704 end
680 705
681 706 def self.by_version(project)
682 707 count_and_group_by(:project => project,
683 708 :field => 'fixed_version_id',
684 709 :joins => Version.table_name)
685 710 end
686 711
687 712 def self.by_priority(project)
688 713 count_and_group_by(:project => project,
689 714 :field => 'priority_id',
690 715 :joins => IssuePriority.table_name)
691 716 end
692 717
693 718 def self.by_category(project)
694 719 count_and_group_by(:project => project,
695 720 :field => 'category_id',
696 721 :joins => IssueCategory.table_name)
697 722 end
698 723
699 724 def self.by_assigned_to(project)
700 725 count_and_group_by(:project => project,
701 726 :field => 'assigned_to_id',
702 727 :joins => User.table_name)
703 728 end
704 729
705 730 def self.by_author(project)
706 731 count_and_group_by(:project => project,
707 732 :field => 'author_id',
708 733 :joins => User.table_name)
709 734 end
710 735
711 736 def self.by_subproject(project)
712 737 ActiveRecord::Base.connection.select_all("select s.id as status_id,
713 738 s.is_closed as closed,
714 739 #{Issue.table_name}.project_id as project_id,
715 740 count(#{Issue.table_name}.id) as total
716 741 from
717 742 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
718 743 where
719 744 #{Issue.table_name}.status_id=s.id
720 745 and #{Issue.table_name}.project_id = #{Project.table_name}.id
721 746 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
722 747 and #{Issue.table_name}.project_id <> #{project.id}
723 748 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
724 749 end
725 750 # End ReportsController extraction
726 751
727 752 # Returns an array of projects that current user can move issues to
728 def self.allowed_target_projects_on_move
753 def self.allowed_target_projects_on_move(user=User.current)
729 754 projects = []
730 if User.current.admin?
755 if user.admin?
731 756 # admin is allowed to move issues to any active (visible) project
732 projects = Project.visible.all
733 elsif User.current.logged?
757 projects = Project.visible(user).all
758 elsif user.logged?
734 759 if Role.non_member.allowed_to?(:move_issues)
735 projects = Project.visible.all
760 projects = Project.visible(user).all
736 761 else
737 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
762 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
738 763 end
739 764 end
740 765 projects
741 766 end
742 767
743 768 private
744 769
745 770 def after_project_change
746 771 # Update project_id on related time entries
747 772 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
748 773
749 774 # Delete issue relations
750 775 unless Setting.cross_project_issue_relations?
751 776 relations_from.clear
752 777 relations_to.clear
753 778 end
754 779
755 780 # Move subtasks
756 781 children.each do |child|
757 child.project = project
782 # Change project and keep project
783 child.send :project=, project, true
758 784 unless child.save
759 785 raise ActiveRecord::Rollback
760 786 end
761 787 end
762 788 end
763 789
764 790 def update_nested_set_attributes
765 791 if root_id.nil?
766 792 # issue was just created
767 793 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
768 794 set_default_left_and_right
769 795 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
770 796 if @parent_issue
771 797 move_to_child_of(@parent_issue)
772 798 end
773 799 reload
774 800 elsif parent_issue_id != parent_id
775 801 former_parent_id = parent_id
776 802 # moving an existing issue
777 803 if @parent_issue && @parent_issue.root_id == root_id
778 804 # inside the same tree
779 805 move_to_child_of(@parent_issue)
780 806 else
781 807 # to another tree
782 808 unless root?
783 809 move_to_right_of(root)
784 810 reload
785 811 end
786 812 old_root_id = root_id
787 813 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
788 814 target_maxright = nested_set_scope.maximum(right_column_name) || 0
789 815 offset = target_maxright + 1 - lft
790 816 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
791 817 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
792 818 self[left_column_name] = lft + offset
793 819 self[right_column_name] = rgt + offset
794 820 if @parent_issue
795 821 move_to_child_of(@parent_issue)
796 822 end
797 823 end
798 824 reload
799 825 # delete invalid relations of all descendants
800 826 self_and_descendants.each do |issue|
801 827 issue.relations.each do |relation|
802 828 relation.destroy unless relation.valid?
803 829 end
804 830 end
805 831 # update former parent
806 832 recalculate_attributes_for(former_parent_id) if former_parent_id
807 833 end
808 834 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
809 835 end
810 836
811 837 def update_parent_attributes
812 838 recalculate_attributes_for(parent_id) if parent_id
813 839 end
814 840
815 841 def recalculate_attributes_for(issue_id)
816 842 if issue_id && p = Issue.find_by_id(issue_id)
817 843 # priority = highest priority of children
818 844 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
819 845 p.priority = IssuePriority.find_by_position(priority_position)
820 846 end
821 847
822 848 # start/due dates = lowest/highest dates of children
823 849 p.start_date = p.children.minimum(:start_date)
824 850 p.due_date = p.children.maximum(:due_date)
825 851 if p.start_date && p.due_date && p.due_date < p.start_date
826 852 p.start_date, p.due_date = p.due_date, p.start_date
827 853 end
828 854
829 855 # done ratio = weighted average ratio of leaves
830 856 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
831 857 leaves_count = p.leaves.count
832 858 if leaves_count > 0
833 859 average = p.leaves.average(:estimated_hours).to_f
834 860 if average == 0
835 861 average = 1
836 862 end
837 863 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
838 864 progress = done / (average * leaves_count)
839 865 p.done_ratio = progress.round
840 866 end
841 867 end
842 868
843 869 # estimate = sum of leaves estimates
844 870 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
845 871 p.estimated_hours = nil if p.estimated_hours == 0.0
846 872
847 873 # ancestors will be recursively updated
848 874 p.save(false)
849 875 end
850 876 end
851 877
852 878 # Update issues so their versions are not pointing to a
853 879 # fixed_version that is not shared with the issue's project
854 880 def self.update_versions(conditions=nil)
855 881 # Only need to update issues with a fixed_version from
856 882 # a different project and that is not systemwide shared
857 883 Issue.scoped(:conditions => conditions).all(
858 884 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
859 885 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
860 886 " AND #{Version.table_name}.sharing <> 'system'",
861 887 :include => [:project, :fixed_version]
862 888 ).each do |issue|
863 889 next if issue.project.nil? || issue.fixed_version.nil?
864 890 unless issue.project.shared_versions.include?(issue.fixed_version)
865 891 issue.init_journal(User.current)
866 892 issue.fixed_version = nil
867 893 issue.save
868 894 end
869 895 end
870 896 end
871 897
872 898 # Callback on attachment deletion
873 899 def attachment_added(obj)
874 900 if @current_journal && !obj.new_record?
875 901 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
876 902 end
877 903 end
878 904
879 905 # Callback on attachment deletion
880 906 def attachment_removed(obj)
881 907 journal = init_journal(User.current)
882 908 journal.details << JournalDetail.new(:property => 'attachment',
883 909 :prop_key => obj.id,
884 910 :old_value => obj.filename)
885 911 journal.save
886 912 end
887 913
888 914 # Default assignment based on category
889 915 def default_assign
890 916 if assigned_to.nil? && category && category.assigned_to
891 917 self.assigned_to = category.assigned_to
892 918 end
893 919 end
894 920
895 921 # Updates start/due dates of following issues
896 922 def reschedule_following_issues
897 923 if start_date_changed? || due_date_changed?
898 924 relations_from.each do |relation|
899 925 relation.set_issue_to_dates
900 926 end
901 927 end
902 928 end
903 929
904 930 # Closes duplicates if the issue is being closed
905 931 def close_duplicates
906 932 if closing?
907 933 duplicates.each do |duplicate|
908 934 # Reload is need in case the duplicate was updated by a previous duplicate
909 935 duplicate.reload
910 936 # Don't re-close it if it's already closed
911 937 next if duplicate.closed?
912 938 # Same user and notes
913 939 if @current_journal
914 940 duplicate.init_journal(@current_journal.user, @current_journal.notes)
915 941 end
916 942 duplicate.update_attribute :status, self.status
917 943 end
918 944 end
919 945 end
920 946
921 947 # Saves the changes in a Journal
922 948 # Called after_save
923 949 def create_journal
924 950 if @current_journal
925 951 # attributes changes
926 952 if @attributes_before_change
927 953 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
928 954 before = @attributes_before_change[c]
929 955 after = send(c)
930 956 next if before == after || (before.blank? && after.blank?)
931 957 @current_journal.details << JournalDetail.new(:property => 'attr',
932 958 :prop_key => c,
933 959 :old_value => before,
934 960 :value => after)
935 961 }
936 962 end
937 963 if @custom_values_before_change
938 964 # custom fields changes
939 965 custom_values.each {|c|
940 966 before = @custom_values_before_change[c.custom_field_id]
941 967 after = c.value
942 968 next if before == after || (before.blank? && after.blank?)
943 969 @current_journal.details << JournalDetail.new(:property => 'cf',
944 970 :prop_key => c.custom_field_id,
945 971 :old_value => before,
946 972 :value => after)
947 973 }
948 974 end
949 975 @current_journal.save
950 976 # reset current journal
951 977 init_journal @current_journal.user, @current_journal.notes
952 978 end
953 979 end
954 980
955 981 # Query generator for selecting groups of issue counts for a project
956 982 # based on specific criteria
957 983 #
958 984 # Options
959 985 # * project - Project to search in.
960 986 # * field - String. Issue field to key off of in the grouping.
961 987 # * joins - String. The table name to join against.
962 988 def self.count_and_group_by(options)
963 989 project = options.delete(:project)
964 990 select_field = options.delete(:field)
965 991 joins = options.delete(:joins)
966 992
967 993 where = "#{Issue.table_name}.#{select_field}=j.id"
968 994
969 995 ActiveRecord::Base.connection.select_all("select s.id as status_id,
970 996 s.is_closed as closed,
971 997 j.id as #{select_field},
972 998 count(#{Issue.table_name}.id) as total
973 999 from
974 1000 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
975 1001 where
976 1002 #{Issue.table_name}.status_id=s.id
977 1003 and #{where}
978 1004 and #{Issue.table_name}.project_id=#{Project.table_name}.id
979 1005 and #{visible_condition(User.current, :project => project)}
980 1006 group by s.id, s.is_closed, j.id")
981 1007 end
982 1008 end
@@ -1,69 +1,69
1 1 <% labelled_fields_for :issue, @issue do |f| %>
2 2
3 3 <div class="splitcontentleft">
4 4 <% if @issue.safe_attribute? 'status_id' %>
5 5 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
6 6 <% else %>
7 7 <p><label><%= l(:field_status) %></label> <%= h(@issue.status.name) %></p>
8 8 <% end %>
9 9
10 10 <% if @issue.safe_attribute? 'priority_id' %>
11 11 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %></p>
12 12 <% end %>
13 13
14 14 <% if @issue.safe_attribute? 'assigned_to_id' %>
15 15 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %></p>
16 16 <% end %>
17 17
18 <% if @issue.safe_attribute?('category_id') && @project.issue_categories.any? %>
19 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
18 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
19 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
20 20 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
21 21 l(:label_issue_category_new),
22 22 'issue_category[name]',
23 {:controller => 'issue_categories', :action => 'create', :project_id => @project},
23 {:controller => 'issue_categories', :action => 'create', :project_id => @issue.project},
24 24 :title => l(:label_issue_category_new),
25 :tabindex => 199) if authorize_for('issue_categories', 'new') %></p>
25 :tabindex => 199) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
26 26 <% end %>
27 27
28 28 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
29 29 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %>
30 30 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
31 31 l(:label_version_new),
32 32 'version[name]',
33 {:controller => 'versions', :action => 'create', :project_id => @project},
33 {:controller => 'versions', :action => 'create', :project_id => @issue.project},
34 34 :title => l(:label_version_new),
35 :tabindex => 200) if authorize_for('versions', 'new') %>
35 :tabindex => 200) if User.current.allowed_to?(:manage_versions, @issue.project) %>
36 36 </p>
37 37 <% end %>
38 38 </div>
39 39
40 40 <div class="splitcontentright">
41 41 <% if @issue.safe_attribute? 'parent_issue_id' %>
42 42 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10 %></p>
43 43 <div id="parent_issue_candidates" class="autocomplete"></div>
44 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @project) }')" %>
44 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @issue.project) }')" %>
45 45 <% end %>
46 46
47 47 <% if @issue.safe_attribute? 'start_date' %>
48 48 <p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
49 49 <% end %>
50 50
51 51 <% if @issue.safe_attribute? 'due_date' %>
52 52 <p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
53 53 <% end %>
54 54
55 55 <% if @issue.safe_attribute? 'estimated_hours' %>
56 56 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p>
57 57 <% end %>
58 58
59 59 <% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
60 60 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
61 61 <% end %>
62 62 </div>
63 63
64 64 <div style="clear:both;"> </div>
65 65 <% if @issue.safe_attribute? 'custom_field_values' %>
66 66 <%= render :partial => 'issues/form_custom_fields' %>
67 67 <% end %>
68 68
69 69 <% end %>
@@ -1,46 +1,48
1 1 <% labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 2 <%= error_messages_for 'issue', 'time_entry' %>
3 3 <div class="box">
4 4 <% if @edit_allowed || !@allowed_statuses.empty? %>
5 5 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
6 <div id="all_attributes">
6 7 <%= render :partial => 'form', :locals => {:f => f} %>
8 </div>
7 9 </fieldset>
8 10 <% end %>
9 11 <% if User.current.allowed_to?(:log_time, @project) %>
10 12 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
11 13 <% labelled_fields_for :time_entry, @time_entry do |time_entry| %>
12 14 <div class="splitcontentleft">
13 15 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
14 16 </div>
15 17 <div class="splitcontentright">
16 18 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
17 19 </div>
18 20 <p><%= time_entry.text_field :comments, :size => 60 %></p>
19 21 <% @time_entry.custom_field_values.each do |value| %>
20 22 <p><%= custom_field_tag_with_label :time_entry, value %></p>
21 23 <% end %>
22 24 <% end %>
23 25 </fieldset>
24 26 <% end %>
25 27
26 28 <fieldset><legend><%= l(:field_notes) %></legend>
27 29 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
28 30 <%= wikitoolbar_for 'notes' %>
29 31 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
30 32
31 33 <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p>
32 34 </fieldset>
33 35 </div>
34 36
35 37 <%= f.hidden_field :lock_version %>
36 38 <%= submit_tag l(:button_submit) %>
37 39 <%= link_to_remote l(:label_preview),
38 40 { :url => preview_issue_path(:project_id => @project, :id => @issue),
39 41 :method => 'post',
40 42 :update => 'preview',
41 43 :with => 'Form.serialize("issue-form")',
42 44 :complete => "Element.scrollTo('preview')"
43 45 }, :accesskey => accesskey(:preview) %>
44 46 <% end %>
45 47
46 48 <div id="preview" class="wiki"></div>
@@ -1,41 +1,48
1 <% labelled_fields_for :issue, @issue do |f| %>
1 2 <%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
2 3
3 4 <% if @issue.safe_attribute? 'is_private' %>
4 5 <p style="float:right; margin-right:1em;">
5 6 <label class="inline" for="issue_is_private" id="issue_is_private_label"><%= f.check_box :is_private, :no_label => true %> <%= l(:field_is_private) %></label>
6 7 </p>
7 8 <% end %>
8 9
10 <% if !@issue.new_record? && @issue.safe_attribute?('project_id') %>
11 <p><%= f.select :project_id, Issue.allowed_target_projects_on_move.collect {|t| [t.name, t.id]}, :required => true %></p>
12 <%= observe_field :issue_project_id, :url => project_issue_form_path(@project, :id => @issue, :project_change => '1'),
13 :with => "Form.serialize('issue-form')" %>
14 <% end %>
15
9 16 <% if @issue.safe_attribute? 'tracker_id' %>
10 <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
17 <p><%= f.select :tracker_id, @issue.project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
11 18 <%= observe_field :issue_tracker_id, :url => project_issue_form_path(@project, :id => @issue),
12 :update => :attributes,
13 19 :with => "Form.serialize('issue-form')" %>
14 20 <% end %>
15 21
16 22 <% if @issue.safe_attribute? 'subject' %>
17 23 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
18 24 <% end %>
19 25
20 26 <% if @issue.safe_attribute? 'description' %>
21 27 <p>
22 28 <label><%= l(:field_description) %></label>
23 29 <%= link_to_function image_tag('edit.png'),
24 30 'Element.hide(this); Effect.toggle("issue_description_and_toolbar", "appear", {duration:0.3})' unless @issue.new_record? %>
25 31 <% content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %>
26 32 <%= f.text_area :description,
27 33 :cols => 60,
28 34 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
29 35 :accesskey => accesskey(:edit),
30 36 :class => 'wiki-edit',
31 37 :no_label => true %>
32 38 <% end %>
33 39 </p>
34 40 <%= wikitoolbar_for 'issue_description' %>
35 41 <% end %>
36 42
37 43 <div id="attributes" class="attributes">
38 44 <%= render :partial => 'issues/attributes' %>
39 45 </div>
40 46
41 47 <%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
48 <% end %>
@@ -1,194 +1,193
1 1 ---
2 2 roles_001:
3 3 name: Manager
4 4 id: 1
5 5 builtin: 0
6 6 issues_visibility: all
7 7 permissions: |
8 8 ---
9 9 - :add_project
10 10 - :edit_project
11 11 - :select_project_modules
12 12 - :manage_members
13 13 - :manage_versions
14 14 - :manage_categories
15 15 - :view_issues
16 16 - :add_issues
17 17 - :edit_issues
18 18 - :manage_issue_relations
19 19 - :manage_subtasks
20 20 - :add_issue_notes
21 21 - :move_issues
22 22 - :delete_issues
23 23 - :view_issue_watchers
24 24 - :add_issue_watchers
25 25 - :set_issues_private
26 26 - :delete_issue_watchers
27 27 - :manage_public_queries
28 28 - :save_queries
29 29 - :view_gantt
30 30 - :view_calendar
31 31 - :log_time
32 32 - :view_time_entries
33 33 - :edit_time_entries
34 34 - :delete_time_entries
35 35 - :manage_news
36 36 - :comment_news
37 37 - :view_documents
38 38 - :manage_documents
39 39 - :view_wiki_pages
40 40 - :export_wiki_pages
41 41 - :view_wiki_edits
42 42 - :edit_wiki_pages
43 43 - :delete_wiki_pages_attachments
44 44 - :protect_wiki_pages
45 45 - :delete_wiki_pages
46 46 - :rename_wiki_pages
47 47 - :add_messages
48 48 - :edit_messages
49 49 - :delete_messages
50 50 - :manage_boards
51 51 - :view_files
52 52 - :manage_files
53 53 - :browse_repository
54 54 - :manage_repository
55 55 - :view_changesets
56 56 - :manage_project_activities
57 57
58 58 position: 1
59 59 roles_002:
60 60 name: Developer
61 61 id: 2
62 62 builtin: 0
63 63 issues_visibility: default
64 64 permissions: |
65 65 ---
66 66 - :edit_project
67 67 - :manage_members
68 68 - :manage_versions
69 69 - :manage_categories
70 70 - :view_issues
71 71 - :add_issues
72 72 - :edit_issues
73 73 - :manage_issue_relations
74 74 - :manage_subtasks
75 75 - :add_issue_notes
76 76 - :move_issues
77 77 - :delete_issues
78 78 - :view_issue_watchers
79 79 - :save_queries
80 80 - :view_gantt
81 81 - :view_calendar
82 82 - :log_time
83 83 - :view_time_entries
84 84 - :edit_own_time_entries
85 85 - :manage_news
86 86 - :comment_news
87 87 - :view_documents
88 88 - :manage_documents
89 89 - :view_wiki_pages
90 90 - :view_wiki_edits
91 91 - :edit_wiki_pages
92 92 - :protect_wiki_pages
93 93 - :delete_wiki_pages
94 94 - :add_messages
95 95 - :edit_own_messages
96 96 - :delete_own_messages
97 97 - :manage_boards
98 98 - :view_files
99 99 - :manage_files
100 100 - :browse_repository
101 101 - :view_changesets
102 102
103 103 position: 2
104 104 roles_003:
105 105 name: Reporter
106 106 id: 3
107 107 builtin: 0
108 108 issues_visibility: default
109 109 permissions: |
110 110 ---
111 111 - :edit_project
112 112 - :manage_members
113 113 - :manage_versions
114 114 - :manage_categories
115 115 - :view_issues
116 116 - :add_issues
117 117 - :edit_issues
118 118 - :manage_issue_relations
119 119 - :add_issue_notes
120 120 - :move_issues
121 121 - :view_issue_watchers
122 122 - :save_queries
123 123 - :view_gantt
124 124 - :view_calendar
125 125 - :log_time
126 126 - :view_time_entries
127 127 - :manage_news
128 128 - :comment_news
129 129 - :view_documents
130 130 - :manage_documents
131 131 - :view_wiki_pages
132 132 - :view_wiki_edits
133 133 - :edit_wiki_pages
134 134 - :delete_wiki_pages
135 135 - :add_messages
136 136 - :manage_boards
137 137 - :view_files
138 138 - :manage_files
139 139 - :browse_repository
140 140 - :view_changesets
141 141
142 142 position: 3
143 143 roles_004:
144 144 name: Non member
145 145 id: 4
146 146 builtin: 1
147 147 issues_visibility: default
148 148 permissions: |
149 149 ---
150 150 - :view_issues
151 151 - :add_issues
152 152 - :edit_issues
153 153 - :manage_issue_relations
154 154 - :add_issue_notes
155 - :move_issues
156 155 - :save_queries
157 156 - :view_gantt
158 157 - :view_calendar
159 158 - :log_time
160 159 - :view_time_entries
161 160 - :comment_news
162 161 - :view_documents
163 162 - :manage_documents
164 163 - :view_wiki_pages
165 164 - :view_wiki_edits
166 165 - :edit_wiki_pages
167 166 - :add_messages
168 167 - :view_files
169 168 - :manage_files
170 169 - :browse_repository
171 170 - :view_changesets
172 171
173 172 position: 4
174 173 roles_005:
175 174 name: Anonymous
176 175 id: 5
177 176 builtin: 2
178 177 issues_visibility: default
179 178 permissions: |
180 179 ---
181 180 - :view_issues
182 181 - :add_issue_notes
183 182 - :view_gantt
184 183 - :view_calendar
185 184 - :view_time_entries
186 185 - :view_documents
187 186 - :view_wiki_pages
188 187 - :view_wiki_edits
189 188 - :view_files
190 189 - :browse_repository
191 190 - :view_changesets
192 191
193 192 position: 5
194 193
@@ -1,2262 +1,2339
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'issues_controller'
20 20
21 21 class IssuesControllerTest < ActionController::TestCase
22 22 fixtures :projects,
23 23 :users,
24 24 :roles,
25 25 :members,
26 26 :member_roles,
27 27 :issues,
28 28 :issue_statuses,
29 29 :versions,
30 30 :trackers,
31 31 :projects_trackers,
32 32 :issue_categories,
33 33 :enabled_modules,
34 34 :enumerations,
35 35 :attachments,
36 36 :workflows,
37 37 :custom_fields,
38 38 :custom_values,
39 39 :custom_fields_projects,
40 40 :custom_fields_trackers,
41 41 :time_entries,
42 42 :journals,
43 43 :journal_details,
44 44 :queries
45 45
46 46 include Redmine::I18n
47 47
48 48 def setup
49 49 @controller = IssuesController.new
50 50 @request = ActionController::TestRequest.new
51 51 @response = ActionController::TestResponse.new
52 52 User.current = nil
53 53 end
54 54
55 55 def test_index
56 56 Setting.default_language = 'en'
57 57
58 58 get :index
59 59 assert_response :success
60 60 assert_template 'index'
61 61 assert_not_nil assigns(:issues)
62 62 assert_nil assigns(:project)
63 63 assert_tag :tag => 'a', :content => /Can't print recipes/
64 64 assert_tag :tag => 'a', :content => /Subproject issue/
65 65 # private projects hidden
66 66 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
67 67 assert_no_tag :tag => 'a', :content => /Issue on project 2/
68 68 # project column
69 69 assert_tag :tag => 'th', :content => /Project/
70 70 end
71 71
72 72 def test_index_should_not_list_issues_when_module_disabled
73 73 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
74 74 get :index
75 75 assert_response :success
76 76 assert_template 'index'
77 77 assert_not_nil assigns(:issues)
78 78 assert_nil assigns(:project)
79 79 assert_no_tag :tag => 'a', :content => /Can't print recipes/
80 80 assert_tag :tag => 'a', :content => /Subproject issue/
81 81 end
82 82
83 83 def test_index_should_list_visible_issues_only
84 84 get :index, :per_page => 100
85 85 assert_response :success
86 86 assert_not_nil assigns(:issues)
87 87 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
88 88 end
89 89
90 90 def test_index_with_project
91 91 Setting.display_subprojects_issues = 0
92 92 get :index, :project_id => 1
93 93 assert_response :success
94 94 assert_template 'index'
95 95 assert_not_nil assigns(:issues)
96 96 assert_tag :tag => 'a', :content => /Can't print recipes/
97 97 assert_no_tag :tag => 'a', :content => /Subproject issue/
98 98 end
99 99
100 100 def test_index_with_project_and_subprojects
101 101 Setting.display_subprojects_issues = 1
102 102 get :index, :project_id => 1
103 103 assert_response :success
104 104 assert_template 'index'
105 105 assert_not_nil assigns(:issues)
106 106 assert_tag :tag => 'a', :content => /Can't print recipes/
107 107 assert_tag :tag => 'a', :content => /Subproject issue/
108 108 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
109 109 end
110 110
111 111 def test_index_with_project_and_subprojects_should_show_private_subprojects
112 112 @request.session[:user_id] = 2
113 113 Setting.display_subprojects_issues = 1
114 114 get :index, :project_id => 1
115 115 assert_response :success
116 116 assert_template 'index'
117 117 assert_not_nil assigns(:issues)
118 118 assert_tag :tag => 'a', :content => /Can't print recipes/
119 119 assert_tag :tag => 'a', :content => /Subproject issue/
120 120 assert_tag :tag => 'a', :content => /Issue of a private subproject/
121 121 end
122 122
123 123 def test_index_with_project_and_default_filter
124 124 get :index, :project_id => 1, :set_filter => 1
125 125 assert_response :success
126 126 assert_template 'index'
127 127 assert_not_nil assigns(:issues)
128 128
129 129 query = assigns(:query)
130 130 assert_not_nil query
131 131 # default filter
132 132 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
133 133 end
134 134
135 135 def test_index_with_project_and_filter
136 136 get :index, :project_id => 1, :set_filter => 1,
137 137 :f => ['tracker_id'],
138 138 :op => {'tracker_id' => '='},
139 139 :v => {'tracker_id' => ['1']}
140 140 assert_response :success
141 141 assert_template 'index'
142 142 assert_not_nil assigns(:issues)
143 143
144 144 query = assigns(:query)
145 145 assert_not_nil query
146 146 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
147 147 end
148 148
149 149 def test_index_with_short_filters
150 150
151 151 to_test = {
152 152 'status_id' => {
153 153 'o' => { :op => 'o', :values => [''] },
154 154 'c' => { :op => 'c', :values => [''] },
155 155 '7' => { :op => '=', :values => ['7'] },
156 156 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
157 157 '=7' => { :op => '=', :values => ['7'] },
158 158 '!3' => { :op => '!', :values => ['3'] },
159 159 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
160 160 'subject' => {
161 161 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
162 162 'o' => { :op => '=', :values => ['o'] },
163 163 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
164 164 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
165 165 'tracker_id' => {
166 166 '3' => { :op => '=', :values => ['3'] },
167 167 '=3' => { :op => '=', :values => ['3'] }},
168 168 'start_date' => {
169 169 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
170 170 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
171 171 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
172 172 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
173 173 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
174 174 '<t+2' => { :op => '<t+', :values => ['2'] },
175 175 '>t+2' => { :op => '>t+', :values => ['2'] },
176 176 't+2' => { :op => 't+', :values => ['2'] },
177 177 't' => { :op => 't', :values => [''] },
178 178 'w' => { :op => 'w', :values => [''] },
179 179 '>t-2' => { :op => '>t-', :values => ['2'] },
180 180 '<t-2' => { :op => '<t-', :values => ['2'] },
181 181 't-2' => { :op => 't-', :values => ['2'] }},
182 182 'created_on' => {
183 183 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
184 184 '<t+2' => { :op => '=', :values => ['<t+2'] },
185 185 '>t+2' => { :op => '=', :values => ['>t+2'] },
186 186 't+2' => { :op => 't', :values => ['+2'] }},
187 187 'cf_1' => {
188 188 'c' => { :op => '=', :values => ['c'] },
189 189 '!c' => { :op => '!', :values => ['c'] },
190 190 '!*' => { :op => '!*', :values => [''] },
191 191 '*' => { :op => '*', :values => [''] }},
192 192 'estimated_hours' => {
193 193 '=13.4' => { :op => '=', :values => ['13.4'] },
194 194 '>=45' => { :op => '>=', :values => ['45'] },
195 195 '<=125' => { :op => '<=', :values => ['125'] },
196 196 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
197 197 '!*' => { :op => '!*', :values => [''] },
198 198 '*' => { :op => '*', :values => [''] }}
199 199 }
200 200
201 201 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
202 202
203 203 to_test.each do |field, expression_and_expected|
204 204 expression_and_expected.each do |filter_expression, expected|
205 205
206 206 get :index, :set_filter => 1, field => filter_expression
207 207
208 208 assert_response :success
209 209 assert_template 'index'
210 210 assert_not_nil assigns(:issues)
211 211
212 212 query = assigns(:query)
213 213 assert_not_nil query
214 214 assert query.has_filter?(field)
215 215 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
216 216 end
217 217 end
218 218
219 219 end
220 220
221 221 def test_index_with_project_and_empty_filters
222 222 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
223 223 assert_response :success
224 224 assert_template 'index'
225 225 assert_not_nil assigns(:issues)
226 226
227 227 query = assigns(:query)
228 228 assert_not_nil query
229 229 # no filter
230 230 assert_equal({}, query.filters)
231 231 end
232 232
233 233 def test_index_with_query
234 234 get :index, :project_id => 1, :query_id => 5
235 235 assert_response :success
236 236 assert_template 'index'
237 237 assert_not_nil assigns(:issues)
238 238 assert_nil assigns(:issue_count_by_group)
239 239 end
240 240
241 241 def test_index_with_query_grouped_by_tracker
242 242 get :index, :project_id => 1, :query_id => 6
243 243 assert_response :success
244 244 assert_template 'index'
245 245 assert_not_nil assigns(:issues)
246 246 assert_not_nil assigns(:issue_count_by_group)
247 247 end
248 248
249 249 def test_index_with_query_grouped_by_list_custom_field
250 250 get :index, :project_id => 1, :query_id => 9
251 251 assert_response :success
252 252 assert_template 'index'
253 253 assert_not_nil assigns(:issues)
254 254 assert_not_nil assigns(:issue_count_by_group)
255 255 end
256 256
257 257 def test_index_with_query_id_and_project_id_should_set_session_query
258 258 get :index, :project_id => 1, :query_id => 4
259 259 assert_response :success
260 260 assert_kind_of Hash, session[:query]
261 261 assert_equal 4, session[:query][:id]
262 262 assert_equal 1, session[:query][:project_id]
263 263 end
264 264
265 265 def test_index_with_cross_project_query_in_session_should_show_project_issues
266 266 q = Query.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil)
267 267 @request.session[:query] = {:id => q.id, :project_id => 1}
268 268
269 269 with_settings :display_subprojects_issues => '0' do
270 270 get :index, :project_id => 1
271 271 end
272 272 assert_response :success
273 273 assert_not_nil assigns(:query)
274 274 assert_equal q.id, assigns(:query).id
275 275 assert_equal 1, assigns(:query).project_id
276 276 assert_equal [1], assigns(:issues).map(&:project_id).uniq
277 277 end
278 278
279 279 def test_private_query_should_not_be_available_to_other_users
280 280 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
281 281 @request.session[:user_id] = 3
282 282
283 283 get :index, :query_id => q.id
284 284 assert_response 403
285 285 end
286 286
287 287 def test_private_query_should_be_available_to_its_user
288 288 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
289 289 @request.session[:user_id] = 2
290 290
291 291 get :index, :query_id => q.id
292 292 assert_response :success
293 293 end
294 294
295 295 def test_public_query_should_be_available_to_other_users
296 296 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
297 297 @request.session[:user_id] = 3
298 298
299 299 get :index, :query_id => q.id
300 300 assert_response :success
301 301 end
302 302
303 303 def test_index_csv
304 304 get :index, :format => 'csv'
305 305 assert_response :success
306 306 assert_not_nil assigns(:issues)
307 307 assert_equal 'text/csv', @response.content_type
308 308 assert @response.body.starts_with?("#,")
309 309 lines = @response.body.chomp.split("\n")
310 310 assert_equal assigns(:query).columns.size + 1, lines[0].split(',').size
311 311 end
312 312
313 313 def test_index_csv_with_project
314 314 get :index, :project_id => 1, :format => 'csv'
315 315 assert_response :success
316 316 assert_not_nil assigns(:issues)
317 317 assert_equal 'text/csv', @response.content_type
318 318 end
319 319
320 320 def test_index_csv_with_description
321 321 get :index, :format => 'csv', :description => '1'
322 322 assert_response :success
323 323 assert_not_nil assigns(:issues)
324 324 assert_equal 'text/csv', @response.content_type
325 325 assert @response.body.starts_with?("#,")
326 326 lines = @response.body.chomp.split("\n")
327 327 assert_equal assigns(:query).columns.size + 2, lines[0].split(',').size
328 328 end
329 329
330 330 def test_index_csv_with_all_columns
331 331 get :index, :format => 'csv', :columns => 'all'
332 332 assert_response :success
333 333 assert_not_nil assigns(:issues)
334 334 assert_equal 'text/csv', @response.content_type
335 335 assert @response.body.starts_with?("#,")
336 336 lines = @response.body.chomp.split("\n")
337 337 assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size
338 338 end
339 339
340 340 def test_index_csv_big_5
341 341 with_settings :default_language => "zh-TW" do
342 342 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
343 343 str_big5 = "\xa4@\xa4\xeb"
344 344 if str_utf8.respond_to?(:force_encoding)
345 345 str_utf8.force_encoding('UTF-8')
346 346 str_big5.force_encoding('Big5')
347 347 end
348 348 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
349 349 :status_id => 1, :priority => IssuePriority.all.first,
350 350 :subject => str_utf8)
351 351 assert issue.save
352 352
353 353 get :index, :project_id => 1,
354 354 :f => ['subject'],
355 355 :op => '=', :values => [str_utf8],
356 356 :format => 'csv'
357 357 assert_equal 'text/csv', @response.content_type
358 358 lines = @response.body.chomp.split("\n")
359 359 s1 = "\xaa\xac\xbaA"
360 360 if str_utf8.respond_to?(:force_encoding)
361 361 s1.force_encoding('Big5')
362 362 end
363 363 assert lines[0].include?(s1)
364 364 assert lines[1].include?(str_big5)
365 365 end
366 366 end
367 367
368 368 def test_index_csv_cannot_convert_should_be_replaced_big_5
369 369 with_settings :default_language => "zh-TW" do
370 370 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
371 371 if str_utf8.respond_to?(:force_encoding)
372 372 str_utf8.force_encoding('UTF-8')
373 373 end
374 374 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
375 375 :status_id => 1, :priority => IssuePriority.all.first,
376 376 :subject => str_utf8)
377 377 assert issue.save
378 378
379 379 get :index, :project_id => 1,
380 380 :f => ['subject'],
381 381 :op => '=', :values => [str_utf8],
382 382 :c => ['status', 'subject'],
383 383 :format => 'csv',
384 384 :set_filter => 1
385 385 assert_equal 'text/csv', @response.content_type
386 386 lines = @response.body.chomp.split("\n")
387 387 s1 = "\xaa\xac\xbaA" # status
388 388 if str_utf8.respond_to?(:force_encoding)
389 389 s1.force_encoding('Big5')
390 390 end
391 391 assert lines[0].include?(s1)
392 392 s2 = lines[1].split(",")[2]
393 393 if s1.respond_to?(:force_encoding)
394 394 s3 = "\xa5H?" # subject
395 395 s3.force_encoding('Big5')
396 396 assert_equal s3, s2
397 397 elsif RUBY_PLATFORM == 'java'
398 398 assert_equal "??", s2
399 399 else
400 400 assert_equal "\xa5H???", s2
401 401 end
402 402 end
403 403 end
404 404
405 405 def test_index_csv_tw
406 406 with_settings :default_language => "zh-TW" do
407 407 str1 = "test_index_csv_tw"
408 408 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
409 409 :status_id => 1, :priority => IssuePriority.all.first,
410 410 :subject => str1, :estimated_hours => '1234.5')
411 411 assert issue.save
412 412 assert_equal 1234.5, issue.estimated_hours
413 413
414 414 get :index, :project_id => 1,
415 415 :f => ['subject'],
416 416 :op => '=', :values => [str1],
417 417 :c => ['estimated_hours', 'subject'],
418 418 :format => 'csv',
419 419 :set_filter => 1
420 420 assert_equal 'text/csv', @response.content_type
421 421 lines = @response.body.chomp.split("\n")
422 422 assert_equal "#{issue.id},1234.5,#{str1}", lines[1]
423 423
424 424 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
425 425 if str_tw.respond_to?(:force_encoding)
426 426 str_tw.force_encoding('UTF-8')
427 427 end
428 428 assert_equal str_tw, l(:general_lang_name)
429 429 assert_equal ',', l(:general_csv_separator)
430 430 assert_equal '.', l(:general_csv_decimal_separator)
431 431 end
432 432 end
433 433
434 434 def test_index_csv_fr
435 435 with_settings :default_language => "fr" do
436 436 str1 = "test_index_csv_fr"
437 437 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
438 438 :status_id => 1, :priority => IssuePriority.all.first,
439 439 :subject => str1, :estimated_hours => '1234.5')
440 440 assert issue.save
441 441 assert_equal 1234.5, issue.estimated_hours
442 442
443 443 get :index, :project_id => 1,
444 444 :f => ['subject'],
445 445 :op => '=', :values => [str1],
446 446 :c => ['estimated_hours', 'subject'],
447 447 :format => 'csv',
448 448 :set_filter => 1
449 449 assert_equal 'text/csv', @response.content_type
450 450 lines = @response.body.chomp.split("\n")
451 451 assert_equal "#{issue.id};1234,5;#{str1}", lines[1]
452 452
453 453 str_fr = "Fran\xc3\xa7ais"
454 454 if str_fr.respond_to?(:force_encoding)
455 455 str_fr.force_encoding('UTF-8')
456 456 end
457 457 assert_equal str_fr, l(:general_lang_name)
458 458 assert_equal ';', l(:general_csv_separator)
459 459 assert_equal ',', l(:general_csv_decimal_separator)
460 460 end
461 461 end
462 462
463 463 def test_index_pdf
464 464 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
465 465 with_settings :default_language => lang do
466 466
467 467 get :index
468 468 assert_response :success
469 469 assert_template 'index'
470 470
471 471 if lang == "ja"
472 472 if RUBY_PLATFORM != 'java'
473 473 assert_equal "CP932", l(:general_pdf_encoding)
474 474 end
475 475 if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932"
476 476 next
477 477 end
478 478 end
479 479
480 480 get :index, :format => 'pdf'
481 481 assert_response :success
482 482 assert_not_nil assigns(:issues)
483 483 assert_equal 'application/pdf', @response.content_type
484 484
485 485 get :index, :project_id => 1, :format => 'pdf'
486 486 assert_response :success
487 487 assert_not_nil assigns(:issues)
488 488 assert_equal 'application/pdf', @response.content_type
489 489
490 490 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
491 491 assert_response :success
492 492 assert_not_nil assigns(:issues)
493 493 assert_equal 'application/pdf', @response.content_type
494 494 end
495 495 end
496 496 end
497 497
498 498 def test_index_pdf_with_query_grouped_by_list_custom_field
499 499 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
500 500 assert_response :success
501 501 assert_not_nil assigns(:issues)
502 502 assert_not_nil assigns(:issue_count_by_group)
503 503 assert_equal 'application/pdf', @response.content_type
504 504 end
505 505
506 506 def test_index_sort
507 507 get :index, :sort => 'tracker,id:desc'
508 508 assert_response :success
509 509
510 510 sort_params = @request.session['issues_index_sort']
511 511 assert sort_params.is_a?(String)
512 512 assert_equal 'tracker,id:desc', sort_params
513 513
514 514 issues = assigns(:issues)
515 515 assert_not_nil issues
516 516 assert !issues.empty?
517 517 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
518 518 end
519 519
520 520 def test_index_sort_by_field_not_included_in_columns
521 521 Setting.issue_list_default_columns = %w(subject author)
522 522 get :index, :sort => 'tracker'
523 523 end
524 524
525 525 def test_index_sort_by_assigned_to
526 526 get :index, :sort => 'assigned_to'
527 527 assert_response :success
528 528 assignees = assigns(:issues).collect(&:assigned_to).compact
529 529 assert_equal assignees.sort, assignees
530 530 end
531 531
532 532 def test_index_sort_by_assigned_to_desc
533 533 get :index, :sort => 'assigned_to:desc'
534 534 assert_response :success
535 535 assignees = assigns(:issues).collect(&:assigned_to).compact
536 536 assert_equal assignees.sort.reverse, assignees
537 537 end
538 538
539 539 def test_index_group_by_assigned_to
540 540 get :index, :group_by => 'assigned_to', :sort => 'priority'
541 541 assert_response :success
542 542 end
543 543
544 544 def test_index_sort_by_author
545 545 get :index, :sort => 'author'
546 546 assert_response :success
547 547 authors = assigns(:issues).collect(&:author)
548 548 assert_equal authors.sort, authors
549 549 end
550 550
551 551 def test_index_sort_by_author_desc
552 552 get :index, :sort => 'author:desc'
553 553 assert_response :success
554 554 authors = assigns(:issues).collect(&:author)
555 555 assert_equal authors.sort.reverse, authors
556 556 end
557 557
558 558 def test_index_group_by_author
559 559 get :index, :group_by => 'author', :sort => 'priority'
560 560 assert_response :success
561 561 end
562 562
563 563 def test_index_sort_by_spent_hours
564 564 get :index, :sort => 'spent_hours:desc'
565 565 assert_response :success
566 566 hours = assigns(:issues).collect(&:spent_hours)
567 567 assert_equal hours.sort.reverse, hours
568 568 end
569 569
570 570 def test_index_with_columns
571 571 columns = ['tracker', 'subject', 'assigned_to']
572 572 get :index, :set_filter => 1, :c => columns
573 573 assert_response :success
574 574
575 575 # query should use specified columns
576 576 query = assigns(:query)
577 577 assert_kind_of Query, query
578 578 assert_equal columns, query.column_names.map(&:to_s)
579 579
580 580 # columns should be stored in session
581 581 assert_kind_of Hash, session[:query]
582 582 assert_kind_of Array, session[:query][:column_names]
583 583 assert_equal columns, session[:query][:column_names].map(&:to_s)
584 584
585 585 # ensure only these columns are kept in the selected columns list
586 586 assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' },
587 587 :children => { :count => 3 }
588 588 assert_no_tag :tag => 'option', :attributes => { :value => 'project' },
589 589 :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } }
590 590 end
591 591
592 592 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
593 593 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
594 594 get :index, :set_filter => 1
595 595
596 596 # query should use specified columns
597 597 query = assigns(:query)
598 598 assert_kind_of Query, query
599 599 assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
600 600 end
601 601
602 602 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
603 603 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
604 604 columns = ['tracker', 'subject', 'assigned_to']
605 605 get :index, :set_filter => 1, :c => columns
606 606
607 607 # query should use specified columns
608 608 query = assigns(:query)
609 609 assert_kind_of Query, query
610 610 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
611 611 end
612 612
613 613 def test_index_with_custom_field_column
614 614 columns = %w(tracker subject cf_2)
615 615 get :index, :set_filter => 1, :c => columns
616 616 assert_response :success
617 617
618 618 # query should use specified columns
619 619 query = assigns(:query)
620 620 assert_kind_of Query, query
621 621 assert_equal columns, query.column_names.map(&:to_s)
622 622
623 623 assert_tag :td,
624 624 :attributes => {:class => 'cf_2 string'},
625 625 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
626 626 end
627 627
628 628 def test_index_with_date_column
629 629 Issue.find(1).update_attribute :start_date, '1987-08-24'
630 630
631 631 with_settings :date_format => '%d/%m/%Y' do
632 632 get :index, :set_filter => 1, :c => %w(start_date)
633 633 assert_tag 'td', :attributes => {:class => /start_date/}, :content => '24/08/1987'
634 634 end
635 635 end
636 636
637 637 def test_index_with_done_ratio
638 638 Issue.find(1).update_attribute :done_ratio, 40
639 639
640 640 get :index, :set_filter => 1, :c => %w(done_ratio)
641 641 assert_tag 'td', :attributes => {:class => /done_ratio/},
642 642 :child => {:tag => 'table', :attributes => {:class => 'progress'},
643 643 :descendant => {:tag => 'td', :attributes => {:class => 'closed', :style => 'width: 40%;'}}
644 644 }
645 645 end
646 646
647 647 def test_index_with_spent_hours_column
648 648 get :index, :set_filter => 1, :c => %w(subject spent_hours)
649 649
650 650 assert_tag 'tr', :attributes => {:id => 'issue-3'},
651 651 :child => {
652 652 :tag => 'td', :attributes => {:class => /spent_hours/}, :content => '1.00'
653 653 }
654 654 end
655 655
656 656 def test_index_should_not_show_spent_hours_column_without_permission
657 657 Role.anonymous.remove_permission! :view_time_entries
658 658 get :index, :set_filter => 1, :c => %w(subject spent_hours)
659 659
660 660 assert_no_tag 'td', :attributes => {:class => /spent_hours/}
661 661 end
662 662
663 663 def test_index_with_fixed_version
664 664 get :index, :set_filter => 1, :c => %w(fixed_version)
665 665 assert_tag 'td', :attributes => {:class => /fixed_version/},
666 666 :child => {:tag => 'a', :content => '1.0', :attributes => {:href => '/versions/2'}}
667 667 end
668 668
669 669 def test_index_send_html_if_query_is_invalid
670 670 get :index, :f => ['start_date'], :op => {:start_date => '='}
671 671 assert_equal 'text/html', @response.content_type
672 672 assert_template 'index'
673 673 end
674 674
675 675 def test_index_send_nothing_if_query_is_invalid
676 676 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
677 677 assert_equal 'text/csv', @response.content_type
678 678 assert @response.body.blank?
679 679 end
680 680
681 681 def test_show_by_anonymous
682 682 get :show, :id => 1
683 683 assert_response :success
684 684 assert_template 'show'
685 685 assert_not_nil assigns(:issue)
686 686 assert_equal Issue.find(1), assigns(:issue)
687 687
688 688 # anonymous role is allowed to add a note
689 689 assert_tag :tag => 'form',
690 690 :descendant => { :tag => 'fieldset',
691 691 :child => { :tag => 'legend',
692 692 :content => /Notes/ } }
693 693 assert_tag :tag => 'title',
694 694 :content => "Bug #1: Can't print recipes - eCookbook - Redmine"
695 695 end
696 696
697 697 def test_show_by_manager
698 698 @request.session[:user_id] = 2
699 699 get :show, :id => 1
700 700 assert_response :success
701 701
702 702 assert_tag :tag => 'a',
703 703 :content => /Quote/
704 704
705 705 assert_tag :tag => 'form',
706 706 :descendant => { :tag => 'fieldset',
707 707 :child => { :tag => 'legend',
708 708 :content => /Change properties/ } },
709 709 :descendant => { :tag => 'fieldset',
710 710 :child => { :tag => 'legend',
711 711 :content => /Log time/ } },
712 712 :descendant => { :tag => 'fieldset',
713 713 :child => { :tag => 'legend',
714 714 :content => /Notes/ } }
715 715 end
716 716
717 717 def test_show_should_display_update_form
718 718 @request.session[:user_id] = 2
719 719 get :show, :id => 1
720 720 assert_response :success
721 721
722 722 assert_tag 'form', :attributes => {:id => 'issue-form'}
723 723 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
724 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
724 725 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
725 726 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
726 727 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
727 728 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
728 729 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
729 730 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
730 731 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
731 732 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
732 733 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
733 734 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
734 735 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
735 736 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
736 737 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
737 738 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
738 739 assert_tag 'textarea', :attributes => {:name => 'notes'}
739 740 end
740 741
741 742 def test_show_should_display_update_form_with_minimal_permissions
742 743 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
743 744 Workflow.delete_all :role_id => 1
744 745
745 746 @request.session[:user_id] = 2
746 747 get :show, :id => 1
747 748 assert_response :success
748 749
749 750 assert_tag 'form', :attributes => {:id => 'issue-form'}
750 751 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
752 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
751 753 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
752 754 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
753 755 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
754 756 assert_no_tag 'select', :attributes => {:name => 'issue[status_id]'}
755 757 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
756 758 assert_no_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
757 759 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
758 760 assert_no_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
759 761 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
760 762 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
761 763 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
762 764 assert_no_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
763 765 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
764 766 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
765 767 assert_tag 'textarea', :attributes => {:name => 'notes'}
766 768 end
767 769
768 770 def test_show_should_display_update_form_with_workflow_permissions
769 771 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
770 772
771 773 @request.session[:user_id] = 2
772 774 get :show, :id => 1
773 775 assert_response :success
774 776
775 777 assert_tag 'form', :attributes => {:id => 'issue-form'}
776 778 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
779 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
777 780 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
778 781 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
779 782 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
780 783 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
781 784 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
782 785 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
783 786 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
784 787 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
785 788 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
786 789 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
787 790 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
788 791 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
789 792 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
790 793 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
791 794 assert_tag 'textarea', :attributes => {:name => 'notes'}
792 795 end
793 796
794 797 def test_show_should_not_display_update_form_without_permissions
795 798 Role.find(1).update_attribute :permissions, [:view_issues]
796 799
797 800 @request.session[:user_id] = 2
798 801 get :show, :id => 1
799 802 assert_response :success
800 803
801 804 assert_no_tag 'form', :attributes => {:id => 'issue-form'}
802 805 end
803 806
804 807 def test_update_form_should_not_display_inactive_enumerations
805 808 @request.session[:user_id] = 2
806 809 get :show, :id => 1
807 810 assert_response :success
808 811
809 812 assert ! IssuePriority.find(15).active?
810 813 assert_no_tag :option, :attributes => {:value => '15'},
811 814 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
812 815 end
813 816
814 817 def test_update_form_should_allow_attachment_upload
815 818 @request.session[:user_id] = 2
816 819 get :show, :id => 1
817 820
818 821 assert_tag :tag => 'form',
819 822 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
820 823 :descendant => {
821 824 :tag => 'input',
822 825 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
823 826 }
824 827 end
825 828
826 829 def test_show_should_deny_anonymous_access_without_permission
827 830 Role.anonymous.remove_permission!(:view_issues)
828 831 get :show, :id => 1
829 832 assert_response :redirect
830 833 end
831 834
832 835 def test_show_should_deny_anonymous_access_to_private_issue
833 836 Issue.update_all(["is_private = ?", true], "id = 1")
834 837 get :show, :id => 1
835 838 assert_response :redirect
836 839 end
837 840
838 841 def test_show_should_deny_non_member_access_without_permission
839 842 Role.non_member.remove_permission!(:view_issues)
840 843 @request.session[:user_id] = 9
841 844 get :show, :id => 1
842 845 assert_response 403
843 846 end
844 847
845 848 def test_show_should_deny_non_member_access_to_private_issue
846 849 Issue.update_all(["is_private = ?", true], "id = 1")
847 850 @request.session[:user_id] = 9
848 851 get :show, :id => 1
849 852 assert_response 403
850 853 end
851 854
852 855 def test_show_should_deny_member_access_without_permission
853 856 Role.find(1).remove_permission!(:view_issues)
854 857 @request.session[:user_id] = 2
855 858 get :show, :id => 1
856 859 assert_response 403
857 860 end
858 861
859 862 def test_show_should_deny_member_access_to_private_issue_without_permission
860 863 Issue.update_all(["is_private = ?", true], "id = 1")
861 864 @request.session[:user_id] = 3
862 865 get :show, :id => 1
863 866 assert_response 403
864 867 end
865 868
866 869 def test_show_should_allow_author_access_to_private_issue
867 870 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
868 871 @request.session[:user_id] = 3
869 872 get :show, :id => 1
870 873 assert_response :success
871 874 end
872 875
873 876 def test_show_should_allow_assignee_access_to_private_issue
874 877 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
875 878 @request.session[:user_id] = 3
876 879 get :show, :id => 1
877 880 assert_response :success
878 881 end
879 882
880 883 def test_show_should_allow_member_access_to_private_issue_with_permission
881 884 Issue.update_all(["is_private = ?", true], "id = 1")
882 885 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
883 886 @request.session[:user_id] = 3
884 887 get :show, :id => 1
885 888 assert_response :success
886 889 end
887 890
888 891 def test_show_should_not_disclose_relations_to_invisible_issues
889 892 Setting.cross_project_issue_relations = '1'
890 893 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
891 894 # Relation to a private project issue
892 895 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
893 896
894 897 get :show, :id => 1
895 898 assert_response :success
896 899
897 900 assert_tag :div, :attributes => { :id => 'relations' },
898 901 :descendant => { :tag => 'a', :content => /#2$/ }
899 902 assert_no_tag :div, :attributes => { :id => 'relations' },
900 903 :descendant => { :tag => 'a', :content => /#4$/ }
901 904 end
902 905
903 906 def test_show_should_list_subtasks
904 907 Issue.generate!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
905 908
906 909 get :show, :id => 1
907 910 assert_response :success
908 911 assert_tag 'div', :attributes => {:id => 'issue_tree'},
909 912 :descendant => {:tag => 'td', :content => /Child Issue/, :attributes => {:class => /subject/}}
910 913 end
911 914
912 915 def test_show_should_list_parents
913 916 issue = Issue.generate!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
914 917
915 918 get :show, :id => issue.id
916 919 assert_response :success
917 920 assert_tag 'div', :attributes => {:class => 'subject'},
918 921 :descendant => {:tag => 'h3', :content => 'Child Issue'}
919 922 assert_tag 'div', :attributes => {:class => 'subject'},
920 923 :descendant => {:tag => 'a', :attributes => {:href => '/issues/1'}}
921 924 end
922 925
923 926 def test_show_should_not_display_prev_next_links_without_query_in_session
924 927 get :show, :id => 1
925 928 assert_response :success
926 929 assert_nil assigns(:prev_issue_id)
927 930 assert_nil assigns(:next_issue_id)
928 931 end
929 932
930 933 def test_show_should_display_prev_next_links_with_query_in_session
931 934 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
932 935 @request.session['issues_index_sort'] = 'id'
933 936
934 937 with_settings :display_subprojects_issues => '0' do
935 938 get :show, :id => 3
936 939 end
937 940
938 941 assert_response :success
939 942 # Previous and next issues for all projects
940 943 assert_equal 2, assigns(:prev_issue_id)
941 944 assert_equal 5, assigns(:next_issue_id)
942 945
943 946 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
944 947 assert_tag 'a', :attributes => {:href => '/issues/5'}, :content => /Next/
945 948 end
946 949
947 950 def test_show_should_display_prev_next_links_with_project_query_in_session
948 951 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
949 952 @request.session['issues_index_sort'] = 'id'
950 953
951 954 with_settings :display_subprojects_issues => '0' do
952 955 get :show, :id => 3
953 956 end
954 957
955 958 assert_response :success
956 959 # Previous and next issues inside project
957 960 assert_equal 2, assigns(:prev_issue_id)
958 961 assert_equal 7, assigns(:next_issue_id)
959 962
960 963 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
961 964 assert_tag 'a', :attributes => {:href => '/issues/7'}, :content => /Next/
962 965 end
963 966
964 967 def test_show_should_not_display_prev_link_for_first_issue
965 968 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
966 969 @request.session['issues_index_sort'] = 'id'
967 970
968 971 with_settings :display_subprojects_issues => '0' do
969 972 get :show, :id => 1
970 973 end
971 974
972 975 assert_response :success
973 976 assert_nil assigns(:prev_issue_id)
974 977 assert_equal 2, assigns(:next_issue_id)
975 978
976 979 assert_no_tag 'a', :content => /Previous/
977 980 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Next/
978 981 end
979 982
980 983 def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
981 984 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
982 985 @request.session['issues_index_sort'] = 'id'
983 986
984 987 get :show, :id => 1
985 988
986 989 assert_response :success
987 990 assert_nil assigns(:prev_issue_id)
988 991 assert_nil assigns(:next_issue_id)
989 992
990 993 assert_no_tag 'a', :content => /Previous/
991 994 assert_no_tag 'a', :content => /Next/
992 995 end
993 996
994 997 def test_show_atom
995 998 get :show, :id => 2, :format => 'atom'
996 999 assert_response :success
997 1000 assert_template 'journals/index'
998 1001 # Inline image
999 1002 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
1000 1003 end
1001 1004
1002 1005 def test_show_export_to_pdf
1003 1006 get :show, :id => 3, :format => 'pdf'
1004 1007 assert_response :success
1005 1008 assert_equal 'application/pdf', @response.content_type
1006 1009 assert @response.body.starts_with?('%PDF')
1007 1010 assert_not_nil assigns(:issue)
1008 1011 end
1009 1012
1010 1013 def test_get_new
1011 1014 @request.session[:user_id] = 2
1012 1015 get :new, :project_id => 1, :tracker_id => 1
1013 1016 assert_response :success
1014 1017 assert_template 'new'
1015 1018
1016 1019 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
1020 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1017 1021 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1018 1022 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1019 1023 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1020 1024 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1021 1025 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1022 1026 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1023 1027 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1024 1028 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1025 1029 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1026 1030 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1027 1031 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1028 1032 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1029 1033 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1030 1034 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1031 1035
1032 1036 # Be sure we don't display inactive IssuePriorities
1033 1037 assert ! IssuePriority.find(15).active?
1034 1038 assert_no_tag :option, :attributes => {:value => '15'},
1035 1039 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1036 1040 end
1037 1041
1038 1042 def test_get_new_with_minimal_permissions
1039 1043 Role.find(1).update_attribute :permissions, [:add_issues]
1040 1044 Workflow.delete_all :role_id => 1
1041 1045
1042 1046 @request.session[:user_id] = 2
1043 1047 get :new, :project_id => 1, :tracker_id => 1
1044 1048 assert_response :success
1045 1049 assert_template 'new'
1046 1050
1047 1051 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
1052 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1048 1053 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1049 1054 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1050 1055 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1051 1056 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1052 1057 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1053 1058 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1054 1059 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1055 1060 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1056 1061 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1057 1062 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1058 1063 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1059 1064 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1060 1065 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1061 1066 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1062 1067 end
1063 1068
1064 1069 def test_get_new_without_default_start_date_is_creation_date
1065 1070 Setting.default_issue_start_date_to_creation_date = 0
1066 1071
1067 1072 @request.session[:user_id] = 2
1068 1073 get :new, :project_id => 1, :tracker_id => 1
1069 1074 assert_response :success
1070 1075 assert_template 'new'
1071 1076
1072 1077 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1073 1078 :value => nil }
1074 1079 end
1075 1080
1076 1081 def test_get_new_with_default_start_date_is_creation_date
1077 1082 Setting.default_issue_start_date_to_creation_date = 1
1078 1083
1079 1084 @request.session[:user_id] = 2
1080 1085 get :new, :project_id => 1, :tracker_id => 1
1081 1086 assert_response :success
1082 1087 assert_template 'new'
1083 1088
1084 1089 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1085 1090 :value => Date.today.to_s }
1086 1091 end
1087 1092
1088 1093 def test_get_new_form_should_allow_attachment_upload
1089 1094 @request.session[:user_id] = 2
1090 1095 get :new, :project_id => 1, :tracker_id => 1
1091 1096
1092 1097 assert_tag :tag => 'form',
1093 1098 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
1094 1099 :descendant => {
1095 1100 :tag => 'input',
1096 1101 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
1097 1102 }
1098 1103 end
1099 1104
1100 1105 def test_get_new_without_tracker_id
1101 1106 @request.session[:user_id] = 2
1102 1107 get :new, :project_id => 1
1103 1108 assert_response :success
1104 1109 assert_template 'new'
1105 1110
1106 1111 issue = assigns(:issue)
1107 1112 assert_not_nil issue
1108 1113 assert_equal Project.find(1).trackers.first, issue.tracker
1109 1114 end
1110 1115
1111 1116 def test_get_new_with_no_default_status_should_display_an_error
1112 1117 @request.session[:user_id] = 2
1113 1118 IssueStatus.delete_all
1114 1119
1115 1120 get :new, :project_id => 1
1116 1121 assert_response 500
1117 1122 assert_error_tag :content => /No default issue/
1118 1123 end
1119 1124
1120 1125 def test_get_new_with_no_tracker_should_display_an_error
1121 1126 @request.session[:user_id] = 2
1122 1127 Tracker.delete_all
1123 1128
1124 1129 get :new, :project_id => 1
1125 1130 assert_response 500
1126 1131 assert_error_tag :content => /No tracker/
1127 1132 end
1128 1133
1129 1134 def test_update_new_form
1130 1135 @request.session[:user_id] = 2
1131 1136 xhr :post, :new, :project_id => 1,
1132 1137 :issue => {:tracker_id => 2,
1133 1138 :subject => 'This is the test_new issue',
1134 1139 :description => 'This is the description',
1135 1140 :priority_id => 5}
1136 1141 assert_response :success
1137 1142 assert_template 'attributes'
1138 1143
1139 1144 issue = assigns(:issue)
1140 1145 assert_kind_of Issue, issue
1141 1146 assert_equal 1, issue.project_id
1142 1147 assert_equal 2, issue.tracker_id
1143 1148 assert_equal 'This is the test_new issue', issue.subject
1144 1149 end
1145 1150
1146 1151 def test_post_create
1147 1152 @request.session[:user_id] = 2
1148 1153 assert_difference 'Issue.count' do
1149 1154 post :create, :project_id => 1,
1150 1155 :issue => {:tracker_id => 3,
1151 1156 :status_id => 2,
1152 1157 :subject => 'This is the test_new issue',
1153 1158 :description => 'This is the description',
1154 1159 :priority_id => 5,
1155 1160 :start_date => '2010-11-07',
1156 1161 :estimated_hours => '',
1157 1162 :custom_field_values => {'2' => 'Value for field 2'}}
1158 1163 end
1159 1164 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1160 1165
1161 1166 issue = Issue.find_by_subject('This is the test_new issue')
1162 1167 assert_not_nil issue
1163 1168 assert_equal 2, issue.author_id
1164 1169 assert_equal 3, issue.tracker_id
1165 1170 assert_equal 2, issue.status_id
1166 1171 assert_equal Date.parse('2010-11-07'), issue.start_date
1167 1172 assert_nil issue.estimated_hours
1168 1173 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
1169 1174 assert_not_nil v
1170 1175 assert_equal 'Value for field 2', v.value
1171 1176 end
1172 1177
1173 1178 def test_post_new_with_group_assignment
1174 1179 group = Group.find(11)
1175 1180 project = Project.find(1)
1176 1181 project.members << Member.new(:principal => group, :roles => [Role.first])
1177 1182
1178 1183 with_settings :issue_group_assignment => '1' do
1179 1184 @request.session[:user_id] = 2
1180 1185 assert_difference 'Issue.count' do
1181 1186 post :create, :project_id => project.id,
1182 1187 :issue => {:tracker_id => 3,
1183 1188 :status_id => 1,
1184 1189 :subject => 'This is the test_new_with_group_assignment issue',
1185 1190 :assigned_to_id => group.id}
1186 1191 end
1187 1192 end
1188 1193 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1189 1194
1190 1195 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
1191 1196 assert_not_nil issue
1192 1197 assert_equal group, issue.assigned_to
1193 1198 end
1194 1199
1195 1200 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
1196 1201 Setting.default_issue_start_date_to_creation_date = 0
1197 1202
1198 1203 @request.session[:user_id] = 2
1199 1204 assert_difference 'Issue.count' do
1200 1205 post :create, :project_id => 1,
1201 1206 :issue => {:tracker_id => 3,
1202 1207 :status_id => 2,
1203 1208 :subject => 'This is the test_new issue',
1204 1209 :description => 'This is the description',
1205 1210 :priority_id => 5,
1206 1211 :estimated_hours => '',
1207 1212 :custom_field_values => {'2' => 'Value for field 2'}}
1208 1213 end
1209 1214 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1210 1215
1211 1216 issue = Issue.find_by_subject('This is the test_new issue')
1212 1217 assert_not_nil issue
1213 1218 assert_nil issue.start_date
1214 1219 end
1215 1220
1216 1221 def test_post_create_without_start_date_and_default_start_date_is_creation_date
1217 1222 Setting.default_issue_start_date_to_creation_date = 1
1218 1223
1219 1224 @request.session[:user_id] = 2
1220 1225 assert_difference 'Issue.count' do
1221 1226 post :create, :project_id => 1,
1222 1227 :issue => {:tracker_id => 3,
1223 1228 :status_id => 2,
1224 1229 :subject => 'This is the test_new issue',
1225 1230 :description => 'This is the description',
1226 1231 :priority_id => 5,
1227 1232 :estimated_hours => '',
1228 1233 :custom_field_values => {'2' => 'Value for field 2'}}
1229 1234 end
1230 1235 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1231 1236
1232 1237 issue = Issue.find_by_subject('This is the test_new issue')
1233 1238 assert_not_nil issue
1234 1239 assert_equal Date.today, issue.start_date
1235 1240 end
1236 1241
1237 1242 def test_post_create_and_continue
1238 1243 @request.session[:user_id] = 2
1239 1244 assert_difference 'Issue.count' do
1240 1245 post :create, :project_id => 1,
1241 1246 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
1242 1247 :continue => ''
1243 1248 end
1244 1249
1245 1250 issue = Issue.first(:order => 'id DESC')
1246 1251 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
1247 1252 assert_not_nil flash[:notice], "flash was not set"
1248 1253 assert flash[:notice].include?("<a href='/issues/#{issue.id}'>##{issue.id}</a>"), "issue link not found in flash: #{flash[:notice]}"
1249 1254 end
1250 1255
1251 1256 def test_post_create_without_custom_fields_param
1252 1257 @request.session[:user_id] = 2
1253 1258 assert_difference 'Issue.count' do
1254 1259 post :create, :project_id => 1,
1255 1260 :issue => {:tracker_id => 1,
1256 1261 :subject => 'This is the test_new issue',
1257 1262 :description => 'This is the description',
1258 1263 :priority_id => 5}
1259 1264 end
1260 1265 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1261 1266 end
1262 1267
1263 1268 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1264 1269 field = IssueCustomField.find_by_name('Database')
1265 1270 field.update_attribute(:is_required, true)
1266 1271
1267 1272 @request.session[:user_id] = 2
1268 1273 post :create, :project_id => 1,
1269 1274 :issue => {:tracker_id => 1,
1270 1275 :subject => 'This is the test_new issue',
1271 1276 :description => 'This is the description',
1272 1277 :priority_id => 5}
1273 1278 assert_response :success
1274 1279 assert_template 'new'
1275 1280 issue = assigns(:issue)
1276 1281 assert_not_nil issue
1277 1282 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
1278 1283 issue.errors[:custom_values].to_s
1279 1284 end
1280 1285
1281 1286 def test_post_create_with_watchers
1282 1287 @request.session[:user_id] = 2
1283 1288 ActionMailer::Base.deliveries.clear
1284 1289
1285 1290 assert_difference 'Watcher.count', 2 do
1286 1291 post :create, :project_id => 1,
1287 1292 :issue => {:tracker_id => 1,
1288 1293 :subject => 'This is a new issue with watchers',
1289 1294 :description => 'This is the description',
1290 1295 :priority_id => 5,
1291 1296 :watcher_user_ids => ['2', '3']}
1292 1297 end
1293 1298 issue = Issue.find_by_subject('This is a new issue with watchers')
1294 1299 assert_not_nil issue
1295 1300 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1296 1301
1297 1302 # Watchers added
1298 1303 assert_equal [2, 3], issue.watcher_user_ids.sort
1299 1304 assert issue.watched_by?(User.find(3))
1300 1305 # Watchers notified
1301 1306 mail = ActionMailer::Base.deliveries.last
1302 1307 assert_kind_of TMail::Mail, mail
1303 1308 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
1304 1309 end
1305 1310
1306 1311 def test_post_create_subissue
1307 1312 @request.session[:user_id] = 2
1308 1313
1309 1314 assert_difference 'Issue.count' do
1310 1315 post :create, :project_id => 1,
1311 1316 :issue => {:tracker_id => 1,
1312 1317 :subject => 'This is a child issue',
1313 1318 :parent_issue_id => 2}
1314 1319 end
1315 1320 issue = Issue.find_by_subject('This is a child issue')
1316 1321 assert_not_nil issue
1317 1322 assert_equal Issue.find(2), issue.parent
1318 1323 end
1319 1324
1320 1325 def test_post_create_subissue_with_non_numeric_parent_id
1321 1326 @request.session[:user_id] = 2
1322 1327
1323 1328 assert_difference 'Issue.count' do
1324 1329 post :create, :project_id => 1,
1325 1330 :issue => {:tracker_id => 1,
1326 1331 :subject => 'This is a child issue',
1327 1332 :parent_issue_id => 'ABC'}
1328 1333 end
1329 1334 issue = Issue.find_by_subject('This is a child issue')
1330 1335 assert_not_nil issue
1331 1336 assert_nil issue.parent
1332 1337 end
1333 1338
1334 1339 def test_post_create_private
1335 1340 @request.session[:user_id] = 2
1336 1341
1337 1342 assert_difference 'Issue.count' do
1338 1343 post :create, :project_id => 1,
1339 1344 :issue => {:tracker_id => 1,
1340 1345 :subject => 'This is a private issue',
1341 1346 :is_private => '1'}
1342 1347 end
1343 1348 issue = Issue.first(:order => 'id DESC')
1344 1349 assert issue.is_private?
1345 1350 end
1346 1351
1347 1352 def test_post_create_private_with_set_own_issues_private_permission
1348 1353 role = Role.find(1)
1349 1354 role.remove_permission! :set_issues_private
1350 1355 role.add_permission! :set_own_issues_private
1351 1356
1352 1357 @request.session[:user_id] = 2
1353 1358
1354 1359 assert_difference 'Issue.count' do
1355 1360 post :create, :project_id => 1,
1356 1361 :issue => {:tracker_id => 1,
1357 1362 :subject => 'This is a private issue',
1358 1363 :is_private => '1'}
1359 1364 end
1360 1365 issue = Issue.first(:order => 'id DESC')
1361 1366 assert issue.is_private?
1362 1367 end
1363 1368
1364 1369 def test_post_create_should_send_a_notification
1365 1370 ActionMailer::Base.deliveries.clear
1366 1371 @request.session[:user_id] = 2
1367 1372 assert_difference 'Issue.count' do
1368 1373 post :create, :project_id => 1,
1369 1374 :issue => {:tracker_id => 3,
1370 1375 :subject => 'This is the test_new issue',
1371 1376 :description => 'This is the description',
1372 1377 :priority_id => 5,
1373 1378 :estimated_hours => '',
1374 1379 :custom_field_values => {'2' => 'Value for field 2'}}
1375 1380 end
1376 1381 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1377 1382
1378 1383 assert_equal 1, ActionMailer::Base.deliveries.size
1379 1384 end
1380 1385
1381 1386 def test_post_create_should_preserve_fields_values_on_validation_failure
1382 1387 @request.session[:user_id] = 2
1383 1388 post :create, :project_id => 1,
1384 1389 :issue => {:tracker_id => 1,
1385 1390 # empty subject
1386 1391 :subject => '',
1387 1392 :description => 'This is a description',
1388 1393 :priority_id => 6,
1389 1394 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
1390 1395 assert_response :success
1391 1396 assert_template 'new'
1392 1397
1393 1398 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
1394 1399 :content => 'This is a description'
1395 1400 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1396 1401 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1397 1402 :value => '6' },
1398 1403 :content => 'High' }
1399 1404 # Custom fields
1400 1405 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
1401 1406 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1402 1407 :value => 'Oracle' },
1403 1408 :content => 'Oracle' }
1404 1409 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
1405 1410 :value => 'Value for field 2'}
1406 1411 end
1407 1412
1408 1413 def test_post_create_should_ignore_non_safe_attributes
1409 1414 @request.session[:user_id] = 2
1410 1415 assert_nothing_raised do
1411 1416 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
1412 1417 end
1413 1418 end
1414 1419
1415 1420 def test_post_create_with_attachment
1416 1421 set_tmp_attachments_directory
1417 1422 @request.session[:user_id] = 2
1418 1423
1419 1424 assert_difference 'Issue.count' do
1420 1425 assert_difference 'Attachment.count' do
1421 1426 post :create, :project_id => 1,
1422 1427 :issue => { :tracker_id => '1', :subject => 'With attachment' },
1423 1428 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1424 1429 end
1425 1430 end
1426 1431
1427 1432 issue = Issue.first(:order => 'id DESC')
1428 1433 attachment = Attachment.first(:order => 'id DESC')
1429 1434
1430 1435 assert_equal issue, attachment.container
1431 1436 assert_equal 2, attachment.author_id
1432 1437 assert_equal 'testfile.txt', attachment.filename
1433 1438 assert_equal 'text/plain', attachment.content_type
1434 1439 assert_equal 'test file', attachment.description
1435 1440 assert_equal 59, attachment.filesize
1436 1441 assert File.exists?(attachment.diskfile)
1437 1442 assert_equal 59, File.size(attachment.diskfile)
1438 1443 end
1439 1444
1440 1445 context "without workflow privilege" do
1441 1446 setup do
1442 1447 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1443 1448 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1444 1449 end
1445 1450
1446 1451 context "#new" do
1447 1452 should "propose default status only" do
1448 1453 get :new, :project_id => 1
1449 1454 assert_response :success
1450 1455 assert_template 'new'
1451 1456 assert_tag :tag => 'select',
1452 1457 :attributes => {:name => 'issue[status_id]'},
1453 1458 :children => {:count => 1},
1454 1459 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
1455 1460 end
1456 1461
1457 1462 should "accept default status" do
1458 1463 assert_difference 'Issue.count' do
1459 1464 post :create, :project_id => 1,
1460 1465 :issue => {:tracker_id => 1,
1461 1466 :subject => 'This is an issue',
1462 1467 :status_id => 1}
1463 1468 end
1464 1469 issue = Issue.last(:order => 'id')
1465 1470 assert_equal IssueStatus.default, issue.status
1466 1471 end
1467 1472
1468 1473 should "ignore unauthorized status" do
1469 1474 assert_difference 'Issue.count' do
1470 1475 post :create, :project_id => 1,
1471 1476 :issue => {:tracker_id => 1,
1472 1477 :subject => 'This is an issue',
1473 1478 :status_id => 3}
1474 1479 end
1475 1480 issue = Issue.last(:order => 'id')
1476 1481 assert_equal IssueStatus.default, issue.status
1477 1482 end
1478 1483 end
1479 1484
1480 1485 context "#update" do
1481 1486 should "ignore status change" do
1482 1487 assert_difference 'Journal.count' do
1483 1488 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1484 1489 end
1485 1490 assert_equal 1, Issue.find(1).status_id
1486 1491 end
1487 1492
1488 1493 should "ignore attributes changes" do
1489 1494 assert_difference 'Journal.count' do
1490 1495 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1491 1496 end
1492 1497 issue = Issue.find(1)
1493 1498 assert_equal "Can't print recipes", issue.subject
1494 1499 assert_nil issue.assigned_to
1495 1500 end
1496 1501 end
1497 1502 end
1498 1503
1499 1504 context "with workflow privilege" do
1500 1505 setup do
1501 1506 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1502 1507 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
1503 1508 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
1504 1509 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1505 1510 end
1506 1511
1507 1512 context "#update" do
1508 1513 should "accept authorized status" do
1509 1514 assert_difference 'Journal.count' do
1510 1515 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1511 1516 end
1512 1517 assert_equal 3, Issue.find(1).status_id
1513 1518 end
1514 1519
1515 1520 should "ignore unauthorized status" do
1516 1521 assert_difference 'Journal.count' do
1517 1522 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1518 1523 end
1519 1524 assert_equal 1, Issue.find(1).status_id
1520 1525 end
1521 1526
1522 1527 should "accept authorized attributes changes" do
1523 1528 assert_difference 'Journal.count' do
1524 1529 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
1525 1530 end
1526 1531 issue = Issue.find(1)
1527 1532 assert_equal 2, issue.assigned_to_id
1528 1533 end
1529 1534
1530 1535 should "ignore unauthorized attributes changes" do
1531 1536 assert_difference 'Journal.count' do
1532 1537 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
1533 1538 end
1534 1539 issue = Issue.find(1)
1535 1540 assert_equal "Can't print recipes", issue.subject
1536 1541 end
1537 1542 end
1538 1543
1539 1544 context "and :edit_issues permission" do
1540 1545 setup do
1541 1546 Role.anonymous.add_permission! :add_issues, :edit_issues
1542 1547 end
1543 1548
1544 1549 should "accept authorized status" do
1545 1550 assert_difference 'Journal.count' do
1546 1551 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1547 1552 end
1548 1553 assert_equal 3, Issue.find(1).status_id
1549 1554 end
1550 1555
1551 1556 should "ignore unauthorized status" do
1552 1557 assert_difference 'Journal.count' do
1553 1558 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1554 1559 end
1555 1560 assert_equal 1, Issue.find(1).status_id
1556 1561 end
1557 1562
1558 1563 should "accept authorized attributes changes" do
1559 1564 assert_difference 'Journal.count' do
1560 1565 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1561 1566 end
1562 1567 issue = Issue.find(1)
1563 1568 assert_equal "changed", issue.subject
1564 1569 assert_equal 2, issue.assigned_to_id
1565 1570 end
1566 1571 end
1567 1572 end
1568 1573
1569 1574 def test_copy_issue
1570 1575 @request.session[:user_id] = 2
1571 1576 get :new, :project_id => 1, :copy_from => 1
1572 1577 assert_template 'new'
1573 1578 assert_not_nil assigns(:issue)
1574 1579 orig = Issue.find(1)
1575 1580 assert_equal orig.subject, assigns(:issue).subject
1576 1581 end
1577 1582
1578 1583 def test_get_edit
1579 1584 @request.session[:user_id] = 2
1580 1585 get :edit, :id => 1
1581 1586 assert_response :success
1582 1587 assert_template 'edit'
1583 1588 assert_not_nil assigns(:issue)
1584 1589 assert_equal Issue.find(1), assigns(:issue)
1585 1590
1586 1591 # Be sure we don't display inactive IssuePriorities
1587 1592 assert ! IssuePriority.find(15).active?
1588 1593 assert_no_tag :option, :attributes => {:value => '15'},
1589 1594 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1590 1595 end
1591 1596
1592 1597 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
1593 1598 @request.session[:user_id] = 2
1594 1599 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
1595 1600
1596 1601 get :edit, :id => 1
1597 1602 assert_tag 'input', :attributes => {:name => 'time_entry[hours]'}
1598 1603 end
1599 1604
1600 1605 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
1601 1606 @request.session[:user_id] = 2
1602 1607 Role.find_by_name('Manager').remove_permission! :log_time
1603 1608
1604 1609 get :edit, :id => 1
1605 1610 assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'}
1606 1611 end
1607 1612
1608 1613 def test_get_edit_with_params
1609 1614 @request.session[:user_id] = 2
1610 1615 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
1611 1616 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
1612 1617 assert_response :success
1613 1618 assert_template 'edit'
1614 1619
1615 1620 issue = assigns(:issue)
1616 1621 assert_not_nil issue
1617 1622
1618 1623 assert_equal 5, issue.status_id
1619 1624 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
1620 1625 :child => { :tag => 'option',
1621 1626 :content => 'Closed',
1622 1627 :attributes => { :selected => 'selected' } }
1623 1628
1624 1629 assert_equal 7, issue.priority_id
1625 1630 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1626 1631 :child => { :tag => 'option',
1627 1632 :content => 'Urgent',
1628 1633 :attributes => { :selected => 'selected' } }
1629 1634
1630 1635 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
1631 1636 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
1632 1637 :child => { :tag => 'option',
1633 1638 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
1634 1639 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
1635 1640 end
1636 1641
1637 1642 def test_update_edit_form
1638 1643 @request.session[:user_id] = 2
1639 xhr :post, :new, :project_id => 1,
1644 xhr :put, :new, :project_id => 1,
1640 1645 :id => 1,
1641 1646 :issue => {:tracker_id => 2,
1642 1647 :subject => 'This is the test_new issue',
1643 1648 :description => 'This is the description',
1644 1649 :priority_id => 5}
1645 1650 assert_response :success
1646 1651 assert_template 'attributes'
1647 1652
1648 1653 issue = assigns(:issue)
1649 1654 assert_kind_of Issue, issue
1650 1655 assert_equal 1, issue.id
1651 1656 assert_equal 1, issue.project_id
1652 1657 assert_equal 2, issue.tracker_id
1653 1658 assert_equal 'This is the test_new issue', issue.subject
1654 1659 end
1655 1660
1661 def test_update_edit_form_with_project_change
1662 @request.session[:user_id] = 2
1663 xhr :put, :new, :project_id => 1,
1664 :id => 1,
1665 :project_change => '1',
1666 :issue => {:project_id => 2,
1667 :tracker_id => 2,
1668 :subject => 'This is the test_new issue',
1669 :description => 'This is the description',
1670 :priority_id => 5}
1671 assert_response :success
1672 assert_template 'form'
1673
1674 issue = assigns(:issue)
1675 assert_kind_of Issue, issue
1676 assert_equal 1, issue.id
1677 assert_equal 2, issue.project_id
1678 assert_equal 2, issue.tracker_id
1679 assert_equal 'This is the test_new issue', issue.subject
1680 end
1681
1656 1682 def test_update_using_invalid_http_verbs
1657 1683 @request.session[:user_id] = 2
1658 1684 subject = 'Updated by an invalid http verb'
1659 1685
1660 1686 get :update, :id => 1, :issue => {:subject => subject}
1661 1687 assert_not_equal subject, Issue.find(1).subject
1662 1688
1663 1689 post :update, :id => 1, :issue => {:subject => subject}
1664 1690 assert_not_equal subject, Issue.find(1).subject
1665 1691
1666 1692 delete :update, :id => 1, :issue => {:subject => subject}
1667 1693 assert_not_equal subject, Issue.find(1).subject
1668 1694 end
1669 1695
1670 1696 def test_put_update_without_custom_fields_param
1671 1697 @request.session[:user_id] = 2
1672 1698 ActionMailer::Base.deliveries.clear
1673 1699
1674 1700 issue = Issue.find(1)
1675 1701 assert_equal '125', issue.custom_value_for(2).value
1676 1702 old_subject = issue.subject
1677 1703 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
1678 1704
1679 1705 assert_difference('Journal.count') do
1680 1706 assert_difference('JournalDetail.count', 2) do
1681 1707 put :update, :id => 1, :issue => {:subject => new_subject,
1682 1708 :priority_id => '6',
1683 1709 :category_id => '1' # no change
1684 1710 }
1685 1711 end
1686 1712 end
1687 1713 assert_redirected_to :action => 'show', :id => '1'
1688 1714 issue.reload
1689 1715 assert_equal new_subject, issue.subject
1690 1716 # Make sure custom fields were not cleared
1691 1717 assert_equal '125', issue.custom_value_for(2).value
1692 1718
1693 1719 mail = ActionMailer::Base.deliveries.last
1694 1720 assert_kind_of TMail::Mail, mail
1695 1721 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
1696 1722 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
1697 1723 end
1698 1724
1725 def test_put_update_with_project_change
1726 @request.session[:user_id] = 2
1727 ActionMailer::Base.deliveries.clear
1728
1729 assert_difference('Journal.count') do
1730 assert_difference('JournalDetail.count', 3) do
1731 put :update, :id => 1, :issue => {:project_id => '2',
1732 :tracker_id => '1', # no change
1733 :priority_id => '6',
1734 :category_id => '3'
1735 }
1736 end
1737 end
1738 assert_redirected_to :action => 'show', :id => '1'
1739 issue = Issue.find(1)
1740 assert_equal 2, issue.project_id
1741 assert_equal 1, issue.tracker_id
1742 assert_equal 6, issue.priority_id
1743 assert_equal 3, issue.category_id
1744
1745 mail = ActionMailer::Base.deliveries.last
1746 assert_not_nil mail
1747 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
1748 assert mail.body.include?("Project changed from eCookbook to OnlineStore")
1749 end
1750
1751 def test_put_update_with_tracker_change
1752 @request.session[:user_id] = 2
1753 ActionMailer::Base.deliveries.clear
1754
1755 assert_difference('Journal.count') do
1756 assert_difference('JournalDetail.count', 2) do
1757 put :update, :id => 1, :issue => {:project_id => '1',
1758 :tracker_id => '2',
1759 :priority_id => '6'
1760 }
1761 end
1762 end
1763 assert_redirected_to :action => 'show', :id => '1'
1764 issue = Issue.find(1)
1765 assert_equal 1, issue.project_id
1766 assert_equal 2, issue.tracker_id
1767 assert_equal 6, issue.priority_id
1768 assert_equal 1, issue.category_id
1769
1770 mail = ActionMailer::Base.deliveries.last
1771 assert_not_nil mail
1772 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
1773 assert mail.body.include?("Tracker changed from Bug to Feature request")
1774 end
1775
1699 1776 def test_put_update_with_custom_field_change
1700 1777 @request.session[:user_id] = 2
1701 1778 issue = Issue.find(1)
1702 1779 assert_equal '125', issue.custom_value_for(2).value
1703 1780
1704 1781 assert_difference('Journal.count') do
1705 1782 assert_difference('JournalDetail.count', 3) do
1706 1783 put :update, :id => 1, :issue => {:subject => 'Custom field change',
1707 1784 :priority_id => '6',
1708 1785 :category_id => '1', # no change
1709 1786 :custom_field_values => { '2' => 'New custom value' }
1710 1787 }
1711 1788 end
1712 1789 end
1713 1790 assert_redirected_to :action => 'show', :id => '1'
1714 1791 issue.reload
1715 1792 assert_equal 'New custom value', issue.custom_value_for(2).value
1716 1793
1717 1794 mail = ActionMailer::Base.deliveries.last
1718 1795 assert_kind_of TMail::Mail, mail
1719 1796 assert mail.body.include?("Searchable field changed from 125 to New custom value")
1720 1797 end
1721 1798
1722 1799 def test_put_update_with_status_and_assignee_change
1723 1800 issue = Issue.find(1)
1724 1801 assert_equal 1, issue.status_id
1725 1802 @request.session[:user_id] = 2
1726 1803 assert_difference('TimeEntry.count', 0) do
1727 1804 put :update,
1728 1805 :id => 1,
1729 1806 :issue => { :status_id => 2, :assigned_to_id => 3 },
1730 1807 :notes => 'Assigned to dlopper',
1731 1808 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
1732 1809 end
1733 1810 assert_redirected_to :action => 'show', :id => '1'
1734 1811 issue.reload
1735 1812 assert_equal 2, issue.status_id
1736 1813 j = Journal.find(:first, :order => 'id DESC')
1737 1814 assert_equal 'Assigned to dlopper', j.notes
1738 1815 assert_equal 2, j.details.size
1739 1816
1740 1817 mail = ActionMailer::Base.deliveries.last
1741 1818 assert mail.body.include?("Status changed from New to Assigned")
1742 1819 # subject should contain the new status
1743 1820 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
1744 1821 end
1745 1822
1746 1823 def test_put_update_with_note_only
1747 1824 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
1748 1825 # anonymous user
1749 1826 put :update,
1750 1827 :id => 1,
1751 1828 :notes => notes
1752 1829 assert_redirected_to :action => 'show', :id => '1'
1753 1830 j = Journal.find(:first, :order => 'id DESC')
1754 1831 assert_equal notes, j.notes
1755 1832 assert_equal 0, j.details.size
1756 1833 assert_equal User.anonymous, j.user
1757 1834
1758 1835 mail = ActionMailer::Base.deliveries.last
1759 1836 assert mail.body.include?(notes)
1760 1837 end
1761 1838
1762 1839 def test_put_update_with_note_and_spent_time
1763 1840 @request.session[:user_id] = 2
1764 1841 spent_hours_before = Issue.find(1).spent_hours
1765 1842 assert_difference('TimeEntry.count') do
1766 1843 put :update,
1767 1844 :id => 1,
1768 1845 :notes => '2.5 hours added',
1769 1846 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
1770 1847 end
1771 1848 assert_redirected_to :action => 'show', :id => '1'
1772 1849
1773 1850 issue = Issue.find(1)
1774 1851
1775 1852 j = Journal.find(:first, :order => 'id DESC')
1776 1853 assert_equal '2.5 hours added', j.notes
1777 1854 assert_equal 0, j.details.size
1778 1855
1779 1856 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
1780 1857 assert_not_nil t
1781 1858 assert_equal 2.5, t.hours
1782 1859 assert_equal spent_hours_before + 2.5, issue.spent_hours
1783 1860 end
1784 1861
1785 1862 def test_put_update_with_attachment_only
1786 1863 set_tmp_attachments_directory
1787 1864
1788 1865 # Delete all fixtured journals, a race condition can occur causing the wrong
1789 1866 # journal to get fetched in the next find.
1790 1867 Journal.delete_all
1791 1868
1792 1869 # anonymous user
1793 1870 assert_difference 'Attachment.count' do
1794 1871 put :update, :id => 1,
1795 1872 :notes => '',
1796 1873 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1797 1874 end
1798 1875
1799 1876 assert_redirected_to :action => 'show', :id => '1'
1800 1877 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
1801 1878 assert j.notes.blank?
1802 1879 assert_equal 1, j.details.size
1803 1880 assert_equal 'testfile.txt', j.details.first.value
1804 1881 assert_equal User.anonymous, j.user
1805 1882
1806 1883 attachment = Attachment.first(:order => 'id DESC')
1807 1884 assert_equal Issue.find(1), attachment.container
1808 1885 assert_equal User.anonymous, attachment.author
1809 1886 assert_equal 'testfile.txt', attachment.filename
1810 1887 assert_equal 'text/plain', attachment.content_type
1811 1888 assert_equal 'test file', attachment.description
1812 1889 assert_equal 59, attachment.filesize
1813 1890 assert File.exists?(attachment.diskfile)
1814 1891 assert_equal 59, File.size(attachment.diskfile)
1815 1892
1816 1893 mail = ActionMailer::Base.deliveries.last
1817 1894 assert mail.body.include?('testfile.txt')
1818 1895 end
1819 1896
1820 1897 def test_put_update_with_attachment_that_fails_to_save
1821 1898 set_tmp_attachments_directory
1822 1899
1823 1900 # Delete all fixtured journals, a race condition can occur causing the wrong
1824 1901 # journal to get fetched in the next find.
1825 1902 Journal.delete_all
1826 1903
1827 1904 # Mock out the unsaved attachment
1828 1905 Attachment.any_instance.stubs(:create).returns(Attachment.new)
1829 1906
1830 1907 # anonymous user
1831 1908 put :update,
1832 1909 :id => 1,
1833 1910 :notes => '',
1834 1911 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
1835 1912 assert_redirected_to :action => 'show', :id => '1'
1836 1913 assert_equal '1 file(s) could not be saved.', flash[:warning]
1837 1914
1838 1915 end if Object.const_defined?(:Mocha)
1839 1916
1840 1917 def test_put_update_with_no_change
1841 1918 issue = Issue.find(1)
1842 1919 issue.journals.clear
1843 1920 ActionMailer::Base.deliveries.clear
1844 1921
1845 1922 put :update,
1846 1923 :id => 1,
1847 1924 :notes => ''
1848 1925 assert_redirected_to :action => 'show', :id => '1'
1849 1926
1850 1927 issue.reload
1851 1928 assert issue.journals.empty?
1852 1929 # No email should be sent
1853 1930 assert ActionMailer::Base.deliveries.empty?
1854 1931 end
1855 1932
1856 1933 def test_put_update_should_send_a_notification
1857 1934 @request.session[:user_id] = 2
1858 1935 ActionMailer::Base.deliveries.clear
1859 1936 issue = Issue.find(1)
1860 1937 old_subject = issue.subject
1861 1938 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
1862 1939
1863 1940 put :update, :id => 1, :issue => {:subject => new_subject,
1864 1941 :priority_id => '6',
1865 1942 :category_id => '1' # no change
1866 1943 }
1867 1944 assert_equal 1, ActionMailer::Base.deliveries.size
1868 1945 end
1869 1946
1870 1947 def test_put_update_with_invalid_spent_time_hours_only
1871 1948 @request.session[:user_id] = 2
1872 1949 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1873 1950
1874 1951 assert_no_difference('Journal.count') do
1875 1952 put :update,
1876 1953 :id => 1,
1877 1954 :notes => notes,
1878 1955 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
1879 1956 end
1880 1957 assert_response :success
1881 1958 assert_template 'edit'
1882 1959
1883 1960 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1884 1961 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1885 1962 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
1886 1963 end
1887 1964
1888 1965 def test_put_update_with_invalid_spent_time_comments_only
1889 1966 @request.session[:user_id] = 2
1890 1967 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1891 1968
1892 1969 assert_no_difference('Journal.count') do
1893 1970 put :update,
1894 1971 :id => 1,
1895 1972 :notes => notes,
1896 1973 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
1897 1974 end
1898 1975 assert_response :success
1899 1976 assert_template 'edit'
1900 1977
1901 1978 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1902 1979 assert_error_tag :descendant => {:content => /Hours can't be blank/}
1903 1980 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1904 1981 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
1905 1982 end
1906 1983
1907 1984 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
1908 1985 issue = Issue.find(2)
1909 1986 @request.session[:user_id] = 2
1910 1987
1911 1988 put :update,
1912 1989 :id => issue.id,
1913 1990 :issue => {
1914 1991 :fixed_version_id => 4
1915 1992 }
1916 1993
1917 1994 assert_response :redirect
1918 1995 issue.reload
1919 1996 assert_equal 4, issue.fixed_version_id
1920 1997 assert_not_equal issue.project_id, issue.fixed_version.project_id
1921 1998 end
1922 1999
1923 2000 def test_put_update_should_redirect_back_using_the_back_url_parameter
1924 2001 issue = Issue.find(2)
1925 2002 @request.session[:user_id] = 2
1926 2003
1927 2004 put :update,
1928 2005 :id => issue.id,
1929 2006 :issue => {
1930 2007 :fixed_version_id => 4
1931 2008 },
1932 2009 :back_url => '/issues'
1933 2010
1934 2011 assert_response :redirect
1935 2012 assert_redirected_to '/issues'
1936 2013 end
1937 2014
1938 2015 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1939 2016 issue = Issue.find(2)
1940 2017 @request.session[:user_id] = 2
1941 2018
1942 2019 put :update,
1943 2020 :id => issue.id,
1944 2021 :issue => {
1945 2022 :fixed_version_id => 4
1946 2023 },
1947 2024 :back_url => 'http://google.com'
1948 2025
1949 2026 assert_response :redirect
1950 2027 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
1951 2028 end
1952 2029
1953 2030 def test_get_bulk_edit
1954 2031 @request.session[:user_id] = 2
1955 2032 get :bulk_edit, :ids => [1, 2]
1956 2033 assert_response :success
1957 2034 assert_template 'bulk_edit'
1958 2035
1959 2036 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1960 2037
1961 2038 # Project specific custom field, date type
1962 2039 field = CustomField.find(9)
1963 2040 assert !field.is_for_all?
1964 2041 assert_equal 'date', field.field_format
1965 2042 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1966 2043
1967 2044 # System wide custom field
1968 2045 assert CustomField.find(1).is_for_all?
1969 2046 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1970 2047
1971 2048 # Be sure we don't display inactive IssuePriorities
1972 2049 assert ! IssuePriority.find(15).active?
1973 2050 assert_no_tag :option, :attributes => {:value => '15'},
1974 2051 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1975 2052 end
1976 2053
1977 2054 def test_get_bulk_edit_on_different_projects
1978 2055 @request.session[:user_id] = 2
1979 2056 get :bulk_edit, :ids => [1, 2, 6]
1980 2057 assert_response :success
1981 2058 assert_template 'bulk_edit'
1982 2059
1983 2060 # Can not set issues from different projects as children of an issue
1984 2061 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1985 2062
1986 2063 # Project specific custom field, date type
1987 2064 field = CustomField.find(9)
1988 2065 assert !field.is_for_all?
1989 2066 assert !field.project_ids.include?(Issue.find(6).project_id)
1990 2067 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1991 2068 end
1992 2069
1993 2070 def test_get_bulk_edit_with_user_custom_field
1994 2071 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
1995 2072
1996 2073 @request.session[:user_id] = 2
1997 2074 get :bulk_edit, :ids => [1, 2]
1998 2075 assert_response :success
1999 2076 assert_template 'bulk_edit'
2000 2077
2001 2078 assert_tag :select,
2002 2079 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2003 2080 :children => {
2004 2081 :only => {:tag => 'option'},
2005 2082 :count => Project.find(1).users.count + 1
2006 2083 }
2007 2084 end
2008 2085
2009 2086 def test_get_bulk_edit_with_version_custom_field
2010 2087 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
2011 2088
2012 2089 @request.session[:user_id] = 2
2013 2090 get :bulk_edit, :ids => [1, 2]
2014 2091 assert_response :success
2015 2092 assert_template 'bulk_edit'
2016 2093
2017 2094 assert_tag :select,
2018 2095 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2019 2096 :children => {
2020 2097 :only => {:tag => 'option'},
2021 2098 :count => Project.find(1).shared_versions.count + 1
2022 2099 }
2023 2100 end
2024 2101
2025 2102 def test_bulk_update
2026 2103 @request.session[:user_id] = 2
2027 2104 # update issues priority
2028 2105 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2029 2106 :issue => {:priority_id => 7,
2030 2107 :assigned_to_id => '',
2031 2108 :custom_field_values => {'2' => ''}}
2032 2109
2033 2110 assert_response 302
2034 2111 # check that the issues were updated
2035 2112 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
2036 2113
2037 2114 issue = Issue.find(1)
2038 2115 journal = issue.journals.find(:first, :order => 'created_on DESC')
2039 2116 assert_equal '125', issue.custom_value_for(2).value
2040 2117 assert_equal 'Bulk editing', journal.notes
2041 2118 assert_equal 1, journal.details.size
2042 2119 end
2043 2120
2044 2121 def test_bulk_update_with_group_assignee
2045 2122 group = Group.find(11)
2046 2123 project = Project.find(1)
2047 2124 project.members << Member.new(:principal => group, :roles => [Role.first])
2048 2125
2049 2126 @request.session[:user_id] = 2
2050 2127 # update issues assignee
2051 2128 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2052 2129 :issue => {:priority_id => '',
2053 2130 :assigned_to_id => group.id,
2054 2131 :custom_field_values => {'2' => ''}}
2055 2132
2056 2133 assert_response 302
2057 2134 assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
2058 2135 end
2059 2136
2060 2137 def test_bulk_update_on_different_projects
2061 2138 @request.session[:user_id] = 2
2062 2139 # update issues priority
2063 2140 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
2064 2141 :issue => {:priority_id => 7,
2065 2142 :assigned_to_id => '',
2066 2143 :custom_field_values => {'2' => ''}}
2067 2144
2068 2145 assert_response 302
2069 2146 # check that the issues were updated
2070 2147 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
2071 2148
2072 2149 issue = Issue.find(1)
2073 2150 journal = issue.journals.find(:first, :order => 'created_on DESC')
2074 2151 assert_equal '125', issue.custom_value_for(2).value
2075 2152 assert_equal 'Bulk editing', journal.notes
2076 2153 assert_equal 1, journal.details.size
2077 2154 end
2078 2155
2079 2156 def test_bulk_update_on_different_projects_without_rights
2080 2157 @request.session[:user_id] = 3
2081 2158 user = User.find(3)
2082 2159 action = { :controller => "issues", :action => "bulk_update" }
2083 2160 assert user.allowed_to?(action, Issue.find(1).project)
2084 2161 assert ! user.allowed_to?(action, Issue.find(6).project)
2085 2162 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
2086 2163 :issue => {:priority_id => 7,
2087 2164 :assigned_to_id => '',
2088 2165 :custom_field_values => {'2' => ''}}
2089 2166 assert_response 403
2090 2167 assert_not_equal "Bulk should fail", Journal.last.notes
2091 2168 end
2092 2169
2093 2170 def test_bullk_update_should_send_a_notification
2094 2171 @request.session[:user_id] = 2
2095 2172 ActionMailer::Base.deliveries.clear
2096 2173 post(:bulk_update,
2097 2174 {
2098 2175 :ids => [1, 2],
2099 2176 :notes => 'Bulk editing',
2100 2177 :issue => {
2101 2178 :priority_id => 7,
2102 2179 :assigned_to_id => '',
2103 2180 :custom_field_values => {'2' => ''}
2104 2181 }
2105 2182 })
2106 2183
2107 2184 assert_response 302
2108 2185 assert_equal 2, ActionMailer::Base.deliveries.size
2109 2186 end
2110 2187
2111 2188 def test_bulk_update_status
2112 2189 @request.session[:user_id] = 2
2113 2190 # update issues priority
2114 2191 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
2115 2192 :issue => {:priority_id => '',
2116 2193 :assigned_to_id => '',
2117 2194 :status_id => '5'}
2118 2195
2119 2196 assert_response 302
2120 2197 issue = Issue.find(1)
2121 2198 assert issue.closed?
2122 2199 end
2123 2200
2124 2201 def test_bulk_update_parent_id
2125 2202 @request.session[:user_id] = 2
2126 2203 post :bulk_update, :ids => [1, 3],
2127 2204 :notes => 'Bulk editing parent',
2128 2205 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
2129 2206
2130 2207 assert_response 302
2131 2208 parent = Issue.find(2)
2132 2209 assert_equal parent.id, Issue.find(1).parent_id
2133 2210 assert_equal parent.id, Issue.find(3).parent_id
2134 2211 assert_equal [1, 3], parent.children.collect(&:id).sort
2135 2212 end
2136 2213
2137 2214 def test_bulk_update_custom_field
2138 2215 @request.session[:user_id] = 2
2139 2216 # update issues priority
2140 2217 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
2141 2218 :issue => {:priority_id => '',
2142 2219 :assigned_to_id => '',
2143 2220 :custom_field_values => {'2' => '777'}}
2144 2221
2145 2222 assert_response 302
2146 2223
2147 2224 issue = Issue.find(1)
2148 2225 journal = issue.journals.find(:first, :order => 'created_on DESC')
2149 2226 assert_equal '777', issue.custom_value_for(2).value
2150 2227 assert_equal 1, journal.details.size
2151 2228 assert_equal '125', journal.details.first.old_value
2152 2229 assert_equal '777', journal.details.first.value
2153 2230 end
2154 2231
2155 2232 def test_bulk_update_unassign
2156 2233 assert_not_nil Issue.find(2).assigned_to
2157 2234 @request.session[:user_id] = 2
2158 2235 # unassign issues
2159 2236 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
2160 2237 assert_response 302
2161 2238 # check that the issues were updated
2162 2239 assert_nil Issue.find(2).assigned_to
2163 2240 end
2164 2241
2165 2242 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
2166 2243 @request.session[:user_id] = 2
2167 2244
2168 2245 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
2169 2246
2170 2247 assert_response :redirect
2171 2248 issues = Issue.find([1,2])
2172 2249 issues.each do |issue|
2173 2250 assert_equal 4, issue.fixed_version_id
2174 2251 assert_not_equal issue.project_id, issue.fixed_version.project_id
2175 2252 end
2176 2253 end
2177 2254
2178 2255 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
2179 2256 @request.session[:user_id] = 2
2180 2257 post :bulk_update, :ids => [1,2], :back_url => '/issues'
2181 2258
2182 2259 assert_response :redirect
2183 2260 assert_redirected_to '/issues'
2184 2261 end
2185 2262
2186 2263 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2187 2264 @request.session[:user_id] = 2
2188 2265 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
2189 2266
2190 2267 assert_response :redirect
2191 2268 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
2192 2269 end
2193 2270
2194 2271 def test_destroy_issue_with_no_time_entries
2195 2272 assert_nil TimeEntry.find_by_issue_id(2)
2196 2273 @request.session[:user_id] = 2
2197 2274 delete :destroy, :id => 2
2198 2275 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2199 2276 assert_nil Issue.find_by_id(2)
2200 2277 end
2201 2278
2202 2279 def test_destroy_issues_with_time_entries
2203 2280 @request.session[:user_id] = 2
2204 2281 delete :destroy, :ids => [1, 3]
2205 2282 assert_response :success
2206 2283 assert_template 'destroy'
2207 2284 assert_not_nil assigns(:hours)
2208 2285 assert Issue.find_by_id(1) && Issue.find_by_id(3)
2209 2286 end
2210 2287
2211 2288 def test_destroy_issues_and_destroy_time_entries
2212 2289 @request.session[:user_id] = 2
2213 2290 delete :destroy, :ids => [1, 3], :todo => 'destroy'
2214 2291 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2215 2292 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2216 2293 assert_nil TimeEntry.find_by_id([1, 2])
2217 2294 end
2218 2295
2219 2296 def test_destroy_issues_and_assign_time_entries_to_project
2220 2297 @request.session[:user_id] = 2
2221 2298 delete :destroy, :ids => [1, 3], :todo => 'nullify'
2222 2299 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2223 2300 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2224 2301 assert_nil TimeEntry.find(1).issue_id
2225 2302 assert_nil TimeEntry.find(2).issue_id
2226 2303 end
2227 2304
2228 2305 def test_destroy_issues_and_reassign_time_entries_to_another_issue
2229 2306 @request.session[:user_id] = 2
2230 2307 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
2231 2308 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2232 2309 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2233 2310 assert_equal 2, TimeEntry.find(1).issue_id
2234 2311 assert_equal 2, TimeEntry.find(2).issue_id
2235 2312 end
2236 2313
2237 2314 def test_destroy_issues_from_different_projects
2238 2315 @request.session[:user_id] = 2
2239 2316 delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
2240 2317 assert_redirected_to :controller => 'issues', :action => 'index'
2241 2318 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
2242 2319 end
2243 2320
2244 2321 def test_destroy_parent_and_child_issues
2245 2322 parent = Issue.generate!(:project_id => 1, :tracker_id => 1)
2246 2323 child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id)
2247 2324 assert child.is_descendant_of?(parent.reload)
2248 2325
2249 2326 @request.session[:user_id] = 2
2250 2327 assert_difference 'Issue.count', -2 do
2251 2328 delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
2252 2329 end
2253 2330 assert_response 302
2254 2331 end
2255 2332
2256 2333 def test_default_search_scope
2257 2334 get :index
2258 2335 assert_tag :div, :attributes => {:id => 'quick-search'},
2259 2336 :child => {:tag => 'form',
2260 2337 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
2261 2338 end
2262 2339 end
@@ -1,574 +1,590
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class ApiTest::IssuesTest < ActionController::IntegrationTest
21 21 fixtures :projects,
22 22 :users,
23 23 :roles,
24 24 :members,
25 25 :member_roles,
26 26 :issues,
27 27 :issue_statuses,
28 28 :versions,
29 29 :trackers,
30 30 :projects_trackers,
31 31 :issue_categories,
32 32 :enabled_modules,
33 33 :enumerations,
34 34 :attachments,
35 35 :workflows,
36 36 :custom_fields,
37 37 :custom_values,
38 38 :custom_fields_projects,
39 39 :custom_fields_trackers,
40 40 :time_entries,
41 41 :journals,
42 42 :journal_details,
43 43 :queries,
44 44 :attachments
45 45
46 46 def setup
47 47 Setting.rest_api_enabled = '1'
48 48 end
49 49
50 50 context "/issues" do
51 51 # Use a private project to make sure auth is really working and not just
52 52 # only showing public issues.
53 53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
54 54
55 55 should "contain metadata" do
56 56 get '/issues.xml'
57 57
58 58 assert_tag :tag => 'issues',
59 59 :attributes => {
60 60 :type => 'array',
61 61 :total_count => assigns(:issue_count),
62 62 :limit => 25,
63 63 :offset => 0
64 64 }
65 65 end
66 66
67 67 context "with offset and limit" do
68 68 should "use the params" do
69 69 get '/issues.xml?offset=2&limit=3'
70 70
71 71 assert_equal 3, assigns(:limit)
72 72 assert_equal 2, assigns(:offset)
73 73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
74 74 end
75 75 end
76 76
77 77 context "with nometa param" do
78 78 should "not contain metadata" do
79 79 get '/issues.xml?nometa=1'
80 80
81 81 assert_tag :tag => 'issues',
82 82 :attributes => {
83 83 :type => 'array',
84 84 :total_count => nil,
85 85 :limit => nil,
86 86 :offset => nil
87 87 }
88 88 end
89 89 end
90 90
91 91 context "with nometa header" do
92 92 should "not contain metadata" do
93 93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
94 94
95 95 assert_tag :tag => 'issues',
96 96 :attributes => {
97 97 :type => 'array',
98 98 :total_count => nil,
99 99 :limit => nil,
100 100 :offset => nil
101 101 }
102 102 end
103 103 end
104 104
105 105 context "with relations" do
106 106 should "display relations" do
107 107 get '/issues.xml?include=relations'
108 108
109 109 assert_response :success
110 110 assert_equal 'application/xml', @response.content_type
111 111 assert_tag 'relations',
112 112 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}},
113 113 :children => {:count => 1},
114 114 :child => {
115 115 :tag => 'relation',
116 116 :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', :relation_type => 'relates'}
117 117 }
118 118 assert_tag 'relations',
119 119 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}},
120 120 :children => {:count => 0}
121 121 end
122 122 end
123 123
124 124 context "with invalid query params" do
125 125 should "return errors" do
126 126 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
127 127
128 128 assert_response :unprocessable_entity
129 129 assert_equal 'application/xml', @response.content_type
130 130 assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
131 131 end
132 132 end
133 133
134 134 context "with custom field filter" do
135 135 should "show only issues with the custom field value" do
136 136 get '/issues.xml', { :set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, :v => {:cf_1 => ['MySQL']}}
137 137
138 138 expected_ids = Issue.visible.all(
139 139 :include => :custom_values,
140 140 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
141 141
142 142 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
143 143 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
144 144 end
145 145 end
146 146 end
147 147
148 148 context "with custom field filter (shorthand method)" do
149 149 should "show only issues with the custom field value" do
150 150 get '/issues.xml', { :cf_1 => 'MySQL' }
151 151
152 152 expected_ids = Issue.visible.all(
153 153 :include => :custom_values,
154 154 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
155 155
156 156 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
157 157 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
158 158 end
159 159 end
160 160 end
161 161 end
162 162
163 163 context "/index.json" do
164 164 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
165 165 end
166 166
167 167 context "/index.xml with filter" do
168 168 should "show only issues with the status_id" do
169 169 get '/issues.xml?status_id=5'
170 170
171 171 expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id)
172 172
173 173 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
174 174 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
175 175 end
176 176 end
177 177 end
178 178
179 179 context "/index.json with filter" do
180 180 should "show only issues with the status_id" do
181 181 get '/issues.json?status_id=5'
182 182
183 183 json = ActiveSupport::JSON.decode(response.body)
184 184 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
185 185 assert_equal 3, status_ids_used.length
186 186 assert status_ids_used.all? {|id| id == 5 }
187 187 end
188 188
189 189 end
190 190
191 191 # Issue 6 is on a private project
192 192 context "/issues/6.xml" do
193 193 should_allow_api_authentication(:get, "/issues/6.xml")
194 194 end
195 195
196 196 context "/issues/6.json" do
197 197 should_allow_api_authentication(:get, "/issues/6.json")
198 198 end
199 199
200 200 context "GET /issues/:id" do
201 201 context "with journals" do
202 202 context ".xml" do
203 203 should "display journals" do
204 204 get '/issues/1.xml?include=journals'
205 205
206 206 assert_tag :tag => 'issue',
207 207 :child => {
208 208 :tag => 'journals',
209 209 :attributes => { :type => 'array' },
210 210 :child => {
211 211 :tag => 'journal',
212 212 :attributes => { :id => '1'},
213 213 :child => {
214 214 :tag => 'details',
215 215 :attributes => { :type => 'array' },
216 216 :child => {
217 217 :tag => 'detail',
218 218 :attributes => { :name => 'status_id' },
219 219 :child => {
220 220 :tag => 'old_value',
221 221 :content => '1',
222 222 :sibling => {
223 223 :tag => 'new_value',
224 224 :content => '2'
225 225 }
226 226 }
227 227 }
228 228 }
229 229 }
230 230 }
231 231 end
232 232 end
233 233 end
234 234
235 235 context "with custom fields" do
236 236 context ".xml" do
237 237 should "display custom fields" do
238 238 get '/issues/3.xml'
239 239
240 240 assert_tag :tag => 'issue',
241 241 :child => {
242 242 :tag => 'custom_fields',
243 243 :attributes => { :type => 'array' },
244 244 :child => {
245 245 :tag => 'custom_field',
246 246 :attributes => { :id => '1'},
247 247 :child => {
248 248 :tag => 'value',
249 249 :content => 'MySQL'
250 250 }
251 251 }
252 252 }
253 253
254 254 assert_nothing_raised do
255 255 Hash.from_xml(response.body).to_xml
256 256 end
257 257 end
258 258 end
259 259 end
260 260
261 261 context "with attachments" do
262 262 context ".xml" do
263 263 should "display attachments" do
264 264 get '/issues/3.xml?include=attachments'
265 265
266 266 assert_tag :tag => 'issue',
267 267 :child => {
268 268 :tag => 'attachments',
269 269 :children => {:count => 5},
270 270 :child => {
271 271 :tag => 'attachment',
272 272 :child => {
273 273 :tag => 'filename',
274 274 :content => 'source.rb',
275 275 :sibling => {
276 276 :tag => 'content_url',
277 277 :content => 'http://www.example.com/attachments/download/4/source.rb'
278 278 }
279 279 }
280 280 }
281 281 }
282 282 end
283 283 end
284 284 end
285 285
286 286 context "with subtasks" do
287 287 setup do
288 288 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
289 289 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
290 290 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
291 291 end
292 292
293 293 context ".xml" do
294 294 should "display children" do
295 295 get '/issues/1.xml?include=children'
296 296
297 297 assert_tag :tag => 'issue',
298 298 :child => {
299 299 :tag => 'children',
300 300 :children => {:count => 2},
301 301 :child => {
302 302 :tag => 'issue',
303 303 :attributes => {:id => @c1.id.to_s},
304 304 :child => {
305 305 :tag => 'subject',
306 306 :content => 'child c1',
307 307 :sibling => {
308 308 :tag => 'children',
309 309 :children => {:count => 1},
310 310 :child => {
311 311 :tag => 'issue',
312 312 :attributes => {:id => @c3.id.to_s}
313 313 }
314 314 }
315 315 }
316 316 }
317 317 }
318 318 end
319 319
320 320 context ".json" do
321 321 should "display children" do
322 322 get '/issues/1.json?include=children'
323 323
324 324 json = ActiveSupport::JSON.decode(response.body)
325 325 assert_equal([
326 326 {
327 327 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
328 328 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
329 329 },
330 330 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
331 331 ],
332 332 json['issue']['children'])
333 333 end
334 334 end
335 335 end
336 336 end
337 337 end
338 338
339 339 context "POST /issues.xml" do
340 340 should_allow_api_authentication(:post,
341 341 '/issues.xml',
342 342 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
343 343 {:success_code => :created})
344 344
345 345 should "create an issue with the attributes" do
346 346 assert_difference('Issue.count') do
347 347 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
348 348 end
349 349
350 350 issue = Issue.first(:order => 'id DESC')
351 351 assert_equal 1, issue.project_id
352 352 assert_equal 2, issue.tracker_id
353 353 assert_equal 3, issue.status_id
354 354 assert_equal 'API test', issue.subject
355 355
356 356 assert_response :created
357 357 assert_equal 'application/xml', @response.content_type
358 358 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
359 359 end
360 360 end
361 361
362 362 context "POST /issues.xml with failure" do
363 363 should "have an errors tag" do
364 364 assert_no_difference('Issue.count') do
365 365 post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
366 366 end
367 367
368 368 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
369 369 end
370 370 end
371 371
372 372 context "POST /issues.json" do
373 373 should_allow_api_authentication(:post,
374 374 '/issues.json',
375 375 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
376 376 {:success_code => :created})
377 377
378 378 should "create an issue with the attributes" do
379 379 assert_difference('Issue.count') do
380 380 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
381 381 end
382 382
383 383 issue = Issue.first(:order => 'id DESC')
384 384 assert_equal 1, issue.project_id
385 385 assert_equal 2, issue.tracker_id
386 386 assert_equal 3, issue.status_id
387 387 assert_equal 'API test', issue.subject
388 388 end
389 389
390 390 end
391 391
392 392 context "POST /issues.json with failure" do
393 393 should "have an errors element" do
394 394 assert_no_difference('Issue.count') do
395 395 post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
396 396 end
397 397
398 398 json = ActiveSupport::JSON.decode(response.body)
399 399 assert json['errors'].include?(['subject', "can't be blank"])
400 400 end
401 401 end
402 402
403 403 # Issue 6 is on a private project
404 404 context "PUT /issues/6.xml" do
405 405 setup do
406 406 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
407 407 end
408 408
409 409 should_allow_api_authentication(:put,
410 410 '/issues/6.xml',
411 411 {:issue => {:subject => 'API update', :notes => 'A new note'}},
412 412 {:success_code => :ok})
413 413
414 414 should "not create a new issue" do
415 415 assert_no_difference('Issue.count') do
416 416 put '/issues/6.xml', @parameters, credentials('jsmith')
417 417 end
418 418 end
419 419
420 420 should "create a new journal" do
421 421 assert_difference('Journal.count') do
422 422 put '/issues/6.xml', @parameters, credentials('jsmith')
423 423 end
424 424 end
425 425
426 426 should "add the note to the journal" do
427 427 put '/issues/6.xml', @parameters, credentials('jsmith')
428 428
429 429 journal = Journal.last
430 430 assert_equal "A new note", journal.notes
431 431 end
432 432
433 433 should "update the issue" do
434 434 put '/issues/6.xml', @parameters, credentials('jsmith')
435 435
436 436 issue = Issue.find(6)
437 437 assert_equal "API update", issue.subject
438 438 end
439 439
440 440 end
441 441
442 442 context "PUT /issues/3.xml with custom fields" do
443 443 setup do
444 444 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
445 445 end
446 446
447 447 should "update custom fields" do
448 448 assert_no_difference('Issue.count') do
449 449 put '/issues/3.xml', @parameters, credentials('jsmith')
450 450 end
451 451
452 452 issue = Issue.find(3)
453 453 assert_equal '150', issue.custom_value_for(2).value
454 454 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
455 455 end
456 456 end
457 457
458 context "PUT /issues/3.xml with project change" do
459 setup do
460 @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
461 end
462
463 should "update project" do
464 assert_no_difference('Issue.count') do
465 put '/issues/3.xml', @parameters, credentials('jsmith')
466 end
467
468 issue = Issue.find(3)
469 assert_equal 2, issue.project_id
470 assert_equal 'Project changed', issue.subject
471 end
472 end
473
458 474 context "PUT /issues/6.xml with failed update" do
459 475 setup do
460 476 @parameters = {:issue => {:subject => ''}}
461 477 end
462 478
463 479 should "not create a new issue" do
464 480 assert_no_difference('Issue.count') do
465 481 put '/issues/6.xml', @parameters, credentials('jsmith')
466 482 end
467 483 end
468 484
469 485 should "not create a new journal" do
470 486 assert_no_difference('Journal.count') do
471 487 put '/issues/6.xml', @parameters, credentials('jsmith')
472 488 end
473 489 end
474 490
475 491 should "have an errors tag" do
476 492 put '/issues/6.xml', @parameters, credentials('jsmith')
477 493
478 494 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
479 495 end
480 496 end
481 497
482 498 context "PUT /issues/6.json" do
483 499 setup do
484 500 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
485 501 end
486 502
487 503 should_allow_api_authentication(:put,
488 504 '/issues/6.json',
489 505 {:issue => {:subject => 'API update', :notes => 'A new note'}},
490 506 {:success_code => :ok})
491 507
492 508 should "not create a new issue" do
493 509 assert_no_difference('Issue.count') do
494 510 put '/issues/6.json', @parameters, credentials('jsmith')
495 511 end
496 512 end
497 513
498 514 should "create a new journal" do
499 515 assert_difference('Journal.count') do
500 516 put '/issues/6.json', @parameters, credentials('jsmith')
501 517 end
502 518 end
503 519
504 520 should "add the note to the journal" do
505 521 put '/issues/6.json', @parameters, credentials('jsmith')
506 522
507 523 journal = Journal.last
508 524 assert_equal "A new note", journal.notes
509 525 end
510 526
511 527 should "update the issue" do
512 528 put '/issues/6.json', @parameters, credentials('jsmith')
513 529
514 530 issue = Issue.find(6)
515 531 assert_equal "API update", issue.subject
516 532 end
517 533
518 534 end
519 535
520 536 context "PUT /issues/6.json with failed update" do
521 537 setup do
522 538 @parameters = {:issue => {:subject => ''}}
523 539 end
524 540
525 541 should "not create a new issue" do
526 542 assert_no_difference('Issue.count') do
527 543 put '/issues/6.json', @parameters, credentials('jsmith')
528 544 end
529 545 end
530 546
531 547 should "not create a new journal" do
532 548 assert_no_difference('Journal.count') do
533 549 put '/issues/6.json', @parameters, credentials('jsmith')
534 550 end
535 551 end
536 552
537 553 should "have an errors attribute" do
538 554 put '/issues/6.json', @parameters, credentials('jsmith')
539 555
540 556 json = ActiveSupport::JSON.decode(response.body)
541 557 assert json['errors'].include?(['subject', "can't be blank"])
542 558 end
543 559 end
544 560
545 561 context "DELETE /issues/1.xml" do
546 562 should_allow_api_authentication(:delete,
547 563 '/issues/6.xml',
548 564 {},
549 565 {:success_code => :ok})
550 566
551 567 should "delete the issue" do
552 568 assert_difference('Issue.count',-1) do
553 569 delete '/issues/6.xml', {}, credentials('jsmith')
554 570 end
555 571
556 572 assert_nil Issue.find_by_id(6)
557 573 end
558 574 end
559 575
560 576 context "DELETE /issues/1.json" do
561 577 should_allow_api_authentication(:delete,
562 578 '/issues/6.json',
563 579 {},
564 580 {:success_code => :ok})
565 581
566 582 should "delete the issue" do
567 583 assert_difference('Issue.count',-1) do
568 584 delete '/issues/6.json', {}, credentials('jsmith')
569 585 end
570 586
571 587 assert_nil Issue.find_by_id(6)
572 588 end
573 589 end
574 590 end
General Comments 0
You need to be logged in to leave comments. Login now