##// END OF EJS Templates
Adds a :copy_issues permission (#18855)....
Jean-Philippe Lang -
r13603:c3c7d9a4d27b
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,97 +1,97
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 ContextMenusController < ApplicationController
19 19 helper :watchers
20 20 helper :issues
21 21
22 22 before_filter :find_issues, :only => :issues
23 23
24 24 def issues
25 25 if (@issues.size == 1)
26 26 @issue = @issues.first
27 27 end
28 28 @issue_ids = @issues.map(&:id).sort
29 29
30 30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
31 31
32 32 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
33 33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
34 :copy => User.current.allowed_to?(:add_issues, @projects),
34 :copy => User.current.allowed_to?(:copy_issues, @projects) && Issue.allowed_target_projects.any?,
35 35 :delete => User.current.allowed_to?(:delete_issues, @projects)
36 36 }
37 37 if @project
38 38 if @issue
39 39 @assignables = @issue.assignable_users
40 40 else
41 41 @assignables = @project.assignable_users
42 42 end
43 43 @trackers = @project.trackers
44 44 else
45 45 #when multiple projects, we only keep the intersection of each set
46 46 @assignables = @projects.map(&:assignable_users).reduce(:&)
47 47 @trackers = @projects.map(&:trackers).reduce(:&)
48 48 end
49 49 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
50 50
51 51 @priorities = IssuePriority.active.reverse
52 52 @back = back_url
53 53
54 54 @options_by_custom_field = {}
55 55 if @can[:edit]
56 56 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
57 57 custom_fields.each do |field|
58 58 values = field.possible_values_options(@projects)
59 59 if values.present?
60 60 @options_by_custom_field[field] = values
61 61 end
62 62 end
63 63 end
64 64
65 65 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
66 66 render :layout => false
67 67 end
68 68
69 69 def time_entries
70 70 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
71 71 (render_404; return) unless @time_entries.present?
72 72 if (@time_entries.size == 1)
73 73 @time_entry = @time_entries.first
74 74 end
75 75
76 76 @projects = @time_entries.collect(&:project).compact.uniq
77 77 @project = @projects.first if @projects.size == 1
78 78 @activities = TimeEntryActivity.shared.active
79 79 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
80 80 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
81 81 }
82 82 @back = back_url
83 83
84 84 @options_by_custom_field = {}
85 85 if @can[:edit]
86 86 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
87 87 custom_fields.each do |field|
88 88 values = field.possible_values_options(@projects)
89 89 if values.present?
90 90 @options_by_custom_field[field] = values
91 91 end
92 92 end
93 93 end
94 94
95 95 render :layout => false
96 96 end
97 97 end
@@ -1,498 +1,523
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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, :update_form]
25 25 before_filter :authorize, :except => [:index]
26 26 before_filter :find_optional_project, :only => [:index]
27 27 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
28 28 accept_rss_auth :index, :show
29 29 accept_api_auth :index, :show, :create, :update, :destroy
30 30
31 31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 32
33 33 helper :journals
34 34 helper :projects
35 35 include ProjectsHelper
36 36 helper :custom_fields
37 37 include CustomFieldsHelper
38 38 helper :issue_relations
39 39 include IssueRelationsHelper
40 40 helper :watchers
41 41 include WatchersHelper
42 42 helper :attachments
43 43 include AttachmentsHelper
44 44 helper :queries
45 45 include QueriesHelper
46 46 helper :repositories
47 47 include RepositoriesHelper
48 48 helper :sort
49 49 include SortHelper
50 50 include IssuesHelper
51 51 helper :timelog
52 52
53 53 def index
54 54 retrieve_query
55 55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 56 sort_update(@query.sortable_columns)
57 57 @query.sort_criteria = sort_criteria.to_a
58 58
59 59 if @query.valid?
60 60 case params[:format]
61 61 when 'csv', 'pdf'
62 62 @limit = Setting.issues_export_limit.to_i
63 63 if params[:columns] == 'all'
64 64 @query.column_names = @query.available_inline_columns.map(&:name)
65 65 end
66 66 when 'atom'
67 67 @limit = Setting.feeds_limit.to_i
68 68 when 'xml', 'json'
69 69 @offset, @limit = api_offset_and_limit
70 70 @query.column_names = %w(author)
71 71 else
72 72 @limit = per_page_option
73 73 end
74 74
75 75 @issue_count = @query.issue_count
76 76 @issue_pages = Paginator.new @issue_count, @limit, params['page']
77 77 @offset ||= @issue_pages.offset
78 78 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
79 79 :order => sort_clause,
80 80 :offset => @offset,
81 81 :limit => @limit)
82 82 @issue_count_by_group = @query.issue_count_by_group
83 83
84 84 respond_to do |format|
85 85 format.html { render :template => 'issues/index', :layout => !request.xhr? }
86 86 format.api {
87 87 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
88 88 }
89 89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
90 90 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
91 91 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
92 92 end
93 93 else
94 94 respond_to do |format|
95 95 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
96 96 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
97 97 format.api { render_validation_errors(@query) }
98 98 end
99 99 end
100 100 rescue ActiveRecord::RecordNotFound
101 101 render_404
102 102 end
103 103
104 104 def show
105 105 @journals = @issue.journals.includes(:user, :details).
106 106 references(:user, :details).
107 107 reorder("#{Journal.table_name}.id ASC").to_a
108 108 @journals.each_with_index {|j,i| j.indice = i+1}
109 109 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
110 110 Journal.preload_journals_details_custom_fields(@journals)
111 111 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
112 112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
113 113
114 114 @changesets = @issue.changesets.visible.to_a
115 115 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
116 116
117 117 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
118 118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 119 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 120 @priorities = IssuePriority.active
121 121 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
122 122 @relation = IssueRelation.new
123 123
124 124 respond_to do |format|
125 125 format.html {
126 126 retrieve_previous_and_next_issue_ids
127 127 render :template => 'issues/show'
128 128 }
129 129 format.api
130 130 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
131 131 format.pdf {
132 132 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
133 133 }
134 134 end
135 135 end
136 136
137 137 # Add a new issue
138 138 # The new issue will be created from an existing one if copy_from parameter is given
139 139 def new
140 140 respond_to do |format|
141 141 format.html { render :action => 'new', :layout => !request.xhr? }
142 142 end
143 143 end
144 144
145 145 def create
146 unless User.current.allowed_to?(:add_issues, @issue.project)
147 raise ::Unauthorized
148 end
146 149 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
147 150 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
148 151 if @issue.save
149 152 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
150 153 respond_to do |format|
151 154 format.html {
152 155 render_attachment_warning_if_needed(@issue)
153 156 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
154 157 if params[:continue]
155 158 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
156 159 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
157 160 else
158 161 redirect_to issue_path(@issue)
159 162 end
160 163 }
161 164 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
162 165 end
163 166 return
164 167 else
165 168 respond_to do |format|
166 169 format.html { render :action => 'new' }
167 170 format.api { render_validation_errors(@issue) }
168 171 end
169 172 end
170 173 end
171 174
172 175 def edit
173 176 return unless update_issue_from_params
174 177
175 178 respond_to do |format|
176 179 format.html { }
177 180 format.xml { }
178 181 end
179 182 end
180 183
181 184 def update
182 185 return unless update_issue_from_params
183 186 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
184 187 saved = false
185 188 begin
186 189 saved = save_issue_with_child_records
187 190 rescue ActiveRecord::StaleObjectError
188 191 @conflict = true
189 192 if params[:last_journal_id]
190 193 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
191 194 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
192 195 end
193 196 end
194 197
195 198 if saved
196 199 render_attachment_warning_if_needed(@issue)
197 200 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
198 201
199 202 respond_to do |format|
200 203 format.html { redirect_back_or_default issue_path(@issue) }
201 204 format.api { render_api_ok }
202 205 end
203 206 else
204 207 respond_to do |format|
205 208 format.html { render :action => 'edit' }
206 209 format.api { render_validation_errors(@issue) }
207 210 end
208 211 end
209 212 end
210 213
211 214 # Updates the issue form when changing the project, status or tracker
212 215 # on issue creation/update
213 216 def update_form
214 217 end
215 218
216 219 # Bulk edit/copy a set of issues
217 220 def bulk_edit
218 221 @issues.sort!
219 222 @copy = params[:copy].present?
220 223 @notes = params[:notes]
221 224
225 if @copy
226 unless User.current.allowed_to?(:copy_issues, @projects)
227 raise ::Unauthorized
228 end
229 end
230
222 231 @allowed_projects = Issue.allowed_target_projects
223 232 if params[:issue]
224 233 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
225 234 if @target_project
226 235 target_projects = [@target_project]
227 236 end
228 237 end
229 238 target_projects ||= @projects
230 239
231 240 if @copy
232 241 # Copied issues will get their default statuses
233 242 @available_statuses = []
234 243 else
235 244 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
236 245 end
237 246 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
238 247 @assignables = target_projects.map(&:assignable_users).reduce(:&)
239 248 @trackers = target_projects.map(&:trackers).reduce(:&)
240 249 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
241 250 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
242 251 if @copy
243 252 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
244 253 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
245 254 end
246 255
247 256 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
248 257
249 258 @issue_params = params[:issue] || {}
250 259 @issue_params[:custom_field_values] ||= {}
251 260 end
252 261
253 262 def bulk_update
254 263 @issues.sort!
255 264 @copy = params[:copy].present?
256 265 attributes = parse_params_for_bulk_issue_attributes(params)
257 266
267 if @copy
268 unless User.current.allowed_to?(:copy_issues, @projects)
269 raise ::Unauthorized
270 end
271 target_projects = @projects
272 if attributes['project_id'].present?
273 target_projects = Project.where(:id => attributes['project_id']).to_a
274 end
275 unless User.current.allowed_to?(:add_issues, target_projects)
276 raise ::Unauthorized
277 end
278 end
279
258 280 unsaved_issues = []
259 281 saved_issues = []
260 282
261 283 if @copy && params[:copy_subtasks].present?
262 284 # Descendant issues will be copied with the parent task
263 285 # Don't copy them twice
264 286 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
265 287 end
266 288
267 289 @issues.each do |orig_issue|
268 290 orig_issue.reload
269 291 if @copy
270 292 issue = orig_issue.copy({},
271 293 :attachments => params[:copy_attachments].present?,
272 294 :subtasks => params[:copy_subtasks].present?,
273 295 :link => link_copy?(params[:link_copy])
274 296 )
275 297 else
276 298 issue = orig_issue
277 299 end
278 300 journal = issue.init_journal(User.current, params[:notes])
279 301 issue.safe_attributes = attributes
280 302 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
281 303 if issue.save
282 304 saved_issues << issue
283 305 else
284 306 unsaved_issues << orig_issue
285 307 end
286 308 end
287 309
288 310 if unsaved_issues.empty?
289 311 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
290 312 if params[:follow]
291 313 if @issues.size == 1 && saved_issues.size == 1
292 314 redirect_to issue_path(saved_issues.first)
293 315 elsif saved_issues.map(&:project).uniq.size == 1
294 316 redirect_to project_issues_path(saved_issues.map(&:project).first)
295 317 end
296 318 else
297 319 redirect_back_or_default _project_issues_path(@project)
298 320 end
299 321 else
300 322 @saved_issues = @issues
301 323 @unsaved_issues = unsaved_issues
302 324 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
303 325 bulk_edit
304 326 render :action => 'bulk_edit'
305 327 end
306 328 end
307 329
308 330 def destroy
309 331 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
310 332 if @hours > 0
311 333 case params[:todo]
312 334 when 'destroy'
313 335 # nothing to do
314 336 when 'nullify'
315 337 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
316 338 when 'reassign'
317 339 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
318 340 if reassign_to.nil?
319 341 flash.now[:error] = l(:error_issue_not_found_in_project)
320 342 return
321 343 else
322 344 TimeEntry.where(['issue_id IN (?)', @issues]).
323 345 update_all("issue_id = #{reassign_to.id}")
324 346 end
325 347 else
326 348 # display the destroy form if it's a user request
327 349 return unless api_request?
328 350 end
329 351 end
330 352 @issues.each do |issue|
331 353 begin
332 354 issue.reload.destroy
333 355 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
334 356 # nothing to do, issue was already deleted (eg. by a parent)
335 357 end
336 358 end
337 359 respond_to do |format|
338 360 format.html { redirect_back_or_default _project_issues_path(@project) }
339 361 format.api { render_api_ok }
340 362 end
341 363 end
342 364
343 365 private
344 366
345 367 def find_project
346 368 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
347 369 @project = Project.find(project_id)
348 370 rescue ActiveRecord::RecordNotFound
349 371 render_404
350 372 end
351 373
352 374 def retrieve_previous_and_next_issue_ids
353 375 retrieve_query_from_session
354 376 if @query
355 377 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
356 378 sort_update(@query.sortable_columns, 'issues_index_sort')
357 379 limit = 500
358 380 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
359 381 if (idx = issue_ids.index(@issue.id)) && idx < limit
360 382 if issue_ids.size < 500
361 383 @issue_position = idx + 1
362 384 @issue_count = issue_ids.size
363 385 end
364 386 @prev_issue_id = issue_ids[idx - 1] if idx > 0
365 387 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
366 388 end
367 389 end
368 390 end
369 391
370 392 # Used by #edit and #update to set some common instance variables
371 393 # from the params
372 394 # TODO: Refactor, not everything in here is needed by #edit
373 395 def update_issue_from_params
374 396 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
375 397 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
376 398 if params[:time_entry]
377 399 @time_entry.attributes = params[:time_entry]
378 400 end
379 401
380 402 @issue.init_journal(User.current)
381 403
382 404 issue_attributes = params[:issue]
383 405 if issue_attributes && params[:conflict_resolution]
384 406 case params[:conflict_resolution]
385 407 when 'overwrite'
386 408 issue_attributes = issue_attributes.dup
387 409 issue_attributes.delete(:lock_version)
388 410 when 'add_notes'
389 411 issue_attributes = issue_attributes.slice(:notes)
390 412 when 'cancel'
391 413 redirect_to issue_path(@issue)
392 414 return false
393 415 end
394 416 end
395 417 @issue.safe_attributes = issue_attributes
396 418 @priorities = IssuePriority.active
397 419 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
398 420 true
399 421 end
400 422
401 423 # TODO: Refactor, lots of extra code in here
402 424 # TODO: Changing tracker on an existing issue should not trigger this
403 425 def build_new_issue_from_params
404 426 if params[:id].blank?
405 427 @issue = Issue.new
406 428 if params[:copy_from]
407 429 begin
408 430 @issue.init_journal(User.current)
409 431 @copy_from = Issue.visible.find(params[:copy_from])
432 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
433 raise ::Unauthorized
434 end
410 435 @link_copy = link_copy?(params[:link_copy]) || request.get?
411 436 @copy_attachments = params[:copy_attachments].present? || request.get?
412 437 @copy_subtasks = params[:copy_subtasks].present? || request.get?
413 438 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
414 439 rescue ActiveRecord::RecordNotFound
415 440 render_404
416 441 return
417 442 end
418 443 end
419 444 @issue.project = @project
420 445 @issue.author ||= User.current
421 446 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
422 447 else
423 448 @issue = @project.issues.visible.find(params[:id])
424 449 end
425 450
426 451 if attrs = params[:issue].deep_dup
427 452 if params[:was_default_status] == attrs[:status_id]
428 453 attrs.delete(:status_id)
429 454 end
430 455 @issue.safe_attributes = attrs
431 456 end
432 457 @issue.tracker ||= @project.trackers.first
433 458 if @issue.tracker.nil?
434 459 render_error l(:error_no_tracker_in_project)
435 460 return false
436 461 end
437 462 if @issue.status.nil?
438 463 render_error l(:error_no_default_issue_status)
439 464 return false
440 465 end
441 466
442 467 @priorities = IssuePriority.active
443 468 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
444 469 @available_watchers = @issue.watcher_users
445 470 if @issue.project.users.count <= 20
446 471 @available_watchers = (@available_watchers + @issue.project.users.sort).uniq
447 472 end
448 473 end
449 474
450 475 def parse_params_for_bulk_issue_attributes(params)
451 476 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
452 477 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
453 478 if custom = attributes[:custom_field_values]
454 479 custom.reject! {|k,v| v.blank?}
455 480 custom.keys.each do |k|
456 481 if custom[k].is_a?(Array)
457 482 custom[k] << '' if custom[k].delete('__none__')
458 483 else
459 484 custom[k] = '' if custom[k] == '__none__'
460 485 end
461 486 end
462 487 end
463 488 attributes
464 489 end
465 490
466 491 # Saves @issue and a time_entry from the parameters
467 492 def save_issue_with_child_records
468 493 Issue.transaction do
469 494 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
470 495 time_entry = @time_entry || TimeEntry.new
471 496 time_entry.project = @issue.project
472 497 time_entry.issue = @issue
473 498 time_entry.user = User.current
474 499 time_entry.spent_on = User.current.today
475 500 time_entry.attributes = params[:time_entry]
476 501 @issue.time_entries << time_entry
477 502 end
478 503
479 504 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
480 505 if @issue.save
481 506 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
482 507 else
483 508 raise ActiveRecord::Rollback
484 509 end
485 510 end
486 511 end
487 512
488 513 def link_copy?(param)
489 514 case Setting.link_copied_issue
490 515 when 'yes'
491 516 true
492 517 when 'no'
493 518 false
494 519 when 'ask'
495 520 param == '1'
496 521 end
497 522 end
498 523 end
@@ -1,1318 +1,1321
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28
29 29 extend Forwardable
30 30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 31
32 32 # Return true if user is authorized for controller/action, otherwise false
33 33 def authorize_for(controller, action)
34 34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 35 end
36 36
37 37 # Display a link if user is authorized
38 38 #
39 39 # @param [String] name Anchor text (passed to link_to)
40 40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 41 # @param [optional, Hash] html_options Options passed to link_to
42 42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active? || (User.current.admin? && user.logged?)
52 52 link_to name, user_path(user), :class => user.css_classes
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 # Displays a link to +issue+ with its subject.
62 62 # Examples:
63 63 #
64 64 # link_to_issue(issue) # => Defect #6: This is the subject
65 65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 66 # link_to_issue(issue, :subject => false) # => Defect #6
67 67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 69 #
70 70 def link_to_issue(issue, options={})
71 71 title = nil
72 72 subject = nil
73 73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 74 if options[:subject] == false
75 75 title = issue.subject.truncate(60)
76 76 else
77 77 subject = issue.subject
78 78 if truncate_length = options[:truncate]
79 79 subject = subject.truncate(truncate_length)
80 80 end
81 81 end
82 82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 83 s = link_to(text, issue_url(issue, :only_path => only_path),
84 84 :class => issue.css_classes, :title => title)
85 85 s << h(": #{subject}") if subject
86 86 s = h("#{issue.project} - ") + s if options[:project]
87 87 s
88 88 end
89 89
90 90 # Generates a link to an attachment.
91 91 # Options:
92 92 # * :text - Link text (default to attachment filename)
93 93 # * :download - Force download (default: false)
94 94 def link_to_attachment(attachment, options={})
95 95 text = options.delete(:text) || attachment.filename
96 96 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
97 97 html_options = options.slice!(:only_path)
98 98 options[:only_path] = true unless options.key?(:only_path)
99 99 url = send(route_method, attachment, attachment.filename, options)
100 100 link_to text, url, html_options
101 101 end
102 102
103 103 # Generates a link to a SCM revision
104 104 # Options:
105 105 # * :text - Link text (default to the formatted revision)
106 106 def link_to_revision(revision, repository, options={})
107 107 if repository.is_a?(Project)
108 108 repository = repository.repository
109 109 end
110 110 text = options.delete(:text) || format_revision(revision)
111 111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 112 link_to(
113 113 h(text),
114 114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 115 :title => l(:label_revision_id, format_revision(revision)),
116 116 :accesskey => options[:accesskey]
117 117 )
118 118 end
119 119
120 120 # Generates a link to a message
121 121 def link_to_message(message, options={}, html_options = nil)
122 122 link_to(
123 123 message.subject.truncate(60),
124 124 board_message_url(message.board_id, message.parent_id || message.id, {
125 125 :r => (message.parent_id && message.id),
126 126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
127 127 :only_path => true
128 128 }.merge(options)),
129 129 html_options
130 130 )
131 131 end
132 132
133 133 # Generates a link to a project if active
134 134 # Examples:
135 135 #
136 136 # link_to_project(project) # => link to the specified project overview
137 137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 139 #
140 140 def link_to_project(project, options={}, html_options = nil)
141 141 if project.archived?
142 142 h(project.name)
143 143 else
144 144 link_to project.name,
145 145 project_url(project, {:only_path => true}.merge(options)),
146 146 html_options
147 147 end
148 148 end
149 149
150 150 # Generates a link to a project settings if active
151 151 def link_to_project_settings(project, options={}, html_options=nil)
152 152 if project.active?
153 153 link_to project.name, settings_project_path(project, options), html_options
154 154 elsif project.archived?
155 155 h(project.name)
156 156 else
157 157 link_to project.name, project_path(project, options), html_options
158 158 end
159 159 end
160 160
161 161 # Generates a link to a version
162 162 def link_to_version(version, options = {})
163 163 return '' unless version && version.is_a?(Version)
164 164 options = {:title => format_date(version.effective_date)}.merge(options)
165 165 link_to_if version.visible?, format_version_name(version), version_path(version), options
166 166 end
167 167
168 168 # Helper that formats object for html or text rendering
169 169 def format_object(object, html=true, &block)
170 170 if block_given?
171 171 object = yield object
172 172 end
173 173 case object.class.name
174 174 when 'Array'
175 175 object.map {|o| format_object(o, html)}.join(', ').html_safe
176 176 when 'Time'
177 177 format_time(object)
178 178 when 'Date'
179 179 format_date(object)
180 180 when 'Fixnum'
181 181 object.to_s
182 182 when 'Float'
183 183 sprintf "%.2f", object
184 184 when 'User'
185 185 html ? link_to_user(object) : object.to_s
186 186 when 'Project'
187 187 html ? link_to_project(object) : object.to_s
188 188 when 'Version'
189 189 html ? link_to_version(object) : object.to_s
190 190 when 'TrueClass'
191 191 l(:general_text_Yes)
192 192 when 'FalseClass'
193 193 l(:general_text_No)
194 194 when 'Issue'
195 195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
196 196 when 'CustomValue', 'CustomFieldValue'
197 197 if object.custom_field
198 198 f = object.custom_field.format.formatted_custom_value(self, object, html)
199 199 if f.nil? || f.is_a?(String)
200 200 f
201 201 else
202 202 format_object(f, html, &block)
203 203 end
204 204 else
205 205 object.value.to_s
206 206 end
207 207 else
208 208 html ? h(object) : object.to_s
209 209 end
210 210 end
211 211
212 212 def wiki_page_path(page, options={})
213 213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
214 214 end
215 215
216 216 def thumbnail_tag(attachment)
217 217 link_to image_tag(thumbnail_path(attachment)),
218 218 named_attachment_path(attachment, attachment.filename),
219 219 :title => attachment.filename
220 220 end
221 221
222 222 def toggle_link(name, id, options={})
223 223 onclick = "$('##{id}').toggle(); "
224 224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
225 225 onclick << "return false;"
226 226 link_to(name, "#", :onclick => onclick)
227 227 end
228 228
229 229 def format_activity_title(text)
230 230 h(truncate_single_line_raw(text, 100))
231 231 end
232 232
233 233 def format_activity_day(date)
234 234 date == User.current.today ? l(:label_today).titleize : format_date(date)
235 235 end
236 236
237 237 def format_activity_description(text)
238 238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
239 239 ).gsub(/[\r\n]+/, "<br />").html_safe
240 240 end
241 241
242 242 def format_version_name(version)
243 243 if !version.shared? || version.project == @project
244 244 h(version)
245 245 else
246 246 h("#{version.project} - #{version}")
247 247 end
248 248 end
249 249
250 250 def due_date_distance_in_words(date)
251 251 if date
252 252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
253 253 end
254 254 end
255 255
256 256 # Renders a tree of projects as a nested set of unordered lists
257 257 # The given collection may be a subset of the whole project tree
258 258 # (eg. some intermediate nodes are private and can not be seen)
259 259 def render_project_nested_lists(projects, &block)
260 260 s = ''
261 261 if projects.any?
262 262 ancestors = []
263 263 original_project = @project
264 264 projects.sort_by(&:lft).each do |project|
265 265 # set the project environment to please macros.
266 266 @project = project
267 267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
269 269 else
270 270 ancestors.pop
271 271 s << "</li>"
272 272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 273 ancestors.pop
274 274 s << "</ul></li>\n"
275 275 end
276 276 end
277 277 classes = (ancestors.empty? ? 'root' : 'child')
278 278 s << "<li class='#{classes}'><div class='#{classes}'>"
279 279 s << h(block_given? ? capture(project, &block) : project.name)
280 280 s << "</div>\n"
281 281 ancestors << project
282 282 end
283 283 s << ("</li></ul>\n" * ancestors.size)
284 284 @project = original_project
285 285 end
286 286 s.html_safe
287 287 end
288 288
289 289 def render_page_hierarchy(pages, node=nil, options={})
290 290 content = ''
291 291 if pages[node]
292 292 content << "<ul class=\"pages-hierarchy\">\n"
293 293 pages[node].each do |page|
294 294 content << "<li>"
295 295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
296 296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
297 297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
298 298 content << "</li>\n"
299 299 end
300 300 content << "</ul>\n"
301 301 end
302 302 content.html_safe
303 303 end
304 304
305 305 # Renders flash messages
306 306 def render_flash_messages
307 307 s = ''
308 308 flash.each do |k,v|
309 309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
310 310 end
311 311 s.html_safe
312 312 end
313 313
314 314 # Renders tabs and their content
315 315 def render_tabs(tabs, selected=params[:tab])
316 316 if tabs.any?
317 317 unless tabs.detect {|tab| tab[:name] == selected}
318 318 selected = nil
319 319 end
320 320 selected ||= tabs.first[:name]
321 321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
322 322 else
323 323 content_tag 'p', l(:label_no_data), :class => "nodata"
324 324 end
325 325 end
326 326
327 327 # Renders the project quick-jump box
328 328 def render_project_jump_box
329 329 return unless User.current.logged?
330 330 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
331 331 if projects.any?
332 332 options =
333 333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
334 334 '<option value="" disabled="disabled">---</option>').html_safe
335 335
336 336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
337 337 { :value => project_path(:id => p, :jump => current_menu_item) }
338 338 end
339 339
340 340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
341 341 end
342 342 end
343 343
344 344 def project_tree_options_for_select(projects, options = {})
345 345 s = ''.html_safe
346 if options[:include_blank]
347 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
346 if blank_text = options[:include_blank]
347 if blank_text == true
348 blank_text = '&nbsp;'.html_safe
349 end
350 s << content_tag('option', blank_text, :value => '')
348 351 end
349 352 project_tree(projects) do |project, level|
350 353 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
351 354 tag_options = {:value => project.id}
352 355 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
353 356 tag_options[:selected] = 'selected'
354 357 else
355 358 tag_options[:selected] = nil
356 359 end
357 360 tag_options.merge!(yield(project)) if block_given?
358 361 s << content_tag('option', name_prefix + h(project), tag_options)
359 362 end
360 363 s.html_safe
361 364 end
362 365
363 366 # Yields the given block for each project with its level in the tree
364 367 #
365 368 # Wrapper for Project#project_tree
366 369 def project_tree(projects, &block)
367 370 Project.project_tree(projects, &block)
368 371 end
369 372
370 373 def principals_check_box_tags(name, principals)
371 374 s = ''
372 375 principals.each do |principal|
373 376 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
374 377 end
375 378 s.html_safe
376 379 end
377 380
378 381 # Returns a string for users/groups option tags
379 382 def principals_options_for_select(collection, selected=nil)
380 383 s = ''
381 384 if collection.include?(User.current)
382 385 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
383 386 end
384 387 groups = ''
385 388 collection.sort.each do |element|
386 389 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
387 390 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
388 391 end
389 392 unless groups.empty?
390 393 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
391 394 end
392 395 s.html_safe
393 396 end
394 397
395 398 def option_tag(name, text, value, selected=nil, options={})
396 399 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
397 400 end
398 401
399 402 def truncate_single_line_raw(string, length)
400 403 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
401 404 end
402 405
403 406 # Truncates at line break after 250 characters or options[:length]
404 407 def truncate_lines(string, options={})
405 408 length = options[:length] || 250
406 409 if string.to_s =~ /\A(.{#{length}}.*?)$/m
407 410 "#{$1}..."
408 411 else
409 412 string
410 413 end
411 414 end
412 415
413 416 def anchor(text)
414 417 text.to_s.gsub(' ', '_')
415 418 end
416 419
417 420 def html_hours(text)
418 421 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
419 422 end
420 423
421 424 def authoring(created, author, options={})
422 425 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
423 426 end
424 427
425 428 def time_tag(time)
426 429 text = distance_of_time_in_words(Time.now, time)
427 430 if @project
428 431 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
429 432 else
430 433 content_tag('abbr', text, :title => format_time(time))
431 434 end
432 435 end
433 436
434 437 def syntax_highlight_lines(name, content)
435 438 lines = []
436 439 syntax_highlight(name, content).each_line { |line| lines << line }
437 440 lines
438 441 end
439 442
440 443 def syntax_highlight(name, content)
441 444 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
442 445 end
443 446
444 447 def to_path_param(path)
445 448 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
446 449 str.blank? ? nil : str
447 450 end
448 451
449 452 def reorder_links(name, url, method = :post)
450 453 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
451 454 url.merge({"#{name}[move_to]" => 'highest'}),
452 455 :method => method, :title => l(:label_sort_highest)) +
453 456 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
454 457 url.merge({"#{name}[move_to]" => 'higher'}),
455 458 :method => method, :title => l(:label_sort_higher)) +
456 459 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
457 460 url.merge({"#{name}[move_to]" => 'lower'}),
458 461 :method => method, :title => l(:label_sort_lower)) +
459 462 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
460 463 url.merge({"#{name}[move_to]" => 'lowest'}),
461 464 :method => method, :title => l(:label_sort_lowest))
462 465 end
463 466
464 467 def breadcrumb(*args)
465 468 elements = args.flatten
466 469 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
467 470 end
468 471
469 472 def other_formats_links(&block)
470 473 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
471 474 yield Redmine::Views::OtherFormatsBuilder.new(self)
472 475 concat('</p>'.html_safe)
473 476 end
474 477
475 478 def page_header_title
476 479 if @project.nil? || @project.new_record?
477 480 h(Setting.app_title)
478 481 else
479 482 b = []
480 483 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
481 484 if ancestors.any?
482 485 root = ancestors.shift
483 486 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
484 487 if ancestors.size > 2
485 488 b << "\xe2\x80\xa6"
486 489 ancestors = ancestors[-2, 2]
487 490 end
488 491 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
489 492 end
490 493 b << h(@project)
491 494 b.join(" \xc2\xbb ").html_safe
492 495 end
493 496 end
494 497
495 498 # Returns a h2 tag and sets the html title with the given arguments
496 499 def title(*args)
497 500 strings = args.map do |arg|
498 501 if arg.is_a?(Array) && arg.size >= 2
499 502 link_to(*arg)
500 503 else
501 504 h(arg.to_s)
502 505 end
503 506 end
504 507 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
505 508 content_tag('h2', strings.join(' &#187; ').html_safe)
506 509 end
507 510
508 511 # Sets the html title
509 512 # Returns the html title when called without arguments
510 513 # Current project name and app_title and automatically appended
511 514 # Exemples:
512 515 # html_title 'Foo', 'Bar'
513 516 # html_title # => 'Foo - Bar - My Project - Redmine'
514 517 def html_title(*args)
515 518 if args.empty?
516 519 title = @html_title || []
517 520 title << @project.name if @project
518 521 title << Setting.app_title unless Setting.app_title == title.last
519 522 title.reject(&:blank?).join(' - ')
520 523 else
521 524 @html_title ||= []
522 525 @html_title += args
523 526 end
524 527 end
525 528
526 529 # Returns the theme, controller name, and action as css classes for the
527 530 # HTML body.
528 531 def body_css_classes
529 532 css = []
530 533 if theme = Redmine::Themes.theme(Setting.ui_theme)
531 534 css << 'theme-' + theme.name
532 535 end
533 536
534 537 css << 'project-' + @project.identifier if @project && @project.identifier.present?
535 538 css << 'controller-' + controller_name
536 539 css << 'action-' + action_name
537 540 css.join(' ')
538 541 end
539 542
540 543 def accesskey(s)
541 544 @used_accesskeys ||= []
542 545 key = Redmine::AccessKeys.key_for(s)
543 546 return nil if @used_accesskeys.include?(key)
544 547 @used_accesskeys << key
545 548 key
546 549 end
547 550
548 551 # Formats text according to system settings.
549 552 # 2 ways to call this method:
550 553 # * with a String: textilizable(text, options)
551 554 # * with an object and one of its attribute: textilizable(issue, :description, options)
552 555 def textilizable(*args)
553 556 options = args.last.is_a?(Hash) ? args.pop : {}
554 557 case args.size
555 558 when 1
556 559 obj = options[:object]
557 560 text = args.shift
558 561 when 2
559 562 obj = args.shift
560 563 attr = args.shift
561 564 text = obj.send(attr).to_s
562 565 else
563 566 raise ArgumentError, 'invalid arguments to textilizable'
564 567 end
565 568 return '' if text.blank?
566 569 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
567 570 @only_path = only_path = options.delete(:only_path) == false ? false : true
568 571
569 572 text = text.dup
570 573 macros = catch_macros(text)
571 574 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
572 575
573 576 @parsed_headings = []
574 577 @heading_anchors = {}
575 578 @current_section = 0 if options[:edit_section_links]
576 579
577 580 parse_sections(text, project, obj, attr, only_path, options)
578 581 text = parse_non_pre_blocks(text, obj, macros) do |text|
579 582 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
580 583 send method_name, text, project, obj, attr, only_path, options
581 584 end
582 585 end
583 586 parse_headings(text, project, obj, attr, only_path, options)
584 587
585 588 if @parsed_headings.any?
586 589 replace_toc(text, @parsed_headings)
587 590 end
588 591
589 592 text.html_safe
590 593 end
591 594
592 595 def parse_non_pre_blocks(text, obj, macros)
593 596 s = StringScanner.new(text)
594 597 tags = []
595 598 parsed = ''
596 599 while !s.eos?
597 600 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
598 601 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
599 602 if tags.empty?
600 603 yield text
601 604 inject_macros(text, obj, macros) if macros.any?
602 605 else
603 606 inject_macros(text, obj, macros, false) if macros.any?
604 607 end
605 608 parsed << text
606 609 if tag
607 610 if closing
608 611 if tags.last == tag.downcase
609 612 tags.pop
610 613 end
611 614 else
612 615 tags << tag.downcase
613 616 end
614 617 parsed << full_tag
615 618 end
616 619 end
617 620 # Close any non closing tags
618 621 while tag = tags.pop
619 622 parsed << "</#{tag}>"
620 623 end
621 624 parsed
622 625 end
623 626
624 627 def parse_inline_attachments(text, project, obj, attr, only_path, options)
625 628 return if options[:inline_attachments] == false
626 629
627 630 # when using an image link, try to use an attachment, if possible
628 631 attachments = options[:attachments] || []
629 632 attachments += obj.attachments if obj.respond_to?(:attachments)
630 633 if attachments.present?
631 634 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
632 635 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
633 636 # search for the picture in attachments
634 637 if found = Attachment.latest_attach(attachments, filename)
635 638 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
636 639 desc = found.description.to_s.gsub('"', '')
637 640 if !desc.blank? && alttext.blank?
638 641 alt = " title=\"#{desc}\" alt=\"#{desc}\""
639 642 end
640 643 "src=\"#{image_url}\"#{alt}"
641 644 else
642 645 m
643 646 end
644 647 end
645 648 end
646 649 end
647 650
648 651 # Wiki links
649 652 #
650 653 # Examples:
651 654 # [[mypage]]
652 655 # [[mypage|mytext]]
653 656 # wiki links can refer other project wikis, using project name or identifier:
654 657 # [[project:]] -> wiki starting page
655 658 # [[project:|mytext]]
656 659 # [[project:mypage]]
657 660 # [[project:mypage|mytext]]
658 661 def parse_wiki_links(text, project, obj, attr, only_path, options)
659 662 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
660 663 link_project = project
661 664 esc, all, page, title = $1, $2, $3, $5
662 665 if esc.nil?
663 666 if page =~ /^([^\:]+)\:(.*)$/
664 667 identifier, page = $1, $2
665 668 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
666 669 title ||= identifier if page.blank?
667 670 end
668 671
669 672 if link_project && link_project.wiki
670 673 # extract anchor
671 674 anchor = nil
672 675 if page =~ /^(.+?)\#(.+)$/
673 676 page, anchor = $1, $2
674 677 end
675 678 anchor = sanitize_anchor_name(anchor) if anchor.present?
676 679 # check if page exists
677 680 wiki_page = link_project.wiki.find_page(page)
678 681 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
679 682 "##{anchor}"
680 683 else
681 684 case options[:wiki_links]
682 685 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
683 686 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
684 687 else
685 688 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
686 689 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
687 690 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
688 691 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
689 692 end
690 693 end
691 694 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
692 695 else
693 696 # project or wiki doesn't exist
694 697 all
695 698 end
696 699 else
697 700 all
698 701 end
699 702 end
700 703 end
701 704
702 705 # Redmine links
703 706 #
704 707 # Examples:
705 708 # Issues:
706 709 # #52 -> Link to issue #52
707 710 # Changesets:
708 711 # r52 -> Link to revision 52
709 712 # commit:a85130f -> Link to scmid starting with a85130f
710 713 # Documents:
711 714 # document#17 -> Link to document with id 17
712 715 # document:Greetings -> Link to the document with title "Greetings"
713 716 # document:"Some document" -> Link to the document with title "Some document"
714 717 # Versions:
715 718 # version#3 -> Link to version with id 3
716 719 # version:1.0.0 -> Link to version named "1.0.0"
717 720 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
718 721 # Attachments:
719 722 # attachment:file.zip -> Link to the attachment of the current object named file.zip
720 723 # Source files:
721 724 # source:some/file -> Link to the file located at /some/file in the project's repository
722 725 # source:some/file@52 -> Link to the file's revision 52
723 726 # source:some/file#L120 -> Link to line 120 of the file
724 727 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
725 728 # export:some/file -> Force the download of the file
726 729 # Forum messages:
727 730 # message#1218 -> Link to message with id 1218
728 731 # Projects:
729 732 # project:someproject -> Link to project named "someproject"
730 733 # project#3 -> Link to project with id 3
731 734 #
732 735 # Links can refer other objects from other projects, using project identifier:
733 736 # identifier:r52
734 737 # identifier:document:"Some document"
735 738 # identifier:version:1.0.0
736 739 # identifier:source:some/file
737 740 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
738 741 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
739 742 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
740 743 if tag_content
741 744 $&
742 745 else
743 746 link = nil
744 747 project = default_project
745 748 if project_identifier
746 749 project = Project.visible.find_by_identifier(project_identifier)
747 750 end
748 751 if esc.nil?
749 752 if prefix.nil? && sep == 'r'
750 753 if project
751 754 repository = nil
752 755 if repo_identifier
753 756 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
754 757 else
755 758 repository = project.repository
756 759 end
757 760 # project.changesets.visible raises an SQL error because of a double join on repositories
758 761 if repository &&
759 762 (changeset = Changeset.visible.
760 763 find_by_repository_id_and_revision(repository.id, identifier))
761 764 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
762 765 {:only_path => only_path, :controller => 'repositories',
763 766 :action => 'revision', :id => project,
764 767 :repository_id => repository.identifier_param,
765 768 :rev => changeset.revision},
766 769 :class => 'changeset',
767 770 :title => truncate_single_line_raw(changeset.comments, 100))
768 771 end
769 772 end
770 773 elsif sep == '#'
771 774 oid = identifier.to_i
772 775 case prefix
773 776 when nil
774 777 if oid.to_s == identifier &&
775 778 issue = Issue.visible.find_by_id(oid)
776 779 anchor = comment_id ? "note-#{comment_id}" : nil
777 780 link = link_to("##{oid}#{comment_suffix}",
778 781 issue_url(issue, :only_path => only_path, :anchor => anchor),
779 782 :class => issue.css_classes,
780 783 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
781 784 end
782 785 when 'document'
783 786 if document = Document.visible.find_by_id(oid)
784 787 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
785 788 end
786 789 when 'version'
787 790 if version = Version.visible.find_by_id(oid)
788 791 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
789 792 end
790 793 when 'message'
791 794 if message = Message.visible.find_by_id(oid)
792 795 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
793 796 end
794 797 when 'forum'
795 798 if board = Board.visible.find_by_id(oid)
796 799 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
797 800 end
798 801 when 'news'
799 802 if news = News.visible.find_by_id(oid)
800 803 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
801 804 end
802 805 when 'project'
803 806 if p = Project.visible.find_by_id(oid)
804 807 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
805 808 end
806 809 end
807 810 elsif sep == ':'
808 811 # removes the double quotes if any
809 812 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
810 813 name = CGI.unescapeHTML(name)
811 814 case prefix
812 815 when 'document'
813 816 if project && document = project.documents.visible.find_by_title(name)
814 817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
815 818 end
816 819 when 'version'
817 820 if project && version = project.versions.visible.find_by_name(name)
818 821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
819 822 end
820 823 when 'forum'
821 824 if project && board = project.boards.visible.find_by_name(name)
822 825 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
823 826 end
824 827 when 'news'
825 828 if project && news = project.news.visible.find_by_title(name)
826 829 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
827 830 end
828 831 when 'commit', 'source', 'export'
829 832 if project
830 833 repository = nil
831 834 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
832 835 repo_prefix, repo_identifier, name = $1, $2, $3
833 836 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
834 837 else
835 838 repository = project.repository
836 839 end
837 840 if prefix == 'commit'
838 841 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
839 842 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
840 843 :class => 'changeset',
841 844 :title => truncate_single_line_raw(changeset.comments, 100)
842 845 end
843 846 else
844 847 if repository && User.current.allowed_to?(:browse_repository, project)
845 848 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
846 849 path, rev, anchor = $1, $3, $5
847 850 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
848 851 :path => to_path_param(path),
849 852 :rev => rev,
850 853 :anchor => anchor},
851 854 :class => (prefix == 'export' ? 'source download' : 'source')
852 855 end
853 856 end
854 857 repo_prefix = nil
855 858 end
856 859 when 'attachment'
857 860 attachments = options[:attachments] || []
858 861 attachments += obj.attachments if obj.respond_to?(:attachments)
859 862 if attachments && attachment = Attachment.latest_attach(attachments, name)
860 863 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
861 864 end
862 865 when 'project'
863 866 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
864 867 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
865 868 end
866 869 end
867 870 end
868 871 end
869 872 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
870 873 end
871 874 end
872 875 end
873 876
874 877 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
875 878
876 879 def parse_sections(text, project, obj, attr, only_path, options)
877 880 return unless options[:edit_section_links]
878 881 text.gsub!(HEADING_RE) do
879 882 heading = $1
880 883 @current_section += 1
881 884 if @current_section > 1
882 885 content_tag('div',
883 886 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
884 887 :class => 'contextual',
885 888 :title => l(:button_edit_section),
886 889 :id => "section-#{@current_section}") + heading.html_safe
887 890 else
888 891 heading
889 892 end
890 893 end
891 894 end
892 895
893 896 # Headings and TOC
894 897 # Adds ids and links to headings unless options[:headings] is set to false
895 898 def parse_headings(text, project, obj, attr, only_path, options)
896 899 return if options[:headings] == false
897 900
898 901 text.gsub!(HEADING_RE) do
899 902 level, attrs, content = $2.to_i, $3, $4
900 903 item = strip_tags(content).strip
901 904 anchor = sanitize_anchor_name(item)
902 905 # used for single-file wiki export
903 906 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
904 907 @heading_anchors[anchor] ||= 0
905 908 idx = (@heading_anchors[anchor] += 1)
906 909 if idx > 1
907 910 anchor = "#{anchor}-#{idx}"
908 911 end
909 912 @parsed_headings << [level, anchor, item]
910 913 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
911 914 end
912 915 end
913 916
914 917 MACROS_RE = /(
915 918 (!)? # escaping
916 919 (
917 920 \{\{ # opening tag
918 921 ([\w]+) # macro name
919 922 (\(([^\n\r]*?)\))? # optional arguments
920 923 ([\n\r].*?[\n\r])? # optional block of text
921 924 \}\} # closing tag
922 925 )
923 926 )/mx unless const_defined?(:MACROS_RE)
924 927
925 928 MACRO_SUB_RE = /(
926 929 \{\{
927 930 macro\((\d+)\)
928 931 \}\}
929 932 )/x unless const_defined?(:MACRO_SUB_RE)
930 933
931 934 # Extracts macros from text
932 935 def catch_macros(text)
933 936 macros = {}
934 937 text.gsub!(MACROS_RE) do
935 938 all, macro = $1, $4.downcase
936 939 if macro_exists?(macro) || all =~ MACRO_SUB_RE
937 940 index = macros.size
938 941 macros[index] = all
939 942 "{{macro(#{index})}}"
940 943 else
941 944 all
942 945 end
943 946 end
944 947 macros
945 948 end
946 949
947 950 # Executes and replaces macros in text
948 951 def inject_macros(text, obj, macros, execute=true)
949 952 text.gsub!(MACRO_SUB_RE) do
950 953 all, index = $1, $2.to_i
951 954 orig = macros.delete(index)
952 955 if execute && orig && orig =~ MACROS_RE
953 956 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
954 957 if esc.nil?
955 958 h(exec_macro(macro, obj, args, block) || all)
956 959 else
957 960 h(all)
958 961 end
959 962 elsif orig
960 963 h(orig)
961 964 else
962 965 h(all)
963 966 end
964 967 end
965 968 end
966 969
967 970 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
968 971
969 972 # Renders the TOC with given headings
970 973 def replace_toc(text, headings)
971 974 text.gsub!(TOC_RE) do
972 975 left_align, right_align = $2, $3
973 976 # Keep only the 4 first levels
974 977 headings = headings.select{|level, anchor, item| level <= 4}
975 978 if headings.empty?
976 979 ''
977 980 else
978 981 div_class = 'toc'
979 982 div_class << ' right' if right_align
980 983 div_class << ' left' if left_align
981 984 out = "<ul class=\"#{div_class}\"><li>"
982 985 root = headings.map(&:first).min
983 986 current = root
984 987 started = false
985 988 headings.each do |level, anchor, item|
986 989 if level > current
987 990 out << '<ul><li>' * (level - current)
988 991 elsif level < current
989 992 out << "</li></ul>\n" * (current - level) + "</li><li>"
990 993 elsif started
991 994 out << '</li><li>'
992 995 end
993 996 out << "<a href=\"##{anchor}\">#{item}</a>"
994 997 current = level
995 998 started = true
996 999 end
997 1000 out << '</li></ul>' * (current - root)
998 1001 out << '</li></ul>'
999 1002 end
1000 1003 end
1001 1004 end
1002 1005
1003 1006 # Same as Rails' simple_format helper without using paragraphs
1004 1007 def simple_format_without_paragraph(text)
1005 1008 text.to_s.
1006 1009 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1007 1010 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1008 1011 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1009 1012 html_safe
1010 1013 end
1011 1014
1012 1015 def lang_options_for_select(blank=true)
1013 1016 (blank ? [["(auto)", ""]] : []) + languages_options
1014 1017 end
1015 1018
1016 1019 def labelled_form_for(*args, &proc)
1017 1020 args << {} unless args.last.is_a?(Hash)
1018 1021 options = args.last
1019 1022 if args.first.is_a?(Symbol)
1020 1023 options.merge!(:as => args.shift)
1021 1024 end
1022 1025 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1023 1026 form_for(*args, &proc)
1024 1027 end
1025 1028
1026 1029 def labelled_fields_for(*args, &proc)
1027 1030 args << {} unless args.last.is_a?(Hash)
1028 1031 options = args.last
1029 1032 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1030 1033 fields_for(*args, &proc)
1031 1034 end
1032 1035
1033 1036 def error_messages_for(*objects)
1034 1037 html = ""
1035 1038 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1036 1039 errors = objects.map {|o| o.errors.full_messages}.flatten
1037 1040 if errors.any?
1038 1041 html << "<div id='errorExplanation'><ul>\n"
1039 1042 errors.each do |error|
1040 1043 html << "<li>#{h error}</li>\n"
1041 1044 end
1042 1045 html << "</ul></div>\n"
1043 1046 end
1044 1047 html.html_safe
1045 1048 end
1046 1049
1047 1050 def delete_link(url, options={})
1048 1051 options = {
1049 1052 :method => :delete,
1050 1053 :data => {:confirm => l(:text_are_you_sure)},
1051 1054 :class => 'icon icon-del'
1052 1055 }.merge(options)
1053 1056
1054 1057 link_to l(:button_delete), url, options
1055 1058 end
1056 1059
1057 1060 def preview_link(url, form, target='preview', options={})
1058 1061 content_tag 'a', l(:label_preview), {
1059 1062 :href => "#",
1060 1063 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1061 1064 :accesskey => accesskey(:preview)
1062 1065 }.merge(options)
1063 1066 end
1064 1067
1065 1068 def link_to_function(name, function, html_options={})
1066 1069 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1067 1070 end
1068 1071
1069 1072 # Helper to render JSON in views
1070 1073 def raw_json(arg)
1071 1074 arg.to_json.to_s.gsub('/', '\/').html_safe
1072 1075 end
1073 1076
1074 1077 def back_url
1075 1078 url = params[:back_url]
1076 1079 if url.nil? && referer = request.env['HTTP_REFERER']
1077 1080 url = CGI.unescape(referer.to_s)
1078 1081 end
1079 1082 url
1080 1083 end
1081 1084
1082 1085 def back_url_hidden_field_tag
1083 1086 url = back_url
1084 1087 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1085 1088 end
1086 1089
1087 1090 def check_all_links(form_name)
1088 1091 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1089 1092 " | ".html_safe +
1090 1093 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1091 1094 end
1092 1095
1093 1096 def toggle_checkboxes_link(selector)
1094 1097 link_to_function image_tag('toggle_check.png'),
1095 1098 "toggleCheckboxesBySelector('#{selector}')",
1096 1099 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1097 1100 end
1098 1101
1099 1102 def progress_bar(pcts, options={})
1100 1103 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1101 1104 pcts = pcts.collect(&:round)
1102 1105 pcts[1] = pcts[1] - pcts[0]
1103 1106 pcts << (100 - pcts[1] - pcts[0])
1104 1107 width = options[:width] || '100px;'
1105 1108 legend = options[:legend] || ''
1106 1109 content_tag('table',
1107 1110 content_tag('tr',
1108 1111 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1109 1112 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1110 1113 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1111 1114 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1112 1115 content_tag('p', legend, :class => 'percent').html_safe
1113 1116 end
1114 1117
1115 1118 def checked_image(checked=true)
1116 1119 if checked
1117 1120 image_tag 'toggle_check.png'
1118 1121 end
1119 1122 end
1120 1123
1121 1124 def context_menu(url)
1122 1125 unless @context_menu_included
1123 1126 content_for :header_tags do
1124 1127 javascript_include_tag('context_menu') +
1125 1128 stylesheet_link_tag('context_menu')
1126 1129 end
1127 1130 if l(:direction) == 'rtl'
1128 1131 content_for :header_tags do
1129 1132 stylesheet_link_tag('context_menu_rtl')
1130 1133 end
1131 1134 end
1132 1135 @context_menu_included = true
1133 1136 end
1134 1137 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1135 1138 end
1136 1139
1137 1140 def calendar_for(field_id)
1138 1141 include_calendar_headers_tags
1139 1142 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1140 1143 end
1141 1144
1142 1145 def include_calendar_headers_tags
1143 1146 unless @calendar_headers_tags_included
1144 1147 tags = ''.html_safe
1145 1148 @calendar_headers_tags_included = true
1146 1149 content_for :header_tags do
1147 1150 start_of_week = Setting.start_of_week
1148 1151 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1149 1152 # Redmine uses 1..7 (monday..sunday) in settings and locales
1150 1153 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1151 1154 start_of_week = start_of_week.to_i % 7
1152 1155 tags << javascript_tag(
1153 1156 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1154 1157 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1155 1158 path_to_image('/images/calendar.png') +
1156 1159 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1157 1160 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1158 1161 "beforeShow: beforeShowDatePicker};")
1159 1162 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1160 1163 unless jquery_locale == 'en'
1161 1164 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1162 1165 end
1163 1166 tags
1164 1167 end
1165 1168 end
1166 1169 end
1167 1170
1168 1171 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1169 1172 # Examples:
1170 1173 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1171 1174 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1172 1175 #
1173 1176 def stylesheet_link_tag(*sources)
1174 1177 options = sources.last.is_a?(Hash) ? sources.pop : {}
1175 1178 plugin = options.delete(:plugin)
1176 1179 sources = sources.map do |source|
1177 1180 if plugin
1178 1181 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1179 1182 elsif current_theme && current_theme.stylesheets.include?(source)
1180 1183 current_theme.stylesheet_path(source)
1181 1184 else
1182 1185 source
1183 1186 end
1184 1187 end
1185 1188 super *sources, options
1186 1189 end
1187 1190
1188 1191 # Overrides Rails' image_tag with themes and plugins support.
1189 1192 # Examples:
1190 1193 # image_tag('image.png') # => picks image.png from the current theme or defaults
1191 1194 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1192 1195 #
1193 1196 def image_tag(source, options={})
1194 1197 if plugin = options.delete(:plugin)
1195 1198 source = "/plugin_assets/#{plugin}/images/#{source}"
1196 1199 elsif current_theme && current_theme.images.include?(source)
1197 1200 source = current_theme.image_path(source)
1198 1201 end
1199 1202 super source, options
1200 1203 end
1201 1204
1202 1205 # Overrides Rails' javascript_include_tag with plugins support
1203 1206 # Examples:
1204 1207 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1205 1208 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1206 1209 #
1207 1210 def javascript_include_tag(*sources)
1208 1211 options = sources.last.is_a?(Hash) ? sources.pop : {}
1209 1212 if plugin = options.delete(:plugin)
1210 1213 sources = sources.map do |source|
1211 1214 if plugin
1212 1215 "/plugin_assets/#{plugin}/javascripts/#{source}"
1213 1216 else
1214 1217 source
1215 1218 end
1216 1219 end
1217 1220 end
1218 1221 super *sources, options
1219 1222 end
1220 1223
1221 1224 def sidebar_content?
1222 1225 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1223 1226 end
1224 1227
1225 1228 def view_layouts_base_sidebar_hook_response
1226 1229 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1227 1230 end
1228 1231
1229 1232 def email_delivery_enabled?
1230 1233 !!ActionMailer::Base.perform_deliveries
1231 1234 end
1232 1235
1233 1236 # Returns the avatar image tag for the given +user+ if avatars are enabled
1234 1237 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1235 1238 def avatar(user, options = { })
1236 1239 if Setting.gravatar_enabled?
1237 1240 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1238 1241 email = nil
1239 1242 if user.respond_to?(:mail)
1240 1243 email = user.mail
1241 1244 elsif user.to_s =~ %r{<(.+?)>}
1242 1245 email = $1
1243 1246 end
1244 1247 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1245 1248 else
1246 1249 ''
1247 1250 end
1248 1251 end
1249 1252
1250 1253 def sanitize_anchor_name(anchor)
1251 1254 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1252 1255 end
1253 1256
1254 1257 # Returns the javascript tags that are included in the html layout head
1255 1258 def javascript_heads
1256 1259 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1257 1260 unless User.current.pref.warn_on_leaving_unsaved == '0'
1258 1261 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1259 1262 end
1260 1263 tags
1261 1264 end
1262 1265
1263 1266 def favicon
1264 1267 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1265 1268 end
1266 1269
1267 1270 # Returns the path to the favicon
1268 1271 def favicon_path
1269 1272 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1270 1273 image_path(icon)
1271 1274 end
1272 1275
1273 1276 # Returns the full URL to the favicon
1274 1277 def favicon_url
1275 1278 # TODO: use #image_url introduced in Rails4
1276 1279 path = favicon_path
1277 1280 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1278 1281 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1279 1282 end
1280 1283
1281 1284 def robot_exclusion_tag
1282 1285 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1283 1286 end
1284 1287
1285 1288 # Returns true if arg is expected in the API response
1286 1289 def include_in_api_response?(arg)
1287 1290 unless @included_in_api_response
1288 1291 param = params[:include]
1289 1292 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1290 1293 @included_in_api_response.collect!(&:strip)
1291 1294 end
1292 1295 @included_in_api_response.include?(arg.to_s)
1293 1296 end
1294 1297
1295 1298 # Returns options or nil if nometa param or X-Redmine-Nometa header
1296 1299 # was set in the request
1297 1300 def api_meta(options)
1298 1301 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1299 1302 # compatibility mode for activeresource clients that raise
1300 1303 # an error when deserializing an array with attributes
1301 1304 nil
1302 1305 else
1303 1306 options
1304 1307 end
1305 1308 end
1306 1309
1307 1310 private
1308 1311
1309 1312 def wiki_helper
1310 1313 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1311 1314 extend helper
1312 1315 return self
1313 1316 end
1314 1317
1315 1318 def link_to_content_update(text, url_params = {}, html_options = {})
1316 1319 link_to(text, url_params, html_options)
1317 1320 end
1318 1321 end
@@ -1,1562 +1,1565
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::Utils::DateCalculation
21 21 include Redmine::I18n
22 22 before_save :set_parent_id
23 23 include Redmine::NestedSet::IssueNestedSet
24 24
25 25 belongs_to :project
26 26 belongs_to :tracker
27 27 belongs_to :status, :class_name => 'IssueStatus'
28 28 belongs_to :author, :class_name => 'User'
29 29 belongs_to :assigned_to, :class_name => 'Principal'
30 30 belongs_to :fixed_version, :class_name => 'Version'
31 31 belongs_to :priority, :class_name => 'IssuePriority'
32 32 belongs_to :category, :class_name => 'IssueCategory'
33 33
34 34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 35 has_many :visible_journals,
36 36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 37 :class_name => 'Journal',
38 38 :as => :journalized
39 39
40 40 has_many :time_entries, :dependent => :destroy
41 41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 47 acts_as_customizable
48 48 acts_as_watchable
49 49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 50 :preload => [:project, :status, :tracker],
51 51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52 52
53 53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56 56
57 57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
58 58 :author_key => :author_id
59 59
60 60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61 61
62 62 attr_reader :current_journal
63 63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64 64
65 65 validates_presence_of :subject, :project, :tracker
66 66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69 69
70 70 validates_length_of :subject, :maximum => 255
71 71 validates_inclusion_of :done_ratio, :in => 0..100
72 72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 73 validates :start_date, :date => true
74 74 validates :due_date, :date => true
75 75 validate :validate_issue, :validate_required_fields
76 76 attr_protected :id
77 77
78 78 scope :visible, lambda {|*args|
79 79 joins(:project).
80 80 where(Issue.visible_condition(args.shift || User.current, *args))
81 81 }
82 82
83 83 scope :open, lambda {|*args|
84 84 is_closed = args.size > 0 ? !args.first : false
85 85 joins(:status).
86 86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 87 }
88 88
89 89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 90 scope :on_active_project, lambda {
91 91 joins(:project).
92 92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 93 }
94 94 scope :fixed_version, lambda {|versions|
95 95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 97 }
98 98
99 99 before_create :default_assign
100 100 before_save :close_duplicates, :update_done_ratio_from_issue_status,
101 101 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
102 102 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
103 103 after_save :reschedule_following_issues, :update_nested_set_attributes,
104 104 :update_parent_attributes, :create_journal
105 105 # Should be after_create but would be called before previous after_save callbacks
106 106 after_save :after_create_from_copy
107 107 after_destroy :update_parent_attributes
108 108 after_create :send_notification
109 109 # Keep it at the end of after_save callbacks
110 110 after_save :clear_assigned_to_was
111 111
112 112 # Returns a SQL conditions string used to find all issues visible by the specified user
113 113 def self.visible_condition(user, options={})
114 114 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
115 115 if user.id && user.logged?
116 116 case role.issues_visibility
117 117 when 'all'
118 118 nil
119 119 when 'default'
120 120 user_ids = [user.id] + user.groups.map(&:id).compact
121 121 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
122 122 when 'own'
123 123 user_ids = [user.id] + user.groups.map(&:id).compact
124 124 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
125 125 else
126 126 '1=0'
127 127 end
128 128 else
129 129 "(#{table_name}.is_private = #{connection.quoted_false})"
130 130 end
131 131 end
132 132 end
133 133
134 134 # Returns true if usr or current user is allowed to view the issue
135 135 def visible?(usr=nil)
136 136 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
137 137 if user.logged?
138 138 case role.issues_visibility
139 139 when 'all'
140 140 true
141 141 when 'default'
142 142 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
143 143 when 'own'
144 144 self.author == user || user.is_or_belongs_to?(assigned_to)
145 145 else
146 146 false
147 147 end
148 148 else
149 149 !self.is_private?
150 150 end
151 151 end
152 152 end
153 153
154 154 # Returns true if user or current user is allowed to edit or add a note to the issue
155 155 def editable?(user=User.current)
156 156 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
157 157 end
158 158
159 159 def initialize(attributes=nil, *args)
160 160 super
161 161 if new_record?
162 162 # set default values for new records only
163 163 self.priority ||= IssuePriority.default
164 164 self.watcher_user_ids = []
165 165 end
166 166 end
167 167
168 168 def create_or_update
169 169 super
170 170 ensure
171 171 @status_was = nil
172 172 end
173 173 private :create_or_update
174 174
175 175 # AR#Persistence#destroy would raise and RecordNotFound exception
176 176 # if the issue was already deleted or updated (non matching lock_version).
177 177 # This is a problem when bulk deleting issues or deleting a project
178 178 # (because an issue may already be deleted if its parent was deleted
179 179 # first).
180 180 # The issue is reloaded by the nested_set before being deleted so
181 181 # the lock_version condition should not be an issue but we handle it.
182 182 def destroy
183 183 super
184 184 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
185 185 # Stale or already deleted
186 186 begin
187 187 reload
188 188 rescue ActiveRecord::RecordNotFound
189 189 # The issue was actually already deleted
190 190 @destroyed = true
191 191 return freeze
192 192 end
193 193 # The issue was stale, retry to destroy
194 194 super
195 195 end
196 196
197 197 alias :base_reload :reload
198 198 def reload(*args)
199 199 @workflow_rule_by_attribute = nil
200 200 @assignable_versions = nil
201 201 @relations = nil
202 202 @spent_hours = nil
203 203 base_reload(*args)
204 204 end
205 205
206 206 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
207 207 def available_custom_fields
208 208 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
209 209 end
210 210
211 211 def visible_custom_field_values(user=nil)
212 212 user_real = user || User.current
213 213 custom_field_values.select do |value|
214 214 value.custom_field.visible_by?(project, user_real)
215 215 end
216 216 end
217 217
218 218 # Copies attributes from another issue, arg can be an id or an Issue
219 219 def copy_from(arg, options={})
220 220 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
221 221 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
222 222 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
223 223 self.status = issue.status
224 224 self.author = User.current
225 225 unless options[:attachments] == false
226 226 self.attachments = issue.attachments.map do |attachement|
227 227 attachement.copy(:container => self)
228 228 end
229 229 end
230 230 @copied_from = issue
231 231 @copy_options = options
232 232 self
233 233 end
234 234
235 235 # Returns an unsaved copy of the issue
236 236 def copy(attributes=nil, copy_options={})
237 237 copy = self.class.new.copy_from(self, copy_options)
238 238 copy.attributes = attributes if attributes
239 239 copy
240 240 end
241 241
242 242 # Returns true if the issue is a copy
243 243 def copy?
244 244 @copied_from.present?
245 245 end
246 246
247 247 def status_id=(status_id)
248 248 if status_id.to_s != self.status_id.to_s
249 249 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
250 250 end
251 251 self.status_id
252 252 end
253 253
254 254 # Sets the status.
255 255 def status=(status)
256 256 if status != self.status
257 257 @workflow_rule_by_attribute = nil
258 258 end
259 259 association(:status).writer(status)
260 260 end
261 261
262 262 def priority_id=(pid)
263 263 self.priority = nil
264 264 write_attribute(:priority_id, pid)
265 265 end
266 266
267 267 def category_id=(cid)
268 268 self.category = nil
269 269 write_attribute(:category_id, cid)
270 270 end
271 271
272 272 def fixed_version_id=(vid)
273 273 self.fixed_version = nil
274 274 write_attribute(:fixed_version_id, vid)
275 275 end
276 276
277 277 def tracker_id=(tracker_id)
278 278 if tracker_id.to_s != self.tracker_id.to_s
279 279 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
280 280 end
281 281 self.tracker_id
282 282 end
283 283
284 284 # Sets the tracker.
285 285 # This will set the status to the default status of the new tracker if:
286 286 # * the status was the default for the previous tracker
287 287 # * or if the status was not part of the new tracker statuses
288 288 # * or the status was nil
289 289 def tracker=(tracker)
290 290 if tracker != self.tracker
291 291 if status == default_status
292 292 self.status = nil
293 293 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
294 294 self.status = nil
295 295 end
296 296 @custom_field_values = nil
297 297 @workflow_rule_by_attribute = nil
298 298 end
299 299 association(:tracker).writer(tracker)
300 300 self.status ||= default_status
301 301 self.tracker
302 302 end
303 303
304 304 def project_id=(project_id)
305 305 if project_id.to_s != self.project_id.to_s
306 306 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
307 307 end
308 308 self.project_id
309 309 end
310 310
311 311 # Sets the project.
312 312 # Unless keep_tracker argument is set to true, this will change the tracker
313 313 # to the first tracker of the new project if the previous tracker is not part
314 314 # of the new project trackers.
315 315 # This will clear the fixed_version is it's no longer valid for the new project.
316 316 # This will clear the parent issue if it's no longer valid for the new project.
317 317 # This will set the category to the category with the same name in the new
318 318 # project if it exists, or clear it if it doesn't.
319 319 def project=(project, keep_tracker=false)
320 320 project_was = self.project
321 321 association(:project).writer(project)
322 322 if project_was && project && project_was != project
323 323 @assignable_versions = nil
324 324
325 325 unless keep_tracker || project.trackers.include?(tracker)
326 326 self.tracker = project.trackers.first
327 327 end
328 328 # Reassign to the category with same name if any
329 329 if category
330 330 self.category = project.issue_categories.find_by_name(category.name)
331 331 end
332 332 # Keep the fixed_version if it's still valid in the new_project
333 333 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
334 334 self.fixed_version = nil
335 335 end
336 336 # Clear the parent task if it's no longer valid
337 337 unless valid_parent_project?
338 338 self.parent_issue_id = nil
339 339 end
340 340 @custom_field_values = nil
341 341 @workflow_rule_by_attribute = nil
342 342 end
343 343 self.project
344 344 end
345 345
346 346 def description=(arg)
347 347 if arg.is_a?(String)
348 348 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
349 349 end
350 350 write_attribute(:description, arg)
351 351 end
352 352
353 353 # Overrides assign_attributes so that project and tracker get assigned first
354 354 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
355 355 return if new_attributes.nil?
356 356 attrs = new_attributes.dup
357 357 attrs.stringify_keys!
358 358
359 359 %w(project project_id tracker tracker_id).each do |attr|
360 360 if attrs.has_key?(attr)
361 361 send "#{attr}=", attrs.delete(attr)
362 362 end
363 363 end
364 364 send :assign_attributes_without_project_and_tracker_first, attrs, *args
365 365 end
366 366 # Do not redefine alias chain on reload (see #4838)
367 367 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
368 368
369 369 def attributes=(new_attributes)
370 370 assign_attributes new_attributes
371 371 end
372 372
373 373 def estimated_hours=(h)
374 374 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
375 375 end
376 376
377 377 safe_attributes 'project_id',
378 378 :if => lambda {|issue, user|
379 379 if issue.new_record?
380 380 issue.copy?
381 381 else
382 382 user.allowed_to?(:edit_issues, issue.project)
383 383 end
384 384 }
385 385
386 386 safe_attributes 'tracker_id',
387 387 'status_id',
388 388 'category_id',
389 389 'assigned_to_id',
390 390 'priority_id',
391 391 'fixed_version_id',
392 392 'subject',
393 393 'description',
394 394 'start_date',
395 395 'due_date',
396 396 'done_ratio',
397 397 'estimated_hours',
398 398 'custom_field_values',
399 399 'custom_fields',
400 400 'lock_version',
401 401 'notes',
402 402 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
403 403
404 404 safe_attributes 'notes',
405 405 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
406 406
407 407 safe_attributes 'private_notes',
408 408 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
409 409
410 410 safe_attributes 'watcher_user_ids',
411 411 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
412 412
413 413 safe_attributes 'is_private',
414 414 :if => lambda {|issue, user|
415 415 user.allowed_to?(:set_issues_private, issue.project) ||
416 416 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
417 417 }
418 418
419 419 safe_attributes 'parent_issue_id',
420 420 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
421 421 user.allowed_to?(:manage_subtasks, issue.project)}
422 422
423 423 def safe_attribute_names(user=nil)
424 424 names = super
425 425 names -= disabled_core_fields
426 426 names -= read_only_attribute_names(user)
427 if new_record? && copy?
428 names |= %w(project_id)
429 end
427 430 names
428 431 end
429 432
430 433 # Safely sets attributes
431 434 # Should be called from controllers instead of #attributes=
432 435 # attr_accessible is too rough because we still want things like
433 436 # Issue.new(:project => foo) to work
434 437 def safe_attributes=(attrs, user=User.current)
435 438 return unless attrs.is_a?(Hash)
436 439
437 440 attrs = attrs.deep_dup
438 441
439 442 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
440 443 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
441 444 if allowed_target_projects(user).where(:id => p.to_i).exists?
442 445 self.project_id = p
443 446 end
444 447 end
445 448
446 449 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
447 450 self.tracker_id = t
448 451 end
449 452
450 453 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
451 454 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
452 455 self.status_id = s
453 456 end
454 457 end
455 458
456 459 attrs = delete_unsafe_attributes(attrs, user)
457 460 return if attrs.empty?
458 461
459 462 unless leaf?
460 463 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
461 464 end
462 465
463 466 if attrs['parent_issue_id'].present?
464 467 s = attrs['parent_issue_id'].to_s
465 468 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
466 469 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
467 470 end
468 471 end
469 472
470 473 if attrs['custom_field_values'].present?
471 474 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
472 475 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
473 476 end
474 477
475 478 if attrs['custom_fields'].present?
476 479 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
477 480 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
478 481 end
479 482
480 483 # mass-assignment security bypass
481 484 assign_attributes attrs, :without_protection => true
482 485 end
483 486
484 487 def disabled_core_fields
485 488 tracker ? tracker.disabled_core_fields : []
486 489 end
487 490
488 491 # Returns the custom_field_values that can be edited by the given user
489 492 def editable_custom_field_values(user=nil)
490 493 visible_custom_field_values(user).reject do |value|
491 494 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
492 495 end
493 496 end
494 497
495 498 # Returns the custom fields that can be edited by the given user
496 499 def editable_custom_fields(user=nil)
497 500 editable_custom_field_values(user).map(&:custom_field).uniq
498 501 end
499 502
500 503 # Returns the names of attributes that are read-only for user or the current user
501 504 # For users with multiple roles, the read-only fields are the intersection of
502 505 # read-only fields of each role
503 506 # The result is an array of strings where sustom fields are represented with their ids
504 507 #
505 508 # Examples:
506 509 # issue.read_only_attribute_names # => ['due_date', '2']
507 510 # issue.read_only_attribute_names(user) # => []
508 511 def read_only_attribute_names(user=nil)
509 512 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
510 513 end
511 514
512 515 # Returns the names of required attributes for user or the current user
513 516 # For users with multiple roles, the required fields are the intersection of
514 517 # required fields of each role
515 518 # The result is an array of strings where sustom fields are represented with their ids
516 519 #
517 520 # Examples:
518 521 # issue.required_attribute_names # => ['due_date', '2']
519 522 # issue.required_attribute_names(user) # => []
520 523 def required_attribute_names(user=nil)
521 524 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
522 525 end
523 526
524 527 # Returns true if the attribute is required for user
525 528 def required_attribute?(name, user=nil)
526 529 required_attribute_names(user).include?(name.to_s)
527 530 end
528 531
529 532 # Returns a hash of the workflow rule by attribute for the given user
530 533 #
531 534 # Examples:
532 535 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
533 536 def workflow_rule_by_attribute(user=nil)
534 537 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
535 538
536 539 user_real = user || User.current
537 540 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
538 541 roles = roles.select(&:consider_workflow?)
539 542 return {} if roles.empty?
540 543
541 544 result = {}
542 545 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
543 546 if workflow_permissions.any?
544 547 workflow_rules = workflow_permissions.inject({}) do |h, wp|
545 548 h[wp.field_name] ||= []
546 549 h[wp.field_name] << wp.rule
547 550 h
548 551 end
549 552 workflow_rules.each do |attr, rules|
550 553 next if rules.size < roles.size
551 554 uniq_rules = rules.uniq
552 555 if uniq_rules.size == 1
553 556 result[attr] = uniq_rules.first
554 557 else
555 558 result[attr] = 'required'
556 559 end
557 560 end
558 561 end
559 562 @workflow_rule_by_attribute = result if user.nil?
560 563 result
561 564 end
562 565 private :workflow_rule_by_attribute
563 566
564 567 def done_ratio
565 568 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
566 569 status.default_done_ratio
567 570 else
568 571 read_attribute(:done_ratio)
569 572 end
570 573 end
571 574
572 575 def self.use_status_for_done_ratio?
573 576 Setting.issue_done_ratio == 'issue_status'
574 577 end
575 578
576 579 def self.use_field_for_done_ratio?
577 580 Setting.issue_done_ratio == 'issue_field'
578 581 end
579 582
580 583 def validate_issue
581 584 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
582 585 errors.add :due_date, :greater_than_start_date
583 586 end
584 587
585 588 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
586 589 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
587 590 end
588 591
589 592 if fixed_version
590 593 if !assignable_versions.include?(fixed_version)
591 594 errors.add :fixed_version_id, :inclusion
592 595 elsif reopening? && fixed_version.closed?
593 596 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
594 597 end
595 598 end
596 599
597 600 # Checks that the issue can not be added/moved to a disabled tracker
598 601 if project && (tracker_id_changed? || project_id_changed?)
599 602 unless project.trackers.include?(tracker)
600 603 errors.add :tracker_id, :inclusion
601 604 end
602 605 end
603 606
604 607 # Checks parent issue assignment
605 608 if @invalid_parent_issue_id.present?
606 609 errors.add :parent_issue_id, :invalid
607 610 elsif @parent_issue
608 611 if !valid_parent_project?(@parent_issue)
609 612 errors.add :parent_issue_id, :invalid
610 613 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
611 614 errors.add :parent_issue_id, :invalid
612 615 elsif !new_record?
613 616 # moving an existing issue
614 617 if move_possible?(@parent_issue)
615 618 # move accepted
616 619 else
617 620 errors.add :parent_issue_id, :invalid
618 621 end
619 622 end
620 623 end
621 624 end
622 625
623 626 # Validates the issue against additional workflow requirements
624 627 def validate_required_fields
625 628 user = new_record? ? author : current_journal.try(:user)
626 629
627 630 required_attribute_names(user).each do |attribute|
628 631 if attribute =~ /^\d+$/
629 632 attribute = attribute.to_i
630 633 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
631 634 if v && v.value.blank?
632 635 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
633 636 end
634 637 else
635 638 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
636 639 errors.add attribute, :blank
637 640 end
638 641 end
639 642 end
640 643 end
641 644
642 645 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
643 646 # even if the user turns off the setting later
644 647 def update_done_ratio_from_issue_status
645 648 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
646 649 self.done_ratio = status.default_done_ratio
647 650 end
648 651 end
649 652
650 653 def init_journal(user, notes = "")
651 654 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
652 655 end
653 656
654 657 # Returns the current journal or nil if it's not initialized
655 658 def current_journal
656 659 @current_journal
657 660 end
658 661
659 662 # Returns the names of attributes that are journalized when updating the issue
660 663 def journalized_attribute_names
661 664 Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
662 665 end
663 666
664 667 # Returns the id of the last journal or nil
665 668 def last_journal_id
666 669 if new_record?
667 670 nil
668 671 else
669 672 journals.maximum(:id)
670 673 end
671 674 end
672 675
673 676 # Returns a scope for journals that have an id greater than journal_id
674 677 def journals_after(journal_id)
675 678 scope = journals.reorder("#{Journal.table_name}.id ASC")
676 679 if journal_id.present?
677 680 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
678 681 end
679 682 scope
680 683 end
681 684
682 685 # Returns the initial status of the issue
683 686 # Returns nil for a new issue
684 687 def status_was
685 688 if status_id_changed?
686 689 if status_id_was.to_i > 0
687 690 @status_was ||= IssueStatus.find_by_id(status_id_was)
688 691 end
689 692 else
690 693 @status_was ||= status
691 694 end
692 695 end
693 696
694 697 # Return true if the issue is closed, otherwise false
695 698 def closed?
696 699 status.present? && status.is_closed?
697 700 end
698 701
699 702 # Returns true if the issue was closed when loaded
700 703 def was_closed?
701 704 status_was.present? && status_was.is_closed?
702 705 end
703 706
704 707 # Return true if the issue is being reopened
705 708 def reopening?
706 709 if new_record?
707 710 false
708 711 else
709 712 status_id_changed? && !closed? && was_closed?
710 713 end
711 714 end
712 715 alias :reopened? :reopening?
713 716
714 717 # Return true if the issue is being closed
715 718 def closing?
716 719 if new_record?
717 720 closed?
718 721 else
719 722 status_id_changed? && closed? && !was_closed?
720 723 end
721 724 end
722 725
723 726 # Returns true if the issue is overdue
724 727 def overdue?
725 728 due_date.present? && (due_date < Date.today) && !closed?
726 729 end
727 730
728 731 # Is the amount of work done less than it should for the due date
729 732 def behind_schedule?
730 733 return false if start_date.nil? || due_date.nil?
731 734 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
732 735 return done_date <= Date.today
733 736 end
734 737
735 738 # Does this issue have children?
736 739 def children?
737 740 !leaf?
738 741 end
739 742
740 743 # Users the issue can be assigned to
741 744 def assignable_users
742 745 users = project.assignable_users.to_a
743 746 users << author if author
744 747 users << assigned_to if assigned_to
745 748 users.uniq.sort
746 749 end
747 750
748 751 # Versions that the issue can be assigned to
749 752 def assignable_versions
750 753 return @assignable_versions if @assignable_versions
751 754
752 755 versions = project.shared_versions.open.to_a
753 756 if fixed_version
754 757 if fixed_version_id_changed?
755 758 # nothing to do
756 759 elsif project_id_changed?
757 760 if project.shared_versions.include?(fixed_version)
758 761 versions << fixed_version
759 762 end
760 763 else
761 764 versions << fixed_version
762 765 end
763 766 end
764 767 @assignable_versions = versions.uniq.sort
765 768 end
766 769
767 770 # Returns true if this issue is blocked by another issue that is still open
768 771 def blocked?
769 772 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
770 773 end
771 774
772 775 # Returns the default status of the issue based on its tracker
773 776 # Returns nil if tracker is nil
774 777 def default_status
775 778 tracker.try(:default_status)
776 779 end
777 780
778 781 # Returns an array of statuses that user is able to apply
779 782 def new_statuses_allowed_to(user=User.current, include_default=false)
780 783 if new_record? && @copied_from
781 784 [default_status, @copied_from.status].compact.uniq.sort
782 785 else
783 786 initial_status = nil
784 787 if new_record?
785 788 initial_status = default_status
786 789 elsif tracker_id_changed?
787 790 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
788 791 initial_status = default_status
789 792 elsif tracker.issue_status_ids.include?(status_id_was)
790 793 initial_status = IssueStatus.find_by_id(status_id_was)
791 794 else
792 795 initial_status = default_status
793 796 end
794 797 else
795 798 initial_status = status_was
796 799 end
797 800
798 801 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
799 802 assignee_transitions_allowed = initial_assigned_to_id.present? &&
800 803 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
801 804
802 805 statuses = []
803 806 if initial_status
804 807 statuses += initial_status.find_new_statuses_allowed_to(
805 808 user.admin ? Role.all.to_a : user.roles_for_project(project),
806 809 tracker,
807 810 author == user,
808 811 assignee_transitions_allowed
809 812 )
810 813 end
811 814 statuses << initial_status unless statuses.empty?
812 815 statuses << default_status if include_default
813 816 statuses = statuses.compact.uniq.sort
814 817 if blocked?
815 818 statuses.reject!(&:is_closed?)
816 819 end
817 820 statuses
818 821 end
819 822 end
820 823
821 824 # Returns the previous assignee if changed
822 825 def assigned_to_was
823 826 # assigned_to_id_was is reset before after_save callbacks
824 827 user_id = @previous_assigned_to_id || assigned_to_id_was
825 828 if user_id && user_id != assigned_to_id
826 829 @assigned_to_was ||= User.find_by_id(user_id)
827 830 end
828 831 end
829 832
830 833 # Returns the users that should be notified
831 834 def notified_users
832 835 notified = []
833 836 # Author and assignee are always notified unless they have been
834 837 # locked or don't want to be notified
835 838 notified << author if author
836 839 if assigned_to
837 840 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
838 841 end
839 842 if assigned_to_was
840 843 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
841 844 end
842 845 notified = notified.select {|u| u.active? && u.notify_about?(self)}
843 846
844 847 notified += project.notified_users
845 848 notified.uniq!
846 849 # Remove users that can not view the issue
847 850 notified.reject! {|user| !visible?(user)}
848 851 notified
849 852 end
850 853
851 854 # Returns the email addresses that should be notified
852 855 def recipients
853 856 notified_users.collect(&:mail)
854 857 end
855 858
856 859 def each_notification(users, &block)
857 860 if users.any?
858 861 if custom_field_values.detect {|value| !value.custom_field.visible?}
859 862 users_by_custom_field_visibility = users.group_by do |user|
860 863 visible_custom_field_values(user).map(&:custom_field_id).sort
861 864 end
862 865 users_by_custom_field_visibility.values.each do |users|
863 866 yield(users)
864 867 end
865 868 else
866 869 yield(users)
867 870 end
868 871 end
869 872 end
870 873
871 874 # Returns the number of hours spent on this issue
872 875 def spent_hours
873 876 @spent_hours ||= time_entries.sum(:hours) || 0
874 877 end
875 878
876 879 # Returns the total number of hours spent on this issue and its descendants
877 880 #
878 881 # Example:
879 882 # spent_hours => 0.0
880 883 # spent_hours => 50.2
881 884 def total_spent_hours
882 885 @total_spent_hours ||=
883 886 self_and_descendants.
884 887 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
885 888 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
886 889 end
887 890
888 891 def relations
889 892 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
890 893 end
891 894
892 895 # Preloads relations for a collection of issues
893 896 def self.load_relations(issues)
894 897 if issues.any?
895 898 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
896 899 issues.each do |issue|
897 900 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
898 901 end
899 902 end
900 903 end
901 904
902 905 # Preloads visible spent time for a collection of issues
903 906 def self.load_visible_spent_hours(issues, user=User.current)
904 907 if issues.any?
905 908 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
906 909 issues.each do |issue|
907 910 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
908 911 end
909 912 end
910 913 end
911 914
912 915 # Preloads visible relations for a collection of issues
913 916 def self.load_visible_relations(issues, user=User.current)
914 917 if issues.any?
915 918 issue_ids = issues.map(&:id)
916 919 # Relations with issue_from in given issues and visible issue_to
917 920 relations_from = IssueRelation.joins(:issue_to => :project).
918 921 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
919 922 # Relations with issue_to in given issues and visible issue_from
920 923 relations_to = IssueRelation.joins(:issue_from => :project).
921 924 where(visible_condition(user)).
922 925 where(:issue_to_id => issue_ids).to_a
923 926 issues.each do |issue|
924 927 relations =
925 928 relations_from.select {|relation| relation.issue_from_id == issue.id} +
926 929 relations_to.select {|relation| relation.issue_to_id == issue.id}
927 930
928 931 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
929 932 end
930 933 end
931 934 end
932 935
933 936 # Finds an issue relation given its id.
934 937 def find_relation(relation_id)
935 938 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
936 939 end
937 940
938 941 # Returns all the other issues that depend on the issue
939 942 # The algorithm is a modified breadth first search (bfs)
940 943 def all_dependent_issues(except=[])
941 944 # The found dependencies
942 945 dependencies = []
943 946
944 947 # The visited flag for every node (issue) used by the breadth first search
945 948 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
946 949
947 950 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
948 951 # the issue when it is processed.
949 952
950 953 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
951 954 # but its children will not be added to the queue when it is processed.
952 955
953 956 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
954 957 # the queue, but its children have not been added.
955 958
956 959 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
957 960 # the children still need to be processed.
958 961
959 962 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
960 963 # added as dependent issues. It needs no further processing.
961 964
962 965 issue_status = Hash.new(eNOT_DISCOVERED)
963 966
964 967 # The queue
965 968 queue = []
966 969
967 970 # Initialize the bfs, add start node (self) to the queue
968 971 queue << self
969 972 issue_status[self] = ePROCESS_ALL
970 973
971 974 while (!queue.empty?) do
972 975 current_issue = queue.shift
973 976 current_issue_status = issue_status[current_issue]
974 977 dependencies << current_issue
975 978
976 979 # Add parent to queue, if not already in it.
977 980 parent = current_issue.parent
978 981 parent_status = issue_status[parent]
979 982
980 983 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
981 984 queue << parent
982 985 issue_status[parent] = ePROCESS_RELATIONS_ONLY
983 986 end
984 987
985 988 # Add children to queue, but only if they are not already in it and
986 989 # the children of the current node need to be processed.
987 990 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
988 991 current_issue.children.each do |child|
989 992 next if except.include?(child)
990 993
991 994 if (issue_status[child] == eNOT_DISCOVERED)
992 995 queue << child
993 996 issue_status[child] = ePROCESS_ALL
994 997 elsif (issue_status[child] == eRELATIONS_PROCESSED)
995 998 queue << child
996 999 issue_status[child] = ePROCESS_CHILDREN_ONLY
997 1000 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
998 1001 queue << child
999 1002 issue_status[child] = ePROCESS_ALL
1000 1003 end
1001 1004 end
1002 1005 end
1003 1006
1004 1007 # Add related issues to the queue, if they are not already in it.
1005 1008 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1006 1009 next if except.include?(related_issue)
1007 1010
1008 1011 if (issue_status[related_issue] == eNOT_DISCOVERED)
1009 1012 queue << related_issue
1010 1013 issue_status[related_issue] = ePROCESS_ALL
1011 1014 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1012 1015 queue << related_issue
1013 1016 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1014 1017 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1015 1018 queue << related_issue
1016 1019 issue_status[related_issue] = ePROCESS_ALL
1017 1020 end
1018 1021 end
1019 1022
1020 1023 # Set new status for current issue
1021 1024 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1022 1025 issue_status[current_issue] = eALL_PROCESSED
1023 1026 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1024 1027 issue_status[current_issue] = eRELATIONS_PROCESSED
1025 1028 end
1026 1029 end # while
1027 1030
1028 1031 # Remove the issues from the "except" parameter from the result array
1029 1032 dependencies -= except
1030 1033 dependencies.delete(self)
1031 1034
1032 1035 dependencies
1033 1036 end
1034 1037
1035 1038 # Returns an array of issues that duplicate this one
1036 1039 def duplicates
1037 1040 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1038 1041 end
1039 1042
1040 1043 # Returns the due date or the target due date if any
1041 1044 # Used on gantt chart
1042 1045 def due_before
1043 1046 due_date || (fixed_version ? fixed_version.effective_date : nil)
1044 1047 end
1045 1048
1046 1049 # Returns the time scheduled for this issue.
1047 1050 #
1048 1051 # Example:
1049 1052 # Start Date: 2/26/09, End Date: 3/04/09
1050 1053 # duration => 6
1051 1054 def duration
1052 1055 (start_date && due_date) ? due_date - start_date : 0
1053 1056 end
1054 1057
1055 1058 # Returns the duration in working days
1056 1059 def working_duration
1057 1060 (start_date && due_date) ? working_days(start_date, due_date) : 0
1058 1061 end
1059 1062
1060 1063 def soonest_start(reload=false)
1061 1064 @soonest_start = nil if reload
1062 1065 @soonest_start ||= (
1063 1066 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1064 1067 [(@parent_issue || parent).try(:soonest_start)]
1065 1068 ).compact.max
1066 1069 end
1067 1070
1068 1071 # Sets start_date on the given date or the next working day
1069 1072 # and changes due_date to keep the same working duration.
1070 1073 def reschedule_on(date)
1071 1074 wd = working_duration
1072 1075 date = next_working_date(date)
1073 1076 self.start_date = date
1074 1077 self.due_date = add_working_days(date, wd)
1075 1078 end
1076 1079
1077 1080 # Reschedules the issue on the given date or the next working day and saves the record.
1078 1081 # If the issue is a parent task, this is done by rescheduling its subtasks.
1079 1082 def reschedule_on!(date)
1080 1083 return if date.nil?
1081 1084 if leaf?
1082 1085 if start_date.nil? || start_date != date
1083 1086 if start_date && start_date > date
1084 1087 # Issue can not be moved earlier than its soonest start date
1085 1088 date = [soonest_start(true), date].compact.max
1086 1089 end
1087 1090 reschedule_on(date)
1088 1091 begin
1089 1092 save
1090 1093 rescue ActiveRecord::StaleObjectError
1091 1094 reload
1092 1095 reschedule_on(date)
1093 1096 save
1094 1097 end
1095 1098 end
1096 1099 else
1097 1100 leaves.each do |leaf|
1098 1101 if leaf.start_date
1099 1102 # Only move subtask if it starts at the same date as the parent
1100 1103 # or if it starts before the given date
1101 1104 if start_date == leaf.start_date || date > leaf.start_date
1102 1105 leaf.reschedule_on!(date)
1103 1106 end
1104 1107 else
1105 1108 leaf.reschedule_on!(date)
1106 1109 end
1107 1110 end
1108 1111 end
1109 1112 end
1110 1113
1111 1114 def <=>(issue)
1112 1115 if issue.nil?
1113 1116 -1
1114 1117 elsif root_id != issue.root_id
1115 1118 (root_id || 0) <=> (issue.root_id || 0)
1116 1119 else
1117 1120 (lft || 0) <=> (issue.lft || 0)
1118 1121 end
1119 1122 end
1120 1123
1121 1124 def to_s
1122 1125 "#{tracker} ##{id}: #{subject}"
1123 1126 end
1124 1127
1125 1128 # Returns a string of css classes that apply to the issue
1126 1129 def css_classes(user=User.current)
1127 1130 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1128 1131 s << ' closed' if closed?
1129 1132 s << ' overdue' if overdue?
1130 1133 s << ' child' if child?
1131 1134 s << ' parent' unless leaf?
1132 1135 s << ' private' if is_private?
1133 1136 if user.logged?
1134 1137 s << ' created-by-me' if author_id == user.id
1135 1138 s << ' assigned-to-me' if assigned_to_id == user.id
1136 1139 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1137 1140 end
1138 1141 s
1139 1142 end
1140 1143
1141 1144 # Unassigns issues from +version+ if it's no longer shared with issue's project
1142 1145 def self.update_versions_from_sharing_change(version)
1143 1146 # Update issues assigned to the version
1144 1147 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1145 1148 end
1146 1149
1147 1150 # Unassigns issues from versions that are no longer shared
1148 1151 # after +project+ was moved
1149 1152 def self.update_versions_from_hierarchy_change(project)
1150 1153 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1151 1154 # Update issues of the moved projects and issues assigned to a version of a moved project
1152 1155 Issue.update_versions(
1153 1156 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1154 1157 moved_project_ids, moved_project_ids]
1155 1158 )
1156 1159 end
1157 1160
1158 1161 def parent_issue_id=(arg)
1159 1162 s = arg.to_s.strip.presence
1160 1163 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1161 1164 @invalid_parent_issue_id = nil
1162 1165 elsif s.blank?
1163 1166 @parent_issue = nil
1164 1167 @invalid_parent_issue_id = nil
1165 1168 else
1166 1169 @parent_issue = nil
1167 1170 @invalid_parent_issue_id = arg
1168 1171 end
1169 1172 end
1170 1173
1171 1174 def parent_issue_id
1172 1175 if @invalid_parent_issue_id
1173 1176 @invalid_parent_issue_id
1174 1177 elsif instance_variable_defined? :@parent_issue
1175 1178 @parent_issue.nil? ? nil : @parent_issue.id
1176 1179 else
1177 1180 parent_id
1178 1181 end
1179 1182 end
1180 1183
1181 1184 def set_parent_id
1182 1185 self.parent_id = parent_issue_id
1183 1186 end
1184 1187
1185 1188 # Returns true if issue's project is a valid
1186 1189 # parent issue project
1187 1190 def valid_parent_project?(issue=parent)
1188 1191 return true if issue.nil? || issue.project_id == project_id
1189 1192
1190 1193 case Setting.cross_project_subtasks
1191 1194 when 'system'
1192 1195 true
1193 1196 when 'tree'
1194 1197 issue.project.root == project.root
1195 1198 when 'hierarchy'
1196 1199 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1197 1200 when 'descendants'
1198 1201 issue.project.is_or_is_ancestor_of?(project)
1199 1202 else
1200 1203 false
1201 1204 end
1202 1205 end
1203 1206
1204 1207 # Returns an issue scope based on project and scope
1205 1208 def self.cross_project_scope(project, scope=nil)
1206 1209 if project.nil?
1207 1210 return Issue
1208 1211 end
1209 1212 case scope
1210 1213 when 'all', 'system'
1211 1214 Issue
1212 1215 when 'tree'
1213 1216 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1214 1217 :lft => project.root.lft, :rgt => project.root.rgt)
1215 1218 when 'hierarchy'
1216 1219 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1217 1220 :lft => project.lft, :rgt => project.rgt)
1218 1221 when 'descendants'
1219 1222 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1220 1223 :lft => project.lft, :rgt => project.rgt)
1221 1224 else
1222 1225 Issue.where(:project_id => project.id)
1223 1226 end
1224 1227 end
1225 1228
1226 1229 def self.by_tracker(project)
1227 1230 count_and_group_by(:project => project, :association => :tracker)
1228 1231 end
1229 1232
1230 1233 def self.by_version(project)
1231 1234 count_and_group_by(:project => project, :association => :fixed_version)
1232 1235 end
1233 1236
1234 1237 def self.by_priority(project)
1235 1238 count_and_group_by(:project => project, :association => :priority)
1236 1239 end
1237 1240
1238 1241 def self.by_category(project)
1239 1242 count_and_group_by(:project => project, :association => :category)
1240 1243 end
1241 1244
1242 1245 def self.by_assigned_to(project)
1243 1246 count_and_group_by(:project => project, :association => :assigned_to)
1244 1247 end
1245 1248
1246 1249 def self.by_author(project)
1247 1250 count_and_group_by(:project => project, :association => :author)
1248 1251 end
1249 1252
1250 1253 def self.by_subproject(project)
1251 1254 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1252 1255 r.reject {|r| r["project_id"] == project.id.to_s}
1253 1256 end
1254 1257
1255 1258 # Query generator for selecting groups of issue counts for a project
1256 1259 # based on specific criteria
1257 1260 #
1258 1261 # Options
1259 1262 # * project - Project to search in.
1260 1263 # * with_subprojects - Includes subprojects issues if set to true.
1261 1264 # * association - Symbol. Association for grouping.
1262 1265 def self.count_and_group_by(options)
1263 1266 assoc = reflect_on_association(options[:association])
1264 1267 select_field = assoc.foreign_key
1265 1268
1266 1269 Issue.
1267 1270 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1268 1271 joins(:status, assoc.name).
1269 1272 group(:status_id, :is_closed, select_field).
1270 1273 count.
1271 1274 map do |columns, total|
1272 1275 status_id, is_closed, field_value = columns
1273 1276 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1274 1277 {
1275 1278 "status_id" => status_id.to_s,
1276 1279 "closed" => is_closed,
1277 1280 select_field => field_value.to_s,
1278 1281 "total" => total.to_s
1279 1282 }
1280 1283 end
1281 1284 end
1282 1285
1283 1286 # Returns a scope of projects that user can assign the issue to
1284 1287 def allowed_target_projects(user=User.current)
1285 1288 current_project = new_record? ? nil : project
1286 1289 self.class.allowed_target_projects(user, current_project)
1287 1290 end
1288 1291
1289 1292 # Returns a scope of projects that user can assign issues to
1290 1293 # If current_project is given, it will be included in the scope
1291 1294 def self.allowed_target_projects(user=User.current, current_project=nil)
1292 1295 condition = Project.allowed_to_condition(user, :add_issues)
1293 1296 if current_project
1294 1297 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1295 1298 end
1296 1299 Project.where(condition)
1297 1300 end
1298 1301
1299 1302 private
1300 1303
1301 1304 def after_project_change
1302 1305 # Update project_id on related time entries
1303 1306 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1304 1307
1305 1308 # Delete issue relations
1306 1309 unless Setting.cross_project_issue_relations?
1307 1310 relations_from.clear
1308 1311 relations_to.clear
1309 1312 end
1310 1313
1311 1314 # Move subtasks that were in the same project
1312 1315 children.each do |child|
1313 1316 next unless child.project_id == project_id_was
1314 1317 # Change project and keep project
1315 1318 child.send :project=, project, true
1316 1319 unless child.save
1317 1320 raise ActiveRecord::Rollback
1318 1321 end
1319 1322 end
1320 1323 end
1321 1324
1322 1325 # Callback for after the creation of an issue by copy
1323 1326 # * adds a "copied to" relation with the copied issue
1324 1327 # * copies subtasks from the copied issue
1325 1328 def after_create_from_copy
1326 1329 return unless copy? && !@after_create_from_copy_handled
1327 1330
1328 1331 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1329 1332 if @current_journal
1330 1333 @copied_from.init_journal(@current_journal.user)
1331 1334 end
1332 1335 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1333 1336 unless relation.save
1334 1337 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1335 1338 end
1336 1339 end
1337 1340
1338 1341 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1339 1342 copy_options = (@copy_options || {}).merge(:subtasks => false)
1340 1343 copied_issue_ids = {@copied_from.id => self.id}
1341 1344 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1342 1345 # Do not copy self when copying an issue as a descendant of the copied issue
1343 1346 next if child == self
1344 1347 # Do not copy subtasks of issues that were not copied
1345 1348 next unless copied_issue_ids[child.parent_id]
1346 1349 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1347 1350 unless child.visible?
1348 1351 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1349 1352 next
1350 1353 end
1351 1354 copy = Issue.new.copy_from(child, copy_options)
1352 1355 if @current_journal
1353 1356 copy.init_journal(@current_journal.user)
1354 1357 end
1355 1358 copy.author = author
1356 1359 copy.project = project
1357 1360 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1358 1361 unless copy.save
1359 1362 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1360 1363 next
1361 1364 end
1362 1365 copied_issue_ids[child.id] = copy.id
1363 1366 end
1364 1367 end
1365 1368 @after_create_from_copy_handled = true
1366 1369 end
1367 1370
1368 1371 def update_nested_set_attributes
1369 1372 if parent_id_changed?
1370 1373 update_nested_set_attributes_on_parent_change
1371 1374 end
1372 1375 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1373 1376 end
1374 1377
1375 1378 # Updates the nested set for when an existing issue is moved
1376 1379 def update_nested_set_attributes_on_parent_change
1377 1380 former_parent_id = parent_id_was
1378 1381 # delete invalid relations of all descendants
1379 1382 self_and_descendants.each do |issue|
1380 1383 issue.relations.each do |relation|
1381 1384 relation.destroy unless relation.valid?
1382 1385 end
1383 1386 end
1384 1387 # update former parent
1385 1388 recalculate_attributes_for(former_parent_id) if former_parent_id
1386 1389 end
1387 1390
1388 1391 def update_parent_attributes
1389 1392 if parent_id
1390 1393 recalculate_attributes_for(parent_id)
1391 1394 association(:parent).reset
1392 1395 end
1393 1396 end
1394 1397
1395 1398 def recalculate_attributes_for(issue_id)
1396 1399 if issue_id && p = Issue.find_by_id(issue_id)
1397 1400 # priority = highest priority of children
1398 1401 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1399 1402 p.priority = IssuePriority.find_by_position(priority_position)
1400 1403 end
1401 1404
1402 1405 # start/due dates = lowest/highest dates of children
1403 1406 p.start_date = p.children.minimum(:start_date)
1404 1407 p.due_date = p.children.maximum(:due_date)
1405 1408 if p.start_date && p.due_date && p.due_date < p.start_date
1406 1409 p.start_date, p.due_date = p.due_date, p.start_date
1407 1410 end
1408 1411
1409 1412 # done ratio = weighted average ratio of leaves
1410 1413 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1411 1414 leaves_count = p.leaves.count
1412 1415 if leaves_count > 0
1413 1416 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1414 1417 if average == 0
1415 1418 average = 1
1416 1419 end
1417 1420 done = p.leaves.joins(:status).
1418 1421 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1419 1422 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1420 1423 progress = done / (average * leaves_count)
1421 1424 p.done_ratio = progress.round
1422 1425 end
1423 1426 end
1424 1427
1425 1428 # estimate = sum of leaves estimates
1426 1429 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1427 1430 p.estimated_hours = nil if p.estimated_hours == 0.0
1428 1431
1429 1432 # ancestors will be recursively updated
1430 1433 p.save(:validate => false)
1431 1434 end
1432 1435 end
1433 1436
1434 1437 # Update issues so their versions are not pointing to a
1435 1438 # fixed_version that is not shared with the issue's project
1436 1439 def self.update_versions(conditions=nil)
1437 1440 # Only need to update issues with a fixed_version from
1438 1441 # a different project and that is not systemwide shared
1439 1442 Issue.joins(:project, :fixed_version).
1440 1443 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1441 1444 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1442 1445 " AND #{Version.table_name}.sharing <> 'system'").
1443 1446 where(conditions).each do |issue|
1444 1447 next if issue.project.nil? || issue.fixed_version.nil?
1445 1448 unless issue.project.shared_versions.include?(issue.fixed_version)
1446 1449 issue.init_journal(User.current)
1447 1450 issue.fixed_version = nil
1448 1451 issue.save
1449 1452 end
1450 1453 end
1451 1454 end
1452 1455
1453 1456 # Callback on file attachment
1454 1457 def attachment_added(attachment)
1455 1458 if current_journal && !attachment.new_record?
1456 1459 current_journal.journalize_attachment(attachment, :added)
1457 1460 end
1458 1461 end
1459 1462
1460 1463 # Callback on attachment deletion
1461 1464 def attachment_removed(attachment)
1462 1465 if current_journal && !attachment.new_record?
1463 1466 current_journal.journalize_attachment(attachment, :removed)
1464 1467 current_journal.save
1465 1468 end
1466 1469 end
1467 1470
1468 1471 # Called after a relation is added
1469 1472 def relation_added(relation)
1470 1473 if current_journal
1471 1474 current_journal.journalize_relation(relation, :added)
1472 1475 current_journal.save
1473 1476 end
1474 1477 end
1475 1478
1476 1479 # Called after a relation is removed
1477 1480 def relation_removed(relation)
1478 1481 if current_journal
1479 1482 current_journal.journalize_relation(relation, :removed)
1480 1483 current_journal.save
1481 1484 end
1482 1485 end
1483 1486
1484 1487 # Default assignment based on category
1485 1488 def default_assign
1486 1489 if assigned_to.nil? && category && category.assigned_to
1487 1490 self.assigned_to = category.assigned_to
1488 1491 end
1489 1492 end
1490 1493
1491 1494 # Updates start/due dates of following issues
1492 1495 def reschedule_following_issues
1493 1496 if start_date_changed? || due_date_changed?
1494 1497 relations_from.each do |relation|
1495 1498 relation.set_issue_to_dates
1496 1499 end
1497 1500 end
1498 1501 end
1499 1502
1500 1503 # Closes duplicates if the issue is being closed
1501 1504 def close_duplicates
1502 1505 if closing?
1503 1506 duplicates.each do |duplicate|
1504 1507 # Reload is needed in case the duplicate was updated by a previous duplicate
1505 1508 duplicate.reload
1506 1509 # Don't re-close it if it's already closed
1507 1510 next if duplicate.closed?
1508 1511 # Same user and notes
1509 1512 if @current_journal
1510 1513 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1511 1514 end
1512 1515 duplicate.update_attribute :status, self.status
1513 1516 end
1514 1517 end
1515 1518 end
1516 1519
1517 1520 # Make sure updated_on is updated when adding a note and set updated_on now
1518 1521 # so we can set closed_on with the same value on closing
1519 1522 def force_updated_on_change
1520 1523 if @current_journal || changed?
1521 1524 self.updated_on = current_time_from_proper_timezone
1522 1525 if new_record?
1523 1526 self.created_on = updated_on
1524 1527 end
1525 1528 end
1526 1529 end
1527 1530
1528 1531 # Callback for setting closed_on when the issue is closed.
1529 1532 # The closed_on attribute stores the time of the last closing
1530 1533 # and is preserved when the issue is reopened.
1531 1534 def update_closed_on
1532 1535 if closing?
1533 1536 self.closed_on = updated_on
1534 1537 end
1535 1538 end
1536 1539
1537 1540 # Saves the changes in a Journal
1538 1541 # Called after_save
1539 1542 def create_journal
1540 1543 if current_journal
1541 1544 current_journal.save
1542 1545 end
1543 1546 end
1544 1547
1545 1548 def send_notification
1546 1549 if Setting.notified_events.include?('issue_added')
1547 1550 Mailer.deliver_issue_add(self)
1548 1551 end
1549 1552 end
1550 1553
1551 1554 # Stores the previous assignee so we can still have access
1552 1555 # to it during after_save callbacks (assigned_to_id_was is reset)
1553 1556 def set_assigned_to_was
1554 1557 @previous_assigned_to_id = assigned_to_id_was
1555 1558 end
1556 1559
1557 1560 # Clears the previous assignee at the end of after_save callbacks
1558 1561 def clear_assigned_to_was
1559 1562 @assigned_to_was = nil
1560 1563 @previous_assigned_to_id = nil
1561 1564 end
1562 1565 end
@@ -1,7 +1,7
1 1 <div class="contextual">
2 2 <%= link_to l(:button_edit), edit_issue_path(@issue), :onclick => 'showAndScrollTo("update", "issue_notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit) if @issue.editable? %>
3 3 <%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %>
4 4 <%= watcher_link(@issue, User.current) %>
5 <%= link_to l(:button_copy), project_copy_issue_path(@project, @issue), :class => 'icon icon-copy' if User.current.allowed_to?(:add_issues, @project) %>
5 <%= link_to l(:button_copy), project_copy_issue_path(@project, @issue), :class => 'icon icon-copy' if User.current.allowed_to?(:copy_issues, @project) && Issue.allowed_target_projects.any? %>
6 6 <%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %>
7 7 </div>
@@ -1,206 +1,207
1 1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
2 2
3 3 <% if @saved_issues && @unsaved_issues.present? %>
4 4 <div id="errorExplanation">
5 5 <span>
6 6 <%= l(:notice_failed_to_save_issues,
7 7 :count => @unsaved_issues.size,
8 8 :total => @saved_issues.size,
9 9 :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %>
10 10 </span>
11 11 <ul>
12 12 <% bulk_edit_error_messages(@unsaved_issues).each do |message| %>
13 13 <li><%= message %></li>
14 14 <% end %>
15 15 </ul>
16 16 </div>
17 17 <% end %>
18 18
19 19 <ul id="bulk-selection">
20 20 <% @issues.each do |issue| %>
21 21 <%= content_tag 'li', link_to_issue(issue) %>
22 22 <% end %>
23 23 </ul>
24 24
25 25 <%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %>
26 26 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join("\n").html_safe %>
27 27 <div class="box tabular">
28 28 <fieldset class="attributes">
29 29 <legend><%= l(:label_change_properties) %></legend>
30 30
31 31 <div class="splitcontentleft">
32 32 <% if @allowed_projects.present? %>
33 33 <p>
34 34 <label for="issue_project_id"><%= l(:field_project) %></label>
35 35 <%= select_tag('issue[project_id]',
36 content_tag('option', l(:label_no_change_option), :value => '') +
37 project_tree_options_for_select(@allowed_projects, :selected => @target_project),
36 project_tree_options_for_select(@allowed_projects,
37 :include_blank => ((!@copy || (@projects & @allowed_projects == @projects)) ? l(:label_no_change_option) : false),
38 :selected => @target_project),
38 39 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
39 40 </p>
40 41 <% end %>
41 42 <p>
42 43 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
43 44 <%= select_tag('issue[tracker_id]',
44 45 content_tag('option', l(:label_no_change_option), :value => '') +
45 46 options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %>
46 47 </p>
47 48 <% if @available_statuses.any? %>
48 49 <p>
49 50 <label for='issue_status_id'><%= l(:field_status) %></label>
50 51 <%= select_tag('issue[status_id]',
51 52 content_tag('option', l(:label_no_change_option), :value => '') +
52 53 options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %>
53 54 </p>
54 55 <% end %>
55 56
56 57 <% if @safe_attributes.include?('priority_id') -%>
57 58 <p>
58 59 <label for='issue_priority_id'><%= l(:field_priority) %></label>
59 60 <%= select_tag('issue[priority_id]',
60 61 content_tag('option', l(:label_no_change_option), :value => '') +
61 62 options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %>
62 63 </p>
63 64 <% end %>
64 65
65 66 <% if @safe_attributes.include?('assigned_to_id') -%>
66 67 <p>
67 68 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
68 69 <%= select_tag('issue[assigned_to_id]',
69 70 content_tag('option', l(:label_no_change_option), :value => '') +
70 71 content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) +
71 72 principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %>
72 73 </p>
73 74 <% end %>
74 75
75 76 <% if @safe_attributes.include?('category_id') -%>
76 77 <p>
77 78 <label for='issue_category_id'><%= l(:field_category) %></label>
78 79 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
79 80 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) +
80 81 options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %>
81 82 </p>
82 83 <% end %>
83 84
84 85 <% if @safe_attributes.include?('fixed_version_id') -%>
85 86 <p>
86 87 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
87 88 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
88 89 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) +
89 90 version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %>
90 91 </p>
91 92 <% end %>
92 93
93 94 <% @custom_fields.each do |custom_field| %>
94 95 <p>
95 96 <label><%= h(custom_field.name) %></label>
96 97 <%= custom_field_tag_for_bulk_edit('issue', custom_field, @issues, @issue_params[:custom_field_values][custom_field.id.to_s]) %>
97 98 </p>
98 99 <% end %>
99 100
100 101 <% if @copy && Setting.link_copied_issue == 'ask' %>
101 102 <p>
102 103 <label for='link_copy'><%= l(:label_link_copied_issue) %></label>
103 104 <%= hidden_field_tag 'link_copy', '0' %>
104 105 <%= check_box_tag 'link_copy', '1', params[:link_copy] != 0 %>
105 106 </p>
106 107 <% end %>
107 108
108 109 <% if @copy && @attachments_present %>
109 110 <%= hidden_field_tag 'copy_attachments', '0' %>
110 111 <p>
111 112 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
112 113 <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %>
113 114 </p>
114 115 <% end %>
115 116
116 117 <% if @copy && @subtasks_present %>
117 118 <%= hidden_field_tag 'copy_subtasks', '0' %>
118 119 <p>
119 120 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
120 121 <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %>
121 122 </p>
122 123 <% end %>
123 124
124 125 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
125 126 </div>
126 127
127 128 <div class="splitcontentright">
128 129 <% if @safe_attributes.include?('is_private') %>
129 130 <p>
130 131 <label for='issue_is_private'><%= l(:field_is_private) %></label>
131 132 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
132 133 content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) +
133 134 content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %>
134 135 </p>
135 136 <% end %>
136 137
137 138 <% if @safe_attributes.include?('parent_issue_id') && @project %>
138 139 <p>
139 140 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
140 141 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %>
141 142 <label class="inline"><%= check_box_tag 'issue[parent_issue_id]', 'none', (@issue_params[:parent_issue_id] == 'none'), :id => nil, :data => {:disables => '#issue_parent_issue_id'} %><%= l(:button_clear) %></label>
142 143 </p>
143 144 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => Setting.cross_project_subtasks)}')" %>
144 145 <% end %>
145 146
146 147 <% if @safe_attributes.include?('start_date') %>
147 148 <p>
148 149 <label for='issue_start_date'><%= l(:field_start_date) %></label>
149 150 <%= text_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
150 151 <label class="inline"><%= check_box_tag 'issue[start_date]', 'none', (@issue_params[:start_date] == 'none'), :id => nil, :data => {:disables => '#issue_start_date'} %><%= l(:button_clear) %></label>
151 152 </p>
152 153 <% end %>
153 154
154 155 <% if @safe_attributes.include?('due_date') %>
155 156 <p>
156 157 <label for='issue_due_date'><%= l(:field_due_date) %></label>
157 158 <%= text_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
158 159 <label class="inline"><%= check_box_tag 'issue[due_date]', 'none', (@issue_params[:due_date] == 'none'), :id => nil, :data => {:disables => '#issue_due_date'} %><%= l(:button_clear) %></label>
159 160 </p>
160 161 <% end %>
161 162
162 163 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
163 164 <p>
164 165 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
165 166 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }, @issue_params[:done_ratio]) %>
166 167 </p>
167 168 <% end %>
168 169 </div>
169 170 </fieldset>
170 171
171 172 <fieldset>
172 173 <legend><%= l(:field_notes) %></legend>
173 174 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
174 175 <%= wikitoolbar_for 'notes' %>
175 176 </fieldset>
176 177 </div>
177 178
178 179 <p>
179 180 <% if @copy %>
180 181 <%= hidden_field_tag 'copy', '1' %>
181 182 <%= submit_tag l(:button_copy) %>
182 183 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
183 184 <% elsif @target_project %>
184 185 <%= submit_tag l(:button_move) %>
185 186 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
186 187 <% else %>
187 188 <%= submit_tag l(:button_submit) %>
188 189 <% end %>
189 190 </p>
190 191
191 192 <% end %>
192 193
193 194 <%= javascript_tag do %>
194 195 $(window).load(function(){
195 196 $(document).on('change', 'input[data-disables]', function(){
196 197 if ($(this).prop('checked')){
197 198 $($(this).data('disables')).attr('disabled', true).val('');
198 199 } else {
199 200 $($(this).data('disables')).attr('disabled', false);
200 201 }
201 202 });
202 203 });
203 204 $(document).ready(function(){
204 205 $('input[data-disables]').trigger('change');
205 206 });
206 207 <% end %>
@@ -1,1132 +1,1133
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_hours:
53 53 one: "1 hour"
54 54 other: "%{count} hours"
55 55 x_days:
56 56 one: "1 day"
57 57 other: "%{count} days"
58 58 about_x_months:
59 59 one: "about 1 month"
60 60 other: "about %{count} months"
61 61 x_months:
62 62 one: "1 month"
63 63 other: "%{count} months"
64 64 about_x_years:
65 65 one: "about 1 year"
66 66 other: "about %{count} years"
67 67 over_x_years:
68 68 one: "over 1 year"
69 69 other: "over %{count} years"
70 70 almost_x_years:
71 71 one: "almost 1 year"
72 72 other: "almost %{count} years"
73 73
74 74 number:
75 75 format:
76 76 separator: "."
77 77 delimiter: ""
78 78 precision: 3
79 79
80 80 human:
81 81 format:
82 82 delimiter: ""
83 83 precision: 3
84 84 storage_units:
85 85 format: "%n %u"
86 86 units:
87 87 byte:
88 88 one: "Byte"
89 89 other: "Bytes"
90 90 kb: "KB"
91 91 mb: "MB"
92 92 gb: "GB"
93 93 tb: "TB"
94 94
95 95 # Used in array.to_sentence.
96 96 support:
97 97 array:
98 98 sentence_connector: "and"
99 99 skip_last_comma: false
100 100
101 101 activerecord:
102 102 errors:
103 103 template:
104 104 header:
105 105 one: "1 error prohibited this %{model} from being saved"
106 106 other: "%{count} errors prohibited this %{model} from being saved"
107 107 messages:
108 108 inclusion: "is not included in the list"
109 109 exclusion: "is reserved"
110 110 invalid: "is invalid"
111 111 confirmation: "doesn't match confirmation"
112 112 accepted: "must be accepted"
113 113 empty: "cannot be empty"
114 114 blank: "cannot be blank"
115 115 too_long: "is too long (maximum is %{count} characters)"
116 116 too_short: "is too short (minimum is %{count} characters)"
117 117 wrong_length: "is the wrong length (should be %{count} characters)"
118 118 taken: "has already been taken"
119 119 not_a_number: "is not a number"
120 120 not_a_date: "is not a valid date"
121 121 greater_than: "must be greater than %{count}"
122 122 greater_than_or_equal_to: "must be greater than or equal to %{count}"
123 123 equal_to: "must be equal to %{count}"
124 124 less_than: "must be less than %{count}"
125 125 less_than_or_equal_to: "must be less than or equal to %{count}"
126 126 odd: "must be odd"
127 127 even: "must be even"
128 128 greater_than_start_date: "must be greater than start date"
129 129 not_same_project: "doesn't belong to the same project"
130 130 circular_dependency: "This relation would create a circular dependency"
131 131 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
132 132 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
133 133
134 134 actionview_instancetag_blank_option: Please select
135 135
136 136 general_text_No: 'No'
137 137 general_text_Yes: 'Yes'
138 138 general_text_no: 'no'
139 139 general_text_yes: 'yes'
140 140 general_lang_name: 'English'
141 141 general_csv_separator: ','
142 142 general_csv_decimal_separator: '.'
143 143 general_csv_encoding: ISO-8859-1
144 144 general_pdf_fontname: freesans
145 145 general_first_day_of_week: '7'
146 146
147 147 notice_account_updated: Account was successfully updated.
148 148 notice_account_invalid_creditentials: Invalid user or password
149 149 notice_account_password_updated: Password was successfully updated.
150 150 notice_account_wrong_password: Wrong password
151 151 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
152 152 notice_account_unknown_email: Unknown user.
153 153 notice_account_not_activated_yet: You haven't activated your account yet. If you want to receive a new activation email, please <a href="%{url}">click this link</a>.
154 154 notice_account_locked: Your account is locked.
155 155 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
156 156 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
157 157 notice_account_activated: Your account has been activated. You can now log in.
158 158 notice_successful_create: Successful creation.
159 159 notice_successful_update: Successful update.
160 160 notice_successful_delete: Successful deletion.
161 161 notice_successful_connection: Successful connection.
162 162 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
163 163 notice_locking_conflict: Data has been updated by another user.
164 164 notice_not_authorized: You are not authorized to access this page.
165 165 notice_not_authorized_archived_project: The project you're trying to access has been archived.
166 166 notice_email_sent: "An email was sent to %{value}"
167 167 notice_email_error: "An error occurred while sending mail (%{value})"
168 168 notice_feeds_access_key_reseted: Your Atom access key was reset.
169 169 notice_api_access_key_reseted: Your API access key was reset.
170 170 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
171 171 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
172 172 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
173 173 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
174 174 notice_account_pending: "Your account was created and is now pending administrator approval."
175 175 notice_default_data_loaded: Default configuration successfully loaded.
176 176 notice_unable_delete_version: Unable to delete version.
177 177 notice_unable_delete_time_entry: Unable to delete time log entry.
178 178 notice_issue_done_ratios_updated: Issue done ratios updated.
179 179 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
180 180 notice_issue_successful_create: "Issue %{id} created."
181 181 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
182 182 notice_account_deleted: "Your account has been permanently deleted."
183 183 notice_user_successful_create: "User %{id} created."
184 184 notice_new_password_must_be_different: The new password must be different from the current password
185 185
186 186 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
187 187 error_scm_not_found: "The entry or revision was not found in the repository."
188 188 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
189 189 error_scm_annotate: "The entry does not exist or cannot be annotated."
190 190 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
191 191 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
192 192 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
193 193 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
194 194 error_can_not_delete_custom_field: Unable to delete custom field
195 195 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
196 196 error_can_not_remove_role: "This role is in use and cannot be deleted."
197 197 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
198 198 error_can_not_archive_project: This project cannot be archived
199 199 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
200 200 error_workflow_copy_source: 'Please select a source tracker or role'
201 201 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
202 202 error_unable_delete_issue_status: 'Unable to delete issue status'
203 203 error_unable_to_connect: "Unable to connect (%{value})"
204 204 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
205 205 error_session_expired: "Your session has expired. Please login again."
206 206 warning_attachments_not_saved: "%{count} file(s) could not be saved."
207 207
208 208 mail_subject_lost_password: "Your %{value} password"
209 209 mail_body_lost_password: 'To change your password, click on the following link:'
210 210 mail_subject_register: "Your %{value} account activation"
211 211 mail_body_register: 'To activate your account, click on the following link:'
212 212 mail_body_account_information_external: "You can use your %{value} account to log in."
213 213 mail_body_account_information: Your account information
214 214 mail_subject_account_activation_request: "%{value} account activation request"
215 215 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
216 216 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
217 217 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
218 218 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
219 219 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
220 220 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
221 221 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
222 222
223 223 field_name: Name
224 224 field_description: Description
225 225 field_summary: Summary
226 226 field_is_required: Required
227 227 field_firstname: First name
228 228 field_lastname: Last name
229 229 field_mail: Email
230 230 field_address: Email
231 231 field_filename: File
232 232 field_filesize: Size
233 233 field_downloads: Downloads
234 234 field_author: Author
235 235 field_created_on: Created
236 236 field_updated_on: Updated
237 237 field_closed_on: Closed
238 238 field_field_format: Format
239 239 field_is_for_all: For all projects
240 240 field_possible_values: Possible values
241 241 field_regexp: Regular expression
242 242 field_min_length: Minimum length
243 243 field_max_length: Maximum length
244 244 field_value: Value
245 245 field_category: Category
246 246 field_title: Title
247 247 field_project: Project
248 248 field_issue: Issue
249 249 field_status: Status
250 250 field_notes: Notes
251 251 field_is_closed: Issue closed
252 252 field_is_default: Default value
253 253 field_tracker: Tracker
254 254 field_subject: Subject
255 255 field_due_date: Due date
256 256 field_assigned_to: Assignee
257 257 field_priority: Priority
258 258 field_fixed_version: Target version
259 259 field_user: User
260 260 field_principal: Principal
261 261 field_role: Role
262 262 field_homepage: Homepage
263 263 field_is_public: Public
264 264 field_parent: Subproject of
265 265 field_is_in_roadmap: Issues displayed in roadmap
266 266 field_login: Login
267 267 field_mail_notification: Email notifications
268 268 field_admin: Administrator
269 269 field_last_login_on: Last connection
270 270 field_language: Language
271 271 field_effective_date: Date
272 272 field_password: Password
273 273 field_new_password: New password
274 274 field_password_confirmation: Confirmation
275 275 field_version: Version
276 276 field_type: Type
277 277 field_host: Host
278 278 field_port: Port
279 279 field_account: Account
280 280 field_base_dn: Base DN
281 281 field_attr_login: Login attribute
282 282 field_attr_firstname: Firstname attribute
283 283 field_attr_lastname: Lastname attribute
284 284 field_attr_mail: Email attribute
285 285 field_onthefly: On-the-fly user creation
286 286 field_start_date: Start date
287 287 field_done_ratio: "% Done"
288 288 field_auth_source: Authentication mode
289 289 field_hide_mail: Hide my email address
290 290 field_comments: Comment
291 291 field_url: URL
292 292 field_start_page: Start page
293 293 field_subproject: Subproject
294 294 field_hours: Hours
295 295 field_activity: Activity
296 296 field_spent_on: Date
297 297 field_identifier: Identifier
298 298 field_is_filter: Used as a filter
299 299 field_issue_to: Related issue
300 300 field_delay: Delay
301 301 field_assignable: Issues can be assigned to this role
302 302 field_redirect_existing_links: Redirect existing links
303 303 field_estimated_hours: Estimated time
304 304 field_column_names: Columns
305 305 field_time_entries: Log time
306 306 field_time_zone: Time zone
307 307 field_searchable: Searchable
308 308 field_default_value: Default value
309 309 field_comments_sorting: Display comments
310 310 field_parent_title: Parent page
311 311 field_editable: Editable
312 312 field_watcher: Watcher
313 313 field_identity_url: OpenID URL
314 314 field_content: Content
315 315 field_group_by: Group results by
316 316 field_sharing: Sharing
317 317 field_parent_issue: Parent task
318 318 field_member_of_group: "Assignee's group"
319 319 field_assigned_to_role: "Assignee's role"
320 320 field_text: Text field
321 321 field_visible: Visible
322 322 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
323 323 field_issues_visibility: Issues visibility
324 324 field_is_private: Private
325 325 field_commit_logs_encoding: Commit messages encoding
326 326 field_scm_path_encoding: Path encoding
327 327 field_path_to_repository: Path to repository
328 328 field_root_directory: Root directory
329 329 field_cvsroot: CVSROOT
330 330 field_cvs_module: Module
331 331 field_repository_is_default: Main repository
332 332 field_multiple: Multiple values
333 333 field_auth_source_ldap_filter: LDAP filter
334 334 field_core_fields: Standard fields
335 335 field_timeout: "Timeout (in seconds)"
336 336 field_board_parent: Parent forum
337 337 field_private_notes: Private notes
338 338 field_inherit_members: Inherit members
339 339 field_generate_password: Generate password
340 340 field_must_change_passwd: Must change password at next logon
341 341 field_default_status: Default status
342 342 field_users_visibility: Users visibility
343 343
344 344 setting_app_title: Application title
345 345 setting_app_subtitle: Application subtitle
346 346 setting_welcome_text: Welcome text
347 347 setting_default_language: Default language
348 348 setting_login_required: Authentication required
349 349 setting_self_registration: Self-registration
350 350 setting_attachment_max_size: Maximum attachment size
351 351 setting_issues_export_limit: Issues export limit
352 352 setting_mail_from: Emission email address
353 353 setting_bcc_recipients: Blind carbon copy recipients (bcc)
354 354 setting_plain_text_mail: Plain text mail (no HTML)
355 355 setting_host_name: Host name and path
356 356 setting_text_formatting: Text formatting
357 357 setting_wiki_compression: Wiki history compression
358 358 setting_feeds_limit: Maximum number of items in Atom feeds
359 359 setting_default_projects_public: New projects are public by default
360 360 setting_autofetch_changesets: Fetch commits automatically
361 361 setting_sys_api_enabled: Enable WS for repository management
362 362 setting_commit_ref_keywords: Referencing keywords
363 363 setting_commit_fix_keywords: Fixing keywords
364 364 setting_autologin: Autologin
365 365 setting_date_format: Date format
366 366 setting_time_format: Time format
367 367 setting_cross_project_issue_relations: Allow cross-project issue relations
368 368 setting_cross_project_subtasks: Allow cross-project subtasks
369 369 setting_issue_list_default_columns: Default columns displayed on the issue list
370 370 setting_repositories_encodings: Attachments and repositories encodings
371 371 setting_emails_header: Email header
372 372 setting_emails_footer: Email footer
373 373 setting_protocol: Protocol
374 374 setting_per_page_options: Objects per page options
375 375 setting_user_format: Users display format
376 376 setting_activity_days_default: Days displayed on project activity
377 377 setting_display_subprojects_issues: Display subprojects issues on main projects by default
378 378 setting_enabled_scm: Enabled SCM
379 379 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
380 380 setting_mail_handler_api_enabled: Enable WS for incoming emails
381 381 setting_mail_handler_api_key: API key
382 382 setting_sequential_project_identifiers: Generate sequential project identifiers
383 383 setting_gravatar_enabled: Use Gravatar user icons
384 384 setting_gravatar_default: Default Gravatar image
385 385 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
386 386 setting_file_max_size_displayed: Maximum size of text files displayed inline
387 387 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
388 388 setting_openid: Allow OpenID login and registration
389 389 setting_password_min_length: Minimum password length
390 390 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
391 391 setting_default_projects_modules: Default enabled modules for new projects
392 392 setting_issue_done_ratio: Calculate the issue done ratio with
393 393 setting_issue_done_ratio_issue_field: Use the issue field
394 394 setting_issue_done_ratio_issue_status: Use the issue status
395 395 setting_start_of_week: Start calendars on
396 396 setting_rest_api_enabled: Enable REST web service
397 397 setting_cache_formatted_text: Cache formatted text
398 398 setting_default_notification_option: Default notification option
399 399 setting_commit_logtime_enabled: Enable time logging
400 400 setting_commit_logtime_activity_id: Activity for logged time
401 401 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
402 402 setting_issue_group_assignment: Allow issue assignment to groups
403 403 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
404 404 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
405 405 setting_unsubscribe: Allow users to delete their own account
406 406 setting_session_lifetime: Session maximum lifetime
407 407 setting_session_timeout: Session inactivity timeout
408 408 setting_thumbnails_enabled: Display attachment thumbnails
409 409 setting_thumbnails_size: Thumbnails size (in pixels)
410 410 setting_non_working_week_days: Non-working days
411 411 setting_jsonp_enabled: Enable JSONP support
412 412 setting_default_projects_tracker_ids: Default trackers for new projects
413 413 setting_mail_handler_excluded_filenames: Exclude attachments by name
414 414 setting_force_default_language_for_anonymous: Force default language for anonymous users
415 415 setting_force_default_language_for_loggedin: Force default language for logged-in users
416 416 setting_link_copied_issue: Link issues on copy
417 417 setting_max_additional_emails: Maximum number of additional email addresses
418 418 setting_search_results_per_page: Search results per page
419 419
420 420 permission_add_project: Create project
421 421 permission_add_subprojects: Create subprojects
422 422 permission_edit_project: Edit project
423 423 permission_close_project: Close / reopen the project
424 424 permission_select_project_modules: Select project modules
425 425 permission_manage_members: Manage members
426 426 permission_manage_project_activities: Manage project activities
427 427 permission_manage_versions: Manage versions
428 428 permission_manage_categories: Manage issue categories
429 429 permission_view_issues: View Issues
430 430 permission_add_issues: Add issues
431 431 permission_edit_issues: Edit issues
432 permission_copy_issues: Copy issues
432 433 permission_manage_issue_relations: Manage issue relations
433 434 permission_set_issues_private: Set issues public or private
434 435 permission_set_own_issues_private: Set own issues public or private
435 436 permission_add_issue_notes: Add notes
436 437 permission_edit_issue_notes: Edit notes
437 438 permission_edit_own_issue_notes: Edit own notes
438 439 permission_view_private_notes: View private notes
439 440 permission_set_notes_private: Set notes as private
440 441 permission_move_issues: Move issues
441 442 permission_delete_issues: Delete issues
442 443 permission_manage_public_queries: Manage public queries
443 444 permission_save_queries: Save queries
444 445 permission_view_gantt: View gantt chart
445 446 permission_view_calendar: View calendar
446 447 permission_view_issue_watchers: View watchers list
447 448 permission_add_issue_watchers: Add watchers
448 449 permission_delete_issue_watchers: Delete watchers
449 450 permission_log_time: Log spent time
450 451 permission_view_time_entries: View spent time
451 452 permission_edit_time_entries: Edit time logs
452 453 permission_edit_own_time_entries: Edit own time logs
453 454 permission_manage_news: Manage news
454 455 permission_comment_news: Comment news
455 456 permission_view_documents: View documents
456 457 permission_add_documents: Add documents
457 458 permission_edit_documents: Edit documents
458 459 permission_delete_documents: Delete documents
459 460 permission_manage_files: Manage files
460 461 permission_view_files: View files
461 462 permission_manage_wiki: Manage wiki
462 463 permission_rename_wiki_pages: Rename wiki pages
463 464 permission_delete_wiki_pages: Delete wiki pages
464 465 permission_view_wiki_pages: View wiki
465 466 permission_view_wiki_edits: View wiki history
466 467 permission_edit_wiki_pages: Edit wiki pages
467 468 permission_delete_wiki_pages_attachments: Delete attachments
468 469 permission_protect_wiki_pages: Protect wiki pages
469 470 permission_manage_repository: Manage repository
470 471 permission_browse_repository: Browse repository
471 472 permission_view_changesets: View changesets
472 473 permission_commit_access: Commit access
473 474 permission_manage_boards: Manage forums
474 475 permission_view_messages: View messages
475 476 permission_add_messages: Post messages
476 477 permission_edit_messages: Edit messages
477 478 permission_edit_own_messages: Edit own messages
478 479 permission_delete_messages: Delete messages
479 480 permission_delete_own_messages: Delete own messages
480 481 permission_export_wiki_pages: Export wiki pages
481 482 permission_manage_subtasks: Manage subtasks
482 483 permission_manage_related_issues: Manage related issues
483 484
484 485 project_module_issue_tracking: Issue tracking
485 486 project_module_time_tracking: Time tracking
486 487 project_module_news: News
487 488 project_module_documents: Documents
488 489 project_module_files: Files
489 490 project_module_wiki: Wiki
490 491 project_module_repository: Repository
491 492 project_module_boards: Forums
492 493 project_module_calendar: Calendar
493 494 project_module_gantt: Gantt
494 495
495 496 label_user: User
496 497 label_user_plural: Users
497 498 label_user_new: New user
498 499 label_user_anonymous: Anonymous
499 500 label_project: Project
500 501 label_project_new: New project
501 502 label_project_plural: Projects
502 503 label_x_projects:
503 504 zero: no projects
504 505 one: 1 project
505 506 other: "%{count} projects"
506 507 label_project_all: All Projects
507 508 label_project_latest: Latest projects
508 509 label_issue: Issue
509 510 label_issue_new: New issue
510 511 label_issue_plural: Issues
511 512 label_issue_view_all: View all issues
512 513 label_issues_by: "Issues by %{value}"
513 514 label_issue_added: Issue added
514 515 label_issue_updated: Issue updated
515 516 label_issue_note_added: Note added
516 517 label_issue_status_updated: Status updated
517 518 label_issue_assigned_to_updated: Assignee updated
518 519 label_issue_priority_updated: Priority updated
519 520 label_document: Document
520 521 label_document_new: New document
521 522 label_document_plural: Documents
522 523 label_document_added: Document added
523 524 label_role: Role
524 525 label_role_plural: Roles
525 526 label_role_new: New role
526 527 label_role_and_permissions: Roles and permissions
527 528 label_role_anonymous: Anonymous
528 529 label_role_non_member: Non member
529 530 label_member: Member
530 531 label_member_new: New member
531 532 label_member_plural: Members
532 533 label_tracker: Tracker
533 534 label_tracker_plural: Trackers
534 535 label_tracker_new: New tracker
535 536 label_workflow: Workflow
536 537 label_issue_status: Issue status
537 538 label_issue_status_plural: Issue statuses
538 539 label_issue_status_new: New status
539 540 label_issue_category: Issue category
540 541 label_issue_category_plural: Issue categories
541 542 label_issue_category_new: New category
542 543 label_custom_field: Custom field
543 544 label_custom_field_plural: Custom fields
544 545 label_custom_field_new: New custom field
545 546 label_enumerations: Enumerations
546 547 label_enumeration_new: New value
547 548 label_information: Information
548 549 label_information_plural: Information
549 550 label_please_login: Please log in
550 551 label_register: Register
551 552 label_login_with_open_id_option: or login with OpenID
552 553 label_password_lost: Lost password
553 554 label_home: Home
554 555 label_my_page: My page
555 556 label_my_account: My account
556 557 label_my_projects: My projects
557 558 label_my_page_block: My page block
558 559 label_administration: Administration
559 560 label_login: Sign in
560 561 label_logout: Sign out
561 562 label_help: Help
562 563 label_reported_issues: Reported issues
563 564 label_assigned_to_me_issues: Issues assigned to me
564 565 label_last_login: Last connection
565 566 label_registered_on: Registered on
566 567 label_activity: Activity
567 568 label_overall_activity: Overall activity
568 569 label_user_activity: "%{value}'s activity"
569 570 label_new: New
570 571 label_logged_as: Logged in as
571 572 label_environment: Environment
572 573 label_authentication: Authentication
573 574 label_auth_source: Authentication mode
574 575 label_auth_source_new: New authentication mode
575 576 label_auth_source_plural: Authentication modes
576 577 label_subproject_plural: Subprojects
577 578 label_subproject_new: New subproject
578 579 label_and_its_subprojects: "%{value} and its subprojects"
579 580 label_min_max_length: Min - Max length
580 581 label_list: List
581 582 label_date: Date
582 583 label_integer: Integer
583 584 label_float: Float
584 585 label_boolean: Boolean
585 586 label_string: Text
586 587 label_text: Long text
587 588 label_attribute: Attribute
588 589 label_attribute_plural: Attributes
589 590 label_no_data: No data to display
590 591 label_change_status: Change status
591 592 label_history: History
592 593 label_attachment: File
593 594 label_attachment_new: New file
594 595 label_attachment_delete: Delete file
595 596 label_attachment_plural: Files
596 597 label_file_added: File added
597 598 label_report: Report
598 599 label_report_plural: Reports
599 600 label_news: News
600 601 label_news_new: Add news
601 602 label_news_plural: News
602 603 label_news_latest: Latest news
603 604 label_news_view_all: View all news
604 605 label_news_added: News added
605 606 label_news_comment_added: Comment added to a news
606 607 label_settings: Settings
607 608 label_overview: Overview
608 609 label_version: Version
609 610 label_version_new: New version
610 611 label_version_plural: Versions
611 612 label_close_versions: Close completed versions
612 613 label_confirmation: Confirmation
613 614 label_export_to: 'Also available in:'
614 615 label_read: Read...
615 616 label_public_projects: Public projects
616 617 label_open_issues: open
617 618 label_open_issues_plural: open
618 619 label_closed_issues: closed
619 620 label_closed_issues_plural: closed
620 621 label_x_open_issues_abbr_on_total:
621 622 zero: 0 open / %{total}
622 623 one: 1 open / %{total}
623 624 other: "%{count} open / %{total}"
624 625 label_x_open_issues_abbr:
625 626 zero: 0 open
626 627 one: 1 open
627 628 other: "%{count} open"
628 629 label_x_closed_issues_abbr:
629 630 zero: 0 closed
630 631 one: 1 closed
631 632 other: "%{count} closed"
632 633 label_x_issues:
633 634 zero: 0 issues
634 635 one: 1 issue
635 636 other: "%{count} issues"
636 637 label_total: Total
637 638 label_total_time: Total time
638 639 label_permissions: Permissions
639 640 label_current_status: Current status
640 641 label_new_statuses_allowed: New statuses allowed
641 642 label_all: all
642 643 label_any: any
643 644 label_none: none
644 645 label_nobody: nobody
645 646 label_next: Next
646 647 label_previous: Previous
647 648 label_used_by: Used by
648 649 label_details: Details
649 650 label_add_note: Add a note
650 651 label_calendar: Calendar
651 652 label_months_from: months from
652 653 label_gantt: Gantt
653 654 label_internal: Internal
654 655 label_last_changes: "last %{count} changes"
655 656 label_change_view_all: View all changes
656 657 label_personalize_page: Personalize this page
657 658 label_comment: Comment
658 659 label_comment_plural: Comments
659 660 label_x_comments:
660 661 zero: no comments
661 662 one: 1 comment
662 663 other: "%{count} comments"
663 664 label_comment_add: Add a comment
664 665 label_comment_added: Comment added
665 666 label_comment_delete: Delete comments
666 667 label_query: Custom query
667 668 label_query_plural: Custom queries
668 669 label_query_new: New query
669 670 label_my_queries: My custom queries
670 671 label_filter_add: Add filter
671 672 label_filter_plural: Filters
672 673 label_equals: is
673 674 label_not_equals: is not
674 675 label_in_less_than: in less than
675 676 label_in_more_than: in more than
676 677 label_in_the_next_days: in the next
677 678 label_in_the_past_days: in the past
678 679 label_greater_or_equal: '>='
679 680 label_less_or_equal: '<='
680 681 label_between: between
681 682 label_in: in
682 683 label_today: today
683 684 label_all_time: all time
684 685 label_yesterday: yesterday
685 686 label_this_week: this week
686 687 label_last_week: last week
687 688 label_last_n_weeks: "last %{count} weeks"
688 689 label_last_n_days: "last %{count} days"
689 690 label_this_month: this month
690 691 label_last_month: last month
691 692 label_this_year: this year
692 693 label_date_range: Date range
693 694 label_less_than_ago: less than days ago
694 695 label_more_than_ago: more than days ago
695 696 label_ago: days ago
696 697 label_contains: contains
697 698 label_not_contains: doesn't contain
698 699 label_any_issues_in_project: any issues in project
699 700 label_any_issues_not_in_project: any issues not in project
700 701 label_no_issues_in_project: no issues in project
701 702 label_day_plural: days
702 703 label_repository: Repository
703 704 label_repository_new: New repository
704 705 label_repository_plural: Repositories
705 706 label_browse: Browse
706 707 label_branch: Branch
707 708 label_tag: Tag
708 709 label_revision: Revision
709 710 label_revision_plural: Revisions
710 711 label_revision_id: "Revision %{value}"
711 712 label_associated_revisions: Associated revisions
712 713 label_added: added
713 714 label_modified: modified
714 715 label_copied: copied
715 716 label_renamed: renamed
716 717 label_deleted: deleted
717 718 label_latest_revision: Latest revision
718 719 label_latest_revision_plural: Latest revisions
719 720 label_view_revisions: View revisions
720 721 label_view_all_revisions: View all revisions
721 722 label_max_size: Maximum size
722 723 label_sort_highest: Move to top
723 724 label_sort_higher: Move up
724 725 label_sort_lower: Move down
725 726 label_sort_lowest: Move to bottom
726 727 label_roadmap: Roadmap
727 728 label_roadmap_due_in: "Due in %{value}"
728 729 label_roadmap_overdue: "%{value} late"
729 730 label_roadmap_no_issues: No issues for this version
730 731 label_search: Search
731 732 label_result_plural: Results
732 733 label_all_words: All words
733 734 label_wiki: Wiki
734 735 label_wiki_edit: Wiki edit
735 736 label_wiki_edit_plural: Wiki edits
736 737 label_wiki_page: Wiki page
737 738 label_wiki_page_plural: Wiki pages
738 739 label_index_by_title: Index by title
739 740 label_index_by_date: Index by date
740 741 label_current_version: Current version
741 742 label_preview: Preview
742 743 label_feed_plural: Feeds
743 744 label_changes_details: Details of all changes
744 745 label_issue_tracking: Issue tracking
745 746 label_spent_time: Spent time
746 747 label_overall_spent_time: Overall spent time
747 748 label_f_hour: "%{value} hour"
748 749 label_f_hour_plural: "%{value} hours"
749 750 label_time_tracking: Time tracking
750 751 label_change_plural: Changes
751 752 label_statistics: Statistics
752 753 label_commits_per_month: Commits per month
753 754 label_commits_per_author: Commits per author
754 755 label_diff: diff
755 756 label_view_diff: View differences
756 757 label_diff_inline: inline
757 758 label_diff_side_by_side: side by side
758 759 label_options: Options
759 760 label_copy_workflow_from: Copy workflow from
760 761 label_permissions_report: Permissions report
761 762 label_watched_issues: Watched issues
762 763 label_related_issues: Related issues
763 764 label_applied_status: Applied status
764 765 label_loading: Loading...
765 766 label_relation_new: New relation
766 767 label_relation_delete: Delete relation
767 768 label_relates_to: Related to
768 769 label_duplicates: Duplicates
769 770 label_duplicated_by: Duplicated by
770 771 label_blocks: Blocks
771 772 label_blocked_by: Blocked by
772 773 label_precedes: Precedes
773 774 label_follows: Follows
774 775 label_copied_to: Copied to
775 776 label_copied_from: Copied from
776 777 label_end_to_start: end to start
777 778 label_end_to_end: end to end
778 779 label_start_to_start: start to start
779 780 label_start_to_end: start to end
780 781 label_stay_logged_in: Stay logged in
781 782 label_disabled: disabled
782 783 label_show_completed_versions: Show completed versions
783 784 label_me: me
784 785 label_board: Forum
785 786 label_board_new: New forum
786 787 label_board_plural: Forums
787 788 label_board_locked: Locked
788 789 label_board_sticky: Sticky
789 790 label_topic_plural: Topics
790 791 label_message_plural: Messages
791 792 label_message_last: Last message
792 793 label_message_new: New message
793 794 label_message_posted: Message added
794 795 label_reply_plural: Replies
795 796 label_send_information: Send account information to the user
796 797 label_year: Year
797 798 label_month: Month
798 799 label_week: Week
799 800 label_date_from: From
800 801 label_date_to: To
801 802 label_language_based: Based on user's language
802 803 label_sort_by: "Sort by %{value}"
803 804 label_send_test_email: Send a test email
804 805 label_feeds_access_key: Atom access key
805 806 label_missing_feeds_access_key: Missing a Atom access key
806 807 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
807 808 label_module_plural: Modules
808 809 label_added_time_by: "Added by %{author} %{age} ago"
809 810 label_updated_time_by: "Updated by %{author} %{age} ago"
810 811 label_updated_time: "Updated %{value} ago"
811 812 label_jump_to_a_project: Jump to a project...
812 813 label_file_plural: Files
813 814 label_changeset_plural: Changesets
814 815 label_default_columns: Default columns
815 816 label_no_change_option: (No change)
816 817 label_bulk_edit_selected_issues: Bulk edit selected issues
817 818 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
818 819 label_theme: Theme
819 820 label_default: Default
820 821 label_search_titles_only: Search titles only
821 822 label_user_mail_option_all: "For any event on all my projects"
822 823 label_user_mail_option_selected: "For any event on the selected projects only..."
823 824 label_user_mail_option_none: "No events"
824 825 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
825 826 label_user_mail_option_only_assigned: "Only for things I am assigned to"
826 827 label_user_mail_option_only_owner: "Only for things I am the owner of"
827 828 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
828 829 label_registration_activation_by_email: account activation by email
829 830 label_registration_manual_activation: manual account activation
830 831 label_registration_automatic_activation: automatic account activation
831 832 label_display_per_page: "Per page: %{value}"
832 833 label_age: Age
833 834 label_change_properties: Change properties
834 835 label_general: General
835 836 label_more: More
836 837 label_scm: SCM
837 838 label_plugins: Plugins
838 839 label_ldap_authentication: LDAP authentication
839 840 label_downloads_abbr: D/L
840 841 label_optional_description: Optional description
841 842 label_add_another_file: Add another file
842 843 label_preferences: Preferences
843 844 label_chronological_order: In chronological order
844 845 label_reverse_chronological_order: In reverse chronological order
845 846 label_planning: Planning
846 847 label_incoming_emails: Incoming emails
847 848 label_generate_key: Generate a key
848 849 label_issue_watchers: Watchers
849 850 label_example: Example
850 851 label_display: Display
851 852 label_sort: Sort
852 853 label_ascending: Ascending
853 854 label_descending: Descending
854 855 label_date_from_to: From %{start} to %{end}
855 856 label_wiki_content_added: Wiki page added
856 857 label_wiki_content_updated: Wiki page updated
857 858 label_group: Group
858 859 label_group_plural: Groups
859 860 label_group_new: New group
860 861 label_group_anonymous: Anonymous users
861 862 label_group_non_member: Non member users
862 863 label_time_entry_plural: Spent time
863 864 label_version_sharing_none: Not shared
864 865 label_version_sharing_descendants: With subprojects
865 866 label_version_sharing_hierarchy: With project hierarchy
866 867 label_version_sharing_tree: With project tree
867 868 label_version_sharing_system: With all projects
868 869 label_update_issue_done_ratios: Update issue done ratios
869 870 label_copy_source: Source
870 871 label_copy_target: Target
871 872 label_copy_same_as_target: Same as target
872 873 label_display_used_statuses_only: Only display statuses that are used by this tracker
873 874 label_api_access_key: API access key
874 875 label_missing_api_access_key: Missing an API access key
875 876 label_api_access_key_created_on: "API access key created %{value} ago"
876 877 label_profile: Profile
877 878 label_subtask_plural: Subtasks
878 879 label_project_copy_notifications: Send email notifications during the project copy
879 880 label_principal_search: "Search for user or group:"
880 881 label_user_search: "Search for user:"
881 882 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
882 883 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
883 884 label_issues_visibility_all: All issues
884 885 label_issues_visibility_public: All non private issues
885 886 label_issues_visibility_own: Issues created by or assigned to the user
886 887 label_git_report_last_commit: Report last commit for files and directories
887 888 label_parent_revision: Parent
888 889 label_child_revision: Child
889 890 label_export_options: "%{export_format} export options"
890 891 label_copy_attachments: Copy attachments
891 892 label_copy_subtasks: Copy subtasks
892 893 label_item_position: "%{position} of %{count}"
893 894 label_completed_versions: Completed versions
894 895 label_search_for_watchers: Search for watchers to add
895 896 label_session_expiration: Session expiration
896 897 label_show_closed_projects: View closed projects
897 898 label_status_transitions: Status transitions
898 899 label_fields_permissions: Fields permissions
899 900 label_readonly: Read-only
900 901 label_required: Required
901 902 label_hidden: Hidden
902 903 label_attribute_of_project: "Project's %{name}"
903 904 label_attribute_of_issue: "Issue's %{name}"
904 905 label_attribute_of_author: "Author's %{name}"
905 906 label_attribute_of_assigned_to: "Assignee's %{name}"
906 907 label_attribute_of_user: "User's %{name}"
907 908 label_attribute_of_fixed_version: "Target version's %{name}"
908 909 label_cross_project_descendants: With subprojects
909 910 label_cross_project_tree: With project tree
910 911 label_cross_project_hierarchy: With project hierarchy
911 912 label_cross_project_system: With all projects
912 913 label_gantt_progress_line: Progress line
913 914 label_visibility_private: to me only
914 915 label_visibility_roles: to these roles only
915 916 label_visibility_public: to any users
916 917 label_link: Link
917 918 label_only: only
918 919 label_drop_down_list: drop-down list
919 920 label_checkboxes: checkboxes
920 921 label_radio_buttons: radio buttons
921 922 label_link_values_to: Link values to URL
922 923 label_custom_field_select_type: Select the type of object to which the custom field is to be attached
923 924 label_check_for_updates: Check for updates
924 925 label_latest_compatible_version: Latest compatible version
925 926 label_unknown_plugin: Unknown plugin
926 927 label_add_projects: Add projects
927 928 label_users_visibility_all: All active users
928 929 label_users_visibility_members_of_visible_projects: Members of visible projects
929 930 label_edit_attachments: Edit attached files
930 931 label_link_copied_issue: Link copied issue
931 932 label_ask: Ask
932 933 label_search_attachments_yes: Search attachment filenames and descriptions
933 934 label_search_attachments_no: Do not search attachments
934 935 label_search_attachments_only: Search attachments only
935 936 label_search_open_issues_only: Open issues only
936 937 label_email_address_plural: Emails
937 938 label_email_address_add: Add email address
938 939 label_enable_notifications: Enable notifications
939 940 label_disable_notifications: Disable notifications
940 941 label_blank_value: blank
941 942
942 943 button_login: Login
943 944 button_submit: Submit
944 945 button_save: Save
945 946 button_check_all: Check all
946 947 button_uncheck_all: Uncheck all
947 948 button_collapse_all: Collapse all
948 949 button_expand_all: Expand all
949 950 button_delete: Delete
950 951 button_create: Create
951 952 button_create_and_continue: Create and continue
952 953 button_test: Test
953 954 button_edit: Edit
954 955 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
955 956 button_add: Add
956 957 button_change: Change
957 958 button_apply: Apply
958 959 button_clear: Clear
959 960 button_lock: Lock
960 961 button_unlock: Unlock
961 962 button_download: Download
962 963 button_list: List
963 964 button_view: View
964 965 button_move: Move
965 966 button_move_and_follow: Move and follow
966 967 button_back: Back
967 968 button_cancel: Cancel
968 969 button_activate: Activate
969 970 button_sort: Sort
970 971 button_log_time: Log time
971 972 button_rollback: Rollback to this version
972 973 button_watch: Watch
973 974 button_unwatch: Unwatch
974 975 button_reply: Reply
975 976 button_archive: Archive
976 977 button_unarchive: Unarchive
977 978 button_reset: Reset
978 979 button_rename: Rename
979 980 button_change_password: Change password
980 981 button_copy: Copy
981 982 button_copy_and_follow: Copy and follow
982 983 button_annotate: Annotate
983 984 button_update: Update
984 985 button_configure: Configure
985 986 button_quote: Quote
986 987 button_duplicate: Duplicate
987 988 button_show: Show
988 989 button_hide: Hide
989 990 button_edit_section: Edit this section
990 991 button_export: Export
991 992 button_delete_my_account: Delete my account
992 993 button_close: Close
993 994 button_reopen: Reopen
994 995
995 996 status_active: active
996 997 status_registered: registered
997 998 status_locked: locked
998 999
999 1000 project_status_active: active
1000 1001 project_status_closed: closed
1001 1002 project_status_archived: archived
1002 1003
1003 1004 version_status_open: open
1004 1005 version_status_locked: locked
1005 1006 version_status_closed: closed
1006 1007
1007 1008 field_active: Active
1008 1009
1009 1010 text_select_mail_notifications: Select actions for which email notifications should be sent.
1010 1011 text_regexp_info: eg. ^[A-Z0-9]+$
1011 1012 text_min_max_length_info: 0 means no restriction
1012 1013 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
1013 1014 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
1014 1015 text_workflow_edit: Select a role and a tracker to edit the workflow
1015 1016 text_are_you_sure: Are you sure?
1016 1017 text_journal_changed: "%{label} changed from %{old} to %{new}"
1017 1018 text_journal_changed_no_detail: "%{label} updated"
1018 1019 text_journal_set_to: "%{label} set to %{value}"
1019 1020 text_journal_deleted: "%{label} deleted (%{old})"
1020 1021 text_journal_added: "%{label} %{value} added"
1021 1022 text_tip_issue_begin_day: issue beginning this day
1022 1023 text_tip_issue_end_day: issue ending this day
1023 1024 text_tip_issue_begin_end_day: issue beginning and ending this day
1024 1025 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed.'
1025 1026 text_caracters_maximum: "%{count} characters maximum."
1026 1027 text_caracters_minimum: "Must be at least %{count} characters long."
1027 1028 text_length_between: "Length between %{min} and %{max} characters."
1028 1029 text_tracker_no_workflow: No workflow defined for this tracker
1029 1030 text_unallowed_characters: Unallowed characters
1030 1031 text_comma_separated: Multiple values allowed (comma separated).
1031 1032 text_line_separated: Multiple values allowed (one line for each value).
1032 1033 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
1033 1034 text_issue_added: "Issue %{id} has been reported by %{author}."
1034 1035 text_issue_updated: "Issue %{id} has been updated by %{author}."
1035 1036 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
1036 1037 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
1037 1038 text_issue_category_destroy_assignments: Remove category assignments
1038 1039 text_issue_category_reassign_to: Reassign issues to this category
1039 1040 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)."
1040 1041 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."
1041 1042 text_load_default_configuration: Load the default configuration
1042 1043 text_status_changed_by_changeset: "Applied in changeset %{value}."
1043 1044 text_time_logged_by_changeset: "Applied in changeset %{value}."
1044 1045 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1045 1046 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1046 1047 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1047 1048 text_select_project_modules: 'Select modules to enable for this project:'
1048 1049 text_default_administrator_account_changed: Default administrator account changed
1049 1050 text_file_repository_writable: Attachments directory writable
1050 1051 text_plugin_assets_writable: Plugin assets directory writable
1051 1052 text_rmagick_available: RMagick available (optional)
1052 1053 text_convert_available: ImageMagick convert available (optional)
1053 1054 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
1054 1055 text_destroy_time_entries: Delete reported hours
1055 1056 text_assign_time_entries_to_project: Assign reported hours to the project
1056 1057 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1057 1058 text_user_wrote: "%{value} wrote:"
1058 1059 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1059 1060 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1060 1061 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."
1061 1062 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."
1062 1063 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1063 1064 text_custom_field_possible_values_info: 'One line for each value'
1064 1065 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1065 1066 text_wiki_page_nullify_children: "Keep child pages as root pages"
1066 1067 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1067 1068 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1068 1069 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?"
1069 1070 text_zoom_in: Zoom in
1070 1071 text_zoom_out: Zoom out
1071 1072 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1072 1073 text_scm_path_encoding_note: "Default: UTF-8"
1073 1074 text_subversion_repository_note: "Examples: file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1074 1075 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1075 1076 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1076 1077 text_scm_command: Command
1077 1078 text_scm_command_version: Version
1078 1079 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1079 1080 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1080 1081 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1081 1082 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1082 1083 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1083 1084 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1084 1085 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1085 1086 text_project_closed: This project is closed and read-only.
1086 1087 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
1087 1088
1088 1089 default_role_manager: Manager
1089 1090 default_role_developer: Developer
1090 1091 default_role_reporter: Reporter
1091 1092 default_tracker_bug: Bug
1092 1093 default_tracker_feature: Feature
1093 1094 default_tracker_support: Support
1094 1095 default_issue_status_new: New
1095 1096 default_issue_status_in_progress: In Progress
1096 1097 default_issue_status_resolved: Resolved
1097 1098 default_issue_status_feedback: Feedback
1098 1099 default_issue_status_closed: Closed
1099 1100 default_issue_status_rejected: Rejected
1100 1101 default_doc_category_user: User documentation
1101 1102 default_doc_category_tech: Technical documentation
1102 1103 default_priority_low: Low
1103 1104 default_priority_normal: Normal
1104 1105 default_priority_high: High
1105 1106 default_priority_urgent: Urgent
1106 1107 default_priority_immediate: Immediate
1107 1108 default_activity_design: Design
1108 1109 default_activity_development: Development
1109 1110
1110 1111 enumeration_issue_priorities: Issue priorities
1111 1112 enumeration_doc_categories: Document categories
1112 1113 enumeration_activities: Activities (time tracking)
1113 1114 enumeration_system_activity: System Activity
1114 1115 description_filter: Filter
1115 1116 description_search: Searchfield
1116 1117 description_choose_project: Projects
1117 1118 description_project_scope: Search scope
1118 1119 description_notes: Notes
1119 1120 description_message_content: Message content
1120 1121 description_query_sort_criteria_attribute: Sort attribute
1121 1122 description_query_sort_criteria_direction: Sort direction
1122 1123 description_user_mail_notification: Mail notification settings
1123 1124 description_available_columns: Available Columns
1124 1125 description_selected_columns: Selected Columns
1125 1126 description_all_columns: All Columns
1126 1127 description_issue_category_reassign: Choose issue category
1127 1128 description_wiki_subpages_reassign: Choose new parent page
1128 1129 description_date_range_list: Choose range from list
1129 1130 description_date_range_interval: Choose range by selecting start and end date
1130 1131 description_date_from: Enter start date
1131 1132 description_date_to: Enter end date
1132 1133 text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
@@ -1,1152 +1,1153
1 1 # French translations for Ruby on Rails
2 2 # by Christian Lescuyer (christian@flyingcoders.com)
3 3 # contributor: Sebastien Grosjean - ZenCocoon.com
4 4 # contributor: Thibaut Cuvelier - Developpez.com
5 5
6 6 fr:
7 7 direction: ltr
8 8 date:
9 9 formats:
10 10 default: "%d/%m/%Y"
11 11 short: "%e %b"
12 12 long: "%e %B %Y"
13 13 long_ordinal: "%e %B %Y"
14 14 only_day: "%e"
15 15
16 16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
17 17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
18 18
19 19 # Don't forget the nil at the beginning; there's no such thing as a 0th month
20 20 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
21 21 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
22 22 # Used in date_select and datime_select.
23 23 order:
24 24 - :day
25 25 - :month
26 26 - :year
27 27
28 28 time:
29 29 formats:
30 30 default: "%d/%m/%Y %H:%M"
31 31 time: "%H:%M"
32 32 short: "%d %b %H:%M"
33 33 long: "%A %d %B %Y %H:%M:%S %Z"
34 34 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
35 35 only_second: "%S"
36 36 am: 'am'
37 37 pm: 'pm'
38 38
39 39 datetime:
40 40 distance_in_words:
41 41 half_a_minute: "30 secondes"
42 42 less_than_x_seconds:
43 43 zero: "moins d'une seconde"
44 44 one: "moins d'une seconde"
45 45 other: "moins de %{count} secondes"
46 46 x_seconds:
47 47 one: "1 seconde"
48 48 other: "%{count} secondes"
49 49 less_than_x_minutes:
50 50 zero: "moins d'une minute"
51 51 one: "moins d'une minute"
52 52 other: "moins de %{count} minutes"
53 53 x_minutes:
54 54 one: "1 minute"
55 55 other: "%{count} minutes"
56 56 about_x_hours:
57 57 one: "environ une heure"
58 58 other: "environ %{count} heures"
59 59 x_hours:
60 60 one: "une heure"
61 61 other: "%{count} heures"
62 62 x_days:
63 63 one: "un jour"
64 64 other: "%{count} jours"
65 65 about_x_months:
66 66 one: "environ un mois"
67 67 other: "environ %{count} mois"
68 68 x_months:
69 69 one: "un mois"
70 70 other: "%{count} mois"
71 71 about_x_years:
72 72 one: "environ un an"
73 73 other: "environ %{count} ans"
74 74 over_x_years:
75 75 one: "plus d'un an"
76 76 other: "plus de %{count} ans"
77 77 almost_x_years:
78 78 one: "presqu'un an"
79 79 other: "presque %{count} ans"
80 80 prompts:
81 81 year: "Année"
82 82 month: "Mois"
83 83 day: "Jour"
84 84 hour: "Heure"
85 85 minute: "Minute"
86 86 second: "Seconde"
87 87
88 88 number:
89 89 format:
90 90 precision: 3
91 91 separator: ','
92 92 delimiter: ' '
93 93 currency:
94 94 format:
95 95 unit: '€'
96 96 precision: 2
97 97 format: '%n %u'
98 98 human:
99 99 format:
100 100 precision: 3
101 101 storage_units:
102 102 format: "%n %u"
103 103 units:
104 104 byte:
105 105 one: "octet"
106 106 other: "octets"
107 107 kb: "ko"
108 108 mb: "Mo"
109 109 gb: "Go"
110 110 tb: "To"
111 111
112 112 support:
113 113 array:
114 114 sentence_connector: 'et'
115 115 skip_last_comma: true
116 116 word_connector: ", "
117 117 two_words_connector: " et "
118 118 last_word_connector: " et "
119 119
120 120 activerecord:
121 121 errors:
122 122 template:
123 123 header:
124 124 one: "Impossible d'enregistrer %{model} : une erreur"
125 125 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
126 126 body: "Veuillez vérifier les champs suivants :"
127 127 messages:
128 128 inclusion: "n'est pas inclus(e) dans la liste"
129 129 exclusion: "n'est pas disponible"
130 130 invalid: "n'est pas valide"
131 131 confirmation: "ne concorde pas avec la confirmation"
132 132 accepted: "doit être accepté(e)"
133 133 empty: "doit être renseigné(e)"
134 134 blank: "doit être renseigné(e)"
135 135 too_long: "est trop long (pas plus de %{count} caractères)"
136 136 too_short: "est trop court (au moins %{count} caractères)"
137 137 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
138 138 taken: "est déjà utilisé"
139 139 not_a_number: "n'est pas un nombre"
140 140 not_a_date: "n'est pas une date valide"
141 141 greater_than: "doit être supérieur à %{count}"
142 142 greater_than_or_equal_to: "doit être supérieur ou égal à %{count}"
143 143 equal_to: "doit être égal à %{count}"
144 144 less_than: "doit être inférieur à %{count}"
145 145 less_than_or_equal_to: "doit être inférieur ou égal à %{count}"
146 146 odd: "doit être impair"
147 147 even: "doit être pair"
148 148 greater_than_start_date: "doit être postérieure à la date de début"
149 149 not_same_project: "n'appartient pas au même projet"
150 150 circular_dependency: "Cette relation créerait une dépendance circulaire"
151 151 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
152 152 earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précèdent"
153 153
154 154 actionview_instancetag_blank_option: Choisir
155 155
156 156 general_text_No: 'Non'
157 157 general_text_Yes: 'Oui'
158 158 general_text_no: 'non'
159 159 general_text_yes: 'oui'
160 160 general_lang_name: 'French (Français)'
161 161 general_csv_separator: ';'
162 162 general_csv_decimal_separator: ','
163 163 general_csv_encoding: ISO-8859-1
164 164 general_pdf_fontname: freesans
165 165 general_first_day_of_week: '1'
166 166
167 167 notice_account_updated: Le compte a été mis à jour avec succès.
168 168 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
169 169 notice_account_password_updated: Mot de passe mis à jour avec succès.
170 170 notice_account_wrong_password: Mot de passe incorrect
171 171 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}.
172 172 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
173 173 notice_account_not_activated_yet: Vous n'avez pas encore activé votre compte. Si vous voulez recevoir un nouveau message d'activation, veuillez <a href="%{url}">cliquer sur ce lien</a>.
174 174 notice_account_locked: Votre compte est verrouillé.
175 175 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
176 176 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
177 177 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
178 178 notice_successful_create: Création effectuée avec succès.
179 179 notice_successful_update: Mise à jour effectuée avec succès.
180 180 notice_successful_delete: Suppression effectuée avec succès.
181 181 notice_successful_connection: Connexion réussie.
182 182 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
183 183 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
184 184 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
185 185 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
186 186 notice_email_sent: "Un email a été envoyé à %{value}"
187 187 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
188 188 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée."
189 189 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
190 190 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
191 191 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
192 192 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
193 193 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
194 194 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
195 195 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
196 196 notice_unable_delete_version: Impossible de supprimer cette version.
197 197 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
198 198 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
199 199 notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})"
200 200 notice_issue_successful_create: "Demande %{id} créée."
201 201 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
202 202 notice_account_deleted: "Votre compte a été définitivement supprimé."
203 203 notice_user_successful_create: "Utilisateur %{id} créé."
204 204 notice_new_password_must_be_different: Votre nouveau mot de passe doit être différent de votre mot de passe actuel
205 205
206 206 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
207 207 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
208 208 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
209 209 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
210 210 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
211 211 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
212 212 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
213 213 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
214 214 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
215 215 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
216 216 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
217 217 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
218 218 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
219 219 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
220 220 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
221 221 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
222 222 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
223 223 error_unable_to_connect: Connexion impossible (%{value})
224 224 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
225 225 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
226 226 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
227 227
228 228 mail_subject_lost_password: "Votre mot de passe %{value}"
229 229 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
230 230 mail_subject_register: "Activation de votre compte %{value}"
231 231 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
232 232 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
233 233 mail_body_account_information: Paramètres de connexion de votre compte
234 234 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
235 235 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
236 236 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
237 237 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
238 238 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
239 239 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
240 240 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
241 241 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
242 242
243 243 field_name: Nom
244 244 field_description: Description
245 245 field_summary: Résumé
246 246 field_is_required: Obligatoire
247 247 field_firstname: Prénom
248 248 field_lastname: Nom
249 249 field_mail: Email
250 250 field_address: Email
251 251 field_filename: Fichier
252 252 field_filesize: Taille
253 253 field_downloads: Téléchargements
254 254 field_author: Auteur
255 255 field_created_on: Créé
256 256 field_updated_on: Mis-à-jour
257 257 field_closed_on: Fermé
258 258 field_field_format: Format
259 259 field_is_for_all: Pour tous les projets
260 260 field_possible_values: Valeurs possibles
261 261 field_regexp: Expression régulière
262 262 field_min_length: Longueur minimum
263 263 field_max_length: Longueur maximum
264 264 field_value: Valeur
265 265 field_category: Catégorie
266 266 field_title: Titre
267 267 field_project: Projet
268 268 field_issue: Demande
269 269 field_status: Statut
270 270 field_notes: Notes
271 271 field_is_closed: Demande fermée
272 272 field_is_default: Valeur par défaut
273 273 field_tracker: Tracker
274 274 field_subject: Sujet
275 275 field_due_date: Echéance
276 276 field_assigned_to: Assigné à
277 277 field_priority: Priorité
278 278 field_fixed_version: Version cible
279 279 field_user: Utilisateur
280 280 field_principal: Principal
281 281 field_role: Rôle
282 282 field_homepage: Site web
283 283 field_is_public: Public
284 284 field_parent: Sous-projet de
285 285 field_is_in_roadmap: Demandes affichées dans la roadmap
286 286 field_login: Identifiant
287 287 field_mail_notification: Notifications par mail
288 288 field_admin: Administrateur
289 289 field_last_login_on: Dernière connexion
290 290 field_language: Langue
291 291 field_effective_date: Date
292 292 field_password: Mot de passe
293 293 field_new_password: Nouveau mot de passe
294 294 field_password_confirmation: Confirmation
295 295 field_version: Version
296 296 field_type: Type
297 297 field_host: Hôte
298 298 field_port: Port
299 299 field_account: Compte
300 300 field_base_dn: Base DN
301 301 field_attr_login: Attribut Identifiant
302 302 field_attr_firstname: Attribut Prénom
303 303 field_attr_lastname: Attribut Nom
304 304 field_attr_mail: Attribut Email
305 305 field_onthefly: Création des utilisateurs à la volée
306 306 field_start_date: Début
307 307 field_done_ratio: "% réalisé"
308 308 field_auth_source: Mode d'authentification
309 309 field_hide_mail: Cacher mon adresse mail
310 310 field_comments: Commentaire
311 311 field_url: URL
312 312 field_start_page: Page de démarrage
313 313 field_subproject: Sous-projet
314 314 field_hours: Heures
315 315 field_activity: Activité
316 316 field_spent_on: Date
317 317 field_identifier: Identifiant
318 318 field_is_filter: Utilisé comme filtre
319 319 field_issue_to: Demande liée
320 320 field_delay: Retard
321 321 field_assignable: Demandes assignables à ce rôle
322 322 field_redirect_existing_links: Rediriger les liens existants
323 323 field_estimated_hours: Temps estimé
324 324 field_column_names: Colonnes
325 325 field_time_entries: Temps passé
326 326 field_time_zone: Fuseau horaire
327 327 field_searchable: Utilisé pour les recherches
328 328 field_default_value: Valeur par défaut
329 329 field_comments_sorting: Afficher les commentaires
330 330 field_parent_title: Page parent
331 331 field_editable: Modifiable
332 332 field_watcher: Observateur
333 333 field_identity_url: URL OpenID
334 334 field_content: Contenu
335 335 field_group_by: Grouper par
336 336 field_sharing: Partage
337 337 field_parent_issue: Tâche parente
338 338 field_member_of_group: Groupe de l'assigné
339 339 field_assigned_to_role: Rôle de l'assigné
340 340 field_text: Champ texte
341 341 field_visible: Visible
342 342 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
343 343 field_issues_visibility: Visibilité des demandes
344 344 field_is_private: Privée
345 345 field_commit_logs_encoding: Encodage des messages de commit
346 346 field_scm_path_encoding: Encodage des chemins
347 347 field_path_to_repository: Chemin du dépôt
348 348 field_root_directory: Répertoire racine
349 349 field_cvsroot: CVSROOT
350 350 field_cvs_module: Module
351 351 field_repository_is_default: Dépôt principal
352 352 field_multiple: Valeurs multiples
353 353 field_auth_source_ldap_filter: Filtre LDAP
354 354 field_core_fields: Champs standards
355 355 field_timeout: "Timeout (en secondes)"
356 356 field_board_parent: Forum parent
357 357 field_private_notes: Notes privées
358 358 field_inherit_members: Hériter les membres
359 359 field_generate_password: Générer un mot de passe
360 360 field_must_change_passwd: Doit changer de mot de passe à la prochaine connexion
361 361 field_default_status: Statut par défaut
362 362 field_users_visibility: Visibilité des utilisateurs
363 363
364 364 setting_app_title: Titre de l'application
365 365 setting_app_subtitle: Sous-titre de l'application
366 366 setting_welcome_text: Texte d'accueil
367 367 setting_default_language: Langue par défaut
368 368 setting_login_required: Authentification obligatoire
369 369 setting_self_registration: Inscription des nouveaux utilisateurs
370 370 setting_attachment_max_size: Taille maximale des fichiers
371 371 setting_issues_export_limit: Limite d'exportation des demandes
372 372 setting_mail_from: Adresse d'émission
373 373 setting_bcc_recipients: Destinataires en copie cachée (cci)
374 374 setting_plain_text_mail: Mail en texte brut (non HTML)
375 375 setting_host_name: Nom d'hôte et chemin
376 376 setting_text_formatting: Formatage du texte
377 377 setting_wiki_compression: Compression de l'historique des pages wiki
378 378 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
379 379 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
380 380 setting_autofetch_changesets: Récupération automatique des commits
381 381 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
382 382 setting_commit_ref_keywords: Mots-clés de référencement
383 383 setting_commit_fix_keywords: Mots-clés de résolution
384 384 setting_autologin: Durée maximale de connexion automatique
385 385 setting_date_format: Format de date
386 386 setting_time_format: Format d'heure
387 387 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
388 388 setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
389 389 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
390 390 setting_repositories_encodings: Encodages des fichiers et des dépôts
391 391 setting_emails_header: En-tête des emails
392 392 setting_emails_footer: Pied-de-page des emails
393 393 setting_protocol: Protocole
394 394 setting_per_page_options: Options d'objets affichés par page
395 395 setting_user_format: Format d'affichage des utilisateurs
396 396 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
397 397 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
398 398 setting_enabled_scm: SCM activés
399 399 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
400 400 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
401 401 setting_mail_handler_api_key: Clé de protection de l'API
402 402 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
403 403 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
404 404 setting_gravatar_default: Image Gravatar par défaut
405 405 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
406 406 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
407 407 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
408 408 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
409 409 setting_password_min_length: Longueur minimum des mots de passe
410 410 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
411 411 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
412 412 setting_issue_done_ratio: Calcul de l'avancement des demandes
413 413 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
414 414 setting_issue_done_ratio_issue_status: Utiliser le statut
415 415 setting_start_of_week: Jour de début des calendriers
416 416 setting_rest_api_enabled: Activer l'API REST
417 417 setting_cache_formatted_text: Mettre en cache le texte formaté
418 418 setting_default_notification_option: Option de notification par défaut
419 419 setting_commit_logtime_enabled: Permettre la saisie de temps
420 420 setting_commit_logtime_activity_id: Activité pour le temps saisi
421 421 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
422 422 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
423 423 setting_default_issue_start_date_to_creation_date: Donner à la date de début d'une nouvelle demande la valeur de la date du jour
424 424 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
425 425 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
426 426 setting_session_lifetime: Durée de vie maximale des sessions
427 427 setting_session_timeout: Durée maximale d'inactivité
428 428 setting_thumbnails_enabled: Afficher les vignettes des images
429 429 setting_thumbnails_size: Taille des vignettes (en pixels)
430 430 setting_non_working_week_days: Jours non travaillés
431 431 setting_jsonp_enabled: Activer le support JSONP
432 432 setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets
433 433 setting_mail_handler_excluded_filenames: Exclure les fichiers attachés par leur nom
434 434 setting_force_default_language_for_anonymous: Forcer la langue par défault pour les utilisateurs anonymes
435 435 setting_force_default_language_for_loggedin: Forcer la langue par défault pour les utilisateurs identifiés
436 436 setting_link_copied_issue: Lier les demandes lors de la copie
437 437 setting_max_additional_emails: Nombre maximal d'adresses email additionnelles
438 438 setting_search_results_per_page: Résultats de recherche affichés par page
439 439
440 440 permission_add_project: Créer un projet
441 441 permission_add_subprojects: Créer des sous-projets
442 442 permission_edit_project: Modifier le projet
443 443 permission_close_project: Fermer / réouvrir le projet
444 444 permission_select_project_modules: Choisir les modules
445 445 permission_manage_members: Gérer les membres
446 446 permission_manage_project_activities: Gérer les activités
447 447 permission_manage_versions: Gérer les versions
448 448 permission_manage_categories: Gérer les catégories de demandes
449 449 permission_view_issues: Voir les demandes
450 450 permission_add_issues: Créer des demandes
451 451 permission_edit_issues: Modifier les demandes
452 permission_copy_issues: Copier les demandes
452 453 permission_manage_issue_relations: Gérer les relations
453 454 permission_set_issues_private: Rendre les demandes publiques ou privées
454 455 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
455 456 permission_add_issue_notes: Ajouter des notes
456 457 permission_edit_issue_notes: Modifier les notes
457 458 permission_edit_own_issue_notes: Modifier ses propres notes
458 459 permission_view_private_notes: Voir les notes privées
459 460 permission_set_notes_private: Rendre les notes privées
460 461 permission_move_issues: Déplacer les demandes
461 462 permission_delete_issues: Supprimer les demandes
462 463 permission_manage_public_queries: Gérer les requêtes publiques
463 464 permission_save_queries: Sauvegarder les requêtes
464 465 permission_view_gantt: Voir le gantt
465 466 permission_view_calendar: Voir le calendrier
466 467 permission_view_issue_watchers: Voir la liste des observateurs
467 468 permission_add_issue_watchers: Ajouter des observateurs
468 469 permission_delete_issue_watchers: Supprimer des observateurs
469 470 permission_log_time: Saisir le temps passé
470 471 permission_view_time_entries: Voir le temps passé
471 472 permission_edit_time_entries: Modifier les temps passés
472 473 permission_edit_own_time_entries: Modifier son propre temps passé
473 474 permission_manage_news: Gérer les annonces
474 475 permission_comment_news: Commenter les annonces
475 476 permission_view_documents: Voir les documents
476 477 permission_add_documents: Ajouter des documents
477 478 permission_edit_documents: Modifier les documents
478 479 permission_delete_documents: Supprimer les documents
479 480 permission_manage_files: Gérer les fichiers
480 481 permission_view_files: Voir les fichiers
481 482 permission_manage_wiki: Gérer le wiki
482 483 permission_rename_wiki_pages: Renommer les pages
483 484 permission_delete_wiki_pages: Supprimer les pages
484 485 permission_view_wiki_pages: Voir le wiki
485 486 permission_view_wiki_edits: "Voir l'historique des modifications"
486 487 permission_edit_wiki_pages: Modifier les pages
487 488 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
488 489 permission_protect_wiki_pages: Protéger les pages
489 490 permission_manage_repository: Gérer le dépôt de sources
490 491 permission_browse_repository: Parcourir les sources
491 492 permission_view_changesets: Voir les révisions
492 493 permission_commit_access: Droit de commit
493 494 permission_manage_boards: Gérer les forums
494 495 permission_view_messages: Voir les messages
495 496 permission_add_messages: Poster un message
496 497 permission_edit_messages: Modifier les messages
497 498 permission_edit_own_messages: Modifier ses propres messages
498 499 permission_delete_messages: Supprimer les messages
499 500 permission_delete_own_messages: Supprimer ses propres messages
500 501 permission_export_wiki_pages: Exporter les pages
501 502 permission_manage_subtasks: Gérer les sous-tâches
502 503 permission_manage_related_issues: Gérer les demandes associées
503 504
504 505 project_module_issue_tracking: Suivi des demandes
505 506 project_module_time_tracking: Suivi du temps passé
506 507 project_module_news: Publication d'annonces
507 508 project_module_documents: Publication de documents
508 509 project_module_files: Publication de fichiers
509 510 project_module_wiki: Wiki
510 511 project_module_repository: Dépôt de sources
511 512 project_module_boards: Forums de discussion
512 513 project_module_calendar: Calendrier
513 514 project_module_gantt: Gantt
514 515
515 516 label_user: Utilisateur
516 517 label_user_plural: Utilisateurs
517 518 label_user_new: Nouvel utilisateur
518 519 label_user_anonymous: Anonyme
519 520 label_project: Projet
520 521 label_project_new: Nouveau projet
521 522 label_project_plural: Projets
522 523 label_x_projects:
523 524 zero: aucun projet
524 525 one: un projet
525 526 other: "%{count} projets"
526 527 label_project_all: Tous les projets
527 528 label_project_latest: Derniers projets
528 529 label_issue: Demande
529 530 label_issue_new: Nouvelle demande
530 531 label_issue_plural: Demandes
531 532 label_issue_view_all: Voir toutes les demandes
532 533 label_issues_by: "Demandes par %{value}"
533 534 label_issue_added: Demande ajoutée
534 535 label_issue_updated: Demande mise à jour
535 536 label_issue_note_added: Note ajoutée
536 537 label_issue_status_updated: Statut changé
537 538 label_issue_assigned_to_updated: Assigné changé
538 539 label_issue_priority_updated: Priorité changée
539 540 label_document: Document
540 541 label_document_new: Nouveau document
541 542 label_document_plural: Documents
542 543 label_document_added: Document ajouté
543 544 label_role: Rôle
544 545 label_role_plural: Rôles
545 546 label_role_new: Nouveau rôle
546 547 label_role_and_permissions: Rôles et permissions
547 548 label_role_anonymous: Anonyme
548 549 label_role_non_member: Non membre
549 550 label_member: Membre
550 551 label_member_new: Nouveau membre
551 552 label_member_plural: Membres
552 553 label_tracker: Tracker
553 554 label_tracker_plural: Trackers
554 555 label_tracker_new: Nouveau tracker
555 556 label_workflow: Workflow
556 557 label_issue_status: Statut de demandes
557 558 label_issue_status_plural: Statuts de demandes
558 559 label_issue_status_new: Nouveau statut
559 560 label_issue_category: Catégorie de demandes
560 561 label_issue_category_plural: Catégories de demandes
561 562 label_issue_category_new: Nouvelle catégorie
562 563 label_custom_field: Champ personnalisé
563 564 label_custom_field_plural: Champs personnalisés
564 565 label_custom_field_new: Nouveau champ personnalisé
565 566 label_enumerations: Listes de valeurs
566 567 label_enumeration_new: Nouvelle valeur
567 568 label_information: Information
568 569 label_information_plural: Informations
569 570 label_please_login: Identification
570 571 label_register: S'enregistrer
571 572 label_login_with_open_id_option: S'authentifier avec OpenID
572 573 label_password_lost: Mot de passe perdu
573 574 label_home: Accueil
574 575 label_my_page: Ma page
575 576 label_my_account: Mon compte
576 577 label_my_projects: Mes projets
577 578 label_my_page_block: Blocs disponibles
578 579 label_administration: Administration
579 580 label_login: Connexion
580 581 label_logout: Déconnexion
581 582 label_help: Aide
582 583 label_reported_issues: Demandes soumises
583 584 label_assigned_to_me_issues: Demandes qui me sont assignées
584 585 label_last_login: Dernière connexion
585 586 label_registered_on: Inscrit le
586 587 label_activity: Activité
587 588 label_overall_activity: Activité globale
588 589 label_user_activity: "Activité de %{value}"
589 590 label_new: Nouveau
590 591 label_logged_as: Connecté en tant que
591 592 label_environment: Environnement
592 593 label_authentication: Authentification
593 594 label_auth_source: Mode d'authentification
594 595 label_auth_source_new: Nouveau mode d'authentification
595 596 label_auth_source_plural: Modes d'authentification
596 597 label_subproject_plural: Sous-projets
597 598 label_subproject_new: Nouveau sous-projet
598 599 label_and_its_subprojects: "%{value} et ses sous-projets"
599 600 label_min_max_length: Longueurs mini - maxi
600 601 label_list: Liste
601 602 label_date: Date
602 603 label_integer: Entier
603 604 label_float: Nombre décimal
604 605 label_boolean: Booléen
605 606 label_string: Texte
606 607 label_text: Texte long
607 608 label_attribute: Attribut
608 609 label_attribute_plural: Attributs
609 610 label_no_data: Aucune donnée à afficher
610 611 label_change_status: Changer le statut
611 612 label_history: Historique
612 613 label_attachment: Fichier
613 614 label_attachment_new: Nouveau fichier
614 615 label_attachment_delete: Supprimer le fichier
615 616 label_attachment_plural: Fichiers
616 617 label_file_added: Fichier ajouté
617 618 label_report: Rapport
618 619 label_report_plural: Rapports
619 620 label_news: Annonce
620 621 label_news_new: Nouvelle annonce
621 622 label_news_plural: Annonces
622 623 label_news_latest: Dernières annonces
623 624 label_news_view_all: Voir toutes les annonces
624 625 label_news_added: Annonce ajoutée
625 626 label_news_comment_added: Commentaire ajouté à une annonce
626 627 label_settings: Configuration
627 628 label_overview: Aperçu
628 629 label_version: Version
629 630 label_version_new: Nouvelle version
630 631 label_version_plural: Versions
631 632 label_close_versions: Fermer les versions terminées
632 633 label_confirmation: Confirmation
633 634 label_export_to: 'Formats disponibles :'
634 635 label_read: Lire...
635 636 label_public_projects: Projets publics
636 637 label_open_issues: ouvert
637 638 label_open_issues_plural: ouverts
638 639 label_closed_issues: fermé
639 640 label_closed_issues_plural: fermés
640 641 label_x_open_issues_abbr_on_total:
641 642 zero: 0 ouverte sur %{total}
642 643 one: 1 ouverte sur %{total}
643 644 other: "%{count} ouvertes sur %{total}"
644 645 label_x_open_issues_abbr:
645 646 zero: 0 ouverte
646 647 one: 1 ouverte
647 648 other: "%{count} ouvertes"
648 649 label_x_closed_issues_abbr:
649 650 zero: 0 fermée
650 651 one: 1 fermée
651 652 other: "%{count} fermées"
652 653 label_x_issues:
653 654 zero: 0 demande
654 655 one: 1 demande
655 656 other: "%{count} demandes"
656 657 label_total: Total
657 658 label_total_time: Temps total
658 659 label_permissions: Permissions
659 660 label_current_status: Statut actuel
660 661 label_new_statuses_allowed: Nouveaux statuts autorisés
661 662 label_all: tous
662 663 label_any: tous
663 664 label_none: aucun
664 665 label_nobody: personne
665 666 label_next: Suivant
666 667 label_previous: Précédent
667 668 label_used_by: Utilisé par
668 669 label_details: Détails
669 670 label_add_note: Ajouter une note
670 671 label_calendar: Calendrier
671 672 label_months_from: mois depuis
672 673 label_gantt: Gantt
673 674 label_internal: Interne
674 675 label_last_changes: "%{count} derniers changements"
675 676 label_change_view_all: Voir tous les changements
676 677 label_personalize_page: Personnaliser cette page
677 678 label_comment: Commentaire
678 679 label_comment_plural: Commentaires
679 680 label_x_comments:
680 681 zero: aucun commentaire
681 682 one: un commentaire
682 683 other: "%{count} commentaires"
683 684 label_comment_add: Ajouter un commentaire
684 685 label_comment_added: Commentaire ajouté
685 686 label_comment_delete: Supprimer les commentaires
686 687 label_query: Rapport personnalisé
687 688 label_query_plural: Rapports personnalisés
688 689 label_query_new: Nouveau rapport
689 690 label_my_queries: Mes rapports personnalisés
690 691 label_filter_add: Ajouter le filtre
691 692 label_filter_plural: Filtres
692 693 label_equals: égal
693 694 label_not_equals: différent
694 695 label_in_less_than: dans moins de
695 696 label_in_more_than: dans plus de
696 697 label_in_the_next_days: dans les prochains jours
697 698 label_in_the_past_days: dans les derniers jours
698 699 label_greater_or_equal: '>='
699 700 label_less_or_equal: '<='
700 701 label_between: entre
701 702 label_in: dans
702 703 label_today: aujourd'hui
703 704 label_all_time: toute la période
704 705 label_yesterday: hier
705 706 label_this_week: cette semaine
706 707 label_last_week: la semaine dernière
707 708 label_last_n_weeks: "les %{count} dernières semaines"
708 709 label_last_n_days: "les %{count} derniers jours"
709 710 label_this_month: ce mois-ci
710 711 label_last_month: le mois dernier
711 712 label_this_year: cette année
712 713 label_date_range: Période
713 714 label_less_than_ago: il y a moins de
714 715 label_more_than_ago: il y a plus de
715 716 label_ago: il y a
716 717 label_contains: contient
717 718 label_not_contains: ne contient pas
718 719 label_any_issues_in_project: une demande du projet
719 720 label_any_issues_not_in_project: une demande hors du projet
720 721 label_no_issues_in_project: aucune demande du projet
721 722 label_day_plural: jours
722 723 label_repository: Dépôt
723 724 label_repository_new: Nouveau dépôt
724 725 label_repository_plural: Dépôts
725 726 label_browse: Parcourir
726 727 label_branch: Branche
727 728 label_tag: Tag
728 729 label_revision: Révision
729 730 label_revision_plural: Révisions
730 731 label_revision_id: "Révision %{value}"
731 732 label_associated_revisions: Révisions associées
732 733 label_added: ajouté
733 734 label_modified: modifié
734 735 label_copied: copié
735 736 label_renamed: renommé
736 737 label_deleted: supprimé
737 738 label_latest_revision: Dernière révision
738 739 label_latest_revision_plural: Dernières révisions
739 740 label_view_revisions: Voir les révisions
740 741 label_view_all_revisions: Voir toutes les révisions
741 742 label_max_size: Taille maximale
742 743 label_sort_highest: Remonter en premier
743 744 label_sort_higher: Remonter
744 745 label_sort_lower: Descendre
745 746 label_sort_lowest: Descendre en dernier
746 747 label_roadmap: Roadmap
747 748 label_roadmap_due_in: "Échéance dans %{value}"
748 749 label_roadmap_overdue: "En retard de %{value}"
749 750 label_roadmap_no_issues: Aucune demande pour cette version
750 751 label_search: Recherche
751 752 label_result_plural: Résultats
752 753 label_all_words: Tous les mots
753 754 label_wiki: Wiki
754 755 label_wiki_edit: Révision wiki
755 756 label_wiki_edit_plural: Révisions wiki
756 757 label_wiki_page: Page wiki
757 758 label_wiki_page_plural: Pages wiki
758 759 label_index_by_title: Index par titre
759 760 label_index_by_date: Index par date
760 761 label_current_version: Version actuelle
761 762 label_preview: Prévisualisation
762 763 label_feed_plural: Flux Atom
763 764 label_changes_details: Détails de tous les changements
764 765 label_issue_tracking: Suivi des demandes
765 766 label_spent_time: Temps passé
766 767 label_overall_spent_time: Temps passé global
767 768 label_f_hour: "%{value} heure"
768 769 label_f_hour_plural: "%{value} heures"
769 770 label_time_tracking: Suivi du temps
770 771 label_change_plural: Changements
771 772 label_statistics: Statistiques
772 773 label_commits_per_month: Commits par mois
773 774 label_commits_per_author: Commits par auteur
774 775 label_diff: diff
775 776 label_view_diff: Voir les différences
776 777 label_diff_inline: en ligne
777 778 label_diff_side_by_side: côte à côte
778 779 label_options: Options
779 780 label_copy_workflow_from: Copier le workflow de
780 781 label_permissions_report: Synthèse des permissions
781 782 label_watched_issues: Demandes surveillées
782 783 label_related_issues: Demandes liées
783 784 label_applied_status: Statut appliqué
784 785 label_loading: Chargement...
785 786 label_relation_new: Nouvelle relation
786 787 label_relation_delete: Supprimer la relation
787 788 label_relates_to: Lié à
788 789 label_duplicates: Duplique
789 790 label_duplicated_by: Dupliqué par
790 791 label_blocks: Bloque
791 792 label_blocked_by: Bloqué par
792 793 label_precedes: Précède
793 794 label_follows: Suit
794 795 label_copied_to: Copié vers
795 796 label_copied_from: Copié depuis
796 797 label_end_to_start: fin à début
797 798 label_end_to_end: fin à fin
798 799 label_start_to_start: début à début
799 800 label_start_to_end: début à fin
800 801 label_stay_logged_in: Rester connecté
801 802 label_disabled: désactivé
802 803 label_show_completed_versions: Voir les versions passées
803 804 label_me: moi
804 805 label_board: Forum
805 806 label_board_new: Nouveau forum
806 807 label_board_plural: Forums
807 808 label_board_locked: Verrouillé
808 809 label_board_sticky: Sticky
809 810 label_topic_plural: Discussions
810 811 label_message_plural: Messages
811 812 label_message_last: Dernier message
812 813 label_message_new: Nouveau message
813 814 label_message_posted: Message ajouté
814 815 label_reply_plural: Réponses
815 816 label_send_information: Envoyer les informations à l'utilisateur
816 817 label_year: Année
817 818 label_month: Mois
818 819 label_week: Semaine
819 820 label_date_from: Du
820 821 label_date_to: Au
821 822 label_language_based: Basé sur la langue de l'utilisateur
822 823 label_sort_by: "Trier par %{value}"
823 824 label_send_test_email: Envoyer un email de test
824 825 label_feeds_access_key: Clé d'accès Atom
825 826 label_missing_feeds_access_key: Clé d'accès Atom manquante
826 827 label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}"
827 828 label_module_plural: Modules
828 829 label_added_time_by: "Ajouté par %{author} il y a %{age}"
829 830 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
830 831 label_updated_time: "Mis à jour il y a %{value}"
831 832 label_jump_to_a_project: Aller à un projet...
832 833 label_file_plural: Fichiers
833 834 label_changeset_plural: Révisions
834 835 label_default_columns: Colonnes par défaut
835 836 label_no_change_option: (Pas de changement)
836 837 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
837 838 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
838 839 label_theme: Thème
839 840 label_default: Défaut
840 841 label_search_titles_only: Uniquement dans les titres
841 842 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
842 843 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
843 844 label_user_mail_option_none: Aucune notification
844 845 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
845 846 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
846 847 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
847 848 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
848 849 label_registration_activation_by_email: activation du compte par email
849 850 label_registration_manual_activation: activation manuelle du compte
850 851 label_registration_automatic_activation: activation automatique du compte
851 852 label_display_per_page: "Par page : %{value}"
852 853 label_age: Âge
853 854 label_change_properties: Changer les propriétés
854 855 label_general: Général
855 856 label_more: Plus
856 857 label_scm: SCM
857 858 label_plugins: Plugins
858 859 label_ldap_authentication: Authentification LDAP
859 860 label_downloads_abbr: D/L
860 861 label_optional_description: Description facultative
861 862 label_add_another_file: Ajouter un autre fichier
862 863 label_preferences: Préférences
863 864 label_chronological_order: Dans l'ordre chronologique
864 865 label_reverse_chronological_order: Dans l'ordre chronologique inverse
865 866 label_planning: Planning
866 867 label_incoming_emails: Emails entrants
867 868 label_generate_key: Générer une clé
868 869 label_issue_watchers: Observateurs
869 870 label_example: Exemple
870 871 label_display: Affichage
871 872 label_sort: Tri
872 873 label_ascending: Croissant
873 874 label_descending: Décroissant
874 875 label_date_from_to: Du %{start} au %{end}
875 876 label_wiki_content_added: Page wiki ajoutée
876 877 label_wiki_content_updated: Page wiki mise à jour
877 878 label_group: Groupe
878 879 label_group_plural: Groupes
879 880 label_group_new: Nouveau groupe
880 881 label_group_anonymous: Utilisateurs anonymes
881 882 label_group_non_member: Utilisateurs non membres
882 883 label_time_entry_plural: Temps passé
883 884 label_version_sharing_none: Non partagé
884 885 label_version_sharing_descendants: Avec les sous-projets
885 886 label_version_sharing_hierarchy: Avec toute la hiérarchie
886 887 label_version_sharing_tree: Avec tout l'arbre
887 888 label_version_sharing_system: Avec tous les projets
888 889 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
889 890 label_copy_source: Source
890 891 label_copy_target: Cible
891 892 label_copy_same_as_target: Comme la cible
892 893 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
893 894 label_api_access_key: Clé d'accès API
894 895 label_missing_api_access_key: Clé d'accès API manquante
895 896 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
896 897 label_profile: Profil
897 898 label_subtask_plural: Sous-tâches
898 899 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
899 900 label_principal_search: "Rechercher un utilisateur ou un groupe :"
900 901 label_user_search: "Rechercher un utilisateur :"
901 902 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
902 903 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
903 904 label_issues_visibility_all: Toutes les demandes
904 905 label_issues_visibility_public: Toutes les demandes non privées
905 906 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
906 907 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
907 908 label_parent_revision: Parent
908 909 label_child_revision: Enfant
909 910 label_export_options: Options d'exportation %{export_format}
910 911 label_copy_attachments: Copier les fichiers
911 912 label_copy_subtasks: Copier les sous-tâches
912 913 label_item_position: "%{position} sur %{count}"
913 914 label_completed_versions: Versions passées
914 915 label_search_for_watchers: Rechercher des observateurs
915 916 label_session_expiration: Expiration des sessions
916 917 label_show_closed_projects: Voir les projets fermés
917 918 label_status_transitions: Changements de statut
918 919 label_fields_permissions: Permissions sur les champs
919 920 label_readonly: Lecture
920 921 label_required: Obligatoire
921 922 label_hidden: Caché
922 923 label_attribute_of_project: "%{name} du projet"
923 924 label_attribute_of_issue: "%{name} de la demande"
924 925 label_attribute_of_author: "%{name} de l'auteur"
925 926 label_attribute_of_assigned_to: "%{name} de l'assigné"
926 927 label_attribute_of_user: "%{name} de l'utilisateur"
927 928 label_attribute_of_fixed_version: "%{name} de la version cible"
928 929 label_cross_project_descendants: Avec les sous-projets
929 930 label_cross_project_tree: Avec tout l'arbre
930 931 label_cross_project_hierarchy: Avec toute la hiérarchie
931 932 label_cross_project_system: Avec tous les projets
932 933 label_gantt_progress_line: Ligne de progression
933 934 label_visibility_private: par moi uniquement
934 935 label_visibility_roles: par ces rôles uniquement
935 936 label_visibility_public: par tout le monde
936 937 label_link: Lien
937 938 label_only: seulement
938 939 label_drop_down_list: liste déroulante
939 940 label_checkboxes: cases à cocher
940 941 label_radio_buttons: boutons radio
941 942 label_link_values_to: Lier les valeurs vers l'URL
942 943 label_custom_field_select_type: Selectionner le type d'objet auquel attacher le champ personnalisé
943 944 label_check_for_updates: Vérifier les mises à jour
944 945 label_latest_compatible_version: Dernière version compatible
945 946 label_unknown_plugin: Plugin inconnu
946 947 label_add_projects: Ajouter des projets
947 948 label_users_visibility_all: Tous les utilisateurs actifs
948 949 label_users_visibility_members_of_visible_projects: Membres des projets visibles
949 950 label_edit_attachments: Modifier les fichiers attachés
950 951 label_link_copied_issue: Lier la demande copiée
951 952 label_ask: Demander
952 953 label_search_attachments_yes: Rechercher les noms et descriptions de fichiers
953 954 label_search_attachments_no: Ne pas rechercher les fichiers
954 955 label_search_attachments_only: Rechercher les fichiers uniquement
955 956 label_search_open_issues_only: Demandes ouvertes uniquement
956 957 label_email_address_plural: Emails
957 958 label_email_address_add: Ajouter une adresse email
958 959 label_enable_notifications: Activer les notifications
959 960 label_disable_notifications: Désactiver les notifications
960 961 label_blank_value: non renseigné
961 962
962 963 button_login: Connexion
963 964 button_submit: Soumettre
964 965 button_save: Sauvegarder
965 966 button_check_all: Tout cocher
966 967 button_uncheck_all: Tout décocher
967 968 button_collapse_all: Plier tout
968 969 button_expand_all: Déplier tout
969 970 button_delete: Supprimer
970 971 button_create: Créer
971 972 button_create_and_continue: Créer et continuer
972 973 button_test: Tester
973 974 button_edit: Modifier
974 975 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
975 976 button_add: Ajouter
976 977 button_change: Changer
977 978 button_apply: Appliquer
978 979 button_clear: Effacer
979 980 button_lock: Verrouiller
980 981 button_unlock: Déverrouiller
981 982 button_download: Télécharger
982 983 button_list: Lister
983 984 button_view: Voir
984 985 button_move: Déplacer
985 986 button_move_and_follow: Déplacer et suivre
986 987 button_back: Retour
987 988 button_cancel: Annuler
988 989 button_activate: Activer
989 990 button_sort: Trier
990 991 button_log_time: Saisir temps
991 992 button_rollback: Revenir à cette version
992 993 button_watch: Surveiller
993 994 button_unwatch: Ne plus surveiller
994 995 button_reply: Répondre
995 996 button_archive: Archiver
996 997 button_unarchive: Désarchiver
997 998 button_reset: Réinitialiser
998 999 button_rename: Renommer
999 1000 button_change_password: Changer de mot de passe
1000 1001 button_copy: Copier
1001 1002 button_copy_and_follow: Copier et suivre
1002 1003 button_annotate: Annoter
1003 1004 button_update: Mettre à jour
1004 1005 button_configure: Configurer
1005 1006 button_quote: Citer
1006 1007 button_duplicate: Dupliquer
1007 1008 button_show: Afficher
1008 1009 button_hide: Cacher
1009 1010 button_edit_section: Modifier cette section
1010 1011 button_export: Exporter
1011 1012 button_delete_my_account: Supprimer mon compte
1012 1013 button_close: Fermer
1013 1014 button_reopen: Réouvrir
1014 1015
1015 1016 status_active: actif
1016 1017 status_registered: enregistré
1017 1018 status_locked: verrouillé
1018 1019
1019 1020 project_status_active: actif
1020 1021 project_status_closed: fermé
1021 1022 project_status_archived: archivé
1022 1023
1023 1024 version_status_open: ouvert
1024 1025 version_status_locked: verrouillé
1025 1026 version_status_closed: fermé
1026 1027
1027 1028 field_active: Actif
1028 1029
1029 1030 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
1030 1031 text_regexp_info: ex. ^[A-Z0-9]+$
1031 1032 text_min_max_length_info: 0 pour aucune restriction
1032 1033 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
1033 1034 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
1034 1035 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
1035 1036 text_are_you_sure: Êtes-vous sûr ?
1036 1037 text_journal_changed: "%{label} changé de %{old} à %{new}"
1037 1038 text_journal_changed_no_detail: "%{label} mis à jour"
1038 1039 text_journal_set_to: "%{label} mis à %{value}"
1039 1040 text_journal_deleted: "%{label} %{old} supprimé"
1040 1041 text_journal_added: "%{label} %{value} ajouté"
1041 1042 text_tip_issue_begin_day: tâche commençant ce jour
1042 1043 text_tip_issue_end_day: tâche finissant ce jour
1043 1044 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
1044 1045 text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés, doit commencer par une minuscule.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
1045 1046 text_caracters_maximum: "%{count} caractères maximum."
1046 1047 text_caracters_minimum: "%{count} caractères minimum."
1047 1048 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
1048 1049 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
1049 1050 text_unallowed_characters: Caractères non autorisés
1050 1051 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
1051 1052 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
1052 1053 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
1053 1054 text_issue_added: "La demande %{id} a été soumise par %{author}."
1054 1055 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
1055 1056 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
1056 1057 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
1057 1058 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
1058 1059 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
1059 1060 text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)."
1060 1061 text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé."
1061 1062 text_load_default_configuration: Charger le paramétrage par défaut
1062 1063 text_status_changed_by_changeset: "Appliqué par commit %{value}."
1063 1064 text_time_logged_by_changeset: "Appliqué par commit %{value}"
1064 1065 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
1065 1066 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
1066 1067 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1067 1068 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
1068 1069 text_default_administrator_account_changed: Compte administrateur par défaut changé
1069 1070 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
1070 1071 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
1071 1072 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
1072 1073 text_convert_available: Binaire convert de ImageMagick présent (optionel)
1073 1074 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
1074 1075 text_destroy_time_entries: Supprimer les heures
1075 1076 text_assign_time_entries_to_project: Reporter les heures sur le projet
1076 1077 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
1077 1078 text_user_wrote: "%{value} a écrit :"
1078 1079 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
1079 1080 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
1080 1081 text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/configuration.yml et redémarrez l'application pour les activer."
1081 1082 text_repository_usernames_mapping: "Vous pouvez sélectionner ou modifier l'utilisateur Redmine associé à chaque nom d'utilisateur figurant dans l'historique du dépôt.\nLes utilisateurs avec le même identifiant ou la même adresse mail seront automatiquement associés."
1082 1083 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
1083 1084 text_custom_field_possible_values_info: 'Une ligne par valeur'
1084 1085 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
1085 1086 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
1086 1087 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
1087 1088 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
1088 1089 text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?"
1089 1090 text_zoom_in: Zoom avant
1090 1091 text_zoom_out: Zoom arrière
1091 1092 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
1092 1093 text_scm_path_encoding_note: "Défaut : UTF-8"
1093 1094 text_subversion_repository_note: "Exemples (en fonction des protocoles supportés) : file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1094 1095 text_git_repository_note: "Chemin vers un dépôt vide et local (exemples : /gitrepo, c:\\gitrepo)"
1095 1096 text_mercurial_repository_note: "Chemin vers un dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1096 1097 text_scm_command: Commande
1097 1098 text_scm_command_version: Version
1098 1099 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1099 1100 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1100 1101 text_issue_conflict_resolution_overwrite: "Appliquer quand même ma mise à jour (les notes précédentes seront conservées mais des changements pourront être écrasés)"
1101 1102 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
1102 1103 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
1103 1104 text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
1104 1105 text_session_expiration_settings: "Attention : le changement de ces paramètres peut entrainer l'expiration des sessions utilisateurs en cours, y compris la vôtre."
1105 1106 text_project_closed: Ce projet est fermé et accessible en lecture seule.
1106 1107 text_turning_multiple_off: "Si vous désactivez les valeurs multiples, les valeurs multiples seront supprimées pour n'en conserver qu'une par objet."
1107 1108
1108 1109 default_role_manager: Manager
1109 1110 default_role_developer: Développeur
1110 1111 default_role_reporter: Rapporteur
1111 1112 default_tracker_bug: Anomalie
1112 1113 default_tracker_feature: Evolution
1113 1114 default_tracker_support: Assistance
1114 1115 default_issue_status_new: Nouveau
1115 1116 default_issue_status_in_progress: En cours
1116 1117 default_issue_status_resolved: Résolu
1117 1118 default_issue_status_feedback: Commentaire
1118 1119 default_issue_status_closed: Fermé
1119 1120 default_issue_status_rejected: Rejeté
1120 1121 default_doc_category_user: Documentation utilisateur
1121 1122 default_doc_category_tech: Documentation technique
1122 1123 default_priority_low: Bas
1123 1124 default_priority_normal: Normal
1124 1125 default_priority_high: Haut
1125 1126 default_priority_urgent: Urgent
1126 1127 default_priority_immediate: Immédiat
1127 1128 default_activity_design: Conception
1128 1129 default_activity_development: Développement
1129 1130
1130 1131 enumeration_issue_priorities: Priorités des demandes
1131 1132 enumeration_doc_categories: Catégories des documents
1132 1133 enumeration_activities: Activités (suivi du temps)
1133 1134 enumeration_system_activity: Activité système
1134 1135 description_filter: Filtre
1135 1136 description_search: Champ de recherche
1136 1137 description_choose_project: Projets
1137 1138 description_project_scope: Périmètre de recherche
1138 1139 description_notes: Notes
1139 1140 description_message_content: Contenu du message
1140 1141 description_query_sort_criteria_attribute: Critère de tri
1141 1142 description_query_sort_criteria_direction: Ordre de tri
1142 1143 description_user_mail_notification: Option de notification
1143 1144 description_available_columns: Colonnes disponibles
1144 1145 description_selected_columns: Colonnes sélectionnées
1145 1146 description_all_columns: Toutes les colonnes
1146 1147 description_issue_category_reassign: Choisir une catégorie
1147 1148 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1148 1149 description_date_range_list: Choisir une période prédéfinie
1149 1150 description_date_range_interval: Choisir une période
1150 1151 description_date_from: Date de début
1151 1152 description_date_to: Date de fin
1152 1153 text_repository_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
@@ -1,277 +1,278
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 'redmine/core_ext'
19 19
20 20 begin
21 21 require 'RMagick' unless Object.const_defined?(:Magick)
22 22 rescue LoadError
23 23 # RMagick is not available
24 24 end
25 25 begin
26 26 require 'redcarpet' unless Object.const_defined?(:Redcarpet)
27 27 rescue LoadError
28 28 # Redcarpet is not available
29 29 end
30 30
31 31 require 'redmine/scm/base'
32 32 require 'redmine/access_control'
33 33 require 'redmine/access_keys'
34 34 require 'redmine/activity'
35 35 require 'redmine/activity/fetcher'
36 36 require 'redmine/ciphering'
37 37 require 'redmine/codeset_util'
38 38 require 'redmine/field_format'
39 39 require 'redmine/i18n'
40 40 require 'redmine/menu_manager'
41 41 require 'redmine/notifiable'
42 42 require 'redmine/platform'
43 43 require 'redmine/mime_type'
44 44 require 'redmine/notifiable'
45 45 require 'redmine/search'
46 46 require 'redmine/syntax_highlighting'
47 47 require 'redmine/thumbnail'
48 48 require 'redmine/unified_diff'
49 49 require 'redmine/utils'
50 50 require 'redmine/version'
51 51 require 'redmine/wiki_formatting'
52 52
53 53 require 'redmine/default_data/loader'
54 54 require 'redmine/helpers/calendar'
55 55 require 'redmine/helpers/diff'
56 56 require 'redmine/helpers/gantt'
57 57 require 'redmine/helpers/time_report'
58 58 require 'redmine/views/other_formats_builder'
59 59 require 'redmine/views/labelled_form_builder'
60 60 require 'redmine/views/builders'
61 61
62 62 require 'redmine/themes'
63 63 require 'redmine/hook'
64 64 require 'redmine/plugin'
65 65
66 66 require 'csv'
67 67
68 68 Redmine::Scm::Base.add "Subversion"
69 69 Redmine::Scm::Base.add "Darcs"
70 70 Redmine::Scm::Base.add "Mercurial"
71 71 Redmine::Scm::Base.add "Cvs"
72 72 Redmine::Scm::Base.add "Bazaar"
73 73 Redmine::Scm::Base.add "Git"
74 74 Redmine::Scm::Base.add "Filesystem"
75 75
76 76 # Permissions
77 77 Redmine::AccessControl.map do |map|
78 78 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
79 79 map.permission :search_project, {:search => :index}, :public => true, :read => true
80 80 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
81 81 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
82 82 map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
83 83 map.permission :select_project_modules, {:projects => :modules}, :require => :member
84 84 map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true
85 85 map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :update, :destroy, :autocomplete]}, :require => :member
86 86 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
87 87 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
88 88
89 89 map.project_module :issue_tracking do |map|
90 90 # Issue categories
91 91 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
92 92 # Issues
93 93 map.permission :view_issues, {:issues => [:index, :show],
94 94 :auto_complete => [:issues],
95 95 :context_menus => [:issues],
96 96 :versions => [:index, :show, :status_by],
97 97 :journals => [:index, :diff],
98 98 :queries => :index,
99 99 :reports => [:issue_report, :issue_report_details]},
100 100 :read => true
101 101 map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
102 102 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
103 map.permission :copy_issues, {:issues => [:new, :create, :bulk_edit, :bulk_update, :update_form], :attachments => :upload}
103 104 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
104 105 map.permission :manage_subtasks, {}
105 106 map.permission :set_issues_private, {}
106 107 map.permission :set_own_issues_private, {}, :require => :loggedin
107 108 map.permission :add_issue_notes, {:issues => [:edit, :update, :update_form], :journals => [:new], :attachments => :upload}
108 109 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
109 110 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
110 111 map.permission :view_private_notes, {}, :read => true, :require => :member
111 112 map.permission :set_notes_private, {}, :require => :member
112 113 map.permission :delete_issues, {:issues => :destroy}, :require => :member
113 114 # Queries
114 115 map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
115 116 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
116 117 # Watchers
117 118 map.permission :view_issue_watchers, {}, :read => true
118 119 map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]}
119 120 map.permission :delete_issue_watchers, {:watchers => :destroy}
120 121 end
121 122
122 123 map.project_module :time_tracking do |map|
123 124 map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
124 125 map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
125 126 map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
126 127 map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
127 128 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
128 129 end
129 130
130 131 map.project_module :news do |map|
131 132 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member
132 133 map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
133 134 map.permission :comment_news, {:comments => :create}
134 135 end
135 136
136 137 map.project_module :documents do |map|
137 138 map.permission :add_documents, {:documents => [:new, :create, :add_attachment], :attachments => :upload}, :require => :loggedin
138 139 map.permission :edit_documents, {:documents => [:edit, :update, :add_attachment], :attachments => :upload}, :require => :loggedin
139 140 map.permission :delete_documents, {:documents => [:destroy]}, :require => :loggedin
140 141 map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
141 142 end
142 143
143 144 map.project_module :files do |map|
144 145 map.permission :manage_files, {:files => [:new, :create], :attachments => :upload}, :require => :loggedin
145 146 map.permission :view_files, {:files => :index, :versions => :download}, :read => true
146 147 end
147 148
148 149 map.project_module :wiki do |map|
149 150 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
150 151 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
151 152 map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
152 153 map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
153 154 map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
154 155 map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
155 156 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment], :attachments => :upload
156 157 map.permission :delete_wiki_pages_attachments, {}
157 158 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
158 159 end
159 160
160 161 map.project_module :repository do |map|
161 162 map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
162 163 map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
163 164 map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
164 165 map.permission :commit_access, {}
165 166 map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
166 167 end
167 168
168 169 map.project_module :boards do |map|
169 170 map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
170 171 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
171 172 map.permission :add_messages, {:messages => [:new, :reply, :quote], :attachments => :upload}
172 173 map.permission :edit_messages, {:messages => :edit, :attachments => :upload}, :require => :member
173 174 map.permission :edit_own_messages, {:messages => :edit, :attachments => :upload}, :require => :loggedin
174 175 map.permission :delete_messages, {:messages => :destroy}, :require => :member
175 176 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
176 177 end
177 178
178 179 map.project_module :calendar do |map|
179 180 map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
180 181 end
181 182
182 183 map.project_module :gantt do |map|
183 184 map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
184 185 end
185 186 end
186 187
187 188 Redmine::MenuManager.map :top_menu do |menu|
188 189 menu.push :home, :home_path
189 190 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
190 191 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
191 192 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
192 193 menu.push :help, Redmine::Info.help_url, :last => true
193 194 end
194 195
195 196 Redmine::MenuManager.map :account_menu do |menu|
196 197 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
197 198 menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
198 199 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
199 200 menu.push :logout, :signout_path, :html => {:method => 'post'}, :if => Proc.new { User.current.logged? }
200 201 end
201 202
202 203 Redmine::MenuManager.map :application_menu do |menu|
203 204 # Empty
204 205 end
205 206
206 207 Redmine::MenuManager.map :admin_menu do |menu|
207 208 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
208 209 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
209 210 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
210 211 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
211 212 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
212 213 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
213 214 :html => {:class => 'issue_statuses'}
214 215 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
215 216 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
216 217 :html => {:class => 'custom_fields'}
217 218 menu.push :enumerations, {:controller => 'enumerations'}
218 219 menu.push :settings, {:controller => 'settings'}
219 220 menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
220 221 :html => {:class => 'server_authentication'}
221 222 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
222 223 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
223 224 end
224 225
225 226 Redmine::MenuManager.map :project_menu do |menu|
226 227 menu.push :overview, { :controller => 'projects', :action => 'show' }
227 228 menu.push :activity, { :controller => 'activities', :action => 'index' }
228 229 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
229 230 :if => Proc.new { |p| p.shared_versions.any? }
230 231 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
231 232 menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new,
232 233 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) },
233 234 :if => Proc.new { |p| p.trackers.any? }
234 235 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
235 236 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
236 237 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
237 238 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
238 239 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
239 240 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
240 241 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
241 242 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
242 243 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
243 244 menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
244 245 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
245 246 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
246 247 end
247 248
248 249 Redmine::Activity.map do |activity|
249 250 activity.register :issues, :class_name => %w(Issue Journal)
250 251 activity.register :changesets
251 252 activity.register :news
252 253 activity.register :documents, :class_name => %w(Document Attachment)
253 254 activity.register :files, :class_name => 'Attachment'
254 255 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
255 256 activity.register :messages, :default => false
256 257 activity.register :time_entries, :default => false
257 258 end
258 259
259 260 Redmine::Search.map do |search|
260 261 search.register :issues
261 262 search.register :news
262 263 search.register :documents
263 264 search.register :changesets
264 265 search.register :wiki_pages
265 266 search.register :messages
266 267 search.register :projects
267 268 end
268 269
269 270 Redmine::WikiFormatting.map do |format|
270 271 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
271 272 if Object.const_defined?(:Redcarpet)
272 273 format.register :markdown, Redmine::WikiFormatting::Markdown::Formatter, Redmine::WikiFormatting::Markdown::Helper,
273 274 :label => 'Markdown (experimental)'
274 275 end
275 276 end
276 277
277 278 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
@@ -1,204 +1,206
1 1 ---
2 2 roles_001:
3 3 name: Manager
4 4 id: 1
5 5 builtin: 0
6 6 issues_visibility: all
7 7 users_visibility: all
8 8 permissions: |
9 9 ---
10 10 - :add_project
11 11 - :edit_project
12 12 - :close_project
13 13 - :select_project_modules
14 14 - :manage_members
15 15 - :manage_versions
16 16 - :manage_categories
17 17 - :view_issues
18 18 - :add_issues
19 19 - :edit_issues
20 - :copy_issues
20 21 - :manage_issue_relations
21 22 - :manage_subtasks
22 23 - :add_issue_notes
23 24 - :delete_issues
24 25 - :view_issue_watchers
25 26 - :add_issue_watchers
26 27 - :set_issues_private
27 28 - :set_notes_private
28 29 - :view_private_notes
29 30 - :delete_issue_watchers
30 31 - :manage_public_queries
31 32 - :save_queries
32 33 - :view_gantt
33 34 - :view_calendar
34 35 - :log_time
35 36 - :view_time_entries
36 37 - :edit_time_entries
37 38 - :delete_time_entries
38 39 - :manage_news
39 40 - :comment_news
40 41 - :view_documents
41 42 - :add_documents
42 43 - :edit_documents
43 44 - :delete_documents
44 45 - :view_wiki_pages
45 46 - :export_wiki_pages
46 47 - :view_wiki_edits
47 48 - :edit_wiki_pages
48 49 - :delete_wiki_pages_attachments
49 50 - :protect_wiki_pages
50 51 - :delete_wiki_pages
51 52 - :rename_wiki_pages
52 53 - :add_messages
53 54 - :edit_messages
54 55 - :delete_messages
55 56 - :manage_boards
56 57 - :view_files
57 58 - :manage_files
58 59 - :browse_repository
59 60 - :manage_repository
60 61 - :view_changesets
61 62 - :manage_related_issues
62 63 - :manage_project_activities
63 64
64 65 position: 1
65 66 roles_002:
66 67 name: Developer
67 68 id: 2
68 69 builtin: 0
69 70 issues_visibility: default
70 71 users_visibility: all
71 72 permissions: |
72 73 ---
73 74 - :edit_project
74 75 - :manage_members
75 76 - :manage_versions
76 77 - :manage_categories
77 78 - :view_issues
78 79 - :add_issues
79 80 - :edit_issues
81 - :copy_issues
80 82 - :manage_issue_relations
81 83 - :manage_subtasks
82 84 - :add_issue_notes
83 85 - :delete_issues
84 86 - :view_issue_watchers
85 87 - :save_queries
86 88 - :view_gantt
87 89 - :view_calendar
88 90 - :log_time
89 91 - :view_time_entries
90 92 - :edit_own_time_entries
91 93 - :manage_news
92 94 - :comment_news
93 95 - :view_documents
94 96 - :add_documents
95 97 - :edit_documents
96 98 - :delete_documents
97 99 - :view_wiki_pages
98 100 - :view_wiki_edits
99 101 - :edit_wiki_pages
100 102 - :protect_wiki_pages
101 103 - :delete_wiki_pages
102 104 - :add_messages
103 105 - :edit_own_messages
104 106 - :delete_own_messages
105 107 - :manage_boards
106 108 - :view_files
107 109 - :manage_files
108 110 - :browse_repository
109 111 - :view_changesets
110 112
111 113 position: 2
112 114 roles_003:
113 115 name: Reporter
114 116 id: 3
115 117 builtin: 0
116 118 issues_visibility: default
117 119 users_visibility: all
118 120 permissions: |
119 121 ---
120 122 - :edit_project
121 123 - :manage_members
122 124 - :manage_versions
123 125 - :manage_categories
124 126 - :view_issues
125 127 - :add_issues
126 128 - :edit_issues
127 129 - :manage_issue_relations
128 130 - :add_issue_notes
129 131 - :view_issue_watchers
130 132 - :save_queries
131 133 - :view_gantt
132 134 - :view_calendar
133 135 - :log_time
134 136 - :view_time_entries
135 137 - :manage_news
136 138 - :comment_news
137 139 - :view_documents
138 140 - :add_documents
139 141 - :edit_documents
140 142 - :delete_documents
141 143 - :view_wiki_pages
142 144 - :view_wiki_edits
143 145 - :edit_wiki_pages
144 146 - :delete_wiki_pages
145 147 - :add_messages
146 148 - :manage_boards
147 149 - :view_files
148 150 - :manage_files
149 151 - :browse_repository
150 152 - :view_changesets
151 153
152 154 position: 3
153 155 roles_004:
154 156 name: Non member
155 157 id: 4
156 158 builtin: 1
157 159 issues_visibility: default
158 160 users_visibility: all
159 161 permissions: |
160 162 ---
161 163 - :view_issues
162 164 - :add_issues
163 165 - :edit_issues
164 166 - :manage_issue_relations
165 167 - :add_issue_notes
166 168 - :save_queries
167 169 - :view_gantt
168 170 - :view_calendar
169 171 - :log_time
170 172 - :view_time_entries
171 173 - :comment_news
172 174 - :view_documents
173 175 - :view_wiki_pages
174 176 - :view_wiki_edits
175 177 - :edit_wiki_pages
176 178 - :add_messages
177 179 - :view_files
178 180 - :manage_files
179 181 - :browse_repository
180 182 - :view_changesets
181 183
182 184 position: 4
183 185 roles_005:
184 186 name: Anonymous
185 187 id: 5
186 188 builtin: 2
187 189 issues_visibility: default
188 190 users_visibility: all
189 191 permissions: |
190 192 ---
191 193 - :view_issues
192 194 - :add_issue_notes
193 195 - :view_gantt
194 196 - :view_calendar
195 197 - :view_time_entries
196 198 - :view_documents
197 199 - :view_wiki_pages
198 200 - :view_wiki_edits
199 201 - :view_files
200 202 - :browse_repository
201 203 - :view_changesets
202 204
203 205 position: 5
204 206
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now