##// END OF EJS Templates
Ability to add non-member watchers on issue creation (#5159)....
Jean-Philippe Lang -
r9134:fae5250e5250
parent child
Show More
@@ -1,425 +1,426
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, :destroy]
24 24 before_filter :find_project, :only => [:new, :create]
25 25 before_filter :authorize, :except => [:index]
26 26 before_filter :find_optional_project, :only => [:index]
27 27 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 28 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 29 accept_rss_auth :index, :show
30 30 accept_api_auth :index, :show, :create, :update, :destroy
31 31
32 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 33
34 34 helper :journals
35 35 helper :projects
36 36 include ProjectsHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :issue_relations
40 40 include IssueRelationsHelper
41 41 helper :watchers
42 42 include WatchersHelper
43 43 helper :attachments
44 44 include AttachmentsHelper
45 45 helper :queries
46 46 include QueriesHelper
47 47 helper :repositories
48 48 include RepositoriesHelper
49 49 helper :sort
50 50 include SortHelper
51 51 include IssuesHelper
52 52 helper :timelog
53 53 helper :gantt
54 54 include Redmine::Export::PDF
55 55
56 56 def index
57 57 retrieve_query
58 58 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
59 59 sort_update(@query.sortable_columns)
60 60
61 61 if @query.valid?
62 62 case params[:format]
63 63 when 'csv', 'pdf'
64 64 @limit = Setting.issues_export_limit.to_i
65 65 when 'atom'
66 66 @limit = Setting.feeds_limit.to_i
67 67 when 'xml', 'json'
68 68 @offset, @limit = api_offset_and_limit
69 69 else
70 70 @limit = per_page_option
71 71 end
72 72
73 73 @issue_count = @query.issue_count
74 74 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
75 75 @offset ||= @issue_pages.current.offset
76 76 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
77 77 :order => sort_clause,
78 78 :offset => @offset,
79 79 :limit => @limit)
80 80 @issue_count_by_group = @query.issue_count_by_group
81 81
82 82 respond_to do |format|
83 83 format.html { render :template => 'issues/index', :layout => !request.xhr? }
84 84 format.api {
85 85 Issue.load_relations(@issues) if include_in_api_response?('relations')
86 86 }
87 87 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
88 88 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
89 89 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
90 90 end
91 91 else
92 92 respond_to do |format|
93 93 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
94 94 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
95 95 format.api { render_validation_errors(@query) }
96 96 end
97 97 end
98 98 rescue ActiveRecord::RecordNotFound
99 99 render_404
100 100 end
101 101
102 102 def show
103 103 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
104 104 @journals.each_with_index {|j,i| j.indice = i+1}
105 105 @journals.reverse! if User.current.wants_comments_in_reverse_order?
106 106
107 107 @changesets = @issue.changesets.visible.all
108 108 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
109 109
110 110 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
111 111 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
112 112 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
113 113 @priorities = IssuePriority.active
114 114 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
115 115 respond_to do |format|
116 116 format.html {
117 117 retrieve_previous_and_next_issue_ids
118 118 render :template => 'issues/show'
119 119 }
120 120 format.api
121 121 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
122 122 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
123 123 end
124 124 end
125 125
126 126 # Add a new issue
127 127 # The new issue will be created from an existing one if copy_from parameter is given
128 128 def new
129 129 respond_to do |format|
130 130 format.html { render :action => 'new', :layout => !request.xhr? }
131 131 format.js {
132 132 render(:update) { |page|
133 133 if params[:project_change]
134 134 page.replace_html 'all_attributes', :partial => 'form'
135 135 else
136 136 page.replace_html 'attributes', :partial => 'attributes'
137 137 end
138 138 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
139 139 page << "if ($('log_time')) {Element.#{m}('log_time');}"
140 140 }
141 141 }
142 142 end
143 143 end
144 144
145 145 def create
146 146 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
147 147 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
148 148 if @issue.save
149 149 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
150 150 respond_to do |format|
151 151 format.html {
152 152 render_attachment_warning_if_needed(@issue)
153 153 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
154 154 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
155 155 { :action => 'show', :id => @issue })
156 156 }
157 157 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
158 158 end
159 159 return
160 160 else
161 161 respond_to do |format|
162 162 format.html { render :action => 'new' }
163 163 format.api { render_validation_errors(@issue) }
164 164 end
165 165 end
166 166 end
167 167
168 168 def edit
169 169 return unless update_issue_from_params
170 170
171 171 respond_to do |format|
172 172 format.html { }
173 173 format.xml { }
174 174 end
175 175 end
176 176
177 177 def update
178 178 return unless update_issue_from_params
179 179 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
180 180 saved = false
181 181 begin
182 182 saved = @issue.save_issue_with_child_records(params, @time_entry)
183 183 rescue ActiveRecord::StaleObjectError
184 184 @conflict = true
185 185 if params[:last_journal_id]
186 186 if params[:last_journal_id].present?
187 187 last_journal_id = params[:last_journal_id].to_i
188 188 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
189 189 else
190 190 @conflict_journals = @issue.journals.all
191 191 end
192 192 end
193 193 end
194 194
195 195 if saved
196 196 render_attachment_warning_if_needed(@issue)
197 197 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
198 198
199 199 respond_to do |format|
200 200 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
201 201 format.api { head :ok }
202 202 end
203 203 else
204 204 respond_to do |format|
205 205 format.html { render :action => 'edit' }
206 206 format.api { render_validation_errors(@issue) }
207 207 end
208 208 end
209 209 end
210 210
211 211 # Bulk edit/copy a set of issues
212 212 def bulk_edit
213 213 @issues.sort!
214 214 @copy = params[:copy].present?
215 215 @notes = params[:notes]
216 216
217 217 if User.current.allowed_to?(:move_issues, @projects)
218 218 @allowed_projects = Issue.allowed_target_projects_on_move
219 219 if params[:issue]
220 220 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
221 221 if @target_project
222 222 target_projects = [@target_project]
223 223 end
224 224 end
225 225 end
226 226 target_projects ||= @projects
227 227
228 228 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
229 229 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
230 230 @assignables = target_projects.map(&:assignable_users).reduce(:&)
231 231 @trackers = target_projects.map(&:trackers).reduce(:&)
232 232 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
233 233 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
234 234
235 235 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
236 236 render :layout => false if request.xhr?
237 237 end
238 238
239 239 def bulk_update
240 240 @issues.sort!
241 241 @copy = params[:copy].present?
242 242 attributes = parse_params_for_bulk_issue_attributes(params)
243 243
244 244 unsaved_issue_ids = []
245 245 moved_issues = []
246 246 @issues.each do |issue|
247 247 issue.reload
248 248 if @copy
249 249 issue = issue.copy
250 250 end
251 251 journal = issue.init_journal(User.current, params[:notes])
252 252 issue.safe_attributes = attributes
253 253 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
254 254 if issue.save
255 255 moved_issues << issue
256 256 else
257 257 # Keep unsaved issue ids to display them in flash error
258 258 unsaved_issue_ids << issue.id
259 259 end
260 260 end
261 261 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
262 262
263 263 if params[:follow]
264 264 if @issues.size == 1 && moved_issues.size == 1
265 265 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
266 266 elsif moved_issues.map(&:project).uniq.size == 1
267 267 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
268 268 end
269 269 else
270 270 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
271 271 end
272 272 end
273 273
274 274 def destroy
275 275 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
276 276 if @hours > 0
277 277 case params[:todo]
278 278 when 'destroy'
279 279 # nothing to do
280 280 when 'nullify'
281 281 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
282 282 when 'reassign'
283 283 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
284 284 if reassign_to.nil?
285 285 flash.now[:error] = l(:error_issue_not_found_in_project)
286 286 return
287 287 else
288 288 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
289 289 end
290 290 else
291 291 # display the destroy form if it's a user request
292 292 return unless api_request?
293 293 end
294 294 end
295 295 @issues.each do |issue|
296 296 begin
297 297 issue.reload.destroy
298 298 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
299 299 # nothing to do, issue was already deleted (eg. by a parent)
300 300 end
301 301 end
302 302 respond_to do |format|
303 303 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
304 304 format.api { head :ok }
305 305 end
306 306 end
307 307
308 308 private
309 309 def find_issue
310 310 # Issue.visible.find(...) can not be used to redirect user to the login form
311 311 # if the issue actually exists but requires authentication
312 312 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
313 313 unless @issue.visible?
314 314 deny_access
315 315 return
316 316 end
317 317 @project = @issue.project
318 318 rescue ActiveRecord::RecordNotFound
319 319 render_404
320 320 end
321 321
322 322 def find_project
323 323 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
324 324 @project = Project.find(project_id)
325 325 rescue ActiveRecord::RecordNotFound
326 326 render_404
327 327 end
328 328
329 329 def retrieve_previous_and_next_issue_ids
330 330 retrieve_query_from_session
331 331 if @query
332 332 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
333 333 sort_update(@query.sortable_columns, 'issues_index_sort')
334 334 limit = 500
335 335 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
336 336 if (idx = issue_ids.index(@issue.id)) && idx < limit
337 337 if issue_ids.size < 500
338 338 @issue_position = idx + 1
339 339 @issue_count = issue_ids.size
340 340 end
341 341 @prev_issue_id = issue_ids[idx - 1] if idx > 0
342 342 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
343 343 end
344 344 end
345 345 end
346 346
347 347 # Used by #edit and #update to set some common instance variables
348 348 # from the params
349 349 # TODO: Refactor, not everything in here is needed by #edit
350 350 def update_issue_from_params
351 351 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
352 352 @priorities = IssuePriority.active
353 353 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
354 354 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
355 355 @time_entry.attributes = params[:time_entry]
356 356
357 357 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
358 358 @issue.init_journal(User.current, @notes)
359 359
360 360 issue_attributes = params[:issue]
361 361 if issue_attributes && params[:conflict_resolution]
362 362 case params[:conflict_resolution]
363 363 when 'overwrite'
364 364 issue_attributes = issue_attributes.dup
365 365 issue_attributes.delete(:lock_version)
366 366 when 'add_notes'
367 367 issue_attributes = {}
368 368 when 'cancel'
369 369 redirect_to issue_path(@issue)
370 370 return false
371 371 end
372 372 end
373 373 @issue.safe_attributes = issue_attributes
374 374 true
375 375 end
376 376
377 377 # TODO: Refactor, lots of extra code in here
378 378 # TODO: Changing tracker on an existing issue should not trigger this
379 379 def build_new_issue_from_params
380 380 if params[:id].blank?
381 381 @issue = Issue.new
382 382 if params[:copy_from]
383 383 begin
384 384 @copy_from = Issue.visible.find(params[:copy_from])
385 385 @copy_attachments = params[:copy_attachments].present? || request.get?
386 386 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
387 387 rescue ActiveRecord::RecordNotFound
388 388 render_404
389 389 return
390 390 end
391 391 end
392 392 @issue.project = @project
393 393 else
394 394 @issue = @project.issues.visible.find(params[:id])
395 395 end
396 396
397 397 @issue.project = @project
398 398 @issue.author = User.current
399 399 # Tracker must be set before custom field values
400 400 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
401 401 if @issue.tracker.nil?
402 402 render_error l(:error_no_tracker_in_project)
403 403 return false
404 404 end
405 405 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
406 406 @issue.safe_attributes = params[:issue]
407 407
408 408 @priorities = IssuePriority.active
409 409 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
410 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
410 411 end
411 412
412 413 def check_for_default_issue_status
413 414 if IssueStatus.default.nil?
414 415 render_error l(:error_no_default_issue_status)
415 416 return false
416 417 end
417 418 end
418 419
419 420 def parse_params_for_bulk_issue_attributes(params)
420 421 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
421 422 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
422 423 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
423 424 attributes
424 425 end
425 426 end
@@ -1,110 +1,134
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 WatchersController < ApplicationController
19 19 before_filter :find_project
20 20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
21 21 before_filter :authorize, :only => [:new, :destroy]
22 22
23 23 def watch
24 24 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
25 25 render_403
26 26 else
27 27 set_watcher(User.current, true)
28 28 end
29 29 end
30 30
31 31 def unwatch
32 32 set_watcher(User.current, false)
33 33 end
34 34
35 35 def new
36 36 respond_to do |format|
37 37 format.js do
38 38 render :update do |page|
39 39 page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
40 40 page << "showModal('ajax-modal', '400px');"
41 41 page << "$('ajax-modal').addClassName('new-watcher');"
42 42 end
43 43 end
44 44 end
45 45 end
46 46
47 47 def create
48 48 if params[:watcher].is_a?(Hash) && request.post?
49 49 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
50 50 user_ids.each do |user_id|
51 51 Watcher.create(:watchable => @watched, :user_id => user_id)
52 52 end
53 53 end
54 54 respond_to do |format|
55 55 format.html { redirect_to :back }
56 56 format.js do
57 57 render :update do |page|
58 58 page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
59 59 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
60 60 end
61 61 end
62 62 end
63 63 rescue ::ActionController::RedirectBackError
64 64 render :text => 'Watcher added.', :layout => true
65 65 end
66 66
67 def append
68 if params[:watcher].is_a?(Hash)
69 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
70 users = User.active.find_all_by_id(user_ids)
71 respond_to do |format|
72 format.js do
73 render :update do |page|
74 users.each do |user|
75 page.select("#issue_watcher_user_ids_#{user.id}").each(&:hide)
76 end
77 page.insert_html :bottom, 'watchers_inputs', :text => watchers_checkboxes(nil, users, true)
78 end
79 end
80 end
81 end
82 end
83
67 84 def destroy
68 85 @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
69 86 respond_to do |format|
70 87 format.html { redirect_to :back }
71 88 format.js do
72 89 render :update do |page|
73 90 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
74 91 end
75 92 end
76 93 end
77 94 end
78 95
79 96 def autocomplete_for_user
80 @users = User.active.like(params[:q]).find(:all, :limit => 100) - @watched.watcher_users
97 @users = User.active.like(params[:q]).find(:all, :limit => 100)
98 if @watched
99 @user -= @watched.watcher_users
100 end
81 101 render :layout => false
82 102 end
83 103
84 104 private
85 105 def find_project
86 klass = Object.const_get(params[:object_type].camelcase)
87 return false unless klass.respond_to?('watched_by')
88 @watched = klass.find(params[:object_id])
89 @project = @watched.project
106 if params[:object_type] && params[:object_id]
107 klass = Object.const_get(params[:object_type].camelcase)
108 return false unless klass.respond_to?('watched_by')
109 @watched = klass.find(params[:object_id])
110 @project = @watched.project
111 elsif params[:project_id]
112 @project = Project.visible.find(params[:project_id])
113 end
90 114 rescue
91 115 render_404
92 116 end
93 117
94 118 def set_watcher(user, watching)
95 119 @watched.set_watcher(user, watching)
96 120 respond_to do |format|
97 121 format.html { redirect_to :back }
98 122 format.js do
99 123 render(:update) do |page|
100 124 c = watcher_css(@watched)
101 125 page.select(".#{c}").each do |item|
102 126 page.replace_html item, watcher_link(@watched, user)
103 127 end
104 128 end
105 129 end
106 130 end
107 131 rescue ::ActionController::RedirectBackError
108 132 render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true
109 133 end
110 134 end
@@ -1,66 +1,74
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WatchersHelper
21 21
22 22 def watcher_tag(object, user, options={})
23 23 content_tag("span", watcher_link(object, user), :class => watcher_css(object))
24 24 end
25 25
26 26 def watcher_link(object, user)
27 27 return '' unless user && user.logged? && object.respond_to?('watched_by?')
28 28 watched = object.watched_by?(user)
29 29 url = {:controller => 'watchers',
30 30 :action => (watched ? 'unwatch' : 'watch'),
31 31 :object_type => object.class.to_s.underscore,
32 32 :object_id => object.id}
33 33 link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
34 34 {:url => url},
35 35 :href => url_for(url),
36 36 :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
37 37
38 38 end
39 39
40 40 # Returns the css class used to identify watch links for a given +object+
41 41 def watcher_css(object)
42 42 "#{object.class.to_s.underscore}-#{object.id}-watcher"
43 43 end
44 44
45 45 # Returns a comma separated list of users watching the given object
46 46 def watchers_list(object)
47 47 remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
48 48 lis = object.watcher_users.collect do |user|
49 49 s = avatar(user, :size => "16").to_s + link_to_user(user, :class => 'user').to_s
50 50 if remove_allowed
51 51 url = {:controller => 'watchers',
52 52 :action => 'destroy',
53 53 :object_type => object.class.to_s.underscore,
54 54 :object_id => object.id,
55 55 :user_id => user}
56 56 s += ' ' + link_to_remote(image_tag('delete.png'),
57 57 {:url => url},
58 58 :href => url_for(url),
59 59 :style => "vertical-align: middle",
60 60 :class => "delete")
61 61 end
62 62 content_tag :li, s.html_safe
63 63 end
64 64 (lis.empty? ? "" : "<ul>#{ lis.join("\n") }</ul>").html_safe
65 65 end
66
67 def watchers_checkboxes(object, users, checked=nil)
68 users.map do |user|
69 c = checked.nil? ? object.watched_by?(user) : checked
70 tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
71 content_tag 'label', "#{tag} #{h(user)}", :id => "issue_watcher_user_ids_#{user.id}", :class => "floating"
72 end.join
73 end
66 74 end
@@ -1,19 +1,18
1 1 <% if defined?(container) && container && container.saved_attachments %>
2 2 <% container.saved_attachments.each_with_index do |attachment, i| %>
3 3 <span class="icon icon-attachment" style="display:block; line-height:1.5em;">
4 4 <%= h(attachment.filename) %> (<%= number_to_human_size(attachment.filesize) %>)
5 5 <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.id}.#{attachment.digest}" %>
6 6 </span>
7 7 <% end %>
8 8 <% end %>
9 9 <span id="attachments_fields">
10 10 <span>
11 11 <%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil, :class => 'file',
12 12 :onchange => "checkFileSize(this, #{Setting.attachment_max_size.to_i.kilobytes}, '#{escape_javascript(l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)))}');" -%>
13 13 <%= text_field_tag 'attachments[1][description]', '', :id => nil, :class => 'description', :placeholder => l(:label_optional_description) %>
14 14 <%= link_to_function(image_tag('delete.png'), 'removeFileField(this)', :title => (l(:button_delete))) %>
15 15 </span>
16 16 </span>
17 <small><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %>
18 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
19 </small>
17 <span class="add_attachment"><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;', :class => 'add_attachment' %>
18 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)</span>
@@ -1,50 +1,55
1 1 <h2><%=l(:label_issue_new)%></h2>
2 2
3 3 <%= call_hook(:view_issues_new_top, {:issue => @issue}) %>
4 4
5 5 <% labelled_form_for @issue, :url => project_issues_path(@project),
6 6 :html => {:id => 'issue-form', :multipart => true} do |f| %>
7 7 <%= error_messages_for 'issue' %>
8 8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
9 9 <div class="box tabular">
10 10 <div id="all_attributes">
11 11 <%= render :partial => 'issues/form', :locals => {:f => f} %>
12 12 </div>
13 13
14 14 <% if @copy_from && @copy_from.attachments.any? %>
15 15 <p>
16 16 <label for="copy_attachments"><%= l(:label_copy_attachments) %></label>
17 17 <%= check_box_tag 'copy_attachments', '1', @copy_attachments %>
18 18 </p>
19 19 <% end %>
20 20
21 21 <p id="attachments_form"><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
22 22
23 23 <% if @issue.safe_attribute? 'watcher_user_ids' -%>
24 24 <p id="watchers_form"><label><%= l(:label_issue_watchers) %></label>
25 <% @issue.project.users.sort.each do |user| -%>
26 <label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watched_by?(user), :id => nil %> <%=h user %></label>
27 <% end -%>
28 </p>
25 <span id="watchers_inputs">
26 <%= watchers_checkboxes(@issue, @available_watchers) %>
27 </span>
28 <span class="search_for_watchers">
29 <%= link_to_remote l(:label_search_for_watchers),
30 :url => {:controller => 'watchers', :action => 'new', :project_id => @issue.project},
31 :method => 'get' %>
32 </span>
33 </p>
29 34 <% end %>
30 35 </div>
31 36
32 37 <%= submit_tag l(:button_create) %>
33 38 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
34 39 <%= link_to_remote l(:label_preview),
35 40 { :url => preview_new_issue_path(:project_id => @project),
36 41 :method => 'post',
37 42 :update => 'preview',
38 43 :with => "Form.serialize('issue-form')",
39 44 :complete => "Element.scrollTo('preview')"
40 45 }, :accesskey => accesskey(:preview) %>
41 46
42 47 <%= javascript_tag "Form.Element.focus('issue_subject');" %>
43 48 <% end %>
44 49
45 50 <div id="preview" class="wiki"></div>
46 51
47 52 <% content_for :header_tags do %>
48 53 <%= stylesheet_link_tag 'scm' %>
49 54 <%= robot_exclusion_tag %>
50 55 <% end %>
@@ -1,32 +1,32
1 1 <h3 class="title"><%= l(:permission_add_issue_watchers) %></h3>
2 2
3 3 <% form_remote_tag :url => {:controller => 'watchers',
4 :action => 'create',
4 :action => (watched ? 'create' : 'append'),
5 5 :object_type => watched.class.name.underscore,
6 6 :object_id => watched},
7 7 :method => :post,
8 8 :html => {:id => 'new-watcher-form'} do %>
9 9
10 10 <p><%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
11 11 <%= observe_field(:user_search,
12 12 :frequency => 0.5,
13 13 :update => :users_for_watcher,
14 14 :method => :get,
15 15 :before => '$("user_search").addClassName("ajax-loading")',
16 16 :complete => '$("user_search").removeClassName("ajax-loading")',
17 17 :url => {
18 18 :controller => 'watchers',
19 19 :action => 'autocomplete_for_user',
20 20 :object_type => watched.class.name.underscore,
21 21 :object_id => watched},
22 22 :with => 'q') %>
23 23
24 24 <div id="users_for_watcher">
25 <%= principals_check_box_tags 'watcher[user_ids][]', watched.addable_watcher_users %>
25 <%= principals_check_box_tags 'watcher[user_ids][]', (watched ? watched.addable_watcher_users : User.active.all(:limit => 100)) %>
26 26 </div>
27 27
28 28 <p class="buttons">
29 29 <%= submit_tag l(:button_add), :name => nil, :onclick => "hideModal(this);" %>
30 30 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
31 31 </p>
32 32 <% end %>
@@ -1,1024 +1,1025
1 1 en:
2 2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 3 direction: ltr
4 4 date:
5 5 formats:
6 6 # Use the strftime parameters for formats.
7 7 # When no format has been given, it uses default.
8 8 # You can provide other formats here if you like!
9 9 default: "%m/%d/%Y"
10 10 short: "%b %d"
11 11 long: "%B %d, %Y"
12 12
13 13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15 15
16 16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 19 # Used in date_select and datime_select.
20 20 order:
21 21 - :year
22 22 - :month
23 23 - :day
24 24
25 25 time:
26 26 formats:
27 27 default: "%m/%d/%Y %I:%M %p"
28 28 time: "%I:%M %p"
29 29 short: "%d %b %H:%M"
30 30 long: "%B %d, %Y %H:%M"
31 31 am: "am"
32 32 pm: "pm"
33 33
34 34 datetime:
35 35 distance_in_words:
36 36 half_a_minute: "half a minute"
37 37 less_than_x_seconds:
38 38 one: "less than 1 second"
39 39 other: "less than %{count} seconds"
40 40 x_seconds:
41 41 one: "1 second"
42 42 other: "%{count} seconds"
43 43 less_than_x_minutes:
44 44 one: "less than a minute"
45 45 other: "less than %{count} minutes"
46 46 x_minutes:
47 47 one: "1 minute"
48 48 other: "%{count} minutes"
49 49 about_x_hours:
50 50 one: "about 1 hour"
51 51 other: "about %{count} hours"
52 52 x_days:
53 53 one: "1 day"
54 54 other: "%{count} days"
55 55 about_x_months:
56 56 one: "about 1 month"
57 57 other: "about %{count} months"
58 58 x_months:
59 59 one: "1 month"
60 60 other: "%{count} months"
61 61 about_x_years:
62 62 one: "about 1 year"
63 63 other: "about %{count} years"
64 64 over_x_years:
65 65 one: "over 1 year"
66 66 other: "over %{count} years"
67 67 almost_x_years:
68 68 one: "almost 1 year"
69 69 other: "almost %{count} years"
70 70
71 71 number:
72 72 format:
73 73 separator: "."
74 74 delimiter: ""
75 75 precision: 3
76 76
77 77 human:
78 78 format:
79 79 delimiter: ""
80 80 precision: 1
81 81 storage_units:
82 82 format: "%n %u"
83 83 units:
84 84 byte:
85 85 one: "Byte"
86 86 other: "Bytes"
87 87 kb: "kB"
88 88 mb: "MB"
89 89 gb: "GB"
90 90 tb: "TB"
91 91
92 92 # Used in array.to_sentence.
93 93 support:
94 94 array:
95 95 sentence_connector: "and"
96 96 skip_last_comma: false
97 97
98 98 activerecord:
99 99 errors:
100 100 template:
101 101 header:
102 102 one: "1 error prohibited this %{model} from being saved"
103 103 other: "%{count} errors prohibited this %{model} from being saved"
104 104 messages:
105 105 inclusion: "is not included in the list"
106 106 exclusion: "is reserved"
107 107 invalid: "is invalid"
108 108 confirmation: "doesn't match confirmation"
109 109 accepted: "must be accepted"
110 110 empty: "can't be empty"
111 111 blank: "can't be blank"
112 112 too_long: "is too long (maximum is %{count} characters)"
113 113 too_short: "is too short (minimum is %{count} characters)"
114 114 wrong_length: "is the wrong length (should be %{count} characters)"
115 115 taken: "has already been taken"
116 116 not_a_number: "is not a number"
117 117 not_a_date: "is not a valid date"
118 118 greater_than: "must be greater than %{count}"
119 119 greater_than_or_equal_to: "must be greater than or equal to %{count}"
120 120 equal_to: "must be equal to %{count}"
121 121 less_than: "must be less than %{count}"
122 122 less_than_or_equal_to: "must be less than or equal to %{count}"
123 123 odd: "must be odd"
124 124 even: "must be even"
125 125 greater_than_start_date: "must be greater than start date"
126 126 not_same_project: "doesn't belong to the same project"
127 127 circular_dependency: "This relation would create a circular dependency"
128 128 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
129 129
130 130 actionview_instancetag_blank_option: Please select
131 131
132 132 general_text_No: 'No'
133 133 general_text_Yes: 'Yes'
134 134 general_text_no: 'no'
135 135 general_text_yes: 'yes'
136 136 general_lang_name: 'English'
137 137 general_csv_separator: ','
138 138 general_csv_decimal_separator: '.'
139 139 general_csv_encoding: ISO-8859-1
140 140 general_pdf_encoding: UTF-8
141 141 general_first_day_of_week: '7'
142 142
143 143 notice_account_updated: Account was successfully updated.
144 144 notice_account_invalid_creditentials: Invalid user or password
145 145 notice_account_password_updated: Password was successfully updated.
146 146 notice_account_wrong_password: Wrong password
147 147 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
148 148 notice_account_unknown_email: Unknown user.
149 149 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
150 150 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
151 151 notice_account_activated: Your account has been activated. You can now log in.
152 152 notice_successful_create: Successful creation.
153 153 notice_successful_update: Successful update.
154 154 notice_successful_delete: Successful deletion.
155 155 notice_successful_connection: Successful connection.
156 156 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
157 157 notice_locking_conflict: Data has been updated by another user.
158 158 notice_not_authorized: You are not authorized to access this page.
159 159 notice_not_authorized_archived_project: The project you're trying to access has been archived.
160 160 notice_email_sent: "An email was sent to %{value}"
161 161 notice_email_error: "An error occurred while sending mail (%{value})"
162 162 notice_feeds_access_key_reseted: Your RSS access key was reset.
163 163 notice_api_access_key_reseted: Your API access key was reset.
164 164 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
165 165 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
166 166 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
167 167 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
168 168 notice_account_pending: "Your account was created and is now pending administrator approval."
169 169 notice_default_data_loaded: Default configuration successfully loaded.
170 170 notice_unable_delete_version: Unable to delete version.
171 171 notice_unable_delete_time_entry: Unable to delete time log entry.
172 172 notice_issue_done_ratios_updated: Issue done ratios updated.
173 173 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
174 174 notice_issue_successful_create: "Issue %{id} created."
175 175 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
176 176
177 177 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
178 178 error_scm_not_found: "The entry or revision was not found in the repository."
179 179 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
180 180 error_scm_annotate: "The entry does not exist or cannot be annotated."
181 181 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
182 182 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
183 183 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
184 184 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
185 185 error_can_not_delete_custom_field: Unable to delete custom field
186 186 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
187 187 error_can_not_remove_role: "This role is in use and cannot be deleted."
188 188 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
189 189 error_can_not_archive_project: This project cannot be archived
190 190 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
191 191 error_workflow_copy_source: 'Please select a source tracker or role'
192 192 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
193 193 error_unable_delete_issue_status: 'Unable to delete issue status'
194 194 error_unable_to_connect: "Unable to connect (%{value})"
195 195 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
196 196 warning_attachments_not_saved: "%{count} file(s) could not be saved."
197 197
198 198 mail_subject_lost_password: "Your %{value} password"
199 199 mail_body_lost_password: 'To change your password, click on the following link:'
200 200 mail_subject_register: "Your %{value} account activation"
201 201 mail_body_register: 'To activate your account, click on the following link:'
202 202 mail_body_account_information_external: "You can use your %{value} account to log in."
203 203 mail_body_account_information: Your account information
204 204 mail_subject_account_activation_request: "%{value} account activation request"
205 205 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
206 206 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
207 207 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
208 208 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
209 209 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
210 210 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
211 211 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
212 212
213 213 gui_validation_error: 1 error
214 214 gui_validation_error_plural: "%{count} errors"
215 215
216 216 field_name: Name
217 217 field_description: Description
218 218 field_summary: Summary
219 219 field_is_required: Required
220 220 field_firstname: First name
221 221 field_lastname: Last name
222 222 field_mail: Email
223 223 field_filename: File
224 224 field_filesize: Size
225 225 field_downloads: Downloads
226 226 field_author: Author
227 227 field_created_on: Created
228 228 field_updated_on: Updated
229 229 field_field_format: Format
230 230 field_is_for_all: For all projects
231 231 field_possible_values: Possible values
232 232 field_regexp: Regular expression
233 233 field_min_length: Minimum length
234 234 field_max_length: Maximum length
235 235 field_value: Value
236 236 field_category: Category
237 237 field_title: Title
238 238 field_project: Project
239 239 field_issue: Issue
240 240 field_status: Status
241 241 field_notes: Notes
242 242 field_is_closed: Issue closed
243 243 field_is_default: Default value
244 244 field_tracker: Tracker
245 245 field_subject: Subject
246 246 field_due_date: Due date
247 247 field_assigned_to: Assignee
248 248 field_priority: Priority
249 249 field_fixed_version: Target version
250 250 field_user: User
251 251 field_principal: Principal
252 252 field_role: Role
253 253 field_homepage: Homepage
254 254 field_is_public: Public
255 255 field_parent: Subproject of
256 256 field_is_in_roadmap: Issues displayed in roadmap
257 257 field_login: Login
258 258 field_mail_notification: Email notifications
259 259 field_admin: Administrator
260 260 field_last_login_on: Last connection
261 261 field_language: Language
262 262 field_effective_date: Date
263 263 field_password: Password
264 264 field_new_password: New password
265 265 field_password_confirmation: Confirmation
266 266 field_version: Version
267 267 field_type: Type
268 268 field_host: Host
269 269 field_port: Port
270 270 field_account: Account
271 271 field_base_dn: Base DN
272 272 field_attr_login: Login attribute
273 273 field_attr_firstname: Firstname attribute
274 274 field_attr_lastname: Lastname attribute
275 275 field_attr_mail: Email attribute
276 276 field_onthefly: On-the-fly user creation
277 277 field_start_date: Start date
278 278 field_done_ratio: "% Done"
279 279 field_auth_source: Authentication mode
280 280 field_hide_mail: Hide my email address
281 281 field_comments: Comment
282 282 field_url: URL
283 283 field_start_page: Start page
284 284 field_subproject: Subproject
285 285 field_hours: Hours
286 286 field_activity: Activity
287 287 field_spent_on: Date
288 288 field_identifier: Identifier
289 289 field_is_filter: Used as a filter
290 290 field_issue_to: Related issue
291 291 field_delay: Delay
292 292 field_assignable: Issues can be assigned to this role
293 293 field_redirect_existing_links: Redirect existing links
294 294 field_estimated_hours: Estimated time
295 295 field_column_names: Columns
296 296 field_time_entries: Log time
297 297 field_time_zone: Time zone
298 298 field_searchable: Searchable
299 299 field_default_value: Default value
300 300 field_comments_sorting: Display comments
301 301 field_parent_title: Parent page
302 302 field_editable: Editable
303 303 field_watcher: Watcher
304 304 field_identity_url: OpenID URL
305 305 field_content: Content
306 306 field_group_by: Group results by
307 307 field_sharing: Sharing
308 308 field_parent_issue: Parent task
309 309 field_member_of_group: "Assignee's group"
310 310 field_assigned_to_role: "Assignee's role"
311 311 field_text: Text field
312 312 field_visible: Visible
313 313 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
314 314 field_issues_visibility: Issues visibility
315 315 field_is_private: Private
316 316 field_commit_logs_encoding: Commit messages encoding
317 317 field_scm_path_encoding: Path encoding
318 318 field_path_to_repository: Path to repository
319 319 field_root_directory: Root directory
320 320 field_cvsroot: CVSROOT
321 321 field_cvs_module: Module
322 322 field_repository_is_default: Main repository
323 323 field_multiple: Multiple values
324 324 field_ldap_filter: LDAP filter
325 325
326 326 setting_app_title: Application title
327 327 setting_app_subtitle: Application subtitle
328 328 setting_welcome_text: Welcome text
329 329 setting_default_language: Default language
330 330 setting_login_required: Authentication required
331 331 setting_self_registration: Self-registration
332 332 setting_attachment_max_size: Maximum attachment size
333 333 setting_issues_export_limit: Issues export limit
334 334 setting_mail_from: Emission email address
335 335 setting_bcc_recipients: Blind carbon copy recipients (bcc)
336 336 setting_plain_text_mail: Plain text mail (no HTML)
337 337 setting_host_name: Host name and path
338 338 setting_text_formatting: Text formatting
339 339 setting_wiki_compression: Wiki history compression
340 340 setting_feeds_limit: Maximum number of items in Atom feeds
341 341 setting_default_projects_public: New projects are public by default
342 342 setting_autofetch_changesets: Fetch commits automatically
343 343 setting_sys_api_enabled: Enable WS for repository management
344 344 setting_commit_ref_keywords: Referencing keywords
345 345 setting_commit_fix_keywords: Fixing keywords
346 346 setting_autologin: Autologin
347 347 setting_date_format: Date format
348 348 setting_time_format: Time format
349 349 setting_cross_project_issue_relations: Allow cross-project issue relations
350 350 setting_issue_list_default_columns: Default columns displayed on the issue list
351 351 setting_repositories_encodings: Attachments and repositories encodings
352 352 setting_emails_header: Emails header
353 353 setting_emails_footer: Emails footer
354 354 setting_protocol: Protocol
355 355 setting_per_page_options: Objects per page options
356 356 setting_user_format: Users display format
357 357 setting_activity_days_default: Days displayed on project activity
358 358 setting_display_subprojects_issues: Display subprojects issues on main projects by default
359 359 setting_enabled_scm: Enabled SCM
360 360 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
361 361 setting_mail_handler_api_enabled: Enable WS for incoming emails
362 362 setting_mail_handler_api_key: API key
363 363 setting_sequential_project_identifiers: Generate sequential project identifiers
364 364 setting_gravatar_enabled: Use Gravatar user icons
365 365 setting_gravatar_default: Default Gravatar image
366 366 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
367 367 setting_file_max_size_displayed: Maximum size of text files displayed inline
368 368 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
369 369 setting_openid: Allow OpenID login and registration
370 370 setting_password_min_length: Minimum password length
371 371 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
372 372 setting_default_projects_modules: Default enabled modules for new projects
373 373 setting_issue_done_ratio: Calculate the issue done ratio with
374 374 setting_issue_done_ratio_issue_field: Use the issue field
375 375 setting_issue_done_ratio_issue_status: Use the issue status
376 376 setting_start_of_week: Start calendars on
377 377 setting_rest_api_enabled: Enable REST web service
378 378 setting_cache_formatted_text: Cache formatted text
379 379 setting_default_notification_option: Default notification option
380 380 setting_commit_logtime_enabled: Enable time logging
381 381 setting_commit_logtime_activity_id: Activity for logged time
382 382 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
383 383 setting_issue_group_assignment: Allow issue assignment to groups
384 384 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
385 385 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
386 386
387 387 permission_add_project: Create project
388 388 permission_add_subprojects: Create subprojects
389 389 permission_edit_project: Edit project
390 390 permission_select_project_modules: Select project modules
391 391 permission_manage_members: Manage members
392 392 permission_manage_project_activities: Manage project activities
393 393 permission_manage_versions: Manage versions
394 394 permission_manage_categories: Manage issue categories
395 395 permission_view_issues: View Issues
396 396 permission_add_issues: Add issues
397 397 permission_edit_issues: Edit issues
398 398 permission_manage_issue_relations: Manage issue relations
399 399 permission_set_issues_private: Set issues public or private
400 400 permission_set_own_issues_private: Set own issues public or private
401 401 permission_add_issue_notes: Add notes
402 402 permission_edit_issue_notes: Edit notes
403 403 permission_edit_own_issue_notes: Edit own notes
404 404 permission_move_issues: Move issues
405 405 permission_delete_issues: Delete issues
406 406 permission_manage_public_queries: Manage public queries
407 407 permission_save_queries: Save queries
408 408 permission_view_gantt: View gantt chart
409 409 permission_view_calendar: View calendar
410 410 permission_view_issue_watchers: View watchers list
411 411 permission_add_issue_watchers: Add watchers
412 412 permission_delete_issue_watchers: Delete watchers
413 413 permission_log_time: Log spent time
414 414 permission_view_time_entries: View spent time
415 415 permission_edit_time_entries: Edit time logs
416 416 permission_edit_own_time_entries: Edit own time logs
417 417 permission_manage_news: Manage news
418 418 permission_comment_news: Comment news
419 419 permission_manage_documents: Manage documents
420 420 permission_view_documents: View documents
421 421 permission_manage_files: Manage files
422 422 permission_view_files: View files
423 423 permission_manage_wiki: Manage wiki
424 424 permission_rename_wiki_pages: Rename wiki pages
425 425 permission_delete_wiki_pages: Delete wiki pages
426 426 permission_view_wiki_pages: View wiki
427 427 permission_view_wiki_edits: View wiki history
428 428 permission_edit_wiki_pages: Edit wiki pages
429 429 permission_delete_wiki_pages_attachments: Delete attachments
430 430 permission_protect_wiki_pages: Protect wiki pages
431 431 permission_manage_repository: Manage repository
432 432 permission_browse_repository: Browse repository
433 433 permission_view_changesets: View changesets
434 434 permission_commit_access: Commit access
435 435 permission_manage_boards: Manage forums
436 436 permission_view_messages: View messages
437 437 permission_add_messages: Post messages
438 438 permission_edit_messages: Edit messages
439 439 permission_edit_own_messages: Edit own messages
440 440 permission_delete_messages: Delete messages
441 441 permission_delete_own_messages: Delete own messages
442 442 permission_export_wiki_pages: Export wiki pages
443 443 permission_manage_subtasks: Manage subtasks
444 444 permission_manage_related_issues: Manage related issues
445 445
446 446 project_module_issue_tracking: Issue tracking
447 447 project_module_time_tracking: Time tracking
448 448 project_module_news: News
449 449 project_module_documents: Documents
450 450 project_module_files: Files
451 451 project_module_wiki: Wiki
452 452 project_module_repository: Repository
453 453 project_module_boards: Forums
454 454 project_module_calendar: Calendar
455 455 project_module_gantt: Gantt
456 456
457 457 label_user: User
458 458 label_user_plural: Users
459 459 label_user_new: New user
460 460 label_user_anonymous: Anonymous
461 461 label_project: Project
462 462 label_project_new: New project
463 463 label_project_plural: Projects
464 464 label_x_projects:
465 465 zero: no projects
466 466 one: 1 project
467 467 other: "%{count} projects"
468 468 label_project_all: All Projects
469 469 label_project_latest: Latest projects
470 470 label_issue: Issue
471 471 label_issue_new: New issue
472 472 label_issue_plural: Issues
473 473 label_issue_view_all: View all issues
474 474 label_issues_by: "Issues by %{value}"
475 475 label_issue_added: Issue added
476 476 label_issue_updated: Issue updated
477 477 label_issue_note_added: Note added
478 478 label_issue_status_updated: Status updated
479 479 label_issue_priority_updated: Priority updated
480 480 label_document: Document
481 481 label_document_new: New document
482 482 label_document_plural: Documents
483 483 label_document_added: Document added
484 484 label_role: Role
485 485 label_role_plural: Roles
486 486 label_role_new: New role
487 487 label_role_and_permissions: Roles and permissions
488 488 label_role_anonymous: Anonymous
489 489 label_role_non_member: Non member
490 490 label_member: Member
491 491 label_member_new: New member
492 492 label_member_plural: Members
493 493 label_tracker: Tracker
494 494 label_tracker_plural: Trackers
495 495 label_tracker_new: New tracker
496 496 label_workflow: Workflow
497 497 label_issue_status: Issue status
498 498 label_issue_status_plural: Issue statuses
499 499 label_issue_status_new: New status
500 500 label_issue_category: Issue category
501 501 label_issue_category_plural: Issue categories
502 502 label_issue_category_new: New category
503 503 label_custom_field: Custom field
504 504 label_custom_field_plural: Custom fields
505 505 label_custom_field_new: New custom field
506 506 label_enumerations: Enumerations
507 507 label_enumeration_new: New value
508 508 label_information: Information
509 509 label_information_plural: Information
510 510 label_please_login: Please log in
511 511 label_register: Register
512 512 label_login_with_open_id_option: or login with OpenID
513 513 label_password_lost: Lost password
514 514 label_home: Home
515 515 label_my_page: My page
516 516 label_my_account: My account
517 517 label_my_projects: My projects
518 518 label_my_page_block: My page block
519 519 label_administration: Administration
520 520 label_login: Sign in
521 521 label_logout: Sign out
522 522 label_help: Help
523 523 label_reported_issues: Reported issues
524 524 label_assigned_to_me_issues: Issues assigned to me
525 525 label_last_login: Last connection
526 526 label_registered_on: Registered on
527 527 label_activity: Activity
528 528 label_overall_activity: Overall activity
529 529 label_user_activity: "%{value}'s activity"
530 530 label_new: New
531 531 label_logged_as: Logged in as
532 532 label_environment: Environment
533 533 label_authentication: Authentication
534 534 label_auth_source: Authentication mode
535 535 label_auth_source_new: New authentication mode
536 536 label_auth_source_plural: Authentication modes
537 537 label_subproject_plural: Subprojects
538 538 label_subproject_new: New subproject
539 539 label_and_its_subprojects: "%{value} and its subprojects"
540 540 label_min_max_length: Min - Max length
541 541 label_list: List
542 542 label_date: Date
543 543 label_integer: Integer
544 544 label_float: Float
545 545 label_boolean: Boolean
546 546 label_string: Text
547 547 label_text: Long text
548 548 label_attribute: Attribute
549 549 label_attribute_plural: Attributes
550 550 label_download: "%{count} Download"
551 551 label_download_plural: "%{count} Downloads"
552 552 label_no_data: No data to display
553 553 label_change_status: Change status
554 554 label_history: History
555 555 label_attachment: File
556 556 label_attachment_new: New file
557 557 label_attachment_delete: Delete file
558 558 label_attachment_plural: Files
559 559 label_file_added: File added
560 560 label_report: Report
561 561 label_report_plural: Reports
562 562 label_news: News
563 563 label_news_new: Add news
564 564 label_news_plural: News
565 565 label_news_latest: Latest news
566 566 label_news_view_all: View all news
567 567 label_news_added: News added
568 568 label_news_comment_added: Comment added to a news
569 569 label_settings: Settings
570 570 label_overview: Overview
571 571 label_version: Version
572 572 label_version_new: New version
573 573 label_version_plural: Versions
574 574 label_close_versions: Close completed versions
575 575 label_confirmation: Confirmation
576 576 label_export_to: 'Also available in:'
577 577 label_read: Read...
578 578 label_public_projects: Public projects
579 579 label_open_issues: open
580 580 label_open_issues_plural: open
581 581 label_closed_issues: closed
582 582 label_closed_issues_plural: closed
583 583 label_x_open_issues_abbr_on_total:
584 584 zero: 0 open / %{total}
585 585 one: 1 open / %{total}
586 586 other: "%{count} open / %{total}"
587 587 label_x_open_issues_abbr:
588 588 zero: 0 open
589 589 one: 1 open
590 590 other: "%{count} open"
591 591 label_x_closed_issues_abbr:
592 592 zero: 0 closed
593 593 one: 1 closed
594 594 other: "%{count} closed"
595 595 label_x_issues:
596 596 zero: 0 issues
597 597 one: 1 issue
598 598 other: "%{count} issues"
599 599 label_total: Total
600 600 label_permissions: Permissions
601 601 label_current_status: Current status
602 602 label_new_statuses_allowed: New statuses allowed
603 603 label_all: all
604 604 label_none: none
605 605 label_nobody: nobody
606 606 label_next: Next
607 607 label_previous: Previous
608 608 label_used_by: Used by
609 609 label_details: Details
610 610 label_add_note: Add a note
611 611 label_per_page: Per page
612 612 label_calendar: Calendar
613 613 label_months_from: months from
614 614 label_gantt: Gantt
615 615 label_internal: Internal
616 616 label_last_changes: "last %{count} changes"
617 617 label_change_view_all: View all changes
618 618 label_personalize_page: Personalize this page
619 619 label_comment: Comment
620 620 label_comment_plural: Comments
621 621 label_x_comments:
622 622 zero: no comments
623 623 one: 1 comment
624 624 other: "%{count} comments"
625 625 label_comment_add: Add a comment
626 626 label_comment_added: Comment added
627 627 label_comment_delete: Delete comments
628 628 label_query: Custom query
629 629 label_query_plural: Custom queries
630 630 label_query_new: New query
631 631 label_my_queries: My custom queries
632 632 label_filter_add: Add filter
633 633 label_filter_plural: Filters
634 634 label_equals: is
635 635 label_not_equals: is not
636 636 label_in_less_than: in less than
637 637 label_in_more_than: in more than
638 638 label_greater_or_equal: '>='
639 639 label_less_or_equal: '<='
640 640 label_between: between
641 641 label_in: in
642 642 label_today: today
643 643 label_all_time: all time
644 644 label_yesterday: yesterday
645 645 label_this_week: this week
646 646 label_last_week: last week
647 647 label_last_n_days: "last %{count} days"
648 648 label_this_month: this month
649 649 label_last_month: last month
650 650 label_this_year: this year
651 651 label_date_range: Date range
652 652 label_less_than_ago: less than days ago
653 653 label_more_than_ago: more than days ago
654 654 label_ago: days ago
655 655 label_contains: contains
656 656 label_not_contains: doesn't contain
657 657 label_day_plural: days
658 658 label_repository: Repository
659 659 label_repository_new: New repository
660 660 label_repository_plural: Repositories
661 661 label_browse: Browse
662 662 label_modification: "%{count} change"
663 663 label_modification_plural: "%{count} changes"
664 664 label_branch: Branch
665 665 label_tag: Tag
666 666 label_revision: Revision
667 667 label_revision_plural: Revisions
668 668 label_revision_id: "Revision %{value}"
669 669 label_associated_revisions: Associated revisions
670 670 label_added: added
671 671 label_modified: modified
672 672 label_copied: copied
673 673 label_renamed: renamed
674 674 label_deleted: deleted
675 675 label_latest_revision: Latest revision
676 676 label_latest_revision_plural: Latest revisions
677 677 label_view_revisions: View revisions
678 678 label_view_all_revisions: View all revisions
679 679 label_max_size: Maximum size
680 680 label_sort_highest: Move to top
681 681 label_sort_higher: Move up
682 682 label_sort_lower: Move down
683 683 label_sort_lowest: Move to bottom
684 684 label_roadmap: Roadmap
685 685 label_roadmap_due_in: "Due in %{value}"
686 686 label_roadmap_overdue: "%{value} late"
687 687 label_roadmap_no_issues: No issues for this version
688 688 label_search: Search
689 689 label_result_plural: Results
690 690 label_all_words: All words
691 691 label_wiki: Wiki
692 692 label_wiki_edit: Wiki edit
693 693 label_wiki_edit_plural: Wiki edits
694 694 label_wiki_page: Wiki page
695 695 label_wiki_page_plural: Wiki pages
696 696 label_index_by_title: Index by title
697 697 label_index_by_date: Index by date
698 698 label_current_version: Current version
699 699 label_preview: Preview
700 700 label_feed_plural: Feeds
701 701 label_changes_details: Details of all changes
702 702 label_issue_tracking: Issue tracking
703 703 label_spent_time: Spent time
704 704 label_overall_spent_time: Overall spent time
705 705 label_f_hour: "%{value} hour"
706 706 label_f_hour_plural: "%{value} hours"
707 707 label_time_tracking: Time tracking
708 708 label_change_plural: Changes
709 709 label_statistics: Statistics
710 710 label_commits_per_month: Commits per month
711 711 label_commits_per_author: Commits per author
712 712 label_diff: diff
713 713 label_view_diff: View differences
714 714 label_diff_inline: inline
715 715 label_diff_side_by_side: side by side
716 716 label_options: Options
717 717 label_copy_workflow_from: Copy workflow from
718 718 label_permissions_report: Permissions report
719 719 label_watched_issues: Watched issues
720 720 label_related_issues: Related issues
721 721 label_applied_status: Applied status
722 722 label_loading: Loading...
723 723 label_relation_new: New relation
724 724 label_relation_delete: Delete relation
725 725 label_relates_to: related to
726 726 label_duplicates: duplicates
727 727 label_duplicated_by: duplicated by
728 728 label_blocks: blocks
729 729 label_blocked_by: blocked by
730 730 label_precedes: precedes
731 731 label_follows: follows
732 732 label_end_to_start: end to start
733 733 label_end_to_end: end to end
734 734 label_start_to_start: start to start
735 735 label_start_to_end: start to end
736 736 label_stay_logged_in: Stay logged in
737 737 label_disabled: disabled
738 738 label_show_completed_versions: Show completed versions
739 739 label_me: me
740 740 label_board: Forum
741 741 label_board_new: New forum
742 742 label_board_plural: Forums
743 743 label_board_locked: Locked
744 744 label_board_sticky: Sticky
745 745 label_topic_plural: Topics
746 746 label_message_plural: Messages
747 747 label_message_last: Last message
748 748 label_message_new: New message
749 749 label_message_posted: Message added
750 750 label_reply_plural: Replies
751 751 label_send_information: Send account information to the user
752 752 label_year: Year
753 753 label_month: Month
754 754 label_week: Week
755 755 label_date_from: From
756 756 label_date_to: To
757 757 label_language_based: Based on user's language
758 758 label_sort_by: "Sort by %{value}"
759 759 label_send_test_email: Send a test email
760 760 label_feeds_access_key: RSS access key
761 761 label_missing_feeds_access_key: Missing a RSS access key
762 762 label_feeds_access_key_created_on: "RSS access key created %{value} ago"
763 763 label_module_plural: Modules
764 764 label_added_time_by: "Added by %{author} %{age} ago"
765 765 label_updated_time_by: "Updated by %{author} %{age} ago"
766 766 label_updated_time: "Updated %{value} ago"
767 767 label_jump_to_a_project: Jump to a project...
768 768 label_file_plural: Files
769 769 label_changeset_plural: Changesets
770 770 label_default_columns: Default columns
771 771 label_no_change_option: (No change)
772 772 label_bulk_edit_selected_issues: Bulk edit selected issues
773 773 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
774 774 label_theme: Theme
775 775 label_default: Default
776 776 label_search_titles_only: Search titles only
777 777 label_user_mail_option_all: "For any event on all my projects"
778 778 label_user_mail_option_selected: "For any event on the selected projects only..."
779 779 label_user_mail_option_none: "No events"
780 780 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
781 781 label_user_mail_option_only_assigned: "Only for things I am assigned to"
782 782 label_user_mail_option_only_owner: "Only for things I am the owner of"
783 783 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
784 784 label_registration_activation_by_email: account activation by email
785 785 label_registration_manual_activation: manual account activation
786 786 label_registration_automatic_activation: automatic account activation
787 787 label_display_per_page: "Per page: %{value}"
788 788 label_age: Age
789 789 label_change_properties: Change properties
790 790 label_general: General
791 791 label_more: More
792 792 label_scm: SCM
793 793 label_plugins: Plugins
794 794 label_ldap_authentication: LDAP authentication
795 795 label_downloads_abbr: D/L
796 796 label_optional_description: Optional description
797 797 label_add_another_file: Add another file
798 798 label_preferences: Preferences
799 799 label_chronological_order: In chronological order
800 800 label_reverse_chronological_order: In reverse chronological order
801 801 label_planning: Planning
802 802 label_incoming_emails: Incoming emails
803 803 label_generate_key: Generate a key
804 804 label_issue_watchers: Watchers
805 805 label_example: Example
806 806 label_display: Display
807 807 label_sort: Sort
808 808 label_ascending: Ascending
809 809 label_descending: Descending
810 810 label_date_from_to: From %{start} to %{end}
811 811 label_wiki_content_added: Wiki page added
812 812 label_wiki_content_updated: Wiki page updated
813 813 label_group: Group
814 814 label_group_plural: Groups
815 815 label_group_new: New group
816 816 label_time_entry_plural: Spent time
817 817 label_version_sharing_none: Not shared
818 818 label_version_sharing_descendants: With subprojects
819 819 label_version_sharing_hierarchy: With project hierarchy
820 820 label_version_sharing_tree: With project tree
821 821 label_version_sharing_system: With all projects
822 822 label_update_issue_done_ratios: Update issue done ratios
823 823 label_copy_source: Source
824 824 label_copy_target: Target
825 825 label_copy_same_as_target: Same as target
826 826 label_display_used_statuses_only: Only display statuses that are used by this tracker
827 827 label_api_access_key: API access key
828 828 label_missing_api_access_key: Missing an API access key
829 829 label_api_access_key_created_on: "API access key created %{value} ago"
830 830 label_profile: Profile
831 831 label_subtask_plural: Subtasks
832 832 label_project_copy_notifications: Send email notifications during the project copy
833 833 label_principal_search: "Search for user or group:"
834 834 label_user_search: "Search for user:"
835 835 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
836 836 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
837 837 label_issues_visibility_all: All issues
838 838 label_issues_visibility_public: All non private issues
839 839 label_issues_visibility_own: Issues created by or assigned to the user
840 840 label_git_report_last_commit: Report last commit for files and directories
841 841 label_parent_revision: Parent
842 842 label_child_revision: Child
843 843 label_export_options: "%{export_format} export options"
844 844 label_copy_attachments: Copy attachments
845 845 label_item_position: "%{position} of %{count}"
846 846 label_completed_versions: Completed versions
847 label_search_for_watchers: Search for watchers to add
847 848
848 849 button_login: Login
849 850 button_submit: Submit
850 851 button_save: Save
851 852 button_check_all: Check all
852 853 button_uncheck_all: Uncheck all
853 854 button_collapse_all: Collapse all
854 855 button_expand_all: Expand all
855 856 button_delete: Delete
856 857 button_create: Create
857 858 button_create_and_continue: Create and continue
858 859 button_test: Test
859 860 button_edit: Edit
860 861 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
861 862 button_add: Add
862 863 button_change: Change
863 864 button_apply: Apply
864 865 button_clear: Clear
865 866 button_lock: Lock
866 867 button_unlock: Unlock
867 868 button_download: Download
868 869 button_list: List
869 870 button_view: View
870 871 button_move: Move
871 872 button_move_and_follow: Move and follow
872 873 button_back: Back
873 874 button_cancel: Cancel
874 875 button_activate: Activate
875 876 button_sort: Sort
876 877 button_log_time: Log time
877 878 button_rollback: Rollback to this version
878 879 button_watch: Watch
879 880 button_unwatch: Unwatch
880 881 button_reply: Reply
881 882 button_archive: Archive
882 883 button_unarchive: Unarchive
883 884 button_reset: Reset
884 885 button_rename: Rename
885 886 button_change_password: Change password
886 887 button_copy: Copy
887 888 button_copy_and_follow: Copy and follow
888 889 button_annotate: Annotate
889 890 button_update: Update
890 891 button_configure: Configure
891 892 button_quote: Quote
892 893 button_duplicate: Duplicate
893 894 button_show: Show
894 895 button_edit_section: Edit this section
895 896 button_export: Export
896 897
897 898 status_active: active
898 899 status_registered: registered
899 900 status_locked: locked
900 901
901 902 version_status_open: open
902 903 version_status_locked: locked
903 904 version_status_closed: closed
904 905
905 906 field_active: Active
906 907
907 908 text_select_mail_notifications: Select actions for which email notifications should be sent.
908 909 text_regexp_info: eg. ^[A-Z0-9]+$
909 910 text_min_max_length_info: 0 means no restriction
910 911 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
911 912 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
912 913 text_workflow_edit: Select a role and a tracker to edit the workflow
913 914 text_are_you_sure: Are you sure?
914 915 text_are_you_sure_with_children: "Delete issue and all child issues?"
915 916 text_journal_changed: "%{label} changed from %{old} to %{new}"
916 917 text_journal_changed_no_detail: "%{label} updated"
917 918 text_journal_set_to: "%{label} set to %{value}"
918 919 text_journal_deleted: "%{label} deleted (%{old})"
919 920 text_journal_added: "%{label} %{value} added"
920 921 text_tip_issue_begin_day: issue beginning this day
921 922 text_tip_issue_end_day: issue ending this day
922 923 text_tip_issue_begin_end_day: issue beginning and ending this day
923 924 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
924 925 text_caracters_maximum: "%{count} characters maximum."
925 926 text_caracters_minimum: "Must be at least %{count} characters long."
926 927 text_length_between: "Length between %{min} and %{max} characters."
927 928 text_tracker_no_workflow: No workflow defined for this tracker
928 929 text_unallowed_characters: Unallowed characters
929 930 text_comma_separated: Multiple values allowed (comma separated).
930 931 text_line_separated: Multiple values allowed (one line for each value).
931 932 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
932 933 text_issue_added: "Issue %{id} has been reported by %{author}."
933 934 text_issue_updated: "Issue %{id} has been updated by %{author}."
934 935 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
935 936 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
936 937 text_issue_category_destroy_assignments: Remove category assignments
937 938 text_issue_category_reassign_to: Reassign issues to this category
938 939 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
939 940 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
940 941 text_load_default_configuration: Load the default configuration
941 942 text_status_changed_by_changeset: "Applied in changeset %{value}."
942 943 text_time_logged_by_changeset: "Applied in changeset %{value}."
943 944 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
944 945 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
945 946 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
946 947 text_select_project_modules: 'Select modules to enable for this project:'
947 948 text_default_administrator_account_changed: Default administrator account changed
948 949 text_file_repository_writable: Attachments directory writable
949 950 text_plugin_assets_writable: Plugin assets directory writable
950 951 text_rmagick_available: RMagick available (optional)
951 952 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
952 953 text_destroy_time_entries: Delete reported hours
953 954 text_assign_time_entries_to_project: Assign reported hours to the project
954 955 text_reassign_time_entries: 'Reassign reported hours to this issue:'
955 956 text_user_wrote: "%{value} wrote:"
956 957 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
957 958 text_enumeration_category_reassign_to: 'Reassign them to this value:'
958 959 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them."
959 960 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
960 961 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
961 962 text_custom_field_possible_values_info: 'One line for each value'
962 963 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
963 964 text_wiki_page_nullify_children: "Keep child pages as root pages"
964 965 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
965 966 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
966 967 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
967 968 text_zoom_in: Zoom in
968 969 text_zoom_out: Zoom out
969 970 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
970 971 text_scm_path_encoding_note: "Default: UTF-8"
971 972 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
972 973 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
973 974 text_scm_command: Command
974 975 text_scm_command_version: Version
975 976 text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it.
976 977 text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel.
977 978 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
978 979 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
979 980 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
980 981
981 982 default_role_manager: Manager
982 983 default_role_developer: Developer
983 984 default_role_reporter: Reporter
984 985 default_tracker_bug: Bug
985 986 default_tracker_feature: Feature
986 987 default_tracker_support: Support
987 988 default_issue_status_new: New
988 989 default_issue_status_in_progress: In Progress
989 990 default_issue_status_resolved: Resolved
990 991 default_issue_status_feedback: Feedback
991 992 default_issue_status_closed: Closed
992 993 default_issue_status_rejected: Rejected
993 994 default_doc_category_user: User documentation
994 995 default_doc_category_tech: Technical documentation
995 996 default_priority_low: Low
996 997 default_priority_normal: Normal
997 998 default_priority_high: High
998 999 default_priority_urgent: Urgent
999 1000 default_priority_immediate: Immediate
1000 1001 default_activity_design: Design
1001 1002 default_activity_development: Development
1002 1003
1003 1004 enumeration_issue_priorities: Issue priorities
1004 1005 enumeration_doc_categories: Document categories
1005 1006 enumeration_activities: Activities (time tracking)
1006 1007 enumeration_system_activity: System Activity
1007 1008 description_filter: Filter
1008 1009 description_search: Searchfield
1009 1010 description_choose_project: Projects
1010 1011 description_project_scope: Search scope
1011 1012 description_notes: Notes
1012 1013 description_message_content: Message content
1013 1014 description_query_sort_criteria_attribute: Sort attribute
1014 1015 description_query_sort_criteria_direction: Sort direction
1015 1016 description_user_mail_notification: Mail notification settings
1016 1017 description_available_columns: Available Columns
1017 1018 description_selected_columns: Selected Columns
1018 1019 description_all_columns: All Columns
1019 1020 description_issue_category_reassign: Choose issue category
1020 1021 description_wiki_subpages_reassign: Choose new parent page
1021 1022 description_date_range_list: Choose range from list
1022 1023 description_date_range_interval: Choose range by selecting start and end date
1023 1024 description_date_from: Enter start date
1024 1025 description_date_to: Enter end date
@@ -1,391 +1,393
1 1 ActionController::Routing::Routes.draw do |map|
2 2 # Add your own custom routes here.
3 3 # The priority is based upon order of creation: first created -> highest priority.
4 4
5 5 # Here's a sample route:
6 6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 7 # Keep in mind you can assign values other than :controller and :action
8 8
9 9 map.home '', :controller => 'welcome', :conditions => {:method => :get}
10 10
11 11 map.signin 'login', :controller => 'account', :action => 'login',
12 12 :conditions => {:method => [:get, :post]}
13 13 map.signout 'logout', :controller => 'account', :action => 'logout',
14 14 :conditions => {:method => :get}
15 15 map.connect 'account/register', :controller => 'account', :action => 'register',
16 16 :conditions => {:method => [:get, :post]}
17 17 map.connect 'account/lost_password', :controller => 'account', :action => 'lost_password',
18 18 :conditions => {:method => [:get, :post]}
19 19 map.connect 'account/activate', :controller => 'account', :action => 'activate',
20 20 :conditions => {:method => :get}
21 21
22 22 map.connect 'projects/:id/wiki', :controller => 'wikis',
23 23 :action => 'edit', :conditions => {:method => :post}
24 24 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis',
25 25 :action => 'destroy', :conditions => {:method => [:get, :post]}
26 26
27 27 map.with_options :controller => 'messages' do |messages_routes|
28 28 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
29 29 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
30 30 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
31 31 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
32 32 end
33 33 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
34 34 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
35 35 messages_actions.connect 'boards/:board_id/topics/preview', :action => 'preview'
36 36 messages_actions.connect 'boards/:board_id/topics/quote/:id', :action => 'quote'
37 37 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
38 38 messages_actions.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
39 39 messages_actions.connect 'boards/:board_id/topics/:id/destroy', :action => 'destroy'
40 40 end
41 41 end
42 42
43 43 # Misc issue routes. TODO: move into resources
44 44 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes',
45 45 :action => 'issues', :conditions => { :method => :get }
46 46 # TODO: would look nicer as /issues/:id/preview
47 47 map.preview_new_issue '/issues/preview/new/:project_id', :controller => 'previews',
48 48 :action => 'issue'
49 49 map.preview_edit_issue '/issues/preview/edit/:id', :controller => 'previews',
50 50 :action => 'issue'
51 51 map.issues_context_menu '/issues/context_menu',
52 52 :controller => 'context_menus', :action => 'issues'
53 53
54 54 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
55 55 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new',
56 56 :id => /\d+/, :conditions => { :method => :post }
57 57
58 58 map.connect '/journals/diff/:id', :controller => 'journals', :action => 'diff',
59 59 :id => /\d+/, :conditions => { :method => :get }
60 60 map.connect '/journals/edit/:id', :controller => 'journals', :action => 'edit',
61 61 :id => /\d+/, :conditions => { :method => [:get, :post] }
62 62
63 63 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
64 64 gantts_routes.connect '/projects/:project_id/issues/gantt'
65 65 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
66 66 gantts_routes.connect '/issues/gantt.:format'
67 67 end
68 68
69 69 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
70 70 calendars_routes.connect '/projects/:project_id/issues/calendar'
71 71 calendars_routes.connect '/issues/calendar'
72 72 end
73 73
74 74 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
75 75 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
76 76 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
77 77 end
78 78
79 79 map.connect 'my/account', :controller => 'my', :action => 'account',
80 80 :conditions => {:method => [:get, :post]}
81 81 map.connect 'my/page', :controller => 'my', :action => 'page',
82 82 :conditions => {:method => :get}
83 83 # Redirects to my/page
84 84 map.connect 'my', :controller => 'my', :action => 'index',
85 85 :conditions => {:method => :get}
86 86 map.connect 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key',
87 87 :conditions => {:method => :post}
88 88 map.connect 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key',
89 89 :conditions => {:method => :post}
90 90 map.connect 'my/password', :controller => 'my', :action => 'password',
91 91 :conditions => {:method => [:get, :post]}
92 92 map.connect 'my/page_layout', :controller => 'my', :action => 'page_layout',
93 93 :conditions => {:method => :get}
94 94 map.connect 'my/add_block', :controller => 'my', :action => 'add_block',
95 95 :conditions => {:method => :post}
96 96 map.connect 'my/remove_block', :controller => 'my', :action => 'remove_block',
97 97 :conditions => {:method => :post}
98 98 map.connect 'my/order_blocks', :controller => 'my', :action => 'order_blocks',
99 99 :conditions => {:method => :post}
100 100
101 101 map.with_options :controller => 'users' do |users|
102 102 users.user_membership 'users/:id/memberships/:membership_id',
103 103 :action => 'edit_membership',
104 104 :conditions => {:method => :put}
105 105 users.connect 'users/:id/memberships/:membership_id',
106 106 :action => 'destroy_membership',
107 107 :conditions => {:method => :delete}
108 108 users.user_memberships 'users/:id/memberships',
109 109 :action => 'edit_membership',
110 110 :conditions => {:method => :post}
111 111 end
112 112 map.resources :users
113 113
114 114 # For nice "roadmap" in the url for the index action
115 115 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
116 116
117 117 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
118 118 map.connect 'news/:id/comments', :controller => 'comments',
119 119 :action => 'create', :conditions => {:method => :post}
120 120 map.connect 'news/:id/comments/:comment_id', :controller => 'comments',
121 121 :action => 'destroy', :conditions => {:method => :delete}
122 122
123 123 map.connect 'watchers/new', :controller=> 'watchers', :action => 'new',
124 124 :conditions => {:method => :get}
125 125 map.connect 'watchers', :controller=> 'watchers', :action => 'create',
126 126 :conditions => {:method => :post}
127 map.connect 'watchers/append', :controller=> 'watchers', :action => 'append',
128 :conditions => {:method => :post}
127 129 map.connect 'watchers/destroy', :controller=> 'watchers', :action => 'destroy',
128 130 :conditions => {:method => :post}
129 131 map.connect 'watchers/watch', :controller=> 'watchers', :action => 'watch',
130 132 :conditions => {:method => :post}
131 133 map.connect 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch',
132 134 :conditions => {:method => :post}
133 135 map.connect 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user',
134 136 :conditions => {:method => :get}
135 137
136 138 # TODO: port to be part of the resources route(s)
137 139 map.with_options :conditions => {:method => :get} do |project_views|
138 140 project_views.connect 'projects/:id/settings/:tab',
139 141 :controller => 'projects', :action => 'settings'
140 142 project_views.connect 'projects/:project_id/issues/:copy_from/copy',
141 143 :controller => 'issues', :action => 'new'
142 144 end
143 145
144 146 map.resources :projects, :member => {
145 147 :copy => [:get, :post],
146 148 :settings => :get,
147 149 :modules => :post,
148 150 :archive => :post,
149 151 :unarchive => :post
150 152 } do |project|
151 153 project.resource :enumerations, :controller => 'project_enumerations',
152 154 :only => [:update, :destroy]
153 155 # issue form update
154 156 project.issue_form 'issues/new', :controller => 'issues',
155 157 :action => 'new', :conditions => {:method => [:post, :put]}
156 158 project.resources :issues, :only => [:index, :new, :create] do |issues|
157 159 issues.resources :time_entries, :controller => 'timelog',
158 160 :collection => {:report => :get}
159 161 end
160 162
161 163 project.resources :files, :only => [:index, :new, :create]
162 164 project.resources :versions, :shallow => true,
163 165 :collection => {:close_completed => :put},
164 166 :member => {:status_by => :post}
165 167 project.resources :news, :shallow => true
166 168 project.resources :time_entries, :controller => 'timelog',
167 169 :collection => {:report => :get}
168 170 project.resources :queries, :only => [:new, :create]
169 171 project.resources :issue_categories, :shallow => true
170 172 project.resources :documents, :shallow => true, :member => {:add_attachment => :post}
171 173 project.resources :boards
172 174 project.resources :repositories, :shallow => true, :except => [:index, :show],
173 175 :member => {:committers => [:get, :post]}
174 176 project.resources :memberships, :shallow => true, :controller => 'members',
175 177 :only => [:index, :show, :create, :update, :destroy],
176 178 :collection => {:autocomplete => :get}
177 179
178 180 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
179 181 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
180 182 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
181 183 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
182 184 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
183 185 project.resources :wiki, :except => [:new, :create], :member => {
184 186 :rename => [:get, :post],
185 187 :history => :get,
186 188 :preview => :any,
187 189 :protect => :post,
188 190 :add_attachment => :post
189 191 }, :collection => {
190 192 :export => :get,
191 193 :date_index => :get
192 194 }
193 195 end
194 196
195 197 map.connect 'news', :controller => 'news', :action => 'index'
196 198 map.connect 'news.:format', :controller => 'news', :action => 'index'
197 199
198 200 map.resources :queries, :except => [:show]
199 201 map.resources :issues,
200 202 :collection => {:bulk_edit => [:get, :post], :bulk_update => :post} do |issues|
201 203 issues.resources :time_entries, :controller => 'timelog',
202 204 :collection => {:report => :get}
203 205 issues.resources :relations, :shallow => true,
204 206 :controller => 'issue_relations',
205 207 :only => [:index, :show, :create, :destroy]
206 208 end
207 209 # Bulk deletion
208 210 map.connect '/issues', :controller => 'issues', :action => 'destroy',
209 211 :conditions => {:method => :delete}
210 212
211 213 map.connect '/time_entries/destroy',
212 214 :controller => 'timelog', :action => 'destroy',
213 215 :conditions => { :method => :delete }
214 216 map.time_entries_context_menu '/time_entries/context_menu',
215 217 :controller => 'context_menus', :action => 'time_entries'
216 218
217 219 map.resources :time_entries, :controller => 'timelog',
218 220 :collection => {:report => :get, :bulk_edit => :get, :bulk_update => :post}
219 221
220 222 map.with_options :controller => 'activities', :action => 'index',
221 223 :conditions => {:method => :get} do |activity|
222 224 activity.connect 'projects/:id/activity'
223 225 activity.connect 'projects/:id/activity.:format'
224 226 activity.connect 'activity', :id => nil
225 227 activity.connect 'activity.:format', :id => nil
226 228 end
227 229
228 230 map.with_options :controller => 'repositories' do |repositories|
229 231 repositories.with_options :conditions => {:method => :get} do |repository_views|
230 232 repository_views.connect 'projects/:id/repository',
231 233 :action => 'show'
232 234
233 235 repository_views.connect 'projects/:id/repository/:repository_id/statistics',
234 236 :action => 'stats'
235 237 repository_views.connect 'projects/:id/repository/:repository_id/graph',
236 238 :action => 'graph'
237 239
238 240 repository_views.connect 'projects/:id/repository/statistics',
239 241 :action => 'stats'
240 242 repository_views.connect 'projects/:id/repository/graph',
241 243 :action => 'graph'
242 244
243 245 repository_views.connect 'projects/:id/repository/:repository_id/revisions',
244 246 :action => 'revisions'
245 247 repository_views.connect 'projects/:id/repository/:repository_id/revisions.:format',
246 248 :action => 'revisions'
247 249 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev',
248 250 :action => 'revision'
249 251 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/issues',
250 252 :action => 'add_related_issue', :conditions => {:method => :post}
251 253 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id',
252 254 :action => 'remove_related_issue', :conditions => {:method => :delete}
253 255 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff',
254 256 :action => 'diff'
255 257 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff.:format',
256 258 :action => 'diff'
257 259 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/raw/*path',
258 260 :action => 'entry', :format => 'raw'
259 261 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/:action/*path',
260 262 :requirements => {
261 263 :action => /(browse|show|entry|changes|annotate|diff)/,
262 264 :rev => /[a-z0-9\.\-_]+/
263 265 }
264 266 repository_views.connect 'projects/:id/repository/:repository_id/raw/*path',
265 267 :action => 'entry', :format => 'raw'
266 268 repository_views.connect 'projects/:id/repository/:repository_id/:action/*path',
267 269 :requirements => { :action => /(browse|entry|changes|annotate|diff)/ }
268 270 repository_views.connect 'projects/:id/repository/:repository_id/show/*path',
269 271 :requirements => { :path => /.+/ }
270 272
271 273 repository_views.connect 'projects/:id/repository/revisions',
272 274 :action => 'revisions'
273 275 repository_views.connect 'projects/:id/repository/revisions.:format',
274 276 :action => 'revisions'
275 277 repository_views.connect 'projects/:id/repository/revisions/:rev',
276 278 :action => 'revision'
277 279 repository_views.connect 'projects/:id/repository/revisions/:rev/issues',
278 280 :action => 'add_related_issue', :conditions => {:method => :post}
279 281 repository_views.connect 'projects/:id/repository/revisions/:rev/issues/:issue_id',
280 282 :action => 'remove_related_issue', :conditions => {:method => :delete}
281 283 repository_views.connect 'projects/:id/repository/revisions/:rev/diff',
282 284 :action => 'diff'
283 285 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format',
284 286 :action => 'diff'
285 287 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path',
286 288 :action => 'entry', :format => 'raw'
287 289 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path',
288 290 :requirements => {
289 291 :action => /(browse|show|entry|changes|annotate|diff)/,
290 292 :rev => /[a-z0-9\.\-_]+/
291 293 }
292 294 repository_views.connect 'projects/:id/repository/raw/*path',
293 295 :action => 'entry', :format => 'raw'
294 296 repository_views.connect 'projects/:id/repository/:action/*path',
295 297 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
296 298
297 299 repository_views.connect 'projects/:id/repository/:repository_id',
298 300 :action => 'show'
299 301 end
300 302
301 303 repositories.connect 'projects/:id/repository/revision',
302 304 :action => 'revision',
303 305 :conditions => {:method => [:get, :post]}
304 306 end
305 307
306 308 # additional routes for having the file name at the end of url
307 309 map.connect 'attachments/:id/:filename', :controller => 'attachments',
308 310 :action => 'show', :id => /\d+/, :filename => /.*/,
309 311 :conditions => {:method => :get}
310 312 map.connect 'attachments/download/:id/:filename', :controller => 'attachments',
311 313 :action => 'download', :id => /\d+/, :filename => /.*/,
312 314 :conditions => {:method => :get}
313 315 map.connect 'attachments/download/:id', :controller => 'attachments',
314 316 :action => 'download', :id => /\d+/,
315 317 :conditions => {:method => :get}
316 318 map.resources :attachments, :only => [:show, :destroy]
317 319
318 320 map.resources :groups, :member => {:autocomplete_for_user => :get}
319 321 map.group_users 'groups/:id/users', :controller => 'groups',
320 322 :action => 'add_users', :id => /\d+/,
321 323 :conditions => {:method => :post}
322 324 map.group_user 'groups/:id/users/:user_id', :controller => 'groups',
323 325 :action => 'remove_user', :id => /\d+/,
324 326 :conditions => {:method => :delete}
325 327 map.connect 'groups/destroy_membership/:id', :controller => 'groups',
326 328 :action => 'destroy_membership', :id => /\d+/,
327 329 :conditions => {:method => :post}
328 330 map.connect 'groups/edit_membership/:id', :controller => 'groups',
329 331 :action => 'edit_membership', :id => /\d+/,
330 332 :conditions => {:method => :post}
331 333
332 334 map.resources :trackers, :except => :show
333 335 map.resources :issue_statuses, :except => :show, :collection => {:update_issue_done_ratio => :post}
334 336 map.resources :custom_fields, :except => :show
335 337 map.resources :roles, :except => :show, :collection => {:permissions => [:get, :post]}
336 338 map.resources :enumerations, :except => :show
337 339
338 340 map.connect 'search', :controller => 'search', :action => 'index', :conditions => {:method => :get}
339 341
340 342 map.connect 'mail_handler', :controller => 'mail_handler',
341 343 :action => 'index', :conditions => {:method => :post}
342 344
343 345 map.connect 'admin', :controller => 'admin', :action => 'index',
344 346 :conditions => {:method => :get}
345 347 map.connect 'admin/projects', :controller => 'admin', :action => 'projects',
346 348 :conditions => {:method => :get}
347 349 map.connect 'admin/plugins', :controller => 'admin', :action => 'plugins',
348 350 :conditions => {:method => :get}
349 351 map.connect 'admin/info', :controller => 'admin', :action => 'info',
350 352 :conditions => {:method => :get}
351 353 map.connect 'admin/test_email', :controller => 'admin', :action => 'test_email',
352 354 :conditions => {:method => :get}
353 355 map.connect 'admin/default_configuration', :controller => 'admin',
354 356 :action => 'default_configuration', :conditions => {:method => :post}
355 357
356 358 map.resources :auth_sources, :member => {:test_connection => :get}
357 359
358 360 map.connect 'workflows', :controller => 'workflows',
359 361 :action => 'index', :conditions => {:method => :get}
360 362 map.connect 'workflows/edit', :controller => 'workflows',
361 363 :action => 'edit', :conditions => {:method => [:get, :post]}
362 364 map.connect 'workflows/copy', :controller => 'workflows',
363 365 :action => 'copy', :conditions => {:method => [:get, :post]}
364 366
365 367 map.connect 'settings', :controller => 'settings',
366 368 :action => 'index', :conditions => {:method => :get}
367 369 map.connect 'settings/edit', :controller => 'settings',
368 370 :action => 'edit', :conditions => {:method => [:get, :post]}
369 371 map.connect 'settings/plugin/:id', :controller => 'settings',
370 372 :action => 'plugin', :conditions => {:method => [:get, :post]}
371 373
372 374 map.with_options :controller => 'sys' do |sys|
373 375 sys.connect 'sys/projects.:format',
374 376 :action => 'projects',
375 377 :conditions => {:method => :get}
376 378 sys.connect 'sys/projects/:id/repository.:format',
377 379 :action => 'create_project_repository',
378 380 :conditions => {:method => :post}
379 381 sys.connect 'sys/fetch_changesets',
380 382 :action => 'fetch_changesets',
381 383 :conditions => {:method => :get}
382 384 end
383 385
384 386 map.connect 'uploads.:format', :controller => 'attachments', :action => 'upload', :conditions => {:method => :post}
385 387
386 388 map.connect 'robots.txt', :controller => 'welcome',
387 389 :action => 'robots', :conditions => {:method => :get}
388 390
389 391 # Used for OpenID
390 392 map.root :controller => 'account', :action => 'login'
391 393 end
@@ -1,1074 +1,1080
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
5 5 h1 {margin:0; padding:0; font-size: 24px;}
6 6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
8 8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
9 9
10 10 /***** Layout *****/
11 11 #wrapper {background: white;}
12 12
13 13 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 14 #top-menu ul {margin: 0; padding: 0;}
15 15 #top-menu li {
16 16 float:left;
17 17 list-style-type:none;
18 18 margin: 0px 0px 0px 0px;
19 19 padding: 0px 0px 0px 0px;
20 20 white-space:nowrap;
21 21 }
22 22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 24
25 25 #account {float:right;}
26 26
27 27 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 28 #header a {color:#f8f8f8;}
29 29 #header h1 a.ancestor { font-size: 80%; }
30 30 #quick-search {float:right;}
31 31
32 32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 33 #main-menu ul {margin: 0; padding: 0;}
34 34 #main-menu li {
35 35 float:left;
36 36 list-style-type:none;
37 37 margin: 0px 2px 0px 0px;
38 38 padding: 0px 0px 0px 0px;
39 39 white-space:nowrap;
40 40 }
41 41 #main-menu li a {
42 42 display: block;
43 43 color: #fff;
44 44 text-decoration: none;
45 45 font-weight: bold;
46 46 margin: 0;
47 47 padding: 4px 10px 4px 10px;
48 48 }
49 49 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 51
52 52 #admin-menu ul {margin: 0; padding: 0;}
53 53 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
54 54
55 55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 56 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 57 #admin-menu a.users { background-image: url(../images/user.png); }
58 58 #admin-menu a.groups { background-image: url(../images/group.png); }
59 59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 67 #admin-menu a.info { background-image: url(../images/help.png); }
68 68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 69
70 70 #main {background-color:#EEEEEE;}
71 71
72 72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 73 * html #sidebar{ width: 22%; }
74 74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 77 #sidebar .contextual { margin-right: 1em; }
78 78
79 79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 81 html>body #content { min-height: 600px; }
82 82 * html body #content { height: 600px; } /* IE */
83 83
84 84 #main.nosidebar #sidebar{ display: none; }
85 85 #main.nosidebar #content{ width: auto; border-right: 0; }
86 86
87 87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88 88
89 89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 90 #login-form table td {padding: 6px;}
91 91 #login-form label {font-weight: bold;}
92 92 #login-form input#username, #login-form input#password { width: 300px; }
93 93
94 94 #modalbg {position:absolute; top:0; left:0; width:100%; height:100%; background:#ccc; z-index:49; opacity:0.5;}
95 95 html>body #modalbg {position:fixed;}
96 96 div.modal { border-radius:5px; position:absolute; top:25%; background:#fff; border:2px solid #759FCF; z-index:50; padding:0px; padding:8px; box-shadow: 1px 1px 8px #888; }
97 97 div.modal h3.title {background:#759FCF; color:#fff; border:0; padding-left:8px; margin:-8px; margin-bottom: 1em; border-top-left-radius:2px;border-top-right-radius:2px;}
98 98 div.modal p.buttons {text-align:right; margin-bottom:0;}
99 99 html>body div.modal {position:fixed;}
100 100
101 101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
102 102
103 103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
104 104
105 105 /***** Links *****/
106 106 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
107 107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
108 108 a img{ border: 0; }
109 109
110 110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
111 111
112 112 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px; -moz-border-radius:2px;}
113 113 #sidebar a.selected:hover {text-decoration:none;}
114 114 #admin-menu a {line-height:1.7em;}
115 115 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
116 116
117 117 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
118 118 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
119 119
120 120 a#toggle-completed-versions {color:#999;}
121 121 /***** Tables *****/
122 122 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
123 123 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
124 124 table.list td { vertical-align: top; }
125 125 table.list td.id { width: 2%; text-align: center;}
126 126 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
127 127 table.list td.checkbox input {padding:0px;}
128 128 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
129 129 table.list td.buttons a { padding-right: 0.6em; }
130 130 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
131 131
132 132 tr.project td.name a { white-space:nowrap; }
133 133
134 134 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
135 135 tr.project.idnt-1 td.name {padding-left: 0.5em;}
136 136 tr.project.idnt-2 td.name {padding-left: 2em;}
137 137 tr.project.idnt-3 td.name {padding-left: 3.5em;}
138 138 tr.project.idnt-4 td.name {padding-left: 5em;}
139 139 tr.project.idnt-5 td.name {padding-left: 6.5em;}
140 140 tr.project.idnt-6 td.name {padding-left: 8em;}
141 141 tr.project.idnt-7 td.name {padding-left: 9.5em;}
142 142 tr.project.idnt-8 td.name {padding-left: 11em;}
143 143 tr.project.idnt-9 td.name {padding-left: 12.5em;}
144 144
145 145 tr.issue { text-align: center; white-space: nowrap; }
146 146 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
147 147 tr.issue td.subject { text-align: left; }
148 148 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
149 149
150 150 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
151 151 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
152 152 tr.issue.idnt-2 td.subject {padding-left: 2em;}
153 153 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
154 154 tr.issue.idnt-4 td.subject {padding-left: 5em;}
155 155 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
156 156 tr.issue.idnt-6 td.subject {padding-left: 8em;}
157 157 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
158 158 tr.issue.idnt-8 td.subject {padding-left: 11em;}
159 159 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
160 160
161 161 tr.entry { border: 1px solid #f8f8f8; }
162 162 tr.entry td { white-space: nowrap; }
163 163 tr.entry td.filename { width: 30%; }
164 164 tr.entry td.filename_no_report { width: 70%; }
165 165 tr.entry td.size { text-align: right; font-size: 90%; }
166 166 tr.entry td.revision, tr.entry td.author { text-align: center; }
167 167 tr.entry td.age { text-align: right; }
168 168 tr.entry.file td.filename a { margin-left: 16px; }
169 169 tr.entry.file td.filename_no_report a { margin-left: 16px; }
170 170
171 171 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
172 172 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
173 173
174 174 tr.changeset { height: 20px }
175 175 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
176 176 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
177 177 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
178 178 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
179 179
180 180 table.files tr.file td { text-align: center; }
181 181 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
182 182 table.files tr.file td.digest { font-size: 80%; }
183 183
184 184 table.members td.roles, table.memberships td.roles { width: 45%; }
185 185
186 186 tr.message { height: 2.6em; }
187 187 tr.message td.subject { padding-left: 20px; }
188 188 tr.message td.created_on { white-space: nowrap; }
189 189 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
190 190 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
191 191 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
192 192
193 193 tr.version.closed, tr.version.closed a { color: #999; }
194 194 tr.version td.name { padding-left: 20px; }
195 195 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
196 196 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
197 197
198 198 tr.user td { width:13%; }
199 199 tr.user td.email { width:18%; }
200 200 tr.user td { white-space: nowrap; }
201 201 tr.user.locked, tr.user.registered { color: #aaa; }
202 202 tr.user.locked a, tr.user.registered a { color: #aaa; }
203 203
204 204 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
205 205
206 206 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
207 207
208 208 tr.time-entry { text-align: center; white-space: nowrap; }
209 209 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
210 210 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
211 211 td.hours .hours-dec { font-size: 0.9em; }
212 212
213 213 table.plugins td { vertical-align: middle; }
214 214 table.plugins td.configure { text-align: right; padding-right: 1em; }
215 215 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
216 216 table.plugins span.description { display: block; font-size: 0.9em; }
217 217 table.plugins span.url { display: block; font-size: 0.9em; }
218 218
219 219 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
220 220 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
221 221 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
222 222 tr.group:hover a.toggle-all { display:inline;}
223 223 a.toggle-all:hover {text-decoration:none;}
224 224
225 225 table.list tbody tr:hover { background-color:#ffffdd; }
226 226 table.list tbody tr.group:hover { background-color:inherit; }
227 227 table td {padding:2px;}
228 228 table p {margin:0;}
229 229 .odd {background-color:#f6f7f8;}
230 230 .even {background-color: #fff;}
231 231
232 232 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
233 233 a.sort.asc { background-image: url(../images/sort_asc.png); }
234 234 a.sort.desc { background-image: url(../images/sort_desc.png); }
235 235
236 236 table.attributes { width: 100% }
237 237 table.attributes th { vertical-align: top; text-align: left; }
238 238 table.attributes td { vertical-align: top; }
239 239
240 240 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
241 241
242 242 table.query-columns {
243 243 border-collapse: collapse;
244 244 border: 0;
245 245 }
246 246
247 247 table.query-columns td.buttons {
248 248 vertical-align: middle;
249 249 text-align: center;
250 250 }
251 251
252 252 td.center {text-align:center;}
253 253
254 254 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
255 255
256 256 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
257 257 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
258 258 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
259 259 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
260 260
261 261 #watchers ul {margin: 0; padding: 0;}
262 262 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
263 263 #watchers select {width: 95%; display: block;}
264 264 #watchers a.delete {opacity: 0.4;}
265 265 #watchers a.delete:hover {opacity: 1;}
266 266 #watchers img.gravatar {margin: 0 4px 2px 0;}
267 267
268 span#watchers_inputs {overflow:auto; display:block;}
269 span.search_for_watchers {display:block;}
270 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
271 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
272
273
268 274 .highlight { background-color: #FCFD8D;}
269 275 .highlight.token-1 { background-color: #faa;}
270 276 .highlight.token-2 { background-color: #afa;}
271 277 .highlight.token-3 { background-color: #aaf;}
272 278
273 279 .box{
274 280 padding:6px;
275 281 margin-bottom: 10px;
276 282 background-color:#f6f6f6;
277 283 color:#505050;
278 284 line-height:1.5em;
279 285 border: 1px solid #e4e4e4;
280 286 }
281 287
282 288 div.square {
283 289 border: 1px solid #999;
284 290 float: left;
285 291 margin: .3em .4em 0 .4em;
286 292 overflow: hidden;
287 293 width: .6em; height: .6em;
288 294 }
289 295 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
290 296 .contextual input, .contextual select {font-size:0.9em;}
291 297 .message .contextual { margin-top: 0; }
292 298
293 299 .splitcontent {overflow:auto;}
294 300 .splitcontentleft{float:left; width:49%;}
295 301 .splitcontentright{float:right; width:49%;}
296 302 form {display: inline;}
297 303 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
298 304 fieldset {border: 1px solid #e4e4e4; margin:0;}
299 305 legend {color: #484848;}
300 306 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
301 307 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
302 308 blockquote blockquote { margin-left: 0;}
303 309 acronym { border-bottom: 1px dotted; cursor: help; }
304 310 textarea.wiki-edit { width: 99%; }
305 311 li p {margin-top: 0;}
306 312 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
307 313 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
308 314 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
309 315 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
310 316
311 317 div.issue div.subject div div { padding-left: 16px; }
312 318 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
313 319 div.issue div.subject>div>p { margin-top: 0.5em; }
314 320 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
315 321 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px; -moz-border-radius: 2px;}
316 322 div.issue .next-prev-links {color:#999;}
317 323 div.issue table.attributes th {width:22%;}
318 324 div.issue table.attributes td {width:28%;}
319 325
320 326 #issue_tree table.issues, #relations table.issues { border: 0; }
321 327 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
322 328 #relations td.buttons {padding:0;}
323 329
324 330 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
325 331 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
326 332 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
327 333
328 334 fieldset#date-range p { margin: 2px 0 2px 0; }
329 335 fieldset#filters table { border-collapse: collapse; }
330 336 fieldset#filters table td { padding: 0; vertical-align: middle; }
331 337 fieldset#filters tr.filter { height: 2em; }
332 338 fieldset#filters td.field { width:200px; }
333 339 fieldset#filters td.operator { width:170px; }
334 340 fieldset#filters td.values { white-space:nowrap; }
335 341 fieldset#filters td.values select {min-width:130px;}
336 342 fieldset#filters td.values img { vertical-align: middle; margin-left:1px; }
337 343 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
338 344 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
339 345
340 346 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
341 347 div#issue-changesets div.changeset { padding: 4px;}
342 348 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
343 349 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
344 350
345 351 div#activity dl, #search-results { margin-left: 2em; }
346 352 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
347 353 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
348 354 div#activity dt.me .time { border-bottom: 1px solid #999; }
349 355 div#activity dt .time { color: #777; font-size: 80%; }
350 356 div#activity dd .description, #search-results dd .description { font-style: italic; }
351 357 div#activity span.project:after, #search-results span.project:after { content: " -"; }
352 358 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
353 359
354 360 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
355 361
356 362 div#search-results-counts {float:right;}
357 363 div#search-results-counts ul { margin-top: 0.5em; }
358 364 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
359 365
360 366 dt.issue { background-image: url(../images/ticket.png); }
361 367 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
362 368 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
363 369 dt.issue-note { background-image: url(../images/ticket_note.png); }
364 370 dt.changeset { background-image: url(../images/changeset.png); }
365 371 dt.news { background-image: url(../images/news.png); }
366 372 dt.message { background-image: url(../images/message.png); }
367 373 dt.reply { background-image: url(../images/comments.png); }
368 374 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
369 375 dt.attachment { background-image: url(../images/attachment.png); }
370 376 dt.document { background-image: url(../images/document.png); }
371 377 dt.project { background-image: url(../images/projects.png); }
372 378 dt.time-entry { background-image: url(../images/time.png); }
373 379
374 380 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
375 381
376 382 div#roadmap .related-issues { margin-bottom: 1em; }
377 383 div#roadmap .related-issues td.checkbox { display: none; }
378 384 div#roadmap .wiki h1:first-child { display: none; }
379 385 div#roadmap .wiki h1 { font-size: 120%; }
380 386 div#roadmap .wiki h2 { font-size: 110%; }
381 387 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
382 388
383 389 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
384 390 div#version-summary fieldset { margin-bottom: 1em; }
385 391 div#version-summary fieldset.time-tracking table { width:100%; }
386 392 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
387 393
388 394 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
389 395 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
390 396 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
391 397 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
392 398 table#time-report .hours-dec { font-size: 0.9em; }
393 399
394 400 div.wiki-page .contextual a {opacity: 0.4}
395 401 div.wiki-page .contextual a:hover {opacity: 1}
396 402
397 403 form .attributes select { width: 60%; }
398 404 input#issue_subject { width: 99%; }
399 405 select#issue_done_ratio { width: 95px; }
400 406
401 407 ul.projects { margin: 0; padding-left: 1em; }
402 408 ul.projects.root { margin: 0; padding: 0; }
403 409 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
404 410 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
405 411 ul.projects li.child { list-style-type:none; margin-top: 1em;}
406 412 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
407 413 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
408 414
409 415 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
410 416 #tracker_project_ids li { list-style-type:none; }
411 417
412 418 #related-issues li img {vertical-align:middle;}
413 419
414 420 ul.properties {padding:0; font-size: 0.9em; color: #777;}
415 421 ul.properties li {list-style-type:none;}
416 422 ul.properties li span {font-style:italic;}
417 423
418 424 .total-hours { font-size: 110%; font-weight: bold; }
419 425 .total-hours span.hours-int { font-size: 120%; }
420 426
421 427 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
422 428 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
423 429
424 430 #workflow_copy_form select { width: 200px; }
425 431
426 432 textarea#custom_field_possible_values {width: 99%}
427 433 input#content_comments {width: 99%}
428 434
429 435 .pagination {font-size: 90%}
430 436 p.pagination {margin-top:8px;}
431 437
432 438 /***** Tabular forms ******/
433 439 .tabular p{
434 440 margin: 0;
435 441 padding: 3px 0 3px 0;
436 442 padding-left: 180px; /* width of left column containing the label elements */
437 443 min-height: 1.8em;
438 444 clear:left;
439 445 }
440 446
441 447 html>body .tabular p {overflow:hidden;}
442 448
443 449 .tabular label{
444 450 font-weight: bold;
445 451 float: left;
446 452 text-align: right;
447 453 /* width of left column */
448 454 margin-left: -180px;
449 455 /* width of labels. Should be smaller than left column to create some right margin */
450 456 width: 175px;
451 457 }
452 458
453 459 .tabular label.floating{
454 460 font-weight: normal;
455 461 margin-left: 0px;
456 462 text-align: left;
457 463 width: 270px;
458 464 }
459 465
460 466 .tabular label.block{
461 467 font-weight: normal;
462 468 margin-left: 0px !important;
463 469 text-align: left;
464 470 float: none;
465 471 display: block;
466 472 width: auto;
467 473 }
468 474
469 475 .tabular label.inline{
470 476 float:none;
471 477 margin-left: 5px !important;
472 478 width: auto;
473 479 }
474 480
475 481 label.no-css {
476 482 font-weight: inherit;
477 483 float:none;
478 484 text-align:left;
479 485 margin-left:0px;
480 486 width:auto;
481 487 }
482 488 input#time_entry_comments { width: 90%;}
483 489
484 490 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
485 491
486 492 .tabular.settings p{ padding-left: 300px; }
487 493 .tabular.settings label{ margin-left: -300px; width: 295px; }
488 494 .tabular.settings textarea { width: 99%; }
489 495
490 496 .settings.enabled_scm table {width:100%}
491 497 .settings.enabled_scm td.scm_name{ font-weight: bold; }
492 498
493 499 fieldset.settings label { display: block; }
494 500 fieldset#notified_events .parent { padding-left: 20px; }
495 501
496 502 .required {color: #bb0000;}
497 503 .summary {font-style: italic;}
498 504
499 505 #attachments_fields input.description {margin-left: 8px; width:340px;}
500 506 #attachments_fields span {display:block; white-space:nowrap;}
501 507 #attachments_fields img {vertical-align: middle;}
502 508
503 509 div.attachments { margin-top: 12px; }
504 510 div.attachments p { margin:4px 0 2px 0; }
505 511 div.attachments img { vertical-align: middle; }
506 512 div.attachments span.author { font-size: 0.9em; color: #888; }
507 513
508 514 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
509 515 .other-formats span + span:before { content: "| "; }
510 516
511 517 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
512 518
513 519 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
514 520 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
515 521
516 522 /* Project members tab */
517 523 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
518 524 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
519 525 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
520 526 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
521 527 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
522 528 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
523 529
524 530 #users_for_watcher {height: 200px; overflow:auto;}
525 531 #users_for_watcher label {display: block;}
526 532
527 533 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
528 534
529 535 input#principal_search, input#user_search {width:100%}
530 536 input#principal_search, input#user_search {
531 537 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
532 538 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
533 539 }
534 540 input#principal_search.ajax-loading, input#user_search.ajax-loading {
535 541 background-image: url(../images/loading.gif);
536 542 }
537 543
538 544 * html div#tab-content-members fieldset div { height: 450px; }
539 545
540 546 /***** Flash & error messages ****/
541 547 #errorExplanation, div.flash, .nodata, .warning, .conflict {
542 548 padding: 4px 4px 4px 30px;
543 549 margin-bottom: 12px;
544 550 font-size: 1.1em;
545 551 border: 2px solid;
546 552 }
547 553
548 554 div.flash {margin-top: 8px;}
549 555
550 556 div.flash.error, #errorExplanation {
551 557 background: url(../images/exclamation.png) 8px 50% no-repeat;
552 558 background-color: #ffe3e3;
553 559 border-color: #dd0000;
554 560 color: #880000;
555 561 }
556 562
557 563 div.flash.notice {
558 564 background: url(../images/true.png) 8px 5px no-repeat;
559 565 background-color: #dfffdf;
560 566 border-color: #9fcf9f;
561 567 color: #005f00;
562 568 }
563 569
564 570 div.flash.warning, .conflict {
565 571 background: url(../images/warning.png) 8px 5px no-repeat;
566 572 background-color: #FFEBC1;
567 573 border-color: #FDBF3B;
568 574 color: #A6750C;
569 575 text-align: left;
570 576 }
571 577
572 578 .nodata, .warning {
573 579 text-align: center;
574 580 background-color: #FFEBC1;
575 581 border-color: #FDBF3B;
576 582 color: #A6750C;
577 583 }
578 584
579 585 #errorExplanation ul { font-size: 0.9em;}
580 586 #errorExplanation h2, #errorExplanation p { display: none; }
581 587
582 588 .conflict-details {font-size:80%;}
583 589
584 590 /***** Ajax indicator ******/
585 591 #ajax-indicator {
586 592 position: absolute; /* fixed not supported by IE */
587 593 background-color:#eee;
588 594 border: 1px solid #bbb;
589 595 top:35%;
590 596 left:40%;
591 597 width:20%;
592 598 font-weight:bold;
593 599 text-align:center;
594 600 padding:0.6em;
595 601 z-index:100;
596 602 opacity: 0.5;
597 603 }
598 604
599 605 html>body #ajax-indicator { position: fixed; }
600 606
601 607 #ajax-indicator span {
602 608 background-position: 0% 40%;
603 609 background-repeat: no-repeat;
604 610 background-image: url(../images/loading.gif);
605 611 padding-left: 26px;
606 612 vertical-align: bottom;
607 613 }
608 614
609 615 /***** Calendar *****/
610 616 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
611 617 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
612 618 table.cal thead th.week-number {width: auto;}
613 619 table.cal tbody tr {height: 100px;}
614 620 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
615 621 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
616 622 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
617 623 table.cal td.odd p.day-num {color: #bbb;}
618 624 table.cal td.today {background:#ffffdd;}
619 625 table.cal td.today p.day-num {font-weight: bold;}
620 626 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
621 627 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
622 628 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
623 629 p.cal.legend span {display:block;}
624 630
625 631 /***** Tooltips ******/
626 632 .tooltip{position:relative;z-index:24;}
627 633 .tooltip:hover{z-index:25;color:#000;}
628 634 .tooltip span.tip{display: none; text-align:left;}
629 635
630 636 div.tooltip:hover span.tip{
631 637 display:block;
632 638 position:absolute;
633 639 top:12px; left:24px; width:270px;
634 640 border:1px solid #555;
635 641 background-color:#fff;
636 642 padding: 4px;
637 643 font-size: 0.8em;
638 644 color:#505050;
639 645 }
640 646
641 647 /***** Progress bar *****/
642 648 table.progress {
643 649 border-collapse: collapse;
644 650 border-spacing: 0pt;
645 651 empty-cells: show;
646 652 text-align: center;
647 653 float:left;
648 654 margin: 1px 6px 1px 0px;
649 655 }
650 656
651 657 table.progress td { height: 1em; }
652 658 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
653 659 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
654 660 table.progress td.todo { background: #eee none repeat scroll 0%; }
655 661 p.pourcent {font-size: 80%;}
656 662 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
657 663
658 664 #roadmap table.progress td { height: 1.2em; }
659 665 /***** Tabs *****/
660 666 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
661 667 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
662 668 #content .tabs ul li {
663 669 float:left;
664 670 list-style-type:none;
665 671 white-space:nowrap;
666 672 margin-right:4px;
667 673 background:#fff;
668 674 position:relative;
669 675 margin-bottom:-1px;
670 676 }
671 677 #content .tabs ul li a{
672 678 display:block;
673 679 font-size: 0.9em;
674 680 text-decoration:none;
675 681 line-height:1.3em;
676 682 padding:4px 6px 4px 6px;
677 683 border: 1px solid #ccc;
678 684 border-bottom: 1px solid #bbbbbb;
679 685 background-color: #f6f6f6;
680 686 color:#999;
681 687 font-weight:bold;
682 688 border-top-left-radius:3px;
683 689 border-top-right-radius:3px;
684 690 }
685 691
686 692 #content .tabs ul li a:hover {
687 693 background-color: #ffffdd;
688 694 text-decoration:none;
689 695 }
690 696
691 697 #content .tabs ul li a.selected {
692 698 background-color: #fff;
693 699 border: 1px solid #bbbbbb;
694 700 border-bottom: 1px solid #fff;
695 701 color:#444;
696 702 }
697 703
698 704 #content .tabs ul li a.selected:hover {
699 705 background-color: #fff;
700 706 }
701 707
702 708 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
703 709
704 710 button.tab-left, button.tab-right {
705 711 font-size: 0.9em;
706 712 cursor: pointer;
707 713 height:24px;
708 714 border: 1px solid #ccc;
709 715 border-bottom: 1px solid #bbbbbb;
710 716 position:absolute;
711 717 padding:4px;
712 718 width: 20px;
713 719 bottom: -1px;
714 720 }
715 721
716 722 button.tab-left {
717 723 right: 20px;
718 724 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
719 725 border-top-left-radius:3px;
720 726 }
721 727
722 728 button.tab-right {
723 729 right: 0;
724 730 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
725 731 border-top-right-radius:3px;
726 732 }
727 733
728 734 /***** Auto-complete *****/
729 735 div.autocomplete {
730 736 position:absolute;
731 737 width:400px;
732 738 margin:0;
733 739 padding:0;
734 740 }
735 741 div.autocomplete ul {
736 742 list-style-type:none;
737 743 margin:0;
738 744 padding:0;
739 745 }
740 746 div.autocomplete ul li {
741 747 list-style-type:none;
742 748 display:block;
743 749 margin:-1px 0 0 0;
744 750 padding:2px;
745 751 cursor:pointer;
746 752 font-size: 90%;
747 753 border: 1px solid #ccc;
748 754 border-left: 1px solid #ccc;
749 755 border-right: 1px solid #ccc;
750 756 background-color:white;
751 757 }
752 758 div.autocomplete ul li.selected { background-color: #ffb;}
753 759 div.autocomplete ul li span.informal {
754 760 font-size: 80%;
755 761 color: #aaa;
756 762 }
757 763
758 764 #parent_issue_candidates ul li {width: 500px;}
759 765 #related_issue_candidates ul li {width: 500px;}
760 766
761 767 /***** Diff *****/
762 768 .diff_out { background: #fcc; }
763 769 .diff_out span { background: #faa; }
764 770 .diff_in { background: #cfc; }
765 771 .diff_in span { background: #afa; }
766 772
767 773 .text-diff {
768 774 padding: 1em;
769 775 background-color:#f6f6f6;
770 776 color:#505050;
771 777 border: 1px solid #e4e4e4;
772 778 }
773 779
774 780 /***** Wiki *****/
775 781 div.wiki table {
776 782 border-collapse: collapse;
777 783 margin-bottom: 1em;
778 784 }
779 785
780 786 div.wiki table, div.wiki td, div.wiki th {
781 787 border: 1px solid #bbb;
782 788 padding: 4px;
783 789 }
784 790
785 791 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
786 792
787 793 div.wiki .external {
788 794 background-position: 0% 60%;
789 795 background-repeat: no-repeat;
790 796 padding-left: 12px;
791 797 background-image: url(../images/external.png);
792 798 }
793 799
794 800 div.wiki a.new {
795 801 color: #b73535;
796 802 }
797 803
798 804 div.wiki ul, div.wiki ol {margin-bottom:1em;}
799 805
800 806 div.wiki pre {
801 807 margin: 1em 1em 1em 1.6em;
802 808 padding: 2px 2px 2px 0;
803 809 background-color: #fafafa;
804 810 border: 1px solid #dadada;
805 811 width:auto;
806 812 overflow-x: auto;
807 813 overflow-y: hidden;
808 814 }
809 815
810 816 div.wiki ul.toc {
811 817 background-color: #ffffdd;
812 818 border: 1px solid #e4e4e4;
813 819 padding: 4px;
814 820 line-height: 1.2em;
815 821 margin-bottom: 12px;
816 822 margin-right: 12px;
817 823 margin-left: 0;
818 824 display: table
819 825 }
820 826 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
821 827
822 828 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
823 829 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
824 830 div.wiki ul.toc ul { margin: 0; padding: 0; }
825 831 div.wiki ul.toc li { list-style-type:none; margin: 0;}
826 832 div.wiki ul.toc li li { margin-left: 1.5em; }
827 833 div.wiki ul.toc li li li { font-size: 0.8em; }
828 834
829 835 div.wiki ul.toc a {
830 836 font-size: 0.9em;
831 837 font-weight: normal;
832 838 text-decoration: none;
833 839 color: #606060;
834 840 }
835 841 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
836 842
837 843 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
838 844 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
839 845 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
840 846
841 847 div.wiki img { vertical-align: middle; }
842 848
843 849 /***** My page layout *****/
844 850 .block-receiver {
845 851 border:1px dashed #c0c0c0;
846 852 margin-bottom: 20px;
847 853 padding: 15px 0 15px 0;
848 854 }
849 855
850 856 .mypage-box {
851 857 margin:0 0 20px 0;
852 858 color:#505050;
853 859 line-height:1.5em;
854 860 }
855 861
856 862 .handle {
857 863 cursor: move;
858 864 }
859 865
860 866 a.close-icon {
861 867 display:block;
862 868 margin-top:3px;
863 869 overflow:hidden;
864 870 width:12px;
865 871 height:12px;
866 872 background-repeat: no-repeat;
867 873 cursor:pointer;
868 874 background-image:url('../images/close.png');
869 875 }
870 876
871 877 a.close-icon:hover {
872 878 background-image:url('../images/close_hl.png');
873 879 }
874 880
875 881 /***** Gantt chart *****/
876 882 .gantt_hdr {
877 883 position:absolute;
878 884 top:0;
879 885 height:16px;
880 886 border-top: 1px solid #c0c0c0;
881 887 border-bottom: 1px solid #c0c0c0;
882 888 border-right: 1px solid #c0c0c0;
883 889 text-align: center;
884 890 overflow: hidden;
885 891 }
886 892
887 893 .gantt_subjects { font-size: 0.8em; }
888 894 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
889 895
890 896 .task {
891 897 position: absolute;
892 898 height:8px;
893 899 font-size:0.8em;
894 900 color:#888;
895 901 padding:0;
896 902 margin:0;
897 903 line-height:16px;
898 904 white-space:nowrap;
899 905 }
900 906
901 907 .task.label {width:100%;}
902 908 .task.label.project, .task.label.version { font-weight: bold; }
903 909
904 910 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
905 911 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
906 912 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
907 913
908 914 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
909 915 .task_late.parent, .task_done.parent { height: 3px;}
910 916 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
911 917 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
912 918
913 919 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
914 920 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
915 921 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
916 922 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
917 923
918 924 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
919 925 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
920 926 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
921 927 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
922 928
923 929 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
924 930 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
925 931
926 932 /***** Icons *****/
927 933 .icon {
928 934 background-position: 0% 50%;
929 935 background-repeat: no-repeat;
930 936 padding-left: 20px;
931 937 padding-top: 2px;
932 938 padding-bottom: 3px;
933 939 }
934 940
935 941 .icon-add { background-image: url(../images/add.png); }
936 942 .icon-edit { background-image: url(../images/edit.png); }
937 943 .icon-copy { background-image: url(../images/copy.png); }
938 944 .icon-duplicate { background-image: url(../images/duplicate.png); }
939 945 .icon-del { background-image: url(../images/delete.png); }
940 946 .icon-move { background-image: url(../images/move.png); }
941 947 .icon-save { background-image: url(../images/save.png); }
942 948 .icon-cancel { background-image: url(../images/cancel.png); }
943 949 .icon-multiple { background-image: url(../images/table_multiple.png); }
944 950 .icon-folder { background-image: url(../images/folder.png); }
945 951 .open .icon-folder { background-image: url(../images/folder_open.png); }
946 952 .icon-package { background-image: url(../images/package.png); }
947 953 .icon-user { background-image: url(../images/user.png); }
948 954 .icon-projects { background-image: url(../images/projects.png); }
949 955 .icon-help { background-image: url(../images/help.png); }
950 956 .icon-attachment { background-image: url(../images/attachment.png); }
951 957 .icon-history { background-image: url(../images/history.png); }
952 958 .icon-time { background-image: url(../images/time.png); }
953 959 .icon-time-add { background-image: url(../images/time_add.png); }
954 960 .icon-stats { background-image: url(../images/stats.png); }
955 961 .icon-warning { background-image: url(../images/warning.png); }
956 962 .icon-fav { background-image: url(../images/fav.png); }
957 963 .icon-fav-off { background-image: url(../images/fav_off.png); }
958 964 .icon-reload { background-image: url(../images/reload.png); }
959 965 .icon-lock { background-image: url(../images/locked.png); }
960 966 .icon-unlock { background-image: url(../images/unlock.png); }
961 967 .icon-checked { background-image: url(../images/true.png); }
962 968 .icon-details { background-image: url(../images/zoom_in.png); }
963 969 .icon-report { background-image: url(../images/report.png); }
964 970 .icon-comment { background-image: url(../images/comment.png); }
965 971 .icon-summary { background-image: url(../images/lightning.png); }
966 972 .icon-server-authentication { background-image: url(../images/server_key.png); }
967 973 .icon-issue { background-image: url(../images/ticket.png); }
968 974 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
969 975 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
970 976 .icon-passwd { background-image: url(../images/textfield_key.png); }
971 977 .icon-test { background-image: url(../images/bullet_go.png); }
972 978
973 979 .icon-file { background-image: url(../images/files/default.png); }
974 980 .icon-file.text-plain { background-image: url(../images/files/text.png); }
975 981 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
976 982 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
977 983 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
978 984 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
979 985 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
980 986 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
981 987 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
982 988 .icon-file.text-css { background-image: url(../images/files/css.png); }
983 989 .icon-file.text-html { background-image: url(../images/files/html.png); }
984 990 .icon-file.image-gif { background-image: url(../images/files/image.png); }
985 991 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
986 992 .icon-file.image-png { background-image: url(../images/files/image.png); }
987 993 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
988 994 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
989 995 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
990 996 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
991 997
992 998 img.gravatar {
993 999 padding: 2px;
994 1000 border: solid 1px #d5d5d5;
995 1001 background: #fff;
996 1002 vertical-align: middle;
997 1003 }
998 1004
999 1005 div.issue img.gravatar {
1000 1006 float: left;
1001 1007 margin: 0 6px 0 0;
1002 1008 padding: 5px;
1003 1009 }
1004 1010
1005 1011 div.issue table img.gravatar {
1006 1012 height: 14px;
1007 1013 width: 14px;
1008 1014 padding: 2px;
1009 1015 float: left;
1010 1016 margin: 0 0.5em 0 0;
1011 1017 }
1012 1018
1013 1019 h2 img.gravatar {
1014 1020 margin: -2px 4px -4px 0;
1015 1021 }
1016 1022
1017 1023 h3 img.gravatar {
1018 1024 margin: -4px 4px -4px 0;
1019 1025 }
1020 1026
1021 1027 h4 img.gravatar {
1022 1028 margin: -6px 4px -4px 0;
1023 1029 }
1024 1030
1025 1031 td.username img.gravatar {
1026 1032 margin: 0 0.5em 0 0;
1027 1033 vertical-align: top;
1028 1034 }
1029 1035
1030 1036 #activity dt img.gravatar {
1031 1037 float: left;
1032 1038 margin: 0 1em 1em 0;
1033 1039 }
1034 1040
1035 1041 /* Used on 12px Gravatar img tags without the icon background */
1036 1042 .icon-gravatar {
1037 1043 float: left;
1038 1044 margin-right: 4px;
1039 1045 }
1040 1046
1041 1047 #activity dt,
1042 1048 .journal {
1043 1049 clear: left;
1044 1050 }
1045 1051
1046 1052 .journal-link {
1047 1053 float: right;
1048 1054 }
1049 1055
1050 1056 h2 img { vertical-align:middle; }
1051 1057
1052 1058 .hascontextmenu { cursor: context-menu; }
1053 1059
1054 1060 /***** Media print specific styles *****/
1055 1061 @media print {
1056 1062 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1057 1063 #main { background: #fff; }
1058 1064 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1059 1065 #wiki_add_attachment { display:none; }
1060 1066 .hide-when-print { display: none; }
1061 1067 .autoscroll {overflow-x: visible;}
1062 1068 table.list {margin-top:0.5em;}
1063 1069 table.list th, table.list td {border: 1px solid #aaa;}
1064 1070 }
1065 1071
1066 1072 /* Accessibility specific styles */
1067 1073 .hidden-for-sighted {
1068 1074 position:absolute;
1069 1075 left:-10000px;
1070 1076 top:auto;
1071 1077 width:1px;
1072 1078 height:1px;
1073 1079 overflow:hidden;
1074 1080 }
@@ -1,3153 +1,3168
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 :repositories,
46 46 :changesets
47 47
48 48 include Redmine::I18n
49 49
50 50 def setup
51 51 @controller = IssuesController.new
52 52 @request = ActionController::TestRequest.new
53 53 @response = ActionController::TestResponse.new
54 54 User.current = nil
55 55 end
56 56
57 57 def test_index
58 58 with_settings :default_language => "en" do
59 59 get :index
60 60 assert_response :success
61 61 assert_template 'index'
62 62 assert_not_nil assigns(:issues)
63 63 assert_nil assigns(:project)
64 64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 65 assert_tag :tag => 'a', :content => /Subproject issue/
66 66 # private projects hidden
67 67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 69 # project column
70 70 assert_tag :tag => 'th', :content => /Project/
71 71 end
72 72 end
73 73
74 74 def test_index_should_not_list_issues_when_module_disabled
75 75 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
76 76 get :index
77 77 assert_response :success
78 78 assert_template 'index'
79 79 assert_not_nil assigns(:issues)
80 80 assert_nil assigns(:project)
81 81 assert_no_tag :tag => 'a', :content => /Can't print recipes/
82 82 assert_tag :tag => 'a', :content => /Subproject issue/
83 83 end
84 84
85 85 def test_index_should_list_visible_issues_only
86 86 get :index, :per_page => 100
87 87 assert_response :success
88 88 assert_not_nil assigns(:issues)
89 89 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
90 90 end
91 91
92 92 def test_index_with_project
93 93 Setting.display_subprojects_issues = 0
94 94 get :index, :project_id => 1
95 95 assert_response :success
96 96 assert_template 'index'
97 97 assert_not_nil assigns(:issues)
98 98 assert_tag :tag => 'a', :content => /Can't print recipes/
99 99 assert_no_tag :tag => 'a', :content => /Subproject issue/
100 100 end
101 101
102 102 def test_index_with_project_and_subprojects
103 103 Setting.display_subprojects_issues = 1
104 104 get :index, :project_id => 1
105 105 assert_response :success
106 106 assert_template 'index'
107 107 assert_not_nil assigns(:issues)
108 108 assert_tag :tag => 'a', :content => /Can't print recipes/
109 109 assert_tag :tag => 'a', :content => /Subproject issue/
110 110 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
111 111 end
112 112
113 113 def test_index_with_project_and_subprojects_should_show_private_subprojects
114 114 @request.session[:user_id] = 2
115 115 Setting.display_subprojects_issues = 1
116 116 get :index, :project_id => 1
117 117 assert_response :success
118 118 assert_template 'index'
119 119 assert_not_nil assigns(:issues)
120 120 assert_tag :tag => 'a', :content => /Can't print recipes/
121 121 assert_tag :tag => 'a', :content => /Subproject issue/
122 122 assert_tag :tag => 'a', :content => /Issue of a private subproject/
123 123 end
124 124
125 125 def test_index_with_project_and_default_filter
126 126 get :index, :project_id => 1, :set_filter => 1
127 127 assert_response :success
128 128 assert_template 'index'
129 129 assert_not_nil assigns(:issues)
130 130
131 131 query = assigns(:query)
132 132 assert_not_nil query
133 133 # default filter
134 134 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
135 135 end
136 136
137 137 def test_index_with_project_and_filter
138 138 get :index, :project_id => 1, :set_filter => 1,
139 139 :f => ['tracker_id'],
140 140 :op => {'tracker_id' => '='},
141 141 :v => {'tracker_id' => ['1']}
142 142 assert_response :success
143 143 assert_template 'index'
144 144 assert_not_nil assigns(:issues)
145 145
146 146 query = assigns(:query)
147 147 assert_not_nil query
148 148 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
149 149 end
150 150
151 151 def test_index_with_short_filters
152 152 to_test = {
153 153 'status_id' => {
154 154 'o' => { :op => 'o', :values => [''] },
155 155 'c' => { :op => 'c', :values => [''] },
156 156 '7' => { :op => '=', :values => ['7'] },
157 157 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
158 158 '=7' => { :op => '=', :values => ['7'] },
159 159 '!3' => { :op => '!', :values => ['3'] },
160 160 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
161 161 'subject' => {
162 162 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
163 163 'o' => { :op => '=', :values => ['o'] },
164 164 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
165 165 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
166 166 'tracker_id' => {
167 167 '3' => { :op => '=', :values => ['3'] },
168 168 '=3' => { :op => '=', :values => ['3'] }},
169 169 'start_date' => {
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-12' => { :op => '<=', :values => ['2011-10-12'] },
174 174 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
175 175 '<t+2' => { :op => '<t+', :values => ['2'] },
176 176 '>t+2' => { :op => '>t+', :values => ['2'] },
177 177 't+2' => { :op => 't+', :values => ['2'] },
178 178 't' => { :op => 't', :values => [''] },
179 179 'w' => { :op => 'w', :values => [''] },
180 180 '>t-2' => { :op => '>t-', :values => ['2'] },
181 181 '<t-2' => { :op => '<t-', :values => ['2'] },
182 182 't-2' => { :op => 't-', :values => ['2'] }},
183 183 'created_on' => {
184 184 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
185 185 '<t-2' => { :op => '<t-', :values => ['2'] },
186 186 '>t-2' => { :op => '>t-', :values => ['2'] },
187 187 't-2' => { :op => 't-', :values => ['2'] }},
188 188 'cf_1' => {
189 189 'c' => { :op => '=', :values => ['c'] },
190 190 '!c' => { :op => '!', :values => ['c'] },
191 191 '!*' => { :op => '!*', :values => [''] },
192 192 '*' => { :op => '*', :values => [''] }},
193 193 'estimated_hours' => {
194 194 '=13.4' => { :op => '=', :values => ['13.4'] },
195 195 '>=45' => { :op => '>=', :values => ['45'] },
196 196 '<=125' => { :op => '<=', :values => ['125'] },
197 197 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
198 198 '!*' => { :op => '!*', :values => [''] },
199 199 '*' => { :op => '*', :values => [''] }}
200 200 }
201 201
202 202 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
203 203
204 204 to_test.each do |field, expression_and_expected|
205 205 expression_and_expected.each do |filter_expression, expected|
206 206
207 207 get :index, :set_filter => 1, field => filter_expression
208 208
209 209 assert_response :success
210 210 assert_template 'index'
211 211 assert_not_nil assigns(:issues)
212 212
213 213 query = assigns(:query)
214 214 assert_not_nil query
215 215 assert query.has_filter?(field)
216 216 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
217 217 end
218 218 end
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_invalid_query_id_should_respond_404
266 266 get :index, :project_id => 1, :query_id => 999
267 267 assert_response 404
268 268 end
269 269
270 270 def test_index_with_cross_project_query_in_session_should_show_project_issues
271 271 q = Query.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil)
272 272 @request.session[:query] = {:id => q.id, :project_id => 1}
273 273
274 274 with_settings :display_subprojects_issues => '0' do
275 275 get :index, :project_id => 1
276 276 end
277 277 assert_response :success
278 278 assert_not_nil assigns(:query)
279 279 assert_equal q.id, assigns(:query).id
280 280 assert_equal 1, assigns(:query).project_id
281 281 assert_equal [1], assigns(:issues).map(&:project_id).uniq
282 282 end
283 283
284 284 def test_private_query_should_not_be_available_to_other_users
285 285 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
286 286 @request.session[:user_id] = 3
287 287
288 288 get :index, :query_id => q.id
289 289 assert_response 403
290 290 end
291 291
292 292 def test_private_query_should_be_available_to_its_user
293 293 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
294 294 @request.session[:user_id] = 2
295 295
296 296 get :index, :query_id => q.id
297 297 assert_response :success
298 298 end
299 299
300 300 def test_public_query_should_be_available_to_other_users
301 301 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
302 302 @request.session[:user_id] = 3
303 303
304 304 get :index, :query_id => q.id
305 305 assert_response :success
306 306 end
307 307
308 308 def test_index_csv
309 309 get :index, :format => 'csv'
310 310 assert_response :success
311 311 assert_not_nil assigns(:issues)
312 312 assert_equal 'text/csv', @response.content_type
313 313 assert @response.body.starts_with?("#,")
314 314 lines = @response.body.chomp.split("\n")
315 315 assert_equal assigns(:query).columns.size + 1, lines[0].split(',').size
316 316 end
317 317
318 318 def test_index_csv_with_project
319 319 get :index, :project_id => 1, :format => 'csv'
320 320 assert_response :success
321 321 assert_not_nil assigns(:issues)
322 322 assert_equal 'text/csv', @response.content_type
323 323 end
324 324
325 325 def test_index_csv_with_description
326 326 get :index, :format => 'csv', :description => '1'
327 327 assert_response :success
328 328 assert_not_nil assigns(:issues)
329 329 assert_equal 'text/csv', @response.content_type
330 330 assert @response.body.starts_with?("#,")
331 331 lines = @response.body.chomp.split("\n")
332 332 assert_equal assigns(:query).columns.size + 2, lines[0].split(',').size
333 333 end
334 334
335 335 def test_index_csv_with_spent_time_column
336 336 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column', :author_id => 2)
337 337 TimeEntry.create!(:project => issue.project, :issue => issue, :hours => 7.33, :user => User.find(2), :spent_on => Date.today)
338 338
339 339 get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
340 340 assert_response :success
341 341 assert_equal 'text/csv', @response.content_type
342 342 lines = @response.body.chomp.split("\n")
343 343 assert_include "#{issue.id},#{issue.subject},7.33", lines
344 344 end
345 345
346 346 def test_index_csv_with_all_columns
347 347 get :index, :format => 'csv', :columns => 'all'
348 348 assert_response :success
349 349 assert_not_nil assigns(:issues)
350 350 assert_equal 'text/csv', @response.content_type
351 351 assert @response.body.starts_with?("#,")
352 352 lines = @response.body.chomp.split("\n")
353 353 assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size
354 354 end
355 355
356 356 def test_index_csv_with_multi_column_field
357 357 CustomField.find(1).update_attribute :multiple, true
358 358 issue = Issue.find(1)
359 359 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
360 360 issue.save!
361 361
362 362 get :index, :format => 'csv', :columns => 'all'
363 363 assert_response :success
364 364 lines = @response.body.chomp.split("\n")
365 365 assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
366 366 end
367 367
368 368 def test_index_csv_big_5
369 369 with_settings :default_language => "zh-TW" do
370 370 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
371 371 str_big5 = "\xa4@\xa4\xeb"
372 372 if str_utf8.respond_to?(:force_encoding)
373 373 str_utf8.force_encoding('UTF-8')
374 374 str_big5.force_encoding('Big5')
375 375 end
376 376 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
377 377 :status_id => 1, :priority => IssuePriority.all.first,
378 378 :subject => str_utf8)
379 379 assert issue.save
380 380
381 381 get :index, :project_id => 1,
382 382 :f => ['subject'],
383 383 :op => '=', :values => [str_utf8],
384 384 :format => 'csv'
385 385 assert_equal 'text/csv', @response.content_type
386 386 lines = @response.body.chomp.split("\n")
387 387 s1 = "\xaa\xac\xbaA"
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 assert lines[1].include?(str_big5)
393 393 end
394 394 end
395 395
396 396 def test_index_csv_cannot_convert_should_be_replaced_big_5
397 397 with_settings :default_language => "zh-TW" do
398 398 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
399 399 if str_utf8.respond_to?(:force_encoding)
400 400 str_utf8.force_encoding('UTF-8')
401 401 end
402 402 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
403 403 :status_id => 1, :priority => IssuePriority.all.first,
404 404 :subject => str_utf8)
405 405 assert issue.save
406 406
407 407 get :index, :project_id => 1,
408 408 :f => ['subject'],
409 409 :op => '=', :values => [str_utf8],
410 410 :c => ['status', 'subject'],
411 411 :format => 'csv',
412 412 :set_filter => 1
413 413 assert_equal 'text/csv', @response.content_type
414 414 lines = @response.body.chomp.split("\n")
415 415 s1 = "\xaa\xac\xbaA" # status
416 416 if str_utf8.respond_to?(:force_encoding)
417 417 s1.force_encoding('Big5')
418 418 end
419 419 assert lines[0].include?(s1)
420 420 s2 = lines[1].split(",")[2]
421 421 if s1.respond_to?(:force_encoding)
422 422 s3 = "\xa5H?" # subject
423 423 s3.force_encoding('Big5')
424 424 assert_equal s3, s2
425 425 elsif RUBY_PLATFORM == 'java'
426 426 assert_equal "??", s2
427 427 else
428 428 assert_equal "\xa5H???", s2
429 429 end
430 430 end
431 431 end
432 432
433 433 def test_index_csv_tw
434 434 with_settings :default_language => "zh-TW" do
435 435 str1 = "test_index_csv_tw"
436 436 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
437 437 :status_id => 1, :priority => IssuePriority.all.first,
438 438 :subject => str1, :estimated_hours => '1234.5')
439 439 assert issue.save
440 440 assert_equal 1234.5, issue.estimated_hours
441 441
442 442 get :index, :project_id => 1,
443 443 :f => ['subject'],
444 444 :op => '=', :values => [str1],
445 445 :c => ['estimated_hours', 'subject'],
446 446 :format => 'csv',
447 447 :set_filter => 1
448 448 assert_equal 'text/csv', @response.content_type
449 449 lines = @response.body.chomp.split("\n")
450 450 assert_equal "#{issue.id},1234.50,#{str1}", lines[1]
451 451
452 452 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
453 453 if str_tw.respond_to?(:force_encoding)
454 454 str_tw.force_encoding('UTF-8')
455 455 end
456 456 assert_equal str_tw, l(:general_lang_name)
457 457 assert_equal ',', l(:general_csv_separator)
458 458 assert_equal '.', l(:general_csv_decimal_separator)
459 459 end
460 460 end
461 461
462 462 def test_index_csv_fr
463 463 with_settings :default_language => "fr" do
464 464 str1 = "test_index_csv_fr"
465 465 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
466 466 :status_id => 1, :priority => IssuePriority.all.first,
467 467 :subject => str1, :estimated_hours => '1234.5')
468 468 assert issue.save
469 469 assert_equal 1234.5, issue.estimated_hours
470 470
471 471 get :index, :project_id => 1,
472 472 :f => ['subject'],
473 473 :op => '=', :values => [str1],
474 474 :c => ['estimated_hours', 'subject'],
475 475 :format => 'csv',
476 476 :set_filter => 1
477 477 assert_equal 'text/csv', @response.content_type
478 478 lines = @response.body.chomp.split("\n")
479 479 assert_equal "#{issue.id};1234,50;#{str1}", lines[1]
480 480
481 481 str_fr = "Fran\xc3\xa7ais"
482 482 if str_fr.respond_to?(:force_encoding)
483 483 str_fr.force_encoding('UTF-8')
484 484 end
485 485 assert_equal str_fr, l(:general_lang_name)
486 486 assert_equal ';', l(:general_csv_separator)
487 487 assert_equal ',', l(:general_csv_decimal_separator)
488 488 end
489 489 end
490 490
491 491 def test_index_pdf
492 492 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
493 493 with_settings :default_language => lang do
494 494
495 495 get :index
496 496 assert_response :success
497 497 assert_template 'index'
498 498
499 499 if lang == "ja"
500 500 if RUBY_PLATFORM != 'java'
501 501 assert_equal "CP932", l(:general_pdf_encoding)
502 502 end
503 503 if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932"
504 504 next
505 505 end
506 506 end
507 507
508 508 get :index, :format => 'pdf'
509 509 assert_response :success
510 510 assert_not_nil assigns(:issues)
511 511 assert_equal 'application/pdf', @response.content_type
512 512
513 513 get :index, :project_id => 1, :format => 'pdf'
514 514 assert_response :success
515 515 assert_not_nil assigns(:issues)
516 516 assert_equal 'application/pdf', @response.content_type
517 517
518 518 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
519 519 assert_response :success
520 520 assert_not_nil assigns(:issues)
521 521 assert_equal 'application/pdf', @response.content_type
522 522 end
523 523 end
524 524 end
525 525
526 526 def test_index_pdf_with_query_grouped_by_list_custom_field
527 527 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
528 528 assert_response :success
529 529 assert_not_nil assigns(:issues)
530 530 assert_not_nil assigns(:issue_count_by_group)
531 531 assert_equal 'application/pdf', @response.content_type
532 532 end
533 533
534 534 def test_index_atom
535 535 get :index, :project_id => 'ecookbook', :format => 'atom'
536 536 assert_response :success
537 537 assert_template 'common/feed'
538 538
539 539 assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil },
540 540 :attributes => {:rel => 'self', :href => 'http://test.host/projects/ecookbook/issues.atom'}
541 541 assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil },
542 542 :attributes => {:rel => 'alternate', :href => 'http://test.host/projects/ecookbook/issues'}
543 543
544 544 assert_tag :tag => 'entry', :child => {
545 545 :tag => 'link',
546 546 :attributes => {:href => 'http://test.host/issues/1'}}
547 547 end
548 548
549 549 def test_index_sort
550 550 get :index, :sort => 'tracker,id:desc'
551 551 assert_response :success
552 552
553 553 sort_params = @request.session['issues_index_sort']
554 554 assert sort_params.is_a?(String)
555 555 assert_equal 'tracker,id:desc', sort_params
556 556
557 557 issues = assigns(:issues)
558 558 assert_not_nil issues
559 559 assert !issues.empty?
560 560 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
561 561 end
562 562
563 563 def test_index_sort_by_field_not_included_in_columns
564 564 Setting.issue_list_default_columns = %w(subject author)
565 565 get :index, :sort => 'tracker'
566 566 end
567 567
568 568 def test_index_sort_by_assigned_to
569 569 get :index, :sort => 'assigned_to'
570 570 assert_response :success
571 571 assignees = assigns(:issues).collect(&:assigned_to).compact
572 572 assert_equal assignees.sort, assignees
573 573 end
574 574
575 575 def test_index_sort_by_assigned_to_desc
576 576 get :index, :sort => 'assigned_to:desc'
577 577 assert_response :success
578 578 assignees = assigns(:issues).collect(&:assigned_to).compact
579 579 assert_equal assignees.sort.reverse, assignees
580 580 end
581 581
582 582 def test_index_group_by_assigned_to
583 583 get :index, :group_by => 'assigned_to', :sort => 'priority'
584 584 assert_response :success
585 585 end
586 586
587 587 def test_index_sort_by_author
588 588 get :index, :sort => 'author'
589 589 assert_response :success
590 590 authors = assigns(:issues).collect(&:author)
591 591 assert_equal authors.sort, authors
592 592 end
593 593
594 594 def test_index_sort_by_author_desc
595 595 get :index, :sort => 'author:desc'
596 596 assert_response :success
597 597 authors = assigns(:issues).collect(&:author)
598 598 assert_equal authors.sort.reverse, authors
599 599 end
600 600
601 601 def test_index_group_by_author
602 602 get :index, :group_by => 'author', :sort => 'priority'
603 603 assert_response :success
604 604 end
605 605
606 606 def test_index_sort_by_spent_hours
607 607 get :index, :sort => 'spent_hours:desc'
608 608 assert_response :success
609 609 hours = assigns(:issues).collect(&:spent_hours)
610 610 assert_equal hours.sort.reverse, hours
611 611 end
612 612
613 613 def test_index_with_columns
614 614 columns = ['tracker', 'subject', 'assigned_to']
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 # columns should be stored in session
624 624 assert_kind_of Hash, session[:query]
625 625 assert_kind_of Array, session[:query][:column_names]
626 626 assert_equal columns, session[:query][:column_names].map(&:to_s)
627 627
628 628 # ensure only these columns are kept in the selected columns list
629 629 assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' },
630 630 :children => { :count => 3 }
631 631 assert_no_tag :tag => 'option', :attributes => { :value => 'project' },
632 632 :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } }
633 633 end
634 634
635 635 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
636 636 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
637 637 get :index, :set_filter => 1
638 638
639 639 # query should use specified columns
640 640 query = assigns(:query)
641 641 assert_kind_of Query, query
642 642 assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
643 643 end
644 644
645 645 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
646 646 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
647 647 columns = ['tracker', 'subject', 'assigned_to']
648 648 get :index, :set_filter => 1, :c => columns
649 649
650 650 # query should use specified columns
651 651 query = assigns(:query)
652 652 assert_kind_of Query, query
653 653 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
654 654 end
655 655
656 656 def test_index_with_custom_field_column
657 657 columns = %w(tracker subject cf_2)
658 658 get :index, :set_filter => 1, :c => columns
659 659 assert_response :success
660 660
661 661 # query should use specified columns
662 662 query = assigns(:query)
663 663 assert_kind_of Query, query
664 664 assert_equal columns, query.column_names.map(&:to_s)
665 665
666 666 assert_tag :td,
667 667 :attributes => {:class => 'cf_2 string'},
668 668 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
669 669 end
670 670
671 671 def test_index_with_multi_custom_field_column
672 672 field = CustomField.find(1)
673 673 field.update_attribute :multiple, true
674 674 issue = Issue.find(1)
675 675 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
676 676 issue.save!
677 677
678 678 get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
679 679 assert_response :success
680 680
681 681 assert_tag :td,
682 682 :attributes => {:class => /cf_1/},
683 683 :content => 'MySQL, Oracle'
684 684 end
685 685
686 686 def test_index_with_multi_user_custom_field_column
687 687 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
688 688 :tracker_ids => [1], :is_for_all => true)
689 689 issue = Issue.find(1)
690 690 issue.custom_field_values = {field.id => ['2', '3']}
691 691 issue.save!
692 692
693 693 get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
694 694 assert_response :success
695 695
696 696 assert_tag :td,
697 697 :attributes => {:class => /cf_#{field.id}/},
698 698 :child => {:tag => 'a', :content => 'John Smith'}
699 699 end
700 700
701 701 def test_index_with_date_column
702 702 Issue.find(1).update_attribute :start_date, '1987-08-24'
703 703
704 704 with_settings :date_format => '%d/%m/%Y' do
705 705 get :index, :set_filter => 1, :c => %w(start_date)
706 706 assert_tag 'td', :attributes => {:class => /start_date/}, :content => '24/08/1987'
707 707 end
708 708 end
709 709
710 710 def test_index_with_done_ratio
711 711 Issue.find(1).update_attribute :done_ratio, 40
712 712
713 713 get :index, :set_filter => 1, :c => %w(done_ratio)
714 714 assert_tag 'td', :attributes => {:class => /done_ratio/},
715 715 :child => {:tag => 'table', :attributes => {:class => 'progress'},
716 716 :descendant => {:tag => 'td', :attributes => {:class => 'closed', :style => 'width: 40%;'}}
717 717 }
718 718 end
719 719
720 720 def test_index_with_spent_hours_column
721 721 get :index, :set_filter => 1, :c => %w(subject spent_hours)
722 722
723 723 assert_tag 'tr', :attributes => {:id => 'issue-3'},
724 724 :child => {
725 725 :tag => 'td', :attributes => {:class => /spent_hours/}, :content => '1.00'
726 726 }
727 727 end
728 728
729 729 def test_index_should_not_show_spent_hours_column_without_permission
730 730 Role.anonymous.remove_permission! :view_time_entries
731 731 get :index, :set_filter => 1, :c => %w(subject spent_hours)
732 732
733 733 assert_no_tag 'td', :attributes => {:class => /spent_hours/}
734 734 end
735 735
736 736 def test_index_with_fixed_version
737 737 get :index, :set_filter => 1, :c => %w(fixed_version)
738 738 assert_tag 'td', :attributes => {:class => /fixed_version/},
739 739 :child => {:tag => 'a', :content => '1.0', :attributes => {:href => '/versions/2'}}
740 740 end
741 741
742 742 def test_index_send_html_if_query_is_invalid
743 743 get :index, :f => ['start_date'], :op => {:start_date => '='}
744 744 assert_equal 'text/html', @response.content_type
745 745 assert_template 'index'
746 746 end
747 747
748 748 def test_index_send_nothing_if_query_is_invalid
749 749 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
750 750 assert_equal 'text/csv', @response.content_type
751 751 assert @response.body.blank?
752 752 end
753 753
754 754 def test_show_by_anonymous
755 755 get :show, :id => 1
756 756 assert_response :success
757 757 assert_template 'show'
758 758 assert_not_nil assigns(:issue)
759 759 assert_equal Issue.find(1), assigns(:issue)
760 760
761 761 # anonymous role is allowed to add a note
762 762 assert_tag :tag => 'form',
763 763 :descendant => { :tag => 'fieldset',
764 764 :child => { :tag => 'legend',
765 765 :content => /Notes/ } }
766 766 assert_tag :tag => 'title',
767 767 :content => "Bug #1: Can't print recipes - eCookbook - Redmine"
768 768 end
769 769
770 770 def test_show_by_manager
771 771 @request.session[:user_id] = 2
772 772 get :show, :id => 1
773 773 assert_response :success
774 774
775 775 assert_tag :tag => 'a',
776 776 :content => /Quote/
777 777
778 778 assert_tag :tag => 'form',
779 779 :descendant => { :tag => 'fieldset',
780 780 :child => { :tag => 'legend',
781 781 :content => /Change properties/ } },
782 782 :descendant => { :tag => 'fieldset',
783 783 :child => { :tag => 'legend',
784 784 :content => /Log time/ } },
785 785 :descendant => { :tag => 'fieldset',
786 786 :child => { :tag => 'legend',
787 787 :content => /Notes/ } }
788 788 end
789 789
790 790 def test_show_should_display_update_form
791 791 @request.session[:user_id] = 2
792 792 get :show, :id => 1
793 793 assert_response :success
794 794
795 795 assert_tag 'form', :attributes => {:id => 'issue-form'}
796 796 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
797 797 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
798 798 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
799 799 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
800 800 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
801 801 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
802 802 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
803 803 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
804 804 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
805 805 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
806 806 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
807 807 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
808 808 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
809 809 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
810 810 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
811 811 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
812 812 assert_tag 'textarea', :attributes => {:name => 'notes'}
813 813 end
814 814
815 815 def test_show_should_display_update_form_with_minimal_permissions
816 816 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
817 817 Workflow.delete_all :role_id => 1
818 818
819 819 @request.session[:user_id] = 2
820 820 get :show, :id => 1
821 821 assert_response :success
822 822
823 823 assert_tag 'form', :attributes => {:id => 'issue-form'}
824 824 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
825 825 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
826 826 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
827 827 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
828 828 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
829 829 assert_no_tag 'select', :attributes => {:name => 'issue[status_id]'}
830 830 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
831 831 assert_no_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
832 832 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
833 833 assert_no_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
834 834 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
835 835 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
836 836 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
837 837 assert_no_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
838 838 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
839 839 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
840 840 assert_tag 'textarea', :attributes => {:name => 'notes'}
841 841 end
842 842
843 843 def test_show_should_display_update_form_with_workflow_permissions
844 844 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
845 845
846 846 @request.session[:user_id] = 2
847 847 get :show, :id => 1
848 848 assert_response :success
849 849
850 850 assert_tag 'form', :attributes => {:id => 'issue-form'}
851 851 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
852 852 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
853 853 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
854 854 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
855 855 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
856 856 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
857 857 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
858 858 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
859 859 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
860 860 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
861 861 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
862 862 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
863 863 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
864 864 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
865 865 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
866 866 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
867 867 assert_tag 'textarea', :attributes => {:name => 'notes'}
868 868 end
869 869
870 870 def test_show_should_not_display_update_form_without_permissions
871 871 Role.find(1).update_attribute :permissions, [:view_issues]
872 872
873 873 @request.session[:user_id] = 2
874 874 get :show, :id => 1
875 875 assert_response :success
876 876
877 877 assert_no_tag 'form', :attributes => {:id => 'issue-form'}
878 878 end
879 879
880 880 def test_update_form_should_not_display_inactive_enumerations
881 881 @request.session[:user_id] = 2
882 882 get :show, :id => 1
883 883 assert_response :success
884 884
885 885 assert ! IssuePriority.find(15).active?
886 886 assert_no_tag :option, :attributes => {:value => '15'},
887 887 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
888 888 end
889 889
890 890 def test_update_form_should_allow_attachment_upload
891 891 @request.session[:user_id] = 2
892 892 get :show, :id => 1
893 893
894 894 assert_tag :tag => 'form',
895 895 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
896 896 :descendant => {
897 897 :tag => 'input',
898 898 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
899 899 }
900 900 end
901 901
902 902 def test_show_should_deny_anonymous_access_without_permission
903 903 Role.anonymous.remove_permission!(:view_issues)
904 904 get :show, :id => 1
905 905 assert_response :redirect
906 906 end
907 907
908 908 def test_show_should_deny_anonymous_access_to_private_issue
909 909 Issue.update_all(["is_private = ?", true], "id = 1")
910 910 get :show, :id => 1
911 911 assert_response :redirect
912 912 end
913 913
914 914 def test_show_should_deny_non_member_access_without_permission
915 915 Role.non_member.remove_permission!(:view_issues)
916 916 @request.session[:user_id] = 9
917 917 get :show, :id => 1
918 918 assert_response 403
919 919 end
920 920
921 921 def test_show_should_deny_non_member_access_to_private_issue
922 922 Issue.update_all(["is_private = ?", true], "id = 1")
923 923 @request.session[:user_id] = 9
924 924 get :show, :id => 1
925 925 assert_response 403
926 926 end
927 927
928 928 def test_show_should_deny_member_access_without_permission
929 929 Role.find(1).remove_permission!(:view_issues)
930 930 @request.session[:user_id] = 2
931 931 get :show, :id => 1
932 932 assert_response 403
933 933 end
934 934
935 935 def test_show_should_deny_member_access_to_private_issue_without_permission
936 936 Issue.update_all(["is_private = ?", true], "id = 1")
937 937 @request.session[:user_id] = 3
938 938 get :show, :id => 1
939 939 assert_response 403
940 940 end
941 941
942 942 def test_show_should_allow_author_access_to_private_issue
943 943 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
944 944 @request.session[:user_id] = 3
945 945 get :show, :id => 1
946 946 assert_response :success
947 947 end
948 948
949 949 def test_show_should_allow_assignee_access_to_private_issue
950 950 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
951 951 @request.session[:user_id] = 3
952 952 get :show, :id => 1
953 953 assert_response :success
954 954 end
955 955
956 956 def test_show_should_allow_member_access_to_private_issue_with_permission
957 957 Issue.update_all(["is_private = ?", true], "id = 1")
958 958 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
959 959 @request.session[:user_id] = 3
960 960 get :show, :id => 1
961 961 assert_response :success
962 962 end
963 963
964 964 def test_show_should_not_disclose_relations_to_invisible_issues
965 965 Setting.cross_project_issue_relations = '1'
966 966 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
967 967 # Relation to a private project issue
968 968 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
969 969
970 970 get :show, :id => 1
971 971 assert_response :success
972 972
973 973 assert_tag :div, :attributes => { :id => 'relations' },
974 974 :descendant => { :tag => 'a', :content => /#2$/ }
975 975 assert_no_tag :div, :attributes => { :id => 'relations' },
976 976 :descendant => { :tag => 'a', :content => /#4$/ }
977 977 end
978 978
979 979 def test_show_should_list_subtasks
980 980 Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
981 981
982 982 get :show, :id => 1
983 983 assert_response :success
984 984 assert_tag 'div', :attributes => {:id => 'issue_tree'},
985 985 :descendant => {:tag => 'td', :content => /Child Issue/, :attributes => {:class => /subject/}}
986 986 end
987 987
988 988 def test_show_should_list_parents
989 989 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
990 990
991 991 get :show, :id => issue.id
992 992 assert_response :success
993 993 assert_tag 'div', :attributes => {:class => 'subject'},
994 994 :descendant => {:tag => 'h3', :content => 'Child Issue'}
995 995 assert_tag 'div', :attributes => {:class => 'subject'},
996 996 :descendant => {:tag => 'a', :attributes => {:href => '/issues/1'}}
997 997 end
998 998
999 999 def test_show_should_not_display_prev_next_links_without_query_in_session
1000 1000 get :show, :id => 1
1001 1001 assert_response :success
1002 1002 assert_nil assigns(:prev_issue_id)
1003 1003 assert_nil assigns(:next_issue_id)
1004 1004
1005 1005 assert_no_tag 'div', :attributes => {:class => /next-prev-links/}
1006 1006 end
1007 1007
1008 1008 def test_show_should_display_prev_next_links_with_query_in_session
1009 1009 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1010 1010 @request.session['issues_index_sort'] = 'id'
1011 1011
1012 1012 with_settings :display_subprojects_issues => '0' do
1013 1013 get :show, :id => 3
1014 1014 end
1015 1015
1016 1016 assert_response :success
1017 1017 # Previous and next issues for all projects
1018 1018 assert_equal 2, assigns(:prev_issue_id)
1019 1019 assert_equal 5, assigns(:next_issue_id)
1020 1020
1021 1021 assert_tag 'div', :attributes => {:class => /next-prev-links/}
1022 1022 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
1023 1023 assert_tag 'a', :attributes => {:href => '/issues/5'}, :content => /Next/
1024 1024
1025 1025 count = Issue.open.visible.count
1026 1026 assert_tag 'span', :attributes => {:class => 'position'}, :content => "3 of #{count}"
1027 1027 end
1028 1028
1029 1029 def test_show_should_display_prev_next_links_with_saved_query_in_session
1030 1030 query = Query.create!(:name => 'test', :is_public => true, :user_id => 1,
1031 1031 :filters => {'status_id' => {:values => ['5'], :operator => '='}},
1032 1032 :sort_criteria => [['id', 'asc']])
1033 1033 @request.session[:query] = {:id => query.id, :project_id => nil}
1034 1034
1035 1035 get :show, :id => 11
1036 1036
1037 1037 assert_response :success
1038 1038 assert_equal query, assigns(:query)
1039 1039 # Previous and next issues for all projects
1040 1040 assert_equal 8, assigns(:prev_issue_id)
1041 1041 assert_equal 12, assigns(:next_issue_id)
1042 1042
1043 1043 assert_tag 'a', :attributes => {:href => '/issues/8'}, :content => /Previous/
1044 1044 assert_tag 'a', :attributes => {:href => '/issues/12'}, :content => /Next/
1045 1045 end
1046 1046
1047 1047 def test_show_should_display_prev_next_links_with_query_and_sort_on_association
1048 1048 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1049 1049
1050 1050 %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort|
1051 1051 @request.session['issues_index_sort'] = assoc_sort
1052 1052
1053 1053 get :show, :id => 3
1054 1054 assert_response :success, "Wrong response status for #{assoc_sort} sort"
1055 1055
1056 1056 assert_tag 'div', :attributes => {:class => /next-prev-links/}, :content => /Previous/
1057 1057 assert_tag 'div', :attributes => {:class => /next-prev-links/}, :content => /Next/
1058 1058 end
1059 1059 end
1060 1060
1061 1061 def test_show_should_display_prev_next_links_with_project_query_in_session
1062 1062 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1063 1063 @request.session['issues_index_sort'] = 'id'
1064 1064
1065 1065 with_settings :display_subprojects_issues => '0' do
1066 1066 get :show, :id => 3
1067 1067 end
1068 1068
1069 1069 assert_response :success
1070 1070 # Previous and next issues inside project
1071 1071 assert_equal 2, assigns(:prev_issue_id)
1072 1072 assert_equal 7, assigns(:next_issue_id)
1073 1073
1074 1074 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
1075 1075 assert_tag 'a', :attributes => {:href => '/issues/7'}, :content => /Next/
1076 1076 end
1077 1077
1078 1078 def test_show_should_not_display_prev_link_for_first_issue
1079 1079 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1080 1080 @request.session['issues_index_sort'] = 'id'
1081 1081
1082 1082 with_settings :display_subprojects_issues => '0' do
1083 1083 get :show, :id => 1
1084 1084 end
1085 1085
1086 1086 assert_response :success
1087 1087 assert_nil assigns(:prev_issue_id)
1088 1088 assert_equal 2, assigns(:next_issue_id)
1089 1089
1090 1090 assert_no_tag 'a', :content => /Previous/
1091 1091 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Next/
1092 1092 end
1093 1093
1094 1094 def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
1095 1095 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
1096 1096 @request.session['issues_index_sort'] = 'id'
1097 1097
1098 1098 get :show, :id => 1
1099 1099
1100 1100 assert_response :success
1101 1101 assert_nil assigns(:prev_issue_id)
1102 1102 assert_nil assigns(:next_issue_id)
1103 1103
1104 1104 assert_no_tag 'a', :content => /Previous/
1105 1105 assert_no_tag 'a', :content => /Next/
1106 1106 end
1107 1107
1108 1108 def test_show_should_display_visible_changesets_from_other_projects
1109 1109 project = Project.find(2)
1110 1110 issue = project.issues.first
1111 1111 issue.changeset_ids = [102]
1112 1112 issue.save!
1113 1113 project.disable_module! :repository
1114 1114
1115 1115 @request.session[:user_id] = 2
1116 1116 get :show, :id => issue.id
1117 1117 assert_tag 'a', :attributes => {:href => "/projects/ecookbook/repository/revisions/3"}
1118 1118 end
1119 1119
1120 1120 def test_show_with_multi_custom_field
1121 1121 field = CustomField.find(1)
1122 1122 field.update_attribute :multiple, true
1123 1123 issue = Issue.find(1)
1124 1124 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1125 1125 issue.save!
1126 1126
1127 1127 get :show, :id => 1
1128 1128 assert_response :success
1129 1129
1130 1130 assert_tag :td, :content => 'MySQL, Oracle'
1131 1131 end
1132 1132
1133 1133 def test_show_with_multi_user_custom_field
1134 1134 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1135 1135 :tracker_ids => [1], :is_for_all => true)
1136 1136 issue = Issue.find(1)
1137 1137 issue.custom_field_values = {field.id => ['2', '3']}
1138 1138 issue.save!
1139 1139
1140 1140 get :show, :id => 1
1141 1141 assert_response :success
1142 1142
1143 1143 # TODO: should display links
1144 1144 assert_tag :td, :content => 'Dave Lopper, John Smith'
1145 1145 end
1146 1146
1147 1147 def test_show_atom
1148 1148 get :show, :id => 2, :format => 'atom'
1149 1149 assert_response :success
1150 1150 assert_template 'journals/index'
1151 1151 # Inline image
1152 1152 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
1153 1153 end
1154 1154
1155 1155 def test_show_export_to_pdf
1156 1156 get :show, :id => 3, :format => 'pdf'
1157 1157 assert_response :success
1158 1158 assert_equal 'application/pdf', @response.content_type
1159 1159 assert @response.body.starts_with?('%PDF')
1160 1160 assert_not_nil assigns(:issue)
1161 1161 end
1162 1162
1163 1163 def test_get_new
1164 1164 @request.session[:user_id] = 2
1165 1165 get :new, :project_id => 1, :tracker_id => 1
1166 1166 assert_response :success
1167 1167 assert_template 'new'
1168 1168
1169 1169 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
1170 1170 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1171 1171 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1172 1172 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1173 1173 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1174 1174 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1175 1175 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1176 1176 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1177 1177 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1178 1178 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1179 1179 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1180 1180 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1181 1181 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1182 1182 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1183 1183 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1184 1184 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1185 1185
1186 1186 # Be sure we don't display inactive IssuePriorities
1187 1187 assert ! IssuePriority.find(15).active?
1188 1188 assert_no_tag :option, :attributes => {:value => '15'},
1189 1189 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1190 1190 end
1191 1191
1192 1192 def test_get_new_with_minimal_permissions
1193 1193 Role.find(1).update_attribute :permissions, [:add_issues]
1194 1194 Workflow.delete_all :role_id => 1
1195 1195
1196 1196 @request.session[:user_id] = 2
1197 1197 get :new, :project_id => 1, :tracker_id => 1
1198 1198 assert_response :success
1199 1199 assert_template 'new'
1200 1200
1201 1201 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
1202 1202 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1203 1203 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1204 1204 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1205 1205 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1206 1206 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1207 1207 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1208 1208 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1209 1209 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1210 1210 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1211 1211 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1212 1212 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1213 1213 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1214 1214 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1215 1215 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1216 1216 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1217 1217 end
1218 1218
1219 1219 def test_get_new_with_list_custom_field
1220 1220 @request.session[:user_id] = 2
1221 1221 get :new, :project_id => 1, :tracker_id => 1
1222 1222 assert_response :success
1223 1223 assert_template 'new'
1224 1224
1225 1225 assert_tag 'select',
1226 1226 :attributes => {:name => 'issue[custom_field_values][1]'},
1227 1227 :children => {:count => 4},
1228 1228 :child => {:tag => 'option', :attributes => {:value => 'MySQL'}, :content => 'MySQL'}
1229 1229 end
1230 1230
1231 1231 def test_get_new_with_multi_custom_field
1232 1232 field = IssueCustomField.find(1)
1233 1233 field.update_attribute :multiple, true
1234 1234
1235 1235 @request.session[:user_id] = 2
1236 1236 get :new, :project_id => 1, :tracker_id => 1
1237 1237 assert_response :success
1238 1238 assert_template 'new'
1239 1239
1240 1240 assert_tag 'select',
1241 1241 :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'},
1242 1242 :children => {:count => 3},
1243 1243 :child => {:tag => 'option', :attributes => {:value => 'MySQL'}, :content => 'MySQL'}
1244 1244 assert_tag 'input',
1245 1245 :attributes => {:name => 'issue[custom_field_values][1][]', :value => ''}
1246 1246 end
1247 1247
1248 1248 def test_get_new_with_multi_user_custom_field
1249 1249 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1250 1250 :tracker_ids => [1], :is_for_all => true)
1251 1251
1252 1252 @request.session[:user_id] = 2
1253 1253 get :new, :project_id => 1, :tracker_id => 1
1254 1254 assert_response :success
1255 1255 assert_template 'new'
1256 1256
1257 1257 assert_tag 'select',
1258 1258 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :multiple => 'multiple'},
1259 1259 :children => {:count => Project.find(1).users.count},
1260 1260 :child => {:tag => 'option', :attributes => {:value => '2'}, :content => 'John Smith'}
1261 1261 assert_tag 'input',
1262 1262 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :value => ''}
1263 1263 end
1264 1264
1265 1265 def test_get_new_without_default_start_date_is_creation_date
1266 1266 Setting.default_issue_start_date_to_creation_date = 0
1267 1267
1268 1268 @request.session[:user_id] = 2
1269 1269 get :new, :project_id => 1, :tracker_id => 1
1270 1270 assert_response :success
1271 1271 assert_template 'new'
1272 1272
1273 1273 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1274 1274 :value => nil }
1275 1275 end
1276 1276
1277 1277 def test_get_new_with_default_start_date_is_creation_date
1278 1278 Setting.default_issue_start_date_to_creation_date = 1
1279 1279
1280 1280 @request.session[:user_id] = 2
1281 1281 get :new, :project_id => 1, :tracker_id => 1
1282 1282 assert_response :success
1283 1283 assert_template 'new'
1284 1284
1285 1285 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1286 1286 :value => Date.today.to_s }
1287 1287 end
1288 1288
1289 1289 def test_get_new_form_should_allow_attachment_upload
1290 1290 @request.session[:user_id] = 2
1291 1291 get :new, :project_id => 1, :tracker_id => 1
1292 1292
1293 1293 assert_tag :tag => 'form',
1294 1294 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
1295 1295 :descendant => {
1296 1296 :tag => 'input',
1297 1297 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
1298 1298 }
1299 1299 end
1300 1300
1301 1301 def test_get_new_should_prefill_the_form_from_params
1302 1302 @request.session[:user_id] = 2
1303 1303 get :new, :project_id => 1,
1304 1304 :issue => {:tracker_id => 3, :description => 'Prefilled', :custom_field_values => {'2' => 'Custom field value'}}
1305 1305
1306 1306 issue = assigns(:issue)
1307 1307 assert_equal 3, issue.tracker_id
1308 1308 assert_equal 'Prefilled', issue.description
1309 1309 assert_equal 'Custom field value', issue.custom_field_value(2)
1310 1310
1311 1311 assert_tag 'select',
1312 1312 :attributes => {:name => 'issue[tracker_id]'},
1313 1313 :child => {:tag => 'option', :attributes => {:value => '3', :selected => 'selected'}}
1314 1314 assert_tag 'textarea',
1315 1315 :attributes => {:name => 'issue[description]'}, :content => 'Prefilled'
1316 1316 assert_tag 'input',
1317 1317 :attributes => {:name => 'issue[custom_field_values][2]', :value => 'Custom field value'}
1318 1318 end
1319 1319
1320 1320 def test_get_new_without_tracker_id
1321 1321 @request.session[:user_id] = 2
1322 1322 get :new, :project_id => 1
1323 1323 assert_response :success
1324 1324 assert_template 'new'
1325 1325
1326 1326 issue = assigns(:issue)
1327 1327 assert_not_nil issue
1328 1328 assert_equal Project.find(1).trackers.first, issue.tracker
1329 1329 end
1330 1330
1331 1331 def test_get_new_with_no_default_status_should_display_an_error
1332 1332 @request.session[:user_id] = 2
1333 1333 IssueStatus.delete_all
1334 1334
1335 1335 get :new, :project_id => 1
1336 1336 assert_response 500
1337 1337 assert_error_tag :content => /No default issue/
1338 1338 end
1339 1339
1340 1340 def test_get_new_with_no_tracker_should_display_an_error
1341 1341 @request.session[:user_id] = 2
1342 1342 Tracker.delete_all
1343 1343
1344 1344 get :new, :project_id => 1
1345 1345 assert_response 500
1346 1346 assert_error_tag :content => /No tracker/
1347 1347 end
1348 1348
1349 1349 def test_update_new_form
1350 1350 @request.session[:user_id] = 2
1351 1351 xhr :post, :new, :project_id => 1,
1352 1352 :issue => {:tracker_id => 2,
1353 1353 :subject => 'This is the test_new issue',
1354 1354 :description => 'This is the description',
1355 1355 :priority_id => 5}
1356 1356 assert_response :success
1357 1357 assert_template 'attributes'
1358 1358
1359 1359 issue = assigns(:issue)
1360 1360 assert_kind_of Issue, issue
1361 1361 assert_equal 1, issue.project_id
1362 1362 assert_equal 2, issue.tracker_id
1363 1363 assert_equal 'This is the test_new issue', issue.subject
1364 1364 end
1365 1365
1366 1366 def test_post_create
1367 1367 @request.session[:user_id] = 2
1368 1368 assert_difference 'Issue.count' do
1369 1369 post :create, :project_id => 1,
1370 1370 :issue => {:tracker_id => 3,
1371 1371 :status_id => 2,
1372 1372 :subject => 'This is the test_new issue',
1373 1373 :description => 'This is the description',
1374 1374 :priority_id => 5,
1375 1375 :start_date => '2010-11-07',
1376 1376 :estimated_hours => '',
1377 1377 :custom_field_values => {'2' => 'Value for field 2'}}
1378 1378 end
1379 1379 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1380 1380
1381 1381 issue = Issue.find_by_subject('This is the test_new issue')
1382 1382 assert_not_nil issue
1383 1383 assert_equal 2, issue.author_id
1384 1384 assert_equal 3, issue.tracker_id
1385 1385 assert_equal 2, issue.status_id
1386 1386 assert_equal Date.parse('2010-11-07'), issue.start_date
1387 1387 assert_nil issue.estimated_hours
1388 1388 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
1389 1389 assert_not_nil v
1390 1390 assert_equal 'Value for field 2', v.value
1391 1391 end
1392 1392
1393 1393 def test_post_new_with_group_assignment
1394 1394 group = Group.find(11)
1395 1395 project = Project.find(1)
1396 1396 project.members << Member.new(:principal => group, :roles => [Role.givable.first])
1397 1397
1398 1398 with_settings :issue_group_assignment => '1' do
1399 1399 @request.session[:user_id] = 2
1400 1400 assert_difference 'Issue.count' do
1401 1401 post :create, :project_id => project.id,
1402 1402 :issue => {:tracker_id => 3,
1403 1403 :status_id => 1,
1404 1404 :subject => 'This is the test_new_with_group_assignment issue',
1405 1405 :assigned_to_id => group.id}
1406 1406 end
1407 1407 end
1408 1408 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1409 1409
1410 1410 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
1411 1411 assert_not_nil issue
1412 1412 assert_equal group, issue.assigned_to
1413 1413 end
1414 1414
1415 1415 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
1416 1416 Setting.default_issue_start_date_to_creation_date = 0
1417 1417
1418 1418 @request.session[:user_id] = 2
1419 1419 assert_difference 'Issue.count' do
1420 1420 post :create, :project_id => 1,
1421 1421 :issue => {:tracker_id => 3,
1422 1422 :status_id => 2,
1423 1423 :subject => 'This is the test_new issue',
1424 1424 :description => 'This is the description',
1425 1425 :priority_id => 5,
1426 1426 :estimated_hours => '',
1427 1427 :custom_field_values => {'2' => 'Value for field 2'}}
1428 1428 end
1429 1429 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1430 1430
1431 1431 issue = Issue.find_by_subject('This is the test_new issue')
1432 1432 assert_not_nil issue
1433 1433 assert_nil issue.start_date
1434 1434 end
1435 1435
1436 1436 def test_post_create_without_start_date_and_default_start_date_is_creation_date
1437 1437 Setting.default_issue_start_date_to_creation_date = 1
1438 1438
1439 1439 @request.session[:user_id] = 2
1440 1440 assert_difference 'Issue.count' do
1441 1441 post :create, :project_id => 1,
1442 1442 :issue => {:tracker_id => 3,
1443 1443 :status_id => 2,
1444 1444 :subject => 'This is the test_new issue',
1445 1445 :description => 'This is the description',
1446 1446 :priority_id => 5,
1447 1447 :estimated_hours => '',
1448 1448 :custom_field_values => {'2' => 'Value for field 2'}}
1449 1449 end
1450 1450 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1451 1451
1452 1452 issue = Issue.find_by_subject('This is the test_new issue')
1453 1453 assert_not_nil issue
1454 1454 assert_equal Date.today, issue.start_date
1455 1455 end
1456 1456
1457 1457 def test_post_create_and_continue
1458 1458 @request.session[:user_id] = 2
1459 1459 assert_difference 'Issue.count' do
1460 1460 post :create, :project_id => 1,
1461 1461 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
1462 1462 :continue => ''
1463 1463 end
1464 1464
1465 1465 issue = Issue.first(:order => 'id DESC')
1466 1466 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
1467 1467 assert_not_nil flash[:notice], "flash was not set"
1468 1468 assert flash[:notice].include?("<a href='/issues/#{issue.id}'>##{issue.id}</a>"), "issue link not found in flash: #{flash[:notice]}"
1469 1469 end
1470 1470
1471 1471 def test_post_create_without_custom_fields_param
1472 1472 @request.session[:user_id] = 2
1473 1473 assert_difference 'Issue.count' do
1474 1474 post :create, :project_id => 1,
1475 1475 :issue => {:tracker_id => 1,
1476 1476 :subject => 'This is the test_new issue',
1477 1477 :description => 'This is the description',
1478 1478 :priority_id => 5}
1479 1479 end
1480 1480 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1481 1481 end
1482 1482
1483 1483 def test_post_create_with_multi_custom_field
1484 1484 field = IssueCustomField.find_by_name('Database')
1485 1485 field.update_attribute(:multiple, true)
1486 1486
1487 1487 @request.session[:user_id] = 2
1488 1488 assert_difference 'Issue.count' do
1489 1489 post :create, :project_id => 1,
1490 1490 :issue => {:tracker_id => 1,
1491 1491 :subject => 'This is the test_new issue',
1492 1492 :description => 'This is the description',
1493 1493 :priority_id => 5,
1494 1494 :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
1495 1495 end
1496 1496 assert_response 302
1497 1497 issue = Issue.first(:order => 'id DESC')
1498 1498 assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
1499 1499 end
1500 1500
1501 1501 def test_post_create_with_empty_multi_custom_field
1502 1502 field = IssueCustomField.find_by_name('Database')
1503 1503 field.update_attribute(:multiple, true)
1504 1504
1505 1505 @request.session[:user_id] = 2
1506 1506 assert_difference 'Issue.count' do
1507 1507 post :create, :project_id => 1,
1508 1508 :issue => {:tracker_id => 1,
1509 1509 :subject => 'This is the test_new issue',
1510 1510 :description => 'This is the description',
1511 1511 :priority_id => 5,
1512 1512 :custom_field_values => {'1' => ['']}}
1513 1513 end
1514 1514 assert_response 302
1515 1515 issue = Issue.first(:order => 'id DESC')
1516 1516 assert_equal [''], issue.custom_field_value(1).sort
1517 1517 end
1518 1518
1519 1519 def test_post_create_with_multi_user_custom_field
1520 1520 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1521 1521 :tracker_ids => [1], :is_for_all => true)
1522 1522
1523 1523 @request.session[:user_id] = 2
1524 1524 assert_difference 'Issue.count' do
1525 1525 post :create, :project_id => 1,
1526 1526 :issue => {:tracker_id => 1,
1527 1527 :subject => 'This is the test_new issue',
1528 1528 :description => 'This is the description',
1529 1529 :priority_id => 5,
1530 1530 :custom_field_values => {field.id.to_s => ['', '2', '3']}}
1531 1531 end
1532 1532 assert_response 302
1533 1533 issue = Issue.first(:order => 'id DESC')
1534 1534 assert_equal ['2', '3'], issue.custom_field_value(field).sort
1535 1535 end
1536 1536
1537 1537 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1538 1538 field = IssueCustomField.find_by_name('Database')
1539 1539 field.update_attribute(:is_required, true)
1540 1540
1541 1541 @request.session[:user_id] = 2
1542 1542 assert_no_difference 'Issue.count' do
1543 1543 post :create, :project_id => 1,
1544 1544 :issue => {:tracker_id => 1,
1545 1545 :subject => 'This is the test_new issue',
1546 1546 :description => 'This is the description',
1547 1547 :priority_id => 5}
1548 1548 end
1549 1549 assert_response :success
1550 1550 assert_template 'new'
1551 1551 issue = assigns(:issue)
1552 1552 assert_not_nil issue
1553 1553 assert_error_tag :content => /Database can't be blank/
1554 1554 end
1555 1555
1556 1556 def test_post_create_with_watchers
1557 1557 @request.session[:user_id] = 2
1558 1558 ActionMailer::Base.deliveries.clear
1559 1559
1560 1560 assert_difference 'Watcher.count', 2 do
1561 1561 post :create, :project_id => 1,
1562 1562 :issue => {:tracker_id => 1,
1563 1563 :subject => 'This is a new issue with watchers',
1564 1564 :description => 'This is the description',
1565 1565 :priority_id => 5,
1566 1566 :watcher_user_ids => ['2', '3']}
1567 1567 end
1568 1568 issue = Issue.find_by_subject('This is a new issue with watchers')
1569 1569 assert_not_nil issue
1570 1570 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1571 1571
1572 1572 # Watchers added
1573 1573 assert_equal [2, 3], issue.watcher_user_ids.sort
1574 1574 assert issue.watched_by?(User.find(3))
1575 1575 # Watchers notified
1576 1576 mail = ActionMailer::Base.deliveries.last
1577 1577 assert_not_nil mail
1578 1578 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
1579 1579 end
1580 1580
1581 1581 def test_post_create_subissue
1582 1582 @request.session[:user_id] = 2
1583 1583
1584 1584 assert_difference 'Issue.count' do
1585 1585 post :create, :project_id => 1,
1586 1586 :issue => {:tracker_id => 1,
1587 1587 :subject => 'This is a child issue',
1588 1588 :parent_issue_id => 2}
1589 1589 end
1590 1590 issue = Issue.find_by_subject('This is a child issue')
1591 1591 assert_not_nil issue
1592 1592 assert_equal Issue.find(2), issue.parent
1593 1593 end
1594 1594
1595 1595 def test_post_create_subissue_with_non_numeric_parent_id
1596 1596 @request.session[:user_id] = 2
1597 1597
1598 1598 assert_difference 'Issue.count' do
1599 1599 post :create, :project_id => 1,
1600 1600 :issue => {:tracker_id => 1,
1601 1601 :subject => 'This is a child issue',
1602 1602 :parent_issue_id => 'ABC'}
1603 1603 end
1604 1604 issue = Issue.find_by_subject('This is a child issue')
1605 1605 assert_not_nil issue
1606 1606 assert_nil issue.parent
1607 1607 end
1608 1608
1609 1609 def test_post_create_private
1610 1610 @request.session[:user_id] = 2
1611 1611
1612 1612 assert_difference 'Issue.count' do
1613 1613 post :create, :project_id => 1,
1614 1614 :issue => {:tracker_id => 1,
1615 1615 :subject => 'This is a private issue',
1616 1616 :is_private => '1'}
1617 1617 end
1618 1618 issue = Issue.first(:order => 'id DESC')
1619 1619 assert issue.is_private?
1620 1620 end
1621 1621
1622 1622 def test_post_create_private_with_set_own_issues_private_permission
1623 1623 role = Role.find(1)
1624 1624 role.remove_permission! :set_issues_private
1625 1625 role.add_permission! :set_own_issues_private
1626 1626
1627 1627 @request.session[:user_id] = 2
1628 1628
1629 1629 assert_difference 'Issue.count' do
1630 1630 post :create, :project_id => 1,
1631 1631 :issue => {:tracker_id => 1,
1632 1632 :subject => 'This is a private issue',
1633 1633 :is_private => '1'}
1634 1634 end
1635 1635 issue = Issue.first(:order => 'id DESC')
1636 1636 assert issue.is_private?
1637 1637 end
1638 1638
1639 1639 def test_post_create_should_send_a_notification
1640 1640 ActionMailer::Base.deliveries.clear
1641 1641 @request.session[:user_id] = 2
1642 1642 assert_difference 'Issue.count' do
1643 1643 post :create, :project_id => 1,
1644 1644 :issue => {:tracker_id => 3,
1645 1645 :subject => 'This is the test_new issue',
1646 1646 :description => 'This is the description',
1647 1647 :priority_id => 5,
1648 1648 :estimated_hours => '',
1649 1649 :custom_field_values => {'2' => 'Value for field 2'}}
1650 1650 end
1651 1651 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1652 1652
1653 1653 assert_equal 1, ActionMailer::Base.deliveries.size
1654 1654 end
1655 1655
1656 1656 def test_post_create_should_preserve_fields_values_on_validation_failure
1657 1657 @request.session[:user_id] = 2
1658 1658 post :create, :project_id => 1,
1659 1659 :issue => {:tracker_id => 1,
1660 1660 # empty subject
1661 1661 :subject => '',
1662 1662 :description => 'This is a description',
1663 1663 :priority_id => 6,
1664 1664 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
1665 1665 assert_response :success
1666 1666 assert_template 'new'
1667 1667
1668 1668 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
1669 1669 :content => 'This is a description'
1670 1670 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1671 1671 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1672 1672 :value => '6' },
1673 1673 :content => 'High' }
1674 1674 # Custom fields
1675 1675 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
1676 1676 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1677 1677 :value => 'Oracle' },
1678 1678 :content => 'Oracle' }
1679 1679 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
1680 1680 :value => 'Value for field 2'}
1681 1681 end
1682 1682
1683 def test_post_create_with_failure_should_preserve_watchers
1684 assert !User.find(8).member_of?(Project.find(1))
1685
1686 @request.session[:user_id] = 2
1687 post :create, :project_id => 1,
1688 :issue => {:tracker_id => 1,
1689 :watcher_user_ids => ['3', '8']}
1690 assert_response :success
1691 assert_template 'new'
1692
1693 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '2', :checked => nil}
1694 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '3', :checked => 'checked'}
1695 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '8', :checked => 'checked'}
1696 end
1697
1683 1698 def test_post_create_should_ignore_non_safe_attributes
1684 1699 @request.session[:user_id] = 2
1685 1700 assert_nothing_raised do
1686 1701 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
1687 1702 end
1688 1703 end
1689 1704
1690 1705 def test_post_create_with_attachment
1691 1706 set_tmp_attachments_directory
1692 1707 @request.session[:user_id] = 2
1693 1708
1694 1709 assert_difference 'Issue.count' do
1695 1710 assert_difference 'Attachment.count' do
1696 1711 post :create, :project_id => 1,
1697 1712 :issue => { :tracker_id => '1', :subject => 'With attachment' },
1698 1713 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1699 1714 end
1700 1715 end
1701 1716
1702 1717 issue = Issue.first(:order => 'id DESC')
1703 1718 attachment = Attachment.first(:order => 'id DESC')
1704 1719
1705 1720 assert_equal issue, attachment.container
1706 1721 assert_equal 2, attachment.author_id
1707 1722 assert_equal 'testfile.txt', attachment.filename
1708 1723 assert_equal 'text/plain', attachment.content_type
1709 1724 assert_equal 'test file', attachment.description
1710 1725 assert_equal 59, attachment.filesize
1711 1726 assert File.exists?(attachment.diskfile)
1712 1727 assert_equal 59, File.size(attachment.diskfile)
1713 1728 end
1714 1729
1715 1730 def test_post_create_with_failure_should_save_attachments
1716 1731 set_tmp_attachments_directory
1717 1732 @request.session[:user_id] = 2
1718 1733
1719 1734 assert_no_difference 'Issue.count' do
1720 1735 assert_difference 'Attachment.count' do
1721 1736 post :create, :project_id => 1,
1722 1737 :issue => { :tracker_id => '1', :subject => '' },
1723 1738 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1724 1739 assert_response :success
1725 1740 assert_template 'new'
1726 1741 end
1727 1742 end
1728 1743
1729 1744 attachment = Attachment.first(:order => 'id DESC')
1730 1745 assert_equal 'testfile.txt', attachment.filename
1731 1746 assert File.exists?(attachment.diskfile)
1732 1747 assert_nil attachment.container
1733 1748
1734 1749 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
1735 1750 assert_tag 'span', :content => /testfile.txt/
1736 1751 end
1737 1752
1738 1753 def test_post_create_with_failure_should_keep_saved_attachments
1739 1754 set_tmp_attachments_directory
1740 1755 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
1741 1756 @request.session[:user_id] = 2
1742 1757
1743 1758 assert_no_difference 'Issue.count' do
1744 1759 assert_no_difference 'Attachment.count' do
1745 1760 post :create, :project_id => 1,
1746 1761 :issue => { :tracker_id => '1', :subject => '' },
1747 1762 :attachments => {'p0' => {'token' => attachment.token}}
1748 1763 assert_response :success
1749 1764 assert_template 'new'
1750 1765 end
1751 1766 end
1752 1767
1753 1768 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
1754 1769 assert_tag 'span', :content => /testfile.txt/
1755 1770 end
1756 1771
1757 1772 def test_post_create_should_attach_saved_attachments
1758 1773 set_tmp_attachments_directory
1759 1774 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
1760 1775 @request.session[:user_id] = 2
1761 1776
1762 1777 assert_difference 'Issue.count' do
1763 1778 assert_no_difference 'Attachment.count' do
1764 1779 post :create, :project_id => 1,
1765 1780 :issue => { :tracker_id => '1', :subject => 'Saved attachments' },
1766 1781 :attachments => {'p0' => {'token' => attachment.token}}
1767 1782 assert_response 302
1768 1783 end
1769 1784 end
1770 1785
1771 1786 issue = Issue.first(:order => 'id DESC')
1772 1787 assert_equal 1, issue.attachments.count
1773 1788
1774 1789 attachment.reload
1775 1790 assert_equal issue, attachment.container
1776 1791 end
1777 1792
1778 1793 context "without workflow privilege" do
1779 1794 setup do
1780 1795 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1781 1796 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1782 1797 end
1783 1798
1784 1799 context "#new" do
1785 1800 should "propose default status only" do
1786 1801 get :new, :project_id => 1
1787 1802 assert_response :success
1788 1803 assert_template 'new'
1789 1804 assert_tag :tag => 'select',
1790 1805 :attributes => {:name => 'issue[status_id]'},
1791 1806 :children => {:count => 1},
1792 1807 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
1793 1808 end
1794 1809
1795 1810 should "accept default status" do
1796 1811 assert_difference 'Issue.count' do
1797 1812 post :create, :project_id => 1,
1798 1813 :issue => {:tracker_id => 1,
1799 1814 :subject => 'This is an issue',
1800 1815 :status_id => 1}
1801 1816 end
1802 1817 issue = Issue.last(:order => 'id')
1803 1818 assert_equal IssueStatus.default, issue.status
1804 1819 end
1805 1820
1806 1821 should "ignore unauthorized status" do
1807 1822 assert_difference 'Issue.count' do
1808 1823 post :create, :project_id => 1,
1809 1824 :issue => {:tracker_id => 1,
1810 1825 :subject => 'This is an issue',
1811 1826 :status_id => 3}
1812 1827 end
1813 1828 issue = Issue.last(:order => 'id')
1814 1829 assert_equal IssueStatus.default, issue.status
1815 1830 end
1816 1831 end
1817 1832
1818 1833 context "#update" do
1819 1834 should "ignore status change" do
1820 1835 assert_difference 'Journal.count' do
1821 1836 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1822 1837 end
1823 1838 assert_equal 1, Issue.find(1).status_id
1824 1839 end
1825 1840
1826 1841 should "ignore attributes changes" do
1827 1842 assert_difference 'Journal.count' do
1828 1843 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1829 1844 end
1830 1845 issue = Issue.find(1)
1831 1846 assert_equal "Can't print recipes", issue.subject
1832 1847 assert_nil issue.assigned_to
1833 1848 end
1834 1849 end
1835 1850 end
1836 1851
1837 1852 context "with workflow privilege" do
1838 1853 setup do
1839 1854 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1840 1855 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
1841 1856 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
1842 1857 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1843 1858 end
1844 1859
1845 1860 context "#update" do
1846 1861 should "accept authorized status" do
1847 1862 assert_difference 'Journal.count' do
1848 1863 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1849 1864 end
1850 1865 assert_equal 3, Issue.find(1).status_id
1851 1866 end
1852 1867
1853 1868 should "ignore unauthorized status" do
1854 1869 assert_difference 'Journal.count' do
1855 1870 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1856 1871 end
1857 1872 assert_equal 1, Issue.find(1).status_id
1858 1873 end
1859 1874
1860 1875 should "accept authorized attributes changes" do
1861 1876 assert_difference 'Journal.count' do
1862 1877 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
1863 1878 end
1864 1879 issue = Issue.find(1)
1865 1880 assert_equal 2, issue.assigned_to_id
1866 1881 end
1867 1882
1868 1883 should "ignore unauthorized attributes changes" do
1869 1884 assert_difference 'Journal.count' do
1870 1885 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
1871 1886 end
1872 1887 issue = Issue.find(1)
1873 1888 assert_equal "Can't print recipes", issue.subject
1874 1889 end
1875 1890 end
1876 1891
1877 1892 context "and :edit_issues permission" do
1878 1893 setup do
1879 1894 Role.anonymous.add_permission! :add_issues, :edit_issues
1880 1895 end
1881 1896
1882 1897 should "accept authorized status" do
1883 1898 assert_difference 'Journal.count' do
1884 1899 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1885 1900 end
1886 1901 assert_equal 3, Issue.find(1).status_id
1887 1902 end
1888 1903
1889 1904 should "ignore unauthorized status" do
1890 1905 assert_difference 'Journal.count' do
1891 1906 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1892 1907 end
1893 1908 assert_equal 1, Issue.find(1).status_id
1894 1909 end
1895 1910
1896 1911 should "accept authorized attributes changes" do
1897 1912 assert_difference 'Journal.count' do
1898 1913 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1899 1914 end
1900 1915 issue = Issue.find(1)
1901 1916 assert_equal "changed", issue.subject
1902 1917 assert_equal 2, issue.assigned_to_id
1903 1918 end
1904 1919 end
1905 1920 end
1906 1921
1907 1922 def test_new_as_copy
1908 1923 @request.session[:user_id] = 2
1909 1924 get :new, :project_id => 1, :copy_from => 1
1910 1925
1911 1926 assert_response :success
1912 1927 assert_template 'new'
1913 1928
1914 1929 assert_not_nil assigns(:issue)
1915 1930 orig = Issue.find(1)
1916 1931 assert_equal 1, assigns(:issue).project_id
1917 1932 assert_equal orig.subject, assigns(:issue).subject
1918 1933 assert assigns(:issue).copy?
1919 1934
1920 1935 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
1921 1936 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
1922 1937 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1923 1938 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}, :content => 'eCookbook'}
1924 1939 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1925 1940 :child => {:tag => 'option', :attributes => {:value => '2', :selected => nil}, :content => 'OnlineStore'}
1926 1941 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
1927 1942 end
1928 1943
1929 1944 def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox
1930 1945 @request.session[:user_id] = 2
1931 1946 issue = Issue.find(3)
1932 1947 assert issue.attachments.count > 0
1933 1948 get :new, :project_id => 1, :copy_from => 3
1934 1949
1935 1950 assert_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
1936 1951 end
1937 1952
1938 1953 def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox
1939 1954 @request.session[:user_id] = 2
1940 1955 issue = Issue.find(3)
1941 1956 issue.attachments.delete_all
1942 1957 get :new, :project_id => 1, :copy_from => 3
1943 1958
1944 1959 assert_no_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
1945 1960 end
1946 1961
1947 1962 def test_new_as_copy_with_invalid_issue_should_respond_with_404
1948 1963 @request.session[:user_id] = 2
1949 1964 get :new, :project_id => 1, :copy_from => 99999
1950 1965 assert_response 404
1951 1966 end
1952 1967
1953 1968 def test_create_as_copy_on_different_project
1954 1969 @request.session[:user_id] = 2
1955 1970 assert_difference 'Issue.count' do
1956 1971 post :create, :project_id => 1, :copy_from => 1,
1957 1972 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
1958 1973
1959 1974 assert_not_nil assigns(:issue)
1960 1975 assert assigns(:issue).copy?
1961 1976 end
1962 1977 issue = Issue.first(:order => 'id DESC')
1963 1978 assert_redirected_to "/issues/#{issue.id}"
1964 1979
1965 1980 assert_equal 2, issue.project_id
1966 1981 assert_equal 3, issue.tracker_id
1967 1982 assert_equal 'Copy', issue.subject
1968 1983 end
1969 1984
1970 1985 def test_create_as_copy_should_copy_attachments
1971 1986 @request.session[:user_id] = 2
1972 1987 issue = Issue.find(3)
1973 1988 count = issue.attachments.count
1974 1989 assert count > 0
1975 1990
1976 1991 assert_difference 'Issue.count' do
1977 1992 assert_difference 'Attachment.count', count do
1978 1993 assert_no_difference 'Journal.count' do
1979 1994 post :create, :project_id => 1, :copy_from => 3,
1980 1995 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
1981 1996 :copy_attachments => '1'
1982 1997 end
1983 1998 end
1984 1999 end
1985 2000 copy = Issue.first(:order => 'id DESC')
1986 2001 assert_equal count, copy.attachments.count
1987 2002 assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort
1988 2003 end
1989 2004
1990 2005 def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments
1991 2006 @request.session[:user_id] = 2
1992 2007 issue = Issue.find(3)
1993 2008 count = issue.attachments.count
1994 2009 assert count > 0
1995 2010
1996 2011 assert_difference 'Issue.count' do
1997 2012 assert_no_difference 'Attachment.count' do
1998 2013 assert_no_difference 'Journal.count' do
1999 2014 post :create, :project_id => 1, :copy_from => 3,
2000 2015 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'}
2001 2016 end
2002 2017 end
2003 2018 end
2004 2019 copy = Issue.first(:order => 'id DESC')
2005 2020 assert_equal 0, copy.attachments.count
2006 2021 end
2007 2022
2008 2023 def test_create_as_copy_with_attachments_should_add_new_files
2009 2024 @request.session[:user_id] = 2
2010 2025 issue = Issue.find(3)
2011 2026 count = issue.attachments.count
2012 2027 assert count > 0
2013 2028
2014 2029 assert_difference 'Issue.count' do
2015 2030 assert_difference 'Attachment.count', count + 1 do
2016 2031 assert_no_difference 'Journal.count' do
2017 2032 post :create, :project_id => 1, :copy_from => 3,
2018 2033 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
2019 2034 :copy_attachments => '1',
2020 2035 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2021 2036 end
2022 2037 end
2023 2038 end
2024 2039 copy = Issue.first(:order => 'id DESC')
2025 2040 assert_equal count + 1, copy.attachments.count
2026 2041 end
2027 2042
2028 2043 def test_create_as_copy_with_failure
2029 2044 @request.session[:user_id] = 2
2030 2045 post :create, :project_id => 1, :copy_from => 1,
2031 2046 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''}
2032 2047
2033 2048 assert_response :success
2034 2049 assert_template 'new'
2035 2050
2036 2051 assert_not_nil assigns(:issue)
2037 2052 assert assigns(:issue).copy?
2038 2053
2039 2054 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
2040 2055 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
2041 2056 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
2042 2057 :child => {:tag => 'option', :attributes => {:value => '1', :selected => nil}, :content => 'eCookbook'}
2043 2058 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
2044 2059 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}, :content => 'OnlineStore'}
2045 2060 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
2046 2061 end
2047 2062
2048 2063 def test_create_as_copy_on_project_without_permission_should_ignore_target_project
2049 2064 @request.session[:user_id] = 2
2050 2065 assert !User.find(2).member_of?(Project.find(4))
2051 2066
2052 2067 assert_difference 'Issue.count' do
2053 2068 post :create, :project_id => 1, :copy_from => 1,
2054 2069 :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
2055 2070 end
2056 2071 issue = Issue.first(:order => 'id DESC')
2057 2072 assert_equal 1, issue.project_id
2058 2073 end
2059 2074
2060 2075 def test_get_edit
2061 2076 @request.session[:user_id] = 2
2062 2077 get :edit, :id => 1
2063 2078 assert_response :success
2064 2079 assert_template 'edit'
2065 2080 assert_not_nil assigns(:issue)
2066 2081 assert_equal Issue.find(1), assigns(:issue)
2067 2082
2068 2083 # Be sure we don't display inactive IssuePriorities
2069 2084 assert ! IssuePriority.find(15).active?
2070 2085 assert_no_tag :option, :attributes => {:value => '15'},
2071 2086 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
2072 2087 end
2073 2088
2074 2089 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
2075 2090 @request.session[:user_id] = 2
2076 2091 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
2077 2092
2078 2093 get :edit, :id => 1
2079 2094 assert_tag 'input', :attributes => {:name => 'time_entry[hours]'}
2080 2095 end
2081 2096
2082 2097 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
2083 2098 @request.session[:user_id] = 2
2084 2099 Role.find_by_name('Manager').remove_permission! :log_time
2085 2100
2086 2101 get :edit, :id => 1
2087 2102 assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'}
2088 2103 end
2089 2104
2090 2105 def test_get_edit_with_params
2091 2106 @request.session[:user_id] = 2
2092 2107 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
2093 2108 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
2094 2109 assert_response :success
2095 2110 assert_template 'edit'
2096 2111
2097 2112 issue = assigns(:issue)
2098 2113 assert_not_nil issue
2099 2114
2100 2115 assert_equal 5, issue.status_id
2101 2116 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
2102 2117 :child => { :tag => 'option',
2103 2118 :content => 'Closed',
2104 2119 :attributes => { :selected => 'selected' } }
2105 2120
2106 2121 assert_equal 7, issue.priority_id
2107 2122 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
2108 2123 :child => { :tag => 'option',
2109 2124 :content => 'Urgent',
2110 2125 :attributes => { :selected => 'selected' } }
2111 2126
2112 2127 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
2113 2128 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
2114 2129 :child => { :tag => 'option',
2115 2130 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
2116 2131 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
2117 2132 end
2118 2133
2119 2134 def test_get_edit_with_multi_custom_field
2120 2135 field = CustomField.find(1)
2121 2136 field.update_attribute :multiple, true
2122 2137 issue = Issue.find(1)
2123 2138 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2124 2139 issue.save!
2125 2140
2126 2141 @request.session[:user_id] = 2
2127 2142 get :edit, :id => 1
2128 2143 assert_response :success
2129 2144 assert_template 'edit'
2130 2145
2131 2146 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'}
2132 2147 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2133 2148 :child => {:tag => 'option', :attributes => {:value => 'MySQL', :selected => 'selected'}}
2134 2149 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2135 2150 :child => {:tag => 'option', :attributes => {:value => 'PostgreSQL', :selected => nil}}
2136 2151 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2137 2152 :child => {:tag => 'option', :attributes => {:value => 'Oracle', :selected => 'selected'}}
2138 2153 end
2139 2154
2140 2155 def test_update_edit_form
2141 2156 @request.session[:user_id] = 2
2142 2157 xhr :put, :new, :project_id => 1,
2143 2158 :id => 1,
2144 2159 :issue => {:tracker_id => 2,
2145 2160 :subject => 'This is the test_new issue',
2146 2161 :description => 'This is the description',
2147 2162 :priority_id => 5}
2148 2163 assert_response :success
2149 2164 assert_template 'attributes'
2150 2165
2151 2166 issue = assigns(:issue)
2152 2167 assert_kind_of Issue, issue
2153 2168 assert_equal 1, issue.id
2154 2169 assert_equal 1, issue.project_id
2155 2170 assert_equal 2, issue.tracker_id
2156 2171 assert_equal 'This is the test_new issue', issue.subject
2157 2172 end
2158 2173
2159 2174 def test_update_edit_form_with_project_change
2160 2175 @request.session[:user_id] = 2
2161 2176 xhr :put, :new, :project_id => 1,
2162 2177 :id => 1,
2163 2178 :project_change => '1',
2164 2179 :issue => {:project_id => 2,
2165 2180 :tracker_id => 2,
2166 2181 :subject => 'This is the test_new issue',
2167 2182 :description => 'This is the description',
2168 2183 :priority_id => 5}
2169 2184 assert_response :success
2170 2185 assert_template 'form'
2171 2186
2172 2187 issue = assigns(:issue)
2173 2188 assert_kind_of Issue, issue
2174 2189 assert_equal 1, issue.id
2175 2190 assert_equal 2, issue.project_id
2176 2191 assert_equal 2, issue.tracker_id
2177 2192 assert_equal 'This is the test_new issue', issue.subject
2178 2193 end
2179 2194
2180 2195 def test_put_update_without_custom_fields_param
2181 2196 @request.session[:user_id] = 2
2182 2197 ActionMailer::Base.deliveries.clear
2183 2198
2184 2199 issue = Issue.find(1)
2185 2200 assert_equal '125', issue.custom_value_for(2).value
2186 2201 old_subject = issue.subject
2187 2202 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2188 2203
2189 2204 assert_difference('Journal.count') do
2190 2205 assert_difference('JournalDetail.count', 2) do
2191 2206 put :update, :id => 1, :issue => {:subject => new_subject,
2192 2207 :priority_id => '6',
2193 2208 :category_id => '1' # no change
2194 2209 }
2195 2210 end
2196 2211 end
2197 2212 assert_redirected_to :action => 'show', :id => '1'
2198 2213 issue.reload
2199 2214 assert_equal new_subject, issue.subject
2200 2215 # Make sure custom fields were not cleared
2201 2216 assert_equal '125', issue.custom_value_for(2).value
2202 2217
2203 2218 mail = ActionMailer::Base.deliveries.last
2204 2219 assert_not_nil mail
2205 2220 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2206 2221 assert_mail_body_match "Subject changed from #{old_subject} to #{new_subject}", mail
2207 2222 end
2208 2223
2209 2224 def test_put_update_with_project_change
2210 2225 @request.session[:user_id] = 2
2211 2226 ActionMailer::Base.deliveries.clear
2212 2227
2213 2228 assert_difference('Journal.count') do
2214 2229 assert_difference('JournalDetail.count', 3) do
2215 2230 put :update, :id => 1, :issue => {:project_id => '2',
2216 2231 :tracker_id => '1', # no change
2217 2232 :priority_id => '6',
2218 2233 :category_id => '3'
2219 2234 }
2220 2235 end
2221 2236 end
2222 2237 assert_redirected_to :action => 'show', :id => '1'
2223 2238 issue = Issue.find(1)
2224 2239 assert_equal 2, issue.project_id
2225 2240 assert_equal 1, issue.tracker_id
2226 2241 assert_equal 6, issue.priority_id
2227 2242 assert_equal 3, issue.category_id
2228 2243
2229 2244 mail = ActionMailer::Base.deliveries.last
2230 2245 assert_not_nil mail
2231 2246 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2232 2247 assert_mail_body_match "Project changed from eCookbook to OnlineStore", mail
2233 2248 end
2234 2249
2235 2250 def test_put_update_with_tracker_change
2236 2251 @request.session[:user_id] = 2
2237 2252 ActionMailer::Base.deliveries.clear
2238 2253
2239 2254 assert_difference('Journal.count') do
2240 2255 assert_difference('JournalDetail.count', 2) do
2241 2256 put :update, :id => 1, :issue => {:project_id => '1',
2242 2257 :tracker_id => '2',
2243 2258 :priority_id => '6'
2244 2259 }
2245 2260 end
2246 2261 end
2247 2262 assert_redirected_to :action => 'show', :id => '1'
2248 2263 issue = Issue.find(1)
2249 2264 assert_equal 1, issue.project_id
2250 2265 assert_equal 2, issue.tracker_id
2251 2266 assert_equal 6, issue.priority_id
2252 2267 assert_equal 1, issue.category_id
2253 2268
2254 2269 mail = ActionMailer::Base.deliveries.last
2255 2270 assert_not_nil mail
2256 2271 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2257 2272 assert_mail_body_match "Tracker changed from Bug to Feature request", mail
2258 2273 end
2259 2274
2260 2275 def test_put_update_with_custom_field_change
2261 2276 @request.session[:user_id] = 2
2262 2277 issue = Issue.find(1)
2263 2278 assert_equal '125', issue.custom_value_for(2).value
2264 2279
2265 2280 assert_difference('Journal.count') do
2266 2281 assert_difference('JournalDetail.count', 3) do
2267 2282 put :update, :id => 1, :issue => {:subject => 'Custom field change',
2268 2283 :priority_id => '6',
2269 2284 :category_id => '1', # no change
2270 2285 :custom_field_values => { '2' => 'New custom value' }
2271 2286 }
2272 2287 end
2273 2288 end
2274 2289 assert_redirected_to :action => 'show', :id => '1'
2275 2290 issue.reload
2276 2291 assert_equal 'New custom value', issue.custom_value_for(2).value
2277 2292
2278 2293 mail = ActionMailer::Base.deliveries.last
2279 2294 assert_not_nil mail
2280 2295 assert_mail_body_match "Searchable field changed from 125 to New custom value", mail
2281 2296 end
2282 2297
2283 2298 def test_put_update_with_multi_custom_field_change
2284 2299 field = CustomField.find(1)
2285 2300 field.update_attribute :multiple, true
2286 2301 issue = Issue.find(1)
2287 2302 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2288 2303 issue.save!
2289 2304
2290 2305 @request.session[:user_id] = 2
2291 2306 assert_difference('Journal.count') do
2292 2307 assert_difference('JournalDetail.count', 3) do
2293 2308 put :update, :id => 1,
2294 2309 :issue => {
2295 2310 :subject => 'Custom field change',
2296 2311 :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
2297 2312 }
2298 2313 end
2299 2314 end
2300 2315 assert_redirected_to :action => 'show', :id => '1'
2301 2316 assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
2302 2317 end
2303 2318
2304 2319 def test_put_update_with_status_and_assignee_change
2305 2320 issue = Issue.find(1)
2306 2321 assert_equal 1, issue.status_id
2307 2322 @request.session[:user_id] = 2
2308 2323 assert_difference('TimeEntry.count', 0) do
2309 2324 put :update,
2310 2325 :id => 1,
2311 2326 :issue => { :status_id => 2, :assigned_to_id => 3 },
2312 2327 :notes => 'Assigned to dlopper',
2313 2328 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
2314 2329 end
2315 2330 assert_redirected_to :action => 'show', :id => '1'
2316 2331 issue.reload
2317 2332 assert_equal 2, issue.status_id
2318 2333 j = Journal.find(:first, :order => 'id DESC')
2319 2334 assert_equal 'Assigned to dlopper', j.notes
2320 2335 assert_equal 2, j.details.size
2321 2336
2322 2337 mail = ActionMailer::Base.deliveries.last
2323 2338 assert_mail_body_match "Status changed from New to Assigned", mail
2324 2339 # subject should contain the new status
2325 2340 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
2326 2341 end
2327 2342
2328 2343 def test_put_update_with_note_only
2329 2344 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
2330 2345 # anonymous user
2331 2346 put :update,
2332 2347 :id => 1,
2333 2348 :notes => notes
2334 2349 assert_redirected_to :action => 'show', :id => '1'
2335 2350 j = Journal.find(:first, :order => 'id DESC')
2336 2351 assert_equal notes, j.notes
2337 2352 assert_equal 0, j.details.size
2338 2353 assert_equal User.anonymous, j.user
2339 2354
2340 2355 mail = ActionMailer::Base.deliveries.last
2341 2356 assert_mail_body_match notes, mail
2342 2357 end
2343 2358
2344 2359 def test_put_update_with_note_and_spent_time
2345 2360 @request.session[:user_id] = 2
2346 2361 spent_hours_before = Issue.find(1).spent_hours
2347 2362 assert_difference('TimeEntry.count') do
2348 2363 put :update,
2349 2364 :id => 1,
2350 2365 :notes => '2.5 hours added',
2351 2366 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
2352 2367 end
2353 2368 assert_redirected_to :action => 'show', :id => '1'
2354 2369
2355 2370 issue = Issue.find(1)
2356 2371
2357 2372 j = Journal.find(:first, :order => 'id DESC')
2358 2373 assert_equal '2.5 hours added', j.notes
2359 2374 assert_equal 0, j.details.size
2360 2375
2361 2376 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
2362 2377 assert_not_nil t
2363 2378 assert_equal 2.5, t.hours
2364 2379 assert_equal spent_hours_before + 2.5, issue.spent_hours
2365 2380 end
2366 2381
2367 2382 def test_put_update_with_attachment_only
2368 2383 set_tmp_attachments_directory
2369 2384
2370 2385 # Delete all fixtured journals, a race condition can occur causing the wrong
2371 2386 # journal to get fetched in the next find.
2372 2387 Journal.delete_all
2373 2388
2374 2389 # anonymous user
2375 2390 assert_difference 'Attachment.count' do
2376 2391 put :update, :id => 1,
2377 2392 :notes => '',
2378 2393 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2379 2394 end
2380 2395
2381 2396 assert_redirected_to :action => 'show', :id => '1'
2382 2397 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
2383 2398 assert j.notes.blank?
2384 2399 assert_equal 1, j.details.size
2385 2400 assert_equal 'testfile.txt', j.details.first.value
2386 2401 assert_equal User.anonymous, j.user
2387 2402
2388 2403 attachment = Attachment.first(:order => 'id DESC')
2389 2404 assert_equal Issue.find(1), attachment.container
2390 2405 assert_equal User.anonymous, attachment.author
2391 2406 assert_equal 'testfile.txt', attachment.filename
2392 2407 assert_equal 'text/plain', attachment.content_type
2393 2408 assert_equal 'test file', attachment.description
2394 2409 assert_equal 59, attachment.filesize
2395 2410 assert File.exists?(attachment.diskfile)
2396 2411 assert_equal 59, File.size(attachment.diskfile)
2397 2412
2398 2413 mail = ActionMailer::Base.deliveries.last
2399 2414 assert_mail_body_match 'testfile.txt', mail
2400 2415 end
2401 2416
2402 2417 def test_put_update_with_failure_should_save_attachments
2403 2418 set_tmp_attachments_directory
2404 2419 @request.session[:user_id] = 2
2405 2420
2406 2421 assert_no_difference 'Journal.count' do
2407 2422 assert_difference 'Attachment.count' do
2408 2423 put :update, :id => 1,
2409 2424 :issue => { :subject => '' },
2410 2425 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2411 2426 assert_response :success
2412 2427 assert_template 'edit'
2413 2428 end
2414 2429 end
2415 2430
2416 2431 attachment = Attachment.first(:order => 'id DESC')
2417 2432 assert_equal 'testfile.txt', attachment.filename
2418 2433 assert File.exists?(attachment.diskfile)
2419 2434 assert_nil attachment.container
2420 2435
2421 2436 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2422 2437 assert_tag 'span', :content => /testfile.txt/
2423 2438 end
2424 2439
2425 2440 def test_put_update_with_failure_should_keep_saved_attachments
2426 2441 set_tmp_attachments_directory
2427 2442 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2428 2443 @request.session[:user_id] = 2
2429 2444
2430 2445 assert_no_difference 'Journal.count' do
2431 2446 assert_no_difference 'Attachment.count' do
2432 2447 put :update, :id => 1,
2433 2448 :issue => { :subject => '' },
2434 2449 :attachments => {'p0' => {'token' => attachment.token}}
2435 2450 assert_response :success
2436 2451 assert_template 'edit'
2437 2452 end
2438 2453 end
2439 2454
2440 2455 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2441 2456 assert_tag 'span', :content => /testfile.txt/
2442 2457 end
2443 2458
2444 2459 def test_put_update_should_attach_saved_attachments
2445 2460 set_tmp_attachments_directory
2446 2461 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2447 2462 @request.session[:user_id] = 2
2448 2463
2449 2464 assert_difference 'Journal.count' do
2450 2465 assert_difference 'JournalDetail.count' do
2451 2466 assert_no_difference 'Attachment.count' do
2452 2467 put :update, :id => 1,
2453 2468 :notes => 'Attachment added',
2454 2469 :attachments => {'p0' => {'token' => attachment.token}}
2455 2470 assert_redirected_to '/issues/1'
2456 2471 end
2457 2472 end
2458 2473 end
2459 2474
2460 2475 attachment.reload
2461 2476 assert_equal Issue.find(1), attachment.container
2462 2477
2463 2478 journal = Journal.first(:order => 'id DESC')
2464 2479 assert_equal 1, journal.details.size
2465 2480 assert_equal 'testfile.txt', journal.details.first.value
2466 2481 end
2467 2482
2468 2483 def test_put_update_with_attachment_that_fails_to_save
2469 2484 set_tmp_attachments_directory
2470 2485
2471 2486 # Delete all fixtured journals, a race condition can occur causing the wrong
2472 2487 # journal to get fetched in the next find.
2473 2488 Journal.delete_all
2474 2489
2475 2490 # Mock out the unsaved attachment
2476 2491 Attachment.any_instance.stubs(:create).returns(Attachment.new)
2477 2492
2478 2493 # anonymous user
2479 2494 put :update,
2480 2495 :id => 1,
2481 2496 :notes => '',
2482 2497 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
2483 2498 assert_redirected_to :action => 'show', :id => '1'
2484 2499 assert_equal '1 file(s) could not be saved.', flash[:warning]
2485 2500 end
2486 2501
2487 2502 def test_put_update_with_no_change
2488 2503 issue = Issue.find(1)
2489 2504 issue.journals.clear
2490 2505 ActionMailer::Base.deliveries.clear
2491 2506
2492 2507 put :update,
2493 2508 :id => 1,
2494 2509 :notes => ''
2495 2510 assert_redirected_to :action => 'show', :id => '1'
2496 2511
2497 2512 issue.reload
2498 2513 assert issue.journals.empty?
2499 2514 # No email should be sent
2500 2515 assert ActionMailer::Base.deliveries.empty?
2501 2516 end
2502 2517
2503 2518 def test_put_update_should_send_a_notification
2504 2519 @request.session[:user_id] = 2
2505 2520 ActionMailer::Base.deliveries.clear
2506 2521 issue = Issue.find(1)
2507 2522 old_subject = issue.subject
2508 2523 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2509 2524
2510 2525 put :update, :id => 1, :issue => {:subject => new_subject,
2511 2526 :priority_id => '6',
2512 2527 :category_id => '1' # no change
2513 2528 }
2514 2529 assert_equal 1, ActionMailer::Base.deliveries.size
2515 2530 end
2516 2531
2517 2532 def test_put_update_with_invalid_spent_time_hours_only
2518 2533 @request.session[:user_id] = 2
2519 2534 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
2520 2535
2521 2536 assert_no_difference('Journal.count') do
2522 2537 put :update,
2523 2538 :id => 1,
2524 2539 :notes => notes,
2525 2540 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
2526 2541 end
2527 2542 assert_response :success
2528 2543 assert_template 'edit'
2529 2544
2530 2545 assert_error_tag :descendant => {:content => /Activity can't be blank/}
2531 2546 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
2532 2547 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
2533 2548 end
2534 2549
2535 2550 def test_put_update_with_invalid_spent_time_comments_only
2536 2551 @request.session[:user_id] = 2
2537 2552 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
2538 2553
2539 2554 assert_no_difference('Journal.count') do
2540 2555 put :update,
2541 2556 :id => 1,
2542 2557 :notes => notes,
2543 2558 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
2544 2559 end
2545 2560 assert_response :success
2546 2561 assert_template 'edit'
2547 2562
2548 2563 assert_error_tag :descendant => {:content => /Activity can't be blank/}
2549 2564 assert_error_tag :descendant => {:content => /Hours can't be blank/}
2550 2565 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
2551 2566 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
2552 2567 end
2553 2568
2554 2569 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
2555 2570 issue = Issue.find(2)
2556 2571 @request.session[:user_id] = 2
2557 2572
2558 2573 put :update,
2559 2574 :id => issue.id,
2560 2575 :issue => {
2561 2576 :fixed_version_id => 4
2562 2577 }
2563 2578
2564 2579 assert_response :redirect
2565 2580 issue.reload
2566 2581 assert_equal 4, issue.fixed_version_id
2567 2582 assert_not_equal issue.project_id, issue.fixed_version.project_id
2568 2583 end
2569 2584
2570 2585 def test_put_update_should_redirect_back_using_the_back_url_parameter
2571 2586 issue = Issue.find(2)
2572 2587 @request.session[:user_id] = 2
2573 2588
2574 2589 put :update,
2575 2590 :id => issue.id,
2576 2591 :issue => {
2577 2592 :fixed_version_id => 4
2578 2593 },
2579 2594 :back_url => '/issues'
2580 2595
2581 2596 assert_response :redirect
2582 2597 assert_redirected_to '/issues'
2583 2598 end
2584 2599
2585 2600 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2586 2601 issue = Issue.find(2)
2587 2602 @request.session[:user_id] = 2
2588 2603
2589 2604 put :update,
2590 2605 :id => issue.id,
2591 2606 :issue => {
2592 2607 :fixed_version_id => 4
2593 2608 },
2594 2609 :back_url => 'http://google.com'
2595 2610
2596 2611 assert_response :redirect
2597 2612 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
2598 2613 end
2599 2614
2600 2615 def test_get_bulk_edit
2601 2616 @request.session[:user_id] = 2
2602 2617 get :bulk_edit, :ids => [1, 2]
2603 2618 assert_response :success
2604 2619 assert_template 'bulk_edit'
2605 2620
2606 2621 assert_tag :select, :attributes => {:name => 'issue[project_id]'}
2607 2622 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
2608 2623
2609 2624 # Project specific custom field, date type
2610 2625 field = CustomField.find(9)
2611 2626 assert !field.is_for_all?
2612 2627 assert_equal 'date', field.field_format
2613 2628 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
2614 2629
2615 2630 # System wide custom field
2616 2631 assert CustomField.find(1).is_for_all?
2617 2632 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
2618 2633
2619 2634 # Be sure we don't display inactive IssuePriorities
2620 2635 assert ! IssuePriority.find(15).active?
2621 2636 assert_no_tag :option, :attributes => {:value => '15'},
2622 2637 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
2623 2638 end
2624 2639
2625 2640 def test_get_bulk_edit_on_different_projects
2626 2641 @request.session[:user_id] = 2
2627 2642 get :bulk_edit, :ids => [1, 2, 6]
2628 2643 assert_response :success
2629 2644 assert_template 'bulk_edit'
2630 2645
2631 2646 # Can not set issues from different projects as children of an issue
2632 2647 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
2633 2648
2634 2649 # Project specific custom field, date type
2635 2650 field = CustomField.find(9)
2636 2651 assert !field.is_for_all?
2637 2652 assert !field.project_ids.include?(Issue.find(6).project_id)
2638 2653 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
2639 2654 end
2640 2655
2641 2656 def test_get_bulk_edit_with_user_custom_field
2642 2657 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
2643 2658
2644 2659 @request.session[:user_id] = 2
2645 2660 get :bulk_edit, :ids => [1, 2]
2646 2661 assert_response :success
2647 2662 assert_template 'bulk_edit'
2648 2663
2649 2664 assert_tag :select,
2650 2665 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2651 2666 :children => {
2652 2667 :only => {:tag => 'option'},
2653 2668 :count => Project.find(1).users.count + 1
2654 2669 }
2655 2670 end
2656 2671
2657 2672 def test_get_bulk_edit_with_version_custom_field
2658 2673 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
2659 2674
2660 2675 @request.session[:user_id] = 2
2661 2676 get :bulk_edit, :ids => [1, 2]
2662 2677 assert_response :success
2663 2678 assert_template 'bulk_edit'
2664 2679
2665 2680 assert_tag :select,
2666 2681 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2667 2682 :children => {
2668 2683 :only => {:tag => 'option'},
2669 2684 :count => Project.find(1).shared_versions.count + 1
2670 2685 }
2671 2686 end
2672 2687
2673 2688 def test_get_bulk_edit_with_multi_custom_field
2674 2689 field = CustomField.find(1)
2675 2690 field.update_attribute :multiple, true
2676 2691
2677 2692 @request.session[:user_id] = 2
2678 2693 get :bulk_edit, :ids => [1, 2]
2679 2694 assert_response :success
2680 2695 assert_template 'bulk_edit'
2681 2696
2682 2697 assert_tag :select,
2683 2698 :attributes => {:name => "issue[custom_field_values][1][]"},
2684 2699 :children => {
2685 2700 :only => {:tag => 'option'},
2686 2701 :count => 3
2687 2702 }
2688 2703 end
2689 2704
2690 2705 def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues
2691 2706 Workflow.delete_all
2692 2707 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1)
2693 2708 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
2694 2709 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
2695 2710 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
2696 2711 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3)
2697 2712 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
2698 2713 @request.session[:user_id] = 2
2699 2714 get :bulk_edit, :ids => [1, 2]
2700 2715
2701 2716 assert_response :success
2702 2717 statuses = assigns(:available_statuses)
2703 2718 assert_not_nil statuses
2704 2719 assert_equal [1, 3], statuses.map(&:id).sort
2705 2720
2706 2721 assert_tag 'select', :attributes => {:name => 'issue[status_id]'},
2707 2722 :children => {:count => 3} # 2 statuses + "no change" option
2708 2723 end
2709 2724
2710 2725 def test_bulk_edit_should_propose_target_project_open_shared_versions
2711 2726 @request.session[:user_id] = 2
2712 2727 post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
2713 2728 assert_response :success
2714 2729 assert_template 'bulk_edit'
2715 2730 assert_equal Project.find(1).shared_versions.open.all.sort, assigns(:versions).sort
2716 2731 assert_tag 'select',
2717 2732 :attributes => {:name => 'issue[fixed_version_id]'},
2718 2733 :descendant => {:tag => 'option', :content => '2.0'}
2719 2734 end
2720 2735
2721 2736 def test_bulk_edit_should_propose_target_project_categories
2722 2737 @request.session[:user_id] = 2
2723 2738 post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
2724 2739 assert_response :success
2725 2740 assert_template 'bulk_edit'
2726 2741 assert_equal Project.find(1).issue_categories.sort, assigns(:categories).sort
2727 2742 assert_tag 'select',
2728 2743 :attributes => {:name => 'issue[category_id]'},
2729 2744 :descendant => {:tag => 'option', :content => 'Recipes'}
2730 2745 end
2731 2746
2732 2747 def test_bulk_update
2733 2748 @request.session[:user_id] = 2
2734 2749 # update issues priority
2735 2750 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2736 2751 :issue => {:priority_id => 7,
2737 2752 :assigned_to_id => '',
2738 2753 :custom_field_values => {'2' => ''}}
2739 2754
2740 2755 assert_response 302
2741 2756 # check that the issues were updated
2742 2757 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
2743 2758
2744 2759 issue = Issue.find(1)
2745 2760 journal = issue.journals.find(:first, :order => 'created_on DESC')
2746 2761 assert_equal '125', issue.custom_value_for(2).value
2747 2762 assert_equal 'Bulk editing', journal.notes
2748 2763 assert_equal 1, journal.details.size
2749 2764 end
2750 2765
2751 2766 def test_bulk_update_with_group_assignee
2752 2767 group = Group.find(11)
2753 2768 project = Project.find(1)
2754 2769 project.members << Member.new(:principal => group, :roles => [Role.givable.first])
2755 2770
2756 2771 @request.session[:user_id] = 2
2757 2772 # update issues assignee
2758 2773 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2759 2774 :issue => {:priority_id => '',
2760 2775 :assigned_to_id => group.id,
2761 2776 :custom_field_values => {'2' => ''}}
2762 2777
2763 2778 assert_response 302
2764 2779 assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
2765 2780 end
2766 2781
2767 2782 def test_bulk_update_on_different_projects
2768 2783 @request.session[:user_id] = 2
2769 2784 # update issues priority
2770 2785 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
2771 2786 :issue => {:priority_id => 7,
2772 2787 :assigned_to_id => '',
2773 2788 :custom_field_values => {'2' => ''}}
2774 2789
2775 2790 assert_response 302
2776 2791 # check that the issues were updated
2777 2792 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
2778 2793
2779 2794 issue = Issue.find(1)
2780 2795 journal = issue.journals.find(:first, :order => 'created_on DESC')
2781 2796 assert_equal '125', issue.custom_value_for(2).value
2782 2797 assert_equal 'Bulk editing', journal.notes
2783 2798 assert_equal 1, journal.details.size
2784 2799 end
2785 2800
2786 2801 def test_bulk_update_on_different_projects_without_rights
2787 2802 @request.session[:user_id] = 3
2788 2803 user = User.find(3)
2789 2804 action = { :controller => "issues", :action => "bulk_update" }
2790 2805 assert user.allowed_to?(action, Issue.find(1).project)
2791 2806 assert ! user.allowed_to?(action, Issue.find(6).project)
2792 2807 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
2793 2808 :issue => {:priority_id => 7,
2794 2809 :assigned_to_id => '',
2795 2810 :custom_field_values => {'2' => ''}}
2796 2811 assert_response 403
2797 2812 assert_not_equal "Bulk should fail", Journal.last.notes
2798 2813 end
2799 2814
2800 2815 def test_bullk_update_should_send_a_notification
2801 2816 @request.session[:user_id] = 2
2802 2817 ActionMailer::Base.deliveries.clear
2803 2818 post(:bulk_update,
2804 2819 {
2805 2820 :ids => [1, 2],
2806 2821 :notes => 'Bulk editing',
2807 2822 :issue => {
2808 2823 :priority_id => 7,
2809 2824 :assigned_to_id => '',
2810 2825 :custom_field_values => {'2' => ''}
2811 2826 }
2812 2827 })
2813 2828
2814 2829 assert_response 302
2815 2830 assert_equal 2, ActionMailer::Base.deliveries.size
2816 2831 end
2817 2832
2818 2833 def test_bulk_update_project
2819 2834 @request.session[:user_id] = 2
2820 2835 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}
2821 2836 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2822 2837 # Issues moved to project 2
2823 2838 assert_equal 2, Issue.find(1).project_id
2824 2839 assert_equal 2, Issue.find(2).project_id
2825 2840 # No tracker change
2826 2841 assert_equal 1, Issue.find(1).tracker_id
2827 2842 assert_equal 2, Issue.find(2).tracker_id
2828 2843 end
2829 2844
2830 2845 def test_bulk_update_project_on_single_issue_should_follow_when_needed
2831 2846 @request.session[:user_id] = 2
2832 2847 post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1'
2833 2848 assert_redirected_to '/issues/1'
2834 2849 end
2835 2850
2836 2851 def test_bulk_update_project_on_multiple_issues_should_follow_when_needed
2837 2852 @request.session[:user_id] = 2
2838 2853 post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1'
2839 2854 assert_redirected_to '/projects/onlinestore/issues'
2840 2855 end
2841 2856
2842 2857 def test_bulk_update_tracker
2843 2858 @request.session[:user_id] = 2
2844 2859 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'}
2845 2860 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2846 2861 assert_equal 2, Issue.find(1).tracker_id
2847 2862 assert_equal 2, Issue.find(2).tracker_id
2848 2863 end
2849 2864
2850 2865 def test_bulk_update_status
2851 2866 @request.session[:user_id] = 2
2852 2867 # update issues priority
2853 2868 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
2854 2869 :issue => {:priority_id => '',
2855 2870 :assigned_to_id => '',
2856 2871 :status_id => '5'}
2857 2872
2858 2873 assert_response 302
2859 2874 issue = Issue.find(1)
2860 2875 assert issue.closed?
2861 2876 end
2862 2877
2863 2878 def test_bulk_update_priority
2864 2879 @request.session[:user_id] = 2
2865 2880 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
2866 2881
2867 2882 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2868 2883 assert_equal 6, Issue.find(1).priority_id
2869 2884 assert_equal 6, Issue.find(2).priority_id
2870 2885 end
2871 2886
2872 2887 def test_bulk_update_with_notes
2873 2888 @request.session[:user_id] = 2
2874 2889 post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues'
2875 2890
2876 2891 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2877 2892 assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes
2878 2893 assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes
2879 2894 end
2880 2895
2881 2896 def test_bulk_update_parent_id
2882 2897 @request.session[:user_id] = 2
2883 2898 post :bulk_update, :ids => [1, 3],
2884 2899 :notes => 'Bulk editing parent',
2885 2900 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
2886 2901
2887 2902 assert_response 302
2888 2903 parent = Issue.find(2)
2889 2904 assert_equal parent.id, Issue.find(1).parent_id
2890 2905 assert_equal parent.id, Issue.find(3).parent_id
2891 2906 assert_equal [1, 3], parent.children.collect(&:id).sort
2892 2907 end
2893 2908
2894 2909 def test_bulk_update_custom_field
2895 2910 @request.session[:user_id] = 2
2896 2911 # update issues priority
2897 2912 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
2898 2913 :issue => {:priority_id => '',
2899 2914 :assigned_to_id => '',
2900 2915 :custom_field_values => {'2' => '777'}}
2901 2916
2902 2917 assert_response 302
2903 2918
2904 2919 issue = Issue.find(1)
2905 2920 journal = issue.journals.find(:first, :order => 'created_on DESC')
2906 2921 assert_equal '777', issue.custom_value_for(2).value
2907 2922 assert_equal 1, journal.details.size
2908 2923 assert_equal '125', journal.details.first.old_value
2909 2924 assert_equal '777', journal.details.first.value
2910 2925 end
2911 2926
2912 2927 def test_bulk_update_multi_custom_field
2913 2928 field = CustomField.find(1)
2914 2929 field.update_attribute :multiple, true
2915 2930
2916 2931 @request.session[:user_id] = 2
2917 2932 post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
2918 2933 :issue => {:priority_id => '',
2919 2934 :assigned_to_id => '',
2920 2935 :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
2921 2936
2922 2937 assert_response 302
2923 2938
2924 2939 assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
2925 2940 assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
2926 2941 # the custom field is not associated with the issue tracker
2927 2942 assert_nil Issue.find(2).custom_field_value(1)
2928 2943 end
2929 2944
2930 2945 def test_bulk_update_unassign
2931 2946 assert_not_nil Issue.find(2).assigned_to
2932 2947 @request.session[:user_id] = 2
2933 2948 # unassign issues
2934 2949 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
2935 2950 assert_response 302
2936 2951 # check that the issues were updated
2937 2952 assert_nil Issue.find(2).assigned_to
2938 2953 end
2939 2954
2940 2955 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
2941 2956 @request.session[:user_id] = 2
2942 2957
2943 2958 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
2944 2959
2945 2960 assert_response :redirect
2946 2961 issues = Issue.find([1,2])
2947 2962 issues.each do |issue|
2948 2963 assert_equal 4, issue.fixed_version_id
2949 2964 assert_not_equal issue.project_id, issue.fixed_version.project_id
2950 2965 end
2951 2966 end
2952 2967
2953 2968 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
2954 2969 @request.session[:user_id] = 2
2955 2970 post :bulk_update, :ids => [1,2], :back_url => '/issues'
2956 2971
2957 2972 assert_response :redirect
2958 2973 assert_redirected_to '/issues'
2959 2974 end
2960 2975
2961 2976 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2962 2977 @request.session[:user_id] = 2
2963 2978 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
2964 2979
2965 2980 assert_response :redirect
2966 2981 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
2967 2982 end
2968 2983
2969 2984 def test_bulk_update_with_failure_should_set_flash
2970 2985 @request.session[:user_id] = 2
2971 2986 Issue.update_all("subject = ''", "id = 2") # Make it invalid
2972 2987 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
2973 2988
2974 2989 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2975 2990 assert_equal 'Failed to save 1 issue(s) on 2 selected: #2.', flash[:error]
2976 2991 end
2977 2992
2978 2993 def test_bulk_copy_to_another_project
2979 2994 @request.session[:user_id] = 2
2980 2995 assert_difference 'Issue.count', 2 do
2981 2996 assert_no_difference 'Project.find(1).issues.count' do
2982 2997 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1'
2983 2998 end
2984 2999 end
2985 3000 assert_redirected_to '/projects/ecookbook/issues'
2986 3001 end
2987 3002
2988 3003 def test_bulk_copy_should_allow_not_changing_the_issue_attributes
2989 3004 @request.session[:user_id] = 2
2990 3005 issue_before_move = Issue.find(1)
2991 3006 assert_difference 'Issue.count', 1 do
2992 3007 assert_no_difference 'Project.find(1).issues.count' do
2993 3008 post :bulk_update, :ids => [1], :copy => '1',
2994 3009 :issue => {
2995 3010 :project_id => '2', :tracker_id => '', :assigned_to_id => '',
2996 3011 :status_id => '', :start_date => '', :due_date => ''
2997 3012 }
2998 3013 end
2999 3014 end
3000 3015 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
3001 3016 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
3002 3017 assert_equal issue_before_move.status_id, issue_after_move.status_id
3003 3018 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
3004 3019 end
3005 3020
3006 3021 def test_bulk_copy_should_allow_changing_the_issue_attributes
3007 3022 # Fixes random test failure with Mysql
3008 3023 # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
3009 3024 # doesn't return the expected results
3010 3025 Issue.delete_all("project_id=2")
3011 3026
3012 3027 @request.session[:user_id] = 2
3013 3028 assert_difference 'Issue.count', 2 do
3014 3029 assert_no_difference 'Project.find(1).issues.count' do
3015 3030 post :bulk_update, :ids => [1, 2], :copy => '1',
3016 3031 :issue => {
3017 3032 :project_id => '2', :tracker_id => '', :assigned_to_id => '4',
3018 3033 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
3019 3034 }
3020 3035 end
3021 3036 end
3022 3037
3023 3038 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
3024 3039 assert_equal 2, copied_issues.size
3025 3040 copied_issues.each do |issue|
3026 3041 assert_equal 2, issue.project_id, "Project is incorrect"
3027 3042 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
3028 3043 assert_equal 3, issue.status_id, "Status is incorrect"
3029 3044 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
3030 3045 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
3031 3046 end
3032 3047 end
3033 3048
3034 3049 def test_bulk_copy_should_allow_adding_a_note
3035 3050 @request.session[:user_id] = 2
3036 3051 assert_difference 'Issue.count', 1 do
3037 3052 post :bulk_update, :ids => [1], :copy => '1',
3038 3053 :notes => 'Copying one issue',
3039 3054 :issue => {
3040 3055 :project_id => '', :tracker_id => '', :assigned_to_id => '4',
3041 3056 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
3042 3057 }
3043 3058 end
3044 3059
3045 3060 issue = Issue.first(:order => 'id DESC')
3046 3061 assert_equal 1, issue.journals.size
3047 3062 journal = issue.journals.first
3048 3063 assert_equal 0, journal.details.size
3049 3064 assert_equal 'Copying one issue', journal.notes
3050 3065 end
3051 3066
3052 3067 def test_bulk_copy_to_another_project_should_follow_when_needed
3053 3068 @request.session[:user_id] = 2
3054 3069 post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1'
3055 3070 issue = Issue.first(:order => 'id DESC')
3056 3071 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
3057 3072 end
3058 3073
3059 3074 def test_destroy_issue_with_no_time_entries
3060 3075 assert_nil TimeEntry.find_by_issue_id(2)
3061 3076 @request.session[:user_id] = 2
3062 3077
3063 3078 assert_difference 'Issue.count', -1 do
3064 3079 delete :destroy, :id => 2
3065 3080 end
3066 3081 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3067 3082 assert_nil Issue.find_by_id(2)
3068 3083 end
3069 3084
3070 3085 def test_destroy_issues_with_time_entries
3071 3086 @request.session[:user_id] = 2
3072 3087
3073 3088 assert_no_difference 'Issue.count' do
3074 3089 delete :destroy, :ids => [1, 3]
3075 3090 end
3076 3091 assert_response :success
3077 3092 assert_template 'destroy'
3078 3093 assert_not_nil assigns(:hours)
3079 3094 assert Issue.find_by_id(1) && Issue.find_by_id(3)
3080 3095 assert_tag 'form',
3081 3096 :descendant => {:tag => 'input', :attributes => {:name => '_method', :value => 'delete'}}
3082 3097 end
3083 3098
3084 3099 def test_destroy_issues_and_destroy_time_entries
3085 3100 @request.session[:user_id] = 2
3086 3101
3087 3102 assert_difference 'Issue.count', -2 do
3088 3103 assert_difference 'TimeEntry.count', -3 do
3089 3104 delete :destroy, :ids => [1, 3], :todo => 'destroy'
3090 3105 end
3091 3106 end
3092 3107 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3093 3108 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3094 3109 assert_nil TimeEntry.find_by_id([1, 2])
3095 3110 end
3096 3111
3097 3112 def test_destroy_issues_and_assign_time_entries_to_project
3098 3113 @request.session[:user_id] = 2
3099 3114
3100 3115 assert_difference 'Issue.count', -2 do
3101 3116 assert_no_difference 'TimeEntry.count' do
3102 3117 delete :destroy, :ids => [1, 3], :todo => 'nullify'
3103 3118 end
3104 3119 end
3105 3120 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3106 3121 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3107 3122 assert_nil TimeEntry.find(1).issue_id
3108 3123 assert_nil TimeEntry.find(2).issue_id
3109 3124 end
3110 3125
3111 3126 def test_destroy_issues_and_reassign_time_entries_to_another_issue
3112 3127 @request.session[:user_id] = 2
3113 3128
3114 3129 assert_difference 'Issue.count', -2 do
3115 3130 assert_no_difference 'TimeEntry.count' do
3116 3131 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
3117 3132 end
3118 3133 end
3119 3134 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3120 3135 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3121 3136 assert_equal 2, TimeEntry.find(1).issue_id
3122 3137 assert_equal 2, TimeEntry.find(2).issue_id
3123 3138 end
3124 3139
3125 3140 def test_destroy_issues_from_different_projects
3126 3141 @request.session[:user_id] = 2
3127 3142
3128 3143 assert_difference 'Issue.count', -3 do
3129 3144 delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
3130 3145 end
3131 3146 assert_redirected_to :controller => 'issues', :action => 'index'
3132 3147 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
3133 3148 end
3134 3149
3135 3150 def test_destroy_parent_and_child_issues
3136 3151 parent = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Parent Issue')
3137 3152 child = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Child Issue', :parent_issue_id => parent.id)
3138 3153 assert child.is_descendant_of?(parent.reload)
3139 3154
3140 3155 @request.session[:user_id] = 2
3141 3156 assert_difference 'Issue.count', -2 do
3142 3157 delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
3143 3158 end
3144 3159 assert_response 302
3145 3160 end
3146 3161
3147 3162 def test_default_search_scope
3148 3163 get :index
3149 3164 assert_tag :div, :attributes => {:id => 'quick-search'},
3150 3165 :child => {:tag => 'form',
3151 3166 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
3152 3167 end
3153 3168 end
@@ -1,103 +1,122
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 'watchers_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WatchersController; def rescue_action(e) raise e end; end
23 23
24 24 class WatchersControllerTest < ActionController::TestCase
25 25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
26 26 :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers
27 27
28 28 def setup
29 29 @controller = WatchersController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 User.current = nil
33 33 end
34 34
35 35 def test_watch
36 36 @request.session[:user_id] = 3
37 37 assert_difference('Watcher.count') do
38 38 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
39 39 assert_response :success
40 40 assert @response.body.include?('$$(".issue-1-watcher")')
41 41 end
42 42 assert Issue.find(1).watched_by?(User.find(3))
43 43 end
44 44
45 45 def test_watch_should_be_denied_without_permission
46 46 Role.find(2).remove_permission! :view_issues
47 47 @request.session[:user_id] = 3
48 48 assert_no_difference('Watcher.count') do
49 49 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
50 50 assert_response 403
51 51 end
52 52 end
53 53
54 54 def test_unwatch
55 55 @request.session[:user_id] = 3
56 56 assert_difference('Watcher.count', -1) do
57 57 xhr :post, :unwatch, :object_type => 'issue', :object_id => '2'
58 58 assert_response :success
59 59 assert @response.body.include?('$$(".issue-2-watcher")')
60 60 end
61 61 assert !Issue.find(1).watched_by?(User.find(3))
62 62 end
63 63
64 64 def test_new
65 65 @request.session[:user_id] = 2
66 66 xhr :get, :new, :object_type => 'issue', :object_id => '2'
67 67 assert_response :success
68 68 assert_select_rjs :replace_html, 'ajax-modal'
69 69 end
70 70
71 def test_new_for_new_record
72 @request.session[:user_id] = 2
73 xhr :get, :new, :project_id => 1
74 assert_response :success
75 assert_select_rjs :replace_html, 'ajax-modal'
76 end
77
71 78 def test_create
72 79 @request.session[:user_id] = 2
73 80 assert_difference('Watcher.count') do
74 81 xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'}
75 82 assert_response :success
76 83 assert_select_rjs :replace_html, 'watchers'
77 84 assert_select_rjs :replace_html, 'ajax-modal'
78 85 end
79 86 assert Issue.find(2).watched_by?(User.find(4))
80 87 end
81 88
82 89 def test_create_multiple
83 90 @request.session[:user_id] = 2
84 91 assert_difference('Watcher.count', 2) do
85 92 xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_ids => ['4', '7']}
86 93 assert_response :success
87 94 assert_select_rjs :replace_html, 'watchers'
88 95 assert_select_rjs :replace_html, 'ajax-modal'
89 96 end
90 97 assert Issue.find(2).watched_by?(User.find(4))
91 98 assert Issue.find(2).watched_by?(User.find(7))
92 99 end
93 100
101 def test_append
102 @request.session[:user_id] = 2
103 assert_no_difference 'Watcher.count' do
104 xhr :post, :append, :watcher => {:user_ids => ['4', '7']}
105 assert_response :success
106 assert_select_rjs :insert_html, 'watchers_inputs' do
107 assert_select 'input[name=?][value=4]', 'issue[watcher_user_ids][]'
108 assert_select 'input[name=?][value=7]', 'issue[watcher_user_ids][]'
109 end
110 end
111 end
112
94 113 def test_remove_watcher
95 114 @request.session[:user_id] = 2
96 115 assert_difference('Watcher.count', -1) do
97 116 xhr :post, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3'
98 117 assert_response :success
99 118 assert_select_rjs :replace_html, 'watchers'
100 119 end
101 120 assert !Issue.find(2).watched_by?(User.find(3))
102 121 end
103 122 end
@@ -1,47 +1,51
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 RoutingWatchersTest < ActionController::IntegrationTest
21 21 def test_watchers
22 22 assert_routing(
23 23 { :method => 'get', :path => "/watchers/new" },
24 24 { :controller => 'watchers', :action => 'new' }
25 25 )
26 26 assert_routing(
27 { :method => 'post', :path => "/watchers/append" },
28 { :controller => 'watchers', :action => 'append' }
29 )
30 assert_routing(
27 31 { :method => 'post', :path => "/watchers" },
28 32 { :controller => 'watchers', :action => 'create' }
29 33 )
30 34 assert_routing(
31 35 { :method => 'post', :path => "/watchers/destroy" },
32 36 { :controller => 'watchers', :action => 'destroy' }
33 37 )
34 38 assert_routing(
35 39 { :method => 'get', :path => "/watchers/autocomplete_for_user" },
36 40 { :controller => 'watchers', :action => 'autocomplete_for_user' }
37 41 )
38 42 assert_routing(
39 43 { :method => 'post', :path => "/watchers/watch" },
40 44 { :controller => 'watchers', :action => 'watch' }
41 45 )
42 46 assert_routing(
43 47 { :method => 'post', :path => "/watchers/unwatch" },
44 48 { :controller => 'watchers', :action => 'unwatch' }
45 49 )
46 50 end
47 51 end
General Comments 0
You need to be logged in to leave comments. Login now