##// 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 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ContextMenusController < ApplicationController
18 class ContextMenusController < ApplicationController
19 helper :watchers
19 helper :watchers
20 helper :issues
20 helper :issues
21
21
22 before_filter :find_issues, :only => :issues
22 before_filter :find_issues, :only => :issues
23
23
24 def issues
24 def issues
25 if (@issues.size == 1)
25 if (@issues.size == 1)
26 @issue = @issues.first
26 @issue = @issues.first
27 end
27 end
28 @issue_ids = @issues.map(&:id).sort
28 @issue_ids = @issues.map(&:id).sort
29
29
30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
31
31
32 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
32 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
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 :delete => User.current.allowed_to?(:delete_issues, @projects)
35 :delete => User.current.allowed_to?(:delete_issues, @projects)
36 }
36 }
37 if @project
37 if @project
38 if @issue
38 if @issue
39 @assignables = @issue.assignable_users
39 @assignables = @issue.assignable_users
40 else
40 else
41 @assignables = @project.assignable_users
41 @assignables = @project.assignable_users
42 end
42 end
43 @trackers = @project.trackers
43 @trackers = @project.trackers
44 else
44 else
45 #when multiple projects, we only keep the intersection of each set
45 #when multiple projects, we only keep the intersection of each set
46 @assignables = @projects.map(&:assignable_users).reduce(:&)
46 @assignables = @projects.map(&:assignable_users).reduce(:&)
47 @trackers = @projects.map(&:trackers).reduce(:&)
47 @trackers = @projects.map(&:trackers).reduce(:&)
48 end
48 end
49 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
49 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
50
50
51 @priorities = IssuePriority.active.reverse
51 @priorities = IssuePriority.active.reverse
52 @back = back_url
52 @back = back_url
53
53
54 @options_by_custom_field = {}
54 @options_by_custom_field = {}
55 if @can[:edit]
55 if @can[:edit]
56 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
56 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
57 custom_fields.each do |field|
57 custom_fields.each do |field|
58 values = field.possible_values_options(@projects)
58 values = field.possible_values_options(@projects)
59 if values.present?
59 if values.present?
60 @options_by_custom_field[field] = values
60 @options_by_custom_field[field] = values
61 end
61 end
62 end
62 end
63 end
63 end
64
64
65 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
65 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
66 render :layout => false
66 render :layout => false
67 end
67 end
68
68
69 def time_entries
69 def time_entries
70 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
70 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
71 (render_404; return) unless @time_entries.present?
71 (render_404; return) unless @time_entries.present?
72 if (@time_entries.size == 1)
72 if (@time_entries.size == 1)
73 @time_entry = @time_entries.first
73 @time_entry = @time_entries.first
74 end
74 end
75
75
76 @projects = @time_entries.collect(&:project).compact.uniq
76 @projects = @time_entries.collect(&:project).compact.uniq
77 @project = @projects.first if @projects.size == 1
77 @project = @projects.first if @projects.size == 1
78 @activities = TimeEntryActivity.shared.active
78 @activities = TimeEntryActivity.shared.active
79 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
79 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
80 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
80 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
81 }
81 }
82 @back = back_url
82 @back = back_url
83
83
84 @options_by_custom_field = {}
84 @options_by_custom_field = {}
85 if @can[:edit]
85 if @can[:edit]
86 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
86 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
87 custom_fields.each do |field|
87 custom_fields.each do |field|
88 values = field.possible_values_options(@projects)
88 values = field.possible_values_options(@projects)
89 if values.present?
89 if values.present?
90 @options_by_custom_field[field] = values
90 @options_by_custom_field[field] = values
91 end
91 end
92 end
92 end
93 end
93 end
94
94
95 render :layout => false
95 render :layout => false
96 end
96 end
97 end
97 end
@@ -1,498 +1,523
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :find_project, :only => [:new, :create, :update_form]
24 before_filter :find_project, :only => [:new, :create, :update_form]
25 before_filter :authorize, :except => [:index]
25 before_filter :authorize, :except => [:index]
26 before_filter :find_optional_project, :only => [:index]
26 before_filter :find_optional_project, :only => [:index]
27 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
27 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
28 accept_rss_auth :index, :show
28 accept_rss_auth :index, :show
29 accept_api_auth :index, :show, :create, :update, :destroy
29 accept_api_auth :index, :show, :create, :update, :destroy
30
30
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32
32
33 helper :journals
33 helper :journals
34 helper :projects
34 helper :projects
35 include ProjectsHelper
35 include ProjectsHelper
36 helper :custom_fields
36 helper :custom_fields
37 include CustomFieldsHelper
37 include CustomFieldsHelper
38 helper :issue_relations
38 helper :issue_relations
39 include IssueRelationsHelper
39 include IssueRelationsHelper
40 helper :watchers
40 helper :watchers
41 include WatchersHelper
41 include WatchersHelper
42 helper :attachments
42 helper :attachments
43 include AttachmentsHelper
43 include AttachmentsHelper
44 helper :queries
44 helper :queries
45 include QueriesHelper
45 include QueriesHelper
46 helper :repositories
46 helper :repositories
47 include RepositoriesHelper
47 include RepositoriesHelper
48 helper :sort
48 helper :sort
49 include SortHelper
49 include SortHelper
50 include IssuesHelper
50 include IssuesHelper
51 helper :timelog
51 helper :timelog
52
52
53 def index
53 def index
54 retrieve_query
54 retrieve_query
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 sort_update(@query.sortable_columns)
56 sort_update(@query.sortable_columns)
57 @query.sort_criteria = sort_criteria.to_a
57 @query.sort_criteria = sort_criteria.to_a
58
58
59 if @query.valid?
59 if @query.valid?
60 case params[:format]
60 case params[:format]
61 when 'csv', 'pdf'
61 when 'csv', 'pdf'
62 @limit = Setting.issues_export_limit.to_i
62 @limit = Setting.issues_export_limit.to_i
63 if params[:columns] == 'all'
63 if params[:columns] == 'all'
64 @query.column_names = @query.available_inline_columns.map(&:name)
64 @query.column_names = @query.available_inline_columns.map(&:name)
65 end
65 end
66 when 'atom'
66 when 'atom'
67 @limit = Setting.feeds_limit.to_i
67 @limit = Setting.feeds_limit.to_i
68 when 'xml', 'json'
68 when 'xml', 'json'
69 @offset, @limit = api_offset_and_limit
69 @offset, @limit = api_offset_and_limit
70 @query.column_names = %w(author)
70 @query.column_names = %w(author)
71 else
71 else
72 @limit = per_page_option
72 @limit = per_page_option
73 end
73 end
74
74
75 @issue_count = @query.issue_count
75 @issue_count = @query.issue_count
76 @issue_pages = Paginator.new @issue_count, @limit, params['page']
76 @issue_pages = Paginator.new @issue_count, @limit, params['page']
77 @offset ||= @issue_pages.offset
77 @offset ||= @issue_pages.offset
78 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
78 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
79 :order => sort_clause,
79 :order => sort_clause,
80 :offset => @offset,
80 :offset => @offset,
81 :limit => @limit)
81 :limit => @limit)
82 @issue_count_by_group = @query.issue_count_by_group
82 @issue_count_by_group = @query.issue_count_by_group
83
83
84 respond_to do |format|
84 respond_to do |format|
85 format.html { render :template => 'issues/index', :layout => !request.xhr? }
85 format.html { render :template => 'issues/index', :layout => !request.xhr? }
86 format.api {
86 format.api {
87 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
87 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
88 }
88 }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
90 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
90 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
91 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
91 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
92 end
92 end
93 else
93 else
94 respond_to do |format|
94 respond_to do |format|
95 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
95 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
96 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
96 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
97 format.api { render_validation_errors(@query) }
97 format.api { render_validation_errors(@query) }
98 end
98 end
99 end
99 end
100 rescue ActiveRecord::RecordNotFound
100 rescue ActiveRecord::RecordNotFound
101 render_404
101 render_404
102 end
102 end
103
103
104 def show
104 def show
105 @journals = @issue.journals.includes(:user, :details).
105 @journals = @issue.journals.includes(:user, :details).
106 references(:user, :details).
106 references(:user, :details).
107 reorder("#{Journal.table_name}.id ASC").to_a
107 reorder("#{Journal.table_name}.id ASC").to_a
108 @journals.each_with_index {|j,i| j.indice = i+1}
108 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
109 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
110 Journal.preload_journals_details_custom_fields(@journals)
110 Journal.preload_journals_details_custom_fields(@journals)
111 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
111 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
113
113
114 @changesets = @issue.changesets.visible.to_a
114 @changesets = @issue.changesets.visible.to_a
115 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
116
116
117 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
117 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
119 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 @priorities = IssuePriority.active
120 @priorities = IssuePriority.active
121 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
121 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
122 @relation = IssueRelation.new
122 @relation = IssueRelation.new
123
123
124 respond_to do |format|
124 respond_to do |format|
125 format.html {
125 format.html {
126 retrieve_previous_and_next_issue_ids
126 retrieve_previous_and_next_issue_ids
127 render :template => 'issues/show'
127 render :template => 'issues/show'
128 }
128 }
129 format.api
129 format.api
130 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
130 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
131 format.pdf {
131 format.pdf {
132 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
132 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
133 }
133 }
134 end
134 end
135 end
135 end
136
136
137 # Add a new issue
137 # Add a new issue
138 # The new issue will be created from an existing one if copy_from parameter is given
138 # The new issue will be created from an existing one if copy_from parameter is given
139 def new
139 def new
140 respond_to do |format|
140 respond_to do |format|
141 format.html { render :action => 'new', :layout => !request.xhr? }
141 format.html { render :action => 'new', :layout => !request.xhr? }
142 end
142 end
143 end
143 end
144
144
145 def create
145 def create
146 unless User.current.allowed_to?(:add_issues, @issue.project)
147 raise ::Unauthorized
148 end
146 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
149 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
147 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
150 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
148 if @issue.save
151 if @issue.save
149 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
152 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
150 respond_to do |format|
153 respond_to do |format|
151 format.html {
154 format.html {
152 render_attachment_warning_if_needed(@issue)
155 render_attachment_warning_if_needed(@issue)
153 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
156 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
154 if params[:continue]
157 if params[:continue]
155 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
158 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
156 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
159 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
157 else
160 else
158 redirect_to issue_path(@issue)
161 redirect_to issue_path(@issue)
159 end
162 end
160 }
163 }
161 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
164 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
162 end
165 end
163 return
166 return
164 else
167 else
165 respond_to do |format|
168 respond_to do |format|
166 format.html { render :action => 'new' }
169 format.html { render :action => 'new' }
167 format.api { render_validation_errors(@issue) }
170 format.api { render_validation_errors(@issue) }
168 end
171 end
169 end
172 end
170 end
173 end
171
174
172 def edit
175 def edit
173 return unless update_issue_from_params
176 return unless update_issue_from_params
174
177
175 respond_to do |format|
178 respond_to do |format|
176 format.html { }
179 format.html { }
177 format.xml { }
180 format.xml { }
178 end
181 end
179 end
182 end
180
183
181 def update
184 def update
182 return unless update_issue_from_params
185 return unless update_issue_from_params
183 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
186 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
184 saved = false
187 saved = false
185 begin
188 begin
186 saved = save_issue_with_child_records
189 saved = save_issue_with_child_records
187 rescue ActiveRecord::StaleObjectError
190 rescue ActiveRecord::StaleObjectError
188 @conflict = true
191 @conflict = true
189 if params[:last_journal_id]
192 if params[:last_journal_id]
190 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
193 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
191 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
194 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
192 end
195 end
193 end
196 end
194
197
195 if saved
198 if saved
196 render_attachment_warning_if_needed(@issue)
199 render_attachment_warning_if_needed(@issue)
197 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
200 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
198
201
199 respond_to do |format|
202 respond_to do |format|
200 format.html { redirect_back_or_default issue_path(@issue) }
203 format.html { redirect_back_or_default issue_path(@issue) }
201 format.api { render_api_ok }
204 format.api { render_api_ok }
202 end
205 end
203 else
206 else
204 respond_to do |format|
207 respond_to do |format|
205 format.html { render :action => 'edit' }
208 format.html { render :action => 'edit' }
206 format.api { render_validation_errors(@issue) }
209 format.api { render_validation_errors(@issue) }
207 end
210 end
208 end
211 end
209 end
212 end
210
213
211 # Updates the issue form when changing the project, status or tracker
214 # Updates the issue form when changing the project, status or tracker
212 # on issue creation/update
215 # on issue creation/update
213 def update_form
216 def update_form
214 end
217 end
215
218
216 # Bulk edit/copy a set of issues
219 # Bulk edit/copy a set of issues
217 def bulk_edit
220 def bulk_edit
218 @issues.sort!
221 @issues.sort!
219 @copy = params[:copy].present?
222 @copy = params[:copy].present?
220 @notes = params[:notes]
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 @allowed_projects = Issue.allowed_target_projects
231 @allowed_projects = Issue.allowed_target_projects
223 if params[:issue]
232 if params[:issue]
224 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
233 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
225 if @target_project
234 if @target_project
226 target_projects = [@target_project]
235 target_projects = [@target_project]
227 end
236 end
228 end
237 end
229 target_projects ||= @projects
238 target_projects ||= @projects
230
239
231 if @copy
240 if @copy
232 # Copied issues will get their default statuses
241 # Copied issues will get their default statuses
233 @available_statuses = []
242 @available_statuses = []
234 else
243 else
235 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
244 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
236 end
245 end
237 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
246 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
238 @assignables = target_projects.map(&:assignable_users).reduce(:&)
247 @assignables = target_projects.map(&:assignable_users).reduce(:&)
239 @trackers = target_projects.map(&:trackers).reduce(:&)
248 @trackers = target_projects.map(&:trackers).reduce(:&)
240 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
249 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
241 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
250 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
242 if @copy
251 if @copy
243 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
252 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
244 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
253 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
245 end
254 end
246
255
247 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
256 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
248
257
249 @issue_params = params[:issue] || {}
258 @issue_params = params[:issue] || {}
250 @issue_params[:custom_field_values] ||= {}
259 @issue_params[:custom_field_values] ||= {}
251 end
260 end
252
261
253 def bulk_update
262 def bulk_update
254 @issues.sort!
263 @issues.sort!
255 @copy = params[:copy].present?
264 @copy = params[:copy].present?
256 attributes = parse_params_for_bulk_issue_attributes(params)
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 unsaved_issues = []
280 unsaved_issues = []
259 saved_issues = []
281 saved_issues = []
260
282
261 if @copy && params[:copy_subtasks].present?
283 if @copy && params[:copy_subtasks].present?
262 # Descendant issues will be copied with the parent task
284 # Descendant issues will be copied with the parent task
263 # Don't copy them twice
285 # Don't copy them twice
264 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
286 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
265 end
287 end
266
288
267 @issues.each do |orig_issue|
289 @issues.each do |orig_issue|
268 orig_issue.reload
290 orig_issue.reload
269 if @copy
291 if @copy
270 issue = orig_issue.copy({},
292 issue = orig_issue.copy({},
271 :attachments => params[:copy_attachments].present?,
293 :attachments => params[:copy_attachments].present?,
272 :subtasks => params[:copy_subtasks].present?,
294 :subtasks => params[:copy_subtasks].present?,
273 :link => link_copy?(params[:link_copy])
295 :link => link_copy?(params[:link_copy])
274 )
296 )
275 else
297 else
276 issue = orig_issue
298 issue = orig_issue
277 end
299 end
278 journal = issue.init_journal(User.current, params[:notes])
300 journal = issue.init_journal(User.current, params[:notes])
279 issue.safe_attributes = attributes
301 issue.safe_attributes = attributes
280 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
302 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
281 if issue.save
303 if issue.save
282 saved_issues << issue
304 saved_issues << issue
283 else
305 else
284 unsaved_issues << orig_issue
306 unsaved_issues << orig_issue
285 end
307 end
286 end
308 end
287
309
288 if unsaved_issues.empty?
310 if unsaved_issues.empty?
289 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
311 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
290 if params[:follow]
312 if params[:follow]
291 if @issues.size == 1 && saved_issues.size == 1
313 if @issues.size == 1 && saved_issues.size == 1
292 redirect_to issue_path(saved_issues.first)
314 redirect_to issue_path(saved_issues.first)
293 elsif saved_issues.map(&:project).uniq.size == 1
315 elsif saved_issues.map(&:project).uniq.size == 1
294 redirect_to project_issues_path(saved_issues.map(&:project).first)
316 redirect_to project_issues_path(saved_issues.map(&:project).first)
295 end
317 end
296 else
318 else
297 redirect_back_or_default _project_issues_path(@project)
319 redirect_back_or_default _project_issues_path(@project)
298 end
320 end
299 else
321 else
300 @saved_issues = @issues
322 @saved_issues = @issues
301 @unsaved_issues = unsaved_issues
323 @unsaved_issues = unsaved_issues
302 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
324 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
303 bulk_edit
325 bulk_edit
304 render :action => 'bulk_edit'
326 render :action => 'bulk_edit'
305 end
327 end
306 end
328 end
307
329
308 def destroy
330 def destroy
309 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
331 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
310 if @hours > 0
332 if @hours > 0
311 case params[:todo]
333 case params[:todo]
312 when 'destroy'
334 when 'destroy'
313 # nothing to do
335 # nothing to do
314 when 'nullify'
336 when 'nullify'
315 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
337 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
316 when 'reassign'
338 when 'reassign'
317 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
339 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
318 if reassign_to.nil?
340 if reassign_to.nil?
319 flash.now[:error] = l(:error_issue_not_found_in_project)
341 flash.now[:error] = l(:error_issue_not_found_in_project)
320 return
342 return
321 else
343 else
322 TimeEntry.where(['issue_id IN (?)', @issues]).
344 TimeEntry.where(['issue_id IN (?)', @issues]).
323 update_all("issue_id = #{reassign_to.id}")
345 update_all("issue_id = #{reassign_to.id}")
324 end
346 end
325 else
347 else
326 # display the destroy form if it's a user request
348 # display the destroy form if it's a user request
327 return unless api_request?
349 return unless api_request?
328 end
350 end
329 end
351 end
330 @issues.each do |issue|
352 @issues.each do |issue|
331 begin
353 begin
332 issue.reload.destroy
354 issue.reload.destroy
333 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
355 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
334 # nothing to do, issue was already deleted (eg. by a parent)
356 # nothing to do, issue was already deleted (eg. by a parent)
335 end
357 end
336 end
358 end
337 respond_to do |format|
359 respond_to do |format|
338 format.html { redirect_back_or_default _project_issues_path(@project) }
360 format.html { redirect_back_or_default _project_issues_path(@project) }
339 format.api { render_api_ok }
361 format.api { render_api_ok }
340 end
362 end
341 end
363 end
342
364
343 private
365 private
344
366
345 def find_project
367 def find_project
346 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
368 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
347 @project = Project.find(project_id)
369 @project = Project.find(project_id)
348 rescue ActiveRecord::RecordNotFound
370 rescue ActiveRecord::RecordNotFound
349 render_404
371 render_404
350 end
372 end
351
373
352 def retrieve_previous_and_next_issue_ids
374 def retrieve_previous_and_next_issue_ids
353 retrieve_query_from_session
375 retrieve_query_from_session
354 if @query
376 if @query
355 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
377 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
356 sort_update(@query.sortable_columns, 'issues_index_sort')
378 sort_update(@query.sortable_columns, 'issues_index_sort')
357 limit = 500
379 limit = 500
358 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
380 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
359 if (idx = issue_ids.index(@issue.id)) && idx < limit
381 if (idx = issue_ids.index(@issue.id)) && idx < limit
360 if issue_ids.size < 500
382 if issue_ids.size < 500
361 @issue_position = idx + 1
383 @issue_position = idx + 1
362 @issue_count = issue_ids.size
384 @issue_count = issue_ids.size
363 end
385 end
364 @prev_issue_id = issue_ids[idx - 1] if idx > 0
386 @prev_issue_id = issue_ids[idx - 1] if idx > 0
365 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
387 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
366 end
388 end
367 end
389 end
368 end
390 end
369
391
370 # Used by #edit and #update to set some common instance variables
392 # Used by #edit and #update to set some common instance variables
371 # from the params
393 # from the params
372 # TODO: Refactor, not everything in here is needed by #edit
394 # TODO: Refactor, not everything in here is needed by #edit
373 def update_issue_from_params
395 def update_issue_from_params
374 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
396 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
375 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
397 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
376 if params[:time_entry]
398 if params[:time_entry]
377 @time_entry.attributes = params[:time_entry]
399 @time_entry.attributes = params[:time_entry]
378 end
400 end
379
401
380 @issue.init_journal(User.current)
402 @issue.init_journal(User.current)
381
403
382 issue_attributes = params[:issue]
404 issue_attributes = params[:issue]
383 if issue_attributes && params[:conflict_resolution]
405 if issue_attributes && params[:conflict_resolution]
384 case params[:conflict_resolution]
406 case params[:conflict_resolution]
385 when 'overwrite'
407 when 'overwrite'
386 issue_attributes = issue_attributes.dup
408 issue_attributes = issue_attributes.dup
387 issue_attributes.delete(:lock_version)
409 issue_attributes.delete(:lock_version)
388 when 'add_notes'
410 when 'add_notes'
389 issue_attributes = issue_attributes.slice(:notes)
411 issue_attributes = issue_attributes.slice(:notes)
390 when 'cancel'
412 when 'cancel'
391 redirect_to issue_path(@issue)
413 redirect_to issue_path(@issue)
392 return false
414 return false
393 end
415 end
394 end
416 end
395 @issue.safe_attributes = issue_attributes
417 @issue.safe_attributes = issue_attributes
396 @priorities = IssuePriority.active
418 @priorities = IssuePriority.active
397 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
419 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
398 true
420 true
399 end
421 end
400
422
401 # TODO: Refactor, lots of extra code in here
423 # TODO: Refactor, lots of extra code in here
402 # TODO: Changing tracker on an existing issue should not trigger this
424 # TODO: Changing tracker on an existing issue should not trigger this
403 def build_new_issue_from_params
425 def build_new_issue_from_params
404 if params[:id].blank?
426 if params[:id].blank?
405 @issue = Issue.new
427 @issue = Issue.new
406 if params[:copy_from]
428 if params[:copy_from]
407 begin
429 begin
408 @issue.init_journal(User.current)
430 @issue.init_journal(User.current)
409 @copy_from = Issue.visible.find(params[:copy_from])
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 @link_copy = link_copy?(params[:link_copy]) || request.get?
435 @link_copy = link_copy?(params[:link_copy]) || request.get?
411 @copy_attachments = params[:copy_attachments].present? || request.get?
436 @copy_attachments = params[:copy_attachments].present? || request.get?
412 @copy_subtasks = params[:copy_subtasks].present? || request.get?
437 @copy_subtasks = params[:copy_subtasks].present? || request.get?
413 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
438 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
414 rescue ActiveRecord::RecordNotFound
439 rescue ActiveRecord::RecordNotFound
415 render_404
440 render_404
416 return
441 return
417 end
442 end
418 end
443 end
419 @issue.project = @project
444 @issue.project = @project
420 @issue.author ||= User.current
445 @issue.author ||= User.current
421 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
446 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
422 else
447 else
423 @issue = @project.issues.visible.find(params[:id])
448 @issue = @project.issues.visible.find(params[:id])
424 end
449 end
425
450
426 if attrs = params[:issue].deep_dup
451 if attrs = params[:issue].deep_dup
427 if params[:was_default_status] == attrs[:status_id]
452 if params[:was_default_status] == attrs[:status_id]
428 attrs.delete(:status_id)
453 attrs.delete(:status_id)
429 end
454 end
430 @issue.safe_attributes = attrs
455 @issue.safe_attributes = attrs
431 end
456 end
432 @issue.tracker ||= @project.trackers.first
457 @issue.tracker ||= @project.trackers.first
433 if @issue.tracker.nil?
458 if @issue.tracker.nil?
434 render_error l(:error_no_tracker_in_project)
459 render_error l(:error_no_tracker_in_project)
435 return false
460 return false
436 end
461 end
437 if @issue.status.nil?
462 if @issue.status.nil?
438 render_error l(:error_no_default_issue_status)
463 render_error l(:error_no_default_issue_status)
439 return false
464 return false
440 end
465 end
441
466
442 @priorities = IssuePriority.active
467 @priorities = IssuePriority.active
443 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
468 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
444 @available_watchers = @issue.watcher_users
469 @available_watchers = @issue.watcher_users
445 if @issue.project.users.count <= 20
470 if @issue.project.users.count <= 20
446 @available_watchers = (@available_watchers + @issue.project.users.sort).uniq
471 @available_watchers = (@available_watchers + @issue.project.users.sort).uniq
447 end
472 end
448 end
473 end
449
474
450 def parse_params_for_bulk_issue_attributes(params)
475 def parse_params_for_bulk_issue_attributes(params)
451 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
476 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
452 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
477 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
453 if custom = attributes[:custom_field_values]
478 if custom = attributes[:custom_field_values]
454 custom.reject! {|k,v| v.blank?}
479 custom.reject! {|k,v| v.blank?}
455 custom.keys.each do |k|
480 custom.keys.each do |k|
456 if custom[k].is_a?(Array)
481 if custom[k].is_a?(Array)
457 custom[k] << '' if custom[k].delete('__none__')
482 custom[k] << '' if custom[k].delete('__none__')
458 else
483 else
459 custom[k] = '' if custom[k] == '__none__'
484 custom[k] = '' if custom[k] == '__none__'
460 end
485 end
461 end
486 end
462 end
487 end
463 attributes
488 attributes
464 end
489 end
465
490
466 # Saves @issue and a time_entry from the parameters
491 # Saves @issue and a time_entry from the parameters
467 def save_issue_with_child_records
492 def save_issue_with_child_records
468 Issue.transaction do
493 Issue.transaction do
469 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
494 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
470 time_entry = @time_entry || TimeEntry.new
495 time_entry = @time_entry || TimeEntry.new
471 time_entry.project = @issue.project
496 time_entry.project = @issue.project
472 time_entry.issue = @issue
497 time_entry.issue = @issue
473 time_entry.user = User.current
498 time_entry.user = User.current
474 time_entry.spent_on = User.current.today
499 time_entry.spent_on = User.current.today
475 time_entry.attributes = params[:time_entry]
500 time_entry.attributes = params[:time_entry]
476 @issue.time_entries << time_entry
501 @issue.time_entries << time_entry
477 end
502 end
478
503
479 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
504 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
480 if @issue.save
505 if @issue.save
481 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
506 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
482 else
507 else
483 raise ActiveRecord::Rollback
508 raise ActiveRecord::Rollback
484 end
509 end
485 end
510 end
486 end
511 end
487
512
488 def link_copy?(param)
513 def link_copy?(param)
489 case Setting.link_copied_issue
514 case Setting.link_copied_issue
490 when 'yes'
515 when 'yes'
491 true
516 true
492 when 'no'
517 when 'no'
493 false
518 false
494 when 'ask'
519 when 'ask'
495 param == '1'
520 param == '1'
496 end
521 end
497 end
522 end
498 end
523 end
@@ -1,1318 +1,1321
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28
28
29 extend Forwardable
29 extend Forwardable
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31
31
32 # Return true if user is authorized for controller/action, otherwise false
32 # Return true if user is authorized for controller/action, otherwise false
33 def authorize_for(controller, action)
33 def authorize_for(controller, action)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 end
35 end
36
36
37 # Display a link if user is authorized
37 # Display a link if user is authorized
38 #
38 #
39 # @param [String] name Anchor text (passed to link_to)
39 # @param [String] name Anchor text (passed to link_to)
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active? || (User.current.admin? && user.logged?)
51 if user.active? || (User.current.admin? && user.logged?)
52 link_to name, user_path(user), :class => user.css_classes
52 link_to name, user_path(user), :class => user.css_classes
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 #
69 #
70 def link_to_issue(issue, options={})
70 def link_to_issue(issue, options={})
71 title = nil
71 title = nil
72 subject = nil
72 subject = nil
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 if options[:subject] == false
74 if options[:subject] == false
75 title = issue.subject.truncate(60)
75 title = issue.subject.truncate(60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if truncate_length = options[:truncate]
78 if truncate_length = options[:truncate]
79 subject = subject.truncate(truncate_length)
79 subject = subject.truncate(truncate_length)
80 end
80 end
81 end
81 end
82 only_path = options[:only_path].nil? ? true : options[:only_path]
82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to(text, issue_url(issue, :only_path => only_path),
83 s = link_to(text, issue_url(issue, :only_path => only_path),
84 :class => issue.css_classes, :title => title)
84 :class => issue.css_classes, :title => title)
85 s << h(": #{subject}") if subject
85 s << h(": #{subject}") if subject
86 s = h("#{issue.project} - ") + s if options[:project]
86 s = h("#{issue.project} - ") + s if options[:project]
87 s
87 s
88 end
88 end
89
89
90 # Generates a link to an attachment.
90 # Generates a link to an attachment.
91 # Options:
91 # Options:
92 # * :text - Link text (default to attachment filename)
92 # * :text - Link text (default to attachment filename)
93 # * :download - Force download (default: false)
93 # * :download - Force download (default: false)
94 def link_to_attachment(attachment, options={})
94 def link_to_attachment(attachment, options={})
95 text = options.delete(:text) || attachment.filename
95 text = options.delete(:text) || attachment.filename
96 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
96 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
97 html_options = options.slice!(:only_path)
97 html_options = options.slice!(:only_path)
98 options[:only_path] = true unless options.key?(:only_path)
98 options[:only_path] = true unless options.key?(:only_path)
99 url = send(route_method, attachment, attachment.filename, options)
99 url = send(route_method, attachment, attachment.filename, options)
100 link_to text, url, html_options
100 link_to text, url, html_options
101 end
101 end
102
102
103 # Generates a link to a SCM revision
103 # Generates a link to a SCM revision
104 # Options:
104 # Options:
105 # * :text - Link text (default to the formatted revision)
105 # * :text - Link text (default to the formatted revision)
106 def link_to_revision(revision, repository, options={})
106 def link_to_revision(revision, repository, options={})
107 if repository.is_a?(Project)
107 if repository.is_a?(Project)
108 repository = repository.repository
108 repository = repository.repository
109 end
109 end
110 text = options.delete(:text) || format_revision(revision)
110 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 link_to(
112 link_to(
113 h(text),
113 h(text),
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 :title => l(:label_revision_id, format_revision(revision)),
115 :title => l(:label_revision_id, format_revision(revision)),
116 :accesskey => options[:accesskey]
116 :accesskey => options[:accesskey]
117 )
117 )
118 end
118 end
119
119
120 # Generates a link to a message
120 # Generates a link to a message
121 def link_to_message(message, options={}, html_options = nil)
121 def link_to_message(message, options={}, html_options = nil)
122 link_to(
122 link_to(
123 message.subject.truncate(60),
123 message.subject.truncate(60),
124 board_message_url(message.board_id, message.parent_id || message.id, {
124 board_message_url(message.board_id, message.parent_id || message.id, {
125 :r => (message.parent_id && message.id),
125 :r => (message.parent_id && message.id),
126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
127 :only_path => true
127 :only_path => true
128 }.merge(options)),
128 }.merge(options)),
129 html_options
129 html_options
130 )
130 )
131 end
131 end
132
132
133 # Generates a link to a project if active
133 # Generates a link to a project if active
134 # Examples:
134 # Examples:
135 #
135 #
136 # link_to_project(project) # => link to the specified project overview
136 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 #
139 #
140 def link_to_project(project, options={}, html_options = nil)
140 def link_to_project(project, options={}, html_options = nil)
141 if project.archived?
141 if project.archived?
142 h(project.name)
142 h(project.name)
143 else
143 else
144 link_to project.name,
144 link_to project.name,
145 project_url(project, {:only_path => true}.merge(options)),
145 project_url(project, {:only_path => true}.merge(options)),
146 html_options
146 html_options
147 end
147 end
148 end
148 end
149
149
150 # Generates a link to a project settings if active
150 # Generates a link to a project settings if active
151 def link_to_project_settings(project, options={}, html_options=nil)
151 def link_to_project_settings(project, options={}, html_options=nil)
152 if project.active?
152 if project.active?
153 link_to project.name, settings_project_path(project, options), html_options
153 link_to project.name, settings_project_path(project, options), html_options
154 elsif project.archived?
154 elsif project.archived?
155 h(project.name)
155 h(project.name)
156 else
156 else
157 link_to project.name, project_path(project, options), html_options
157 link_to project.name, project_path(project, options), html_options
158 end
158 end
159 end
159 end
160
160
161 # Generates a link to a version
161 # Generates a link to a version
162 def link_to_version(version, options = {})
162 def link_to_version(version, options = {})
163 return '' unless version && version.is_a?(Version)
163 return '' unless version && version.is_a?(Version)
164 options = {:title => format_date(version.effective_date)}.merge(options)
164 options = {:title => format_date(version.effective_date)}.merge(options)
165 link_to_if version.visible?, format_version_name(version), version_path(version), options
165 link_to_if version.visible?, format_version_name(version), version_path(version), options
166 end
166 end
167
167
168 # Helper that formats object for html or text rendering
168 # Helper that formats object for html or text rendering
169 def format_object(object, html=true, &block)
169 def format_object(object, html=true, &block)
170 if block_given?
170 if block_given?
171 object = yield object
171 object = yield object
172 end
172 end
173 case object.class.name
173 case object.class.name
174 when 'Array'
174 when 'Array'
175 object.map {|o| format_object(o, html)}.join(', ').html_safe
175 object.map {|o| format_object(o, html)}.join(', ').html_safe
176 when 'Time'
176 when 'Time'
177 format_time(object)
177 format_time(object)
178 when 'Date'
178 when 'Date'
179 format_date(object)
179 format_date(object)
180 when 'Fixnum'
180 when 'Fixnum'
181 object.to_s
181 object.to_s
182 when 'Float'
182 when 'Float'
183 sprintf "%.2f", object
183 sprintf "%.2f", object
184 when 'User'
184 when 'User'
185 html ? link_to_user(object) : object.to_s
185 html ? link_to_user(object) : object.to_s
186 when 'Project'
186 when 'Project'
187 html ? link_to_project(object) : object.to_s
187 html ? link_to_project(object) : object.to_s
188 when 'Version'
188 when 'Version'
189 html ? link_to_version(object) : object.to_s
189 html ? link_to_version(object) : object.to_s
190 when 'TrueClass'
190 when 'TrueClass'
191 l(:general_text_Yes)
191 l(:general_text_Yes)
192 when 'FalseClass'
192 when 'FalseClass'
193 l(:general_text_No)
193 l(:general_text_No)
194 when 'Issue'
194 when 'Issue'
195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
196 when 'CustomValue', 'CustomFieldValue'
196 when 'CustomValue', 'CustomFieldValue'
197 if object.custom_field
197 if object.custom_field
198 f = object.custom_field.format.formatted_custom_value(self, object, html)
198 f = object.custom_field.format.formatted_custom_value(self, object, html)
199 if f.nil? || f.is_a?(String)
199 if f.nil? || f.is_a?(String)
200 f
200 f
201 else
201 else
202 format_object(f, html, &block)
202 format_object(f, html, &block)
203 end
203 end
204 else
204 else
205 object.value.to_s
205 object.value.to_s
206 end
206 end
207 else
207 else
208 html ? h(object) : object.to_s
208 html ? h(object) : object.to_s
209 end
209 end
210 end
210 end
211
211
212 def wiki_page_path(page, options={})
212 def wiki_page_path(page, options={})
213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
214 end
214 end
215
215
216 def thumbnail_tag(attachment)
216 def thumbnail_tag(attachment)
217 link_to image_tag(thumbnail_path(attachment)),
217 link_to image_tag(thumbnail_path(attachment)),
218 named_attachment_path(attachment, attachment.filename),
218 named_attachment_path(attachment, attachment.filename),
219 :title => attachment.filename
219 :title => attachment.filename
220 end
220 end
221
221
222 def toggle_link(name, id, options={})
222 def toggle_link(name, id, options={})
223 onclick = "$('##{id}').toggle(); "
223 onclick = "$('##{id}').toggle(); "
224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
225 onclick << "return false;"
225 onclick << "return false;"
226 link_to(name, "#", :onclick => onclick)
226 link_to(name, "#", :onclick => onclick)
227 end
227 end
228
228
229 def format_activity_title(text)
229 def format_activity_title(text)
230 h(truncate_single_line_raw(text, 100))
230 h(truncate_single_line_raw(text, 100))
231 end
231 end
232
232
233 def format_activity_day(date)
233 def format_activity_day(date)
234 date == User.current.today ? l(:label_today).titleize : format_date(date)
234 date == User.current.today ? l(:label_today).titleize : format_date(date)
235 end
235 end
236
236
237 def format_activity_description(text)
237 def format_activity_description(text)
238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
239 ).gsub(/[\r\n]+/, "<br />").html_safe
239 ).gsub(/[\r\n]+/, "<br />").html_safe
240 end
240 end
241
241
242 def format_version_name(version)
242 def format_version_name(version)
243 if !version.shared? || version.project == @project
243 if !version.shared? || version.project == @project
244 h(version)
244 h(version)
245 else
245 else
246 h("#{version.project} - #{version}")
246 h("#{version.project} - #{version}")
247 end
247 end
248 end
248 end
249
249
250 def due_date_distance_in_words(date)
250 def due_date_distance_in_words(date)
251 if date
251 if date
252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
253 end
253 end
254 end
254 end
255
255
256 # Renders a tree of projects as a nested set of unordered lists
256 # Renders a tree of projects as a nested set of unordered lists
257 # The given collection may be a subset of the whole project tree
257 # The given collection may be a subset of the whole project tree
258 # (eg. some intermediate nodes are private and can not be seen)
258 # (eg. some intermediate nodes are private and can not be seen)
259 def render_project_nested_lists(projects, &block)
259 def render_project_nested_lists(projects, &block)
260 s = ''
260 s = ''
261 if projects.any?
261 if projects.any?
262 ancestors = []
262 ancestors = []
263 original_project = @project
263 original_project = @project
264 projects.sort_by(&:lft).each do |project|
264 projects.sort_by(&:lft).each do |project|
265 # set the project environment to please macros.
265 # set the project environment to please macros.
266 @project = project
266 @project = project
267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
269 else
269 else
270 ancestors.pop
270 ancestors.pop
271 s << "</li>"
271 s << "</li>"
272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 ancestors.pop
273 ancestors.pop
274 s << "</ul></li>\n"
274 s << "</ul></li>\n"
275 end
275 end
276 end
276 end
277 classes = (ancestors.empty? ? 'root' : 'child')
277 classes = (ancestors.empty? ? 'root' : 'child')
278 s << "<li class='#{classes}'><div class='#{classes}'>"
278 s << "<li class='#{classes}'><div class='#{classes}'>"
279 s << h(block_given? ? capture(project, &block) : project.name)
279 s << h(block_given? ? capture(project, &block) : project.name)
280 s << "</div>\n"
280 s << "</div>\n"
281 ancestors << project
281 ancestors << project
282 end
282 end
283 s << ("</li></ul>\n" * ancestors.size)
283 s << ("</li></ul>\n" * ancestors.size)
284 @project = original_project
284 @project = original_project
285 end
285 end
286 s.html_safe
286 s.html_safe
287 end
287 end
288
288
289 def render_page_hierarchy(pages, node=nil, options={})
289 def render_page_hierarchy(pages, node=nil, options={})
290 content = ''
290 content = ''
291 if pages[node]
291 if pages[node]
292 content << "<ul class=\"pages-hierarchy\">\n"
292 content << "<ul class=\"pages-hierarchy\">\n"
293 pages[node].each do |page|
293 pages[node].each do |page|
294 content << "<li>"
294 content << "<li>"
295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
298 content << "</li>\n"
298 content << "</li>\n"
299 end
299 end
300 content << "</ul>\n"
300 content << "</ul>\n"
301 end
301 end
302 content.html_safe
302 content.html_safe
303 end
303 end
304
304
305 # Renders flash messages
305 # Renders flash messages
306 def render_flash_messages
306 def render_flash_messages
307 s = ''
307 s = ''
308 flash.each do |k,v|
308 flash.each do |k,v|
309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
310 end
310 end
311 s.html_safe
311 s.html_safe
312 end
312 end
313
313
314 # Renders tabs and their content
314 # Renders tabs and their content
315 def render_tabs(tabs, selected=params[:tab])
315 def render_tabs(tabs, selected=params[:tab])
316 if tabs.any?
316 if tabs.any?
317 unless tabs.detect {|tab| tab[:name] == selected}
317 unless tabs.detect {|tab| tab[:name] == selected}
318 selected = nil
318 selected = nil
319 end
319 end
320 selected ||= tabs.first[:name]
320 selected ||= tabs.first[:name]
321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
322 else
322 else
323 content_tag 'p', l(:label_no_data), :class => "nodata"
323 content_tag 'p', l(:label_no_data), :class => "nodata"
324 end
324 end
325 end
325 end
326
326
327 # Renders the project quick-jump box
327 # Renders the project quick-jump box
328 def render_project_jump_box
328 def render_project_jump_box
329 return unless User.current.logged?
329 return unless User.current.logged?
330 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
330 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
331 if projects.any?
331 if projects.any?
332 options =
332 options =
333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
334 '<option value="" disabled="disabled">---</option>').html_safe
334 '<option value="" disabled="disabled">---</option>').html_safe
335
335
336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
337 { :value => project_path(:id => p, :jump => current_menu_item) }
337 { :value => project_path(:id => p, :jump => current_menu_item) }
338 end
338 end
339
339
340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
341 end
341 end
342 end
342 end
343
343
344 def project_tree_options_for_select(projects, options = {})
344 def project_tree_options_for_select(projects, options = {})
345 s = ''.html_safe
345 s = ''.html_safe
346 if options[:include_blank]
346 if blank_text = options[:include_blank]
347 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
347 if blank_text == true
348 blank_text = '&nbsp;'.html_safe
349 end
350 s << content_tag('option', blank_text, :value => '')
348 end
351 end
349 project_tree(projects) do |project, level|
352 project_tree(projects) do |project, level|
350 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
353 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
351 tag_options = {:value => project.id}
354 tag_options = {:value => project.id}
352 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
355 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
353 tag_options[:selected] = 'selected'
356 tag_options[:selected] = 'selected'
354 else
357 else
355 tag_options[:selected] = nil
358 tag_options[:selected] = nil
356 end
359 end
357 tag_options.merge!(yield(project)) if block_given?
360 tag_options.merge!(yield(project)) if block_given?
358 s << content_tag('option', name_prefix + h(project), tag_options)
361 s << content_tag('option', name_prefix + h(project), tag_options)
359 end
362 end
360 s.html_safe
363 s.html_safe
361 end
364 end
362
365
363 # Yields the given block for each project with its level in the tree
366 # Yields the given block for each project with its level in the tree
364 #
367 #
365 # Wrapper for Project#project_tree
368 # Wrapper for Project#project_tree
366 def project_tree(projects, &block)
369 def project_tree(projects, &block)
367 Project.project_tree(projects, &block)
370 Project.project_tree(projects, &block)
368 end
371 end
369
372
370 def principals_check_box_tags(name, principals)
373 def principals_check_box_tags(name, principals)
371 s = ''
374 s = ''
372 principals.each do |principal|
375 principals.each do |principal|
373 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
376 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
374 end
377 end
375 s.html_safe
378 s.html_safe
376 end
379 end
377
380
378 # Returns a string for users/groups option tags
381 # Returns a string for users/groups option tags
379 def principals_options_for_select(collection, selected=nil)
382 def principals_options_for_select(collection, selected=nil)
380 s = ''
383 s = ''
381 if collection.include?(User.current)
384 if collection.include?(User.current)
382 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
385 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
383 end
386 end
384 groups = ''
387 groups = ''
385 collection.sort.each do |element|
388 collection.sort.each do |element|
386 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
389 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
387 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
390 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
388 end
391 end
389 unless groups.empty?
392 unless groups.empty?
390 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
393 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
391 end
394 end
392 s.html_safe
395 s.html_safe
393 end
396 end
394
397
395 def option_tag(name, text, value, selected=nil, options={})
398 def option_tag(name, text, value, selected=nil, options={})
396 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
399 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
397 end
400 end
398
401
399 def truncate_single_line_raw(string, length)
402 def truncate_single_line_raw(string, length)
400 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
403 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
401 end
404 end
402
405
403 # Truncates at line break after 250 characters or options[:length]
406 # Truncates at line break after 250 characters or options[:length]
404 def truncate_lines(string, options={})
407 def truncate_lines(string, options={})
405 length = options[:length] || 250
408 length = options[:length] || 250
406 if string.to_s =~ /\A(.{#{length}}.*?)$/m
409 if string.to_s =~ /\A(.{#{length}}.*?)$/m
407 "#{$1}..."
410 "#{$1}..."
408 else
411 else
409 string
412 string
410 end
413 end
411 end
414 end
412
415
413 def anchor(text)
416 def anchor(text)
414 text.to_s.gsub(' ', '_')
417 text.to_s.gsub(' ', '_')
415 end
418 end
416
419
417 def html_hours(text)
420 def html_hours(text)
418 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
421 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
419 end
422 end
420
423
421 def authoring(created, author, options={})
424 def authoring(created, author, options={})
422 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
425 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
423 end
426 end
424
427
425 def time_tag(time)
428 def time_tag(time)
426 text = distance_of_time_in_words(Time.now, time)
429 text = distance_of_time_in_words(Time.now, time)
427 if @project
430 if @project
428 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
431 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
429 else
432 else
430 content_tag('abbr', text, :title => format_time(time))
433 content_tag('abbr', text, :title => format_time(time))
431 end
434 end
432 end
435 end
433
436
434 def syntax_highlight_lines(name, content)
437 def syntax_highlight_lines(name, content)
435 lines = []
438 lines = []
436 syntax_highlight(name, content).each_line { |line| lines << line }
439 syntax_highlight(name, content).each_line { |line| lines << line }
437 lines
440 lines
438 end
441 end
439
442
440 def syntax_highlight(name, content)
443 def syntax_highlight(name, content)
441 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
444 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
442 end
445 end
443
446
444 def to_path_param(path)
447 def to_path_param(path)
445 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
448 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
446 str.blank? ? nil : str
449 str.blank? ? nil : str
447 end
450 end
448
451
449 def reorder_links(name, url, method = :post)
452 def reorder_links(name, url, method = :post)
450 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
453 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
451 url.merge({"#{name}[move_to]" => 'highest'}),
454 url.merge({"#{name}[move_to]" => 'highest'}),
452 :method => method, :title => l(:label_sort_highest)) +
455 :method => method, :title => l(:label_sort_highest)) +
453 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
456 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
454 url.merge({"#{name}[move_to]" => 'higher'}),
457 url.merge({"#{name}[move_to]" => 'higher'}),
455 :method => method, :title => l(:label_sort_higher)) +
458 :method => method, :title => l(:label_sort_higher)) +
456 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
459 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
457 url.merge({"#{name}[move_to]" => 'lower'}),
460 url.merge({"#{name}[move_to]" => 'lower'}),
458 :method => method, :title => l(:label_sort_lower)) +
461 :method => method, :title => l(:label_sort_lower)) +
459 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
462 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
460 url.merge({"#{name}[move_to]" => 'lowest'}),
463 url.merge({"#{name}[move_to]" => 'lowest'}),
461 :method => method, :title => l(:label_sort_lowest))
464 :method => method, :title => l(:label_sort_lowest))
462 end
465 end
463
466
464 def breadcrumb(*args)
467 def breadcrumb(*args)
465 elements = args.flatten
468 elements = args.flatten
466 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
469 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
467 end
470 end
468
471
469 def other_formats_links(&block)
472 def other_formats_links(&block)
470 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
473 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
471 yield Redmine::Views::OtherFormatsBuilder.new(self)
474 yield Redmine::Views::OtherFormatsBuilder.new(self)
472 concat('</p>'.html_safe)
475 concat('</p>'.html_safe)
473 end
476 end
474
477
475 def page_header_title
478 def page_header_title
476 if @project.nil? || @project.new_record?
479 if @project.nil? || @project.new_record?
477 h(Setting.app_title)
480 h(Setting.app_title)
478 else
481 else
479 b = []
482 b = []
480 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
483 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
481 if ancestors.any?
484 if ancestors.any?
482 root = ancestors.shift
485 root = ancestors.shift
483 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
486 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
484 if ancestors.size > 2
487 if ancestors.size > 2
485 b << "\xe2\x80\xa6"
488 b << "\xe2\x80\xa6"
486 ancestors = ancestors[-2, 2]
489 ancestors = ancestors[-2, 2]
487 end
490 end
488 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
491 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
489 end
492 end
490 b << h(@project)
493 b << h(@project)
491 b.join(" \xc2\xbb ").html_safe
494 b.join(" \xc2\xbb ").html_safe
492 end
495 end
493 end
496 end
494
497
495 # Returns a h2 tag and sets the html title with the given arguments
498 # Returns a h2 tag and sets the html title with the given arguments
496 def title(*args)
499 def title(*args)
497 strings = args.map do |arg|
500 strings = args.map do |arg|
498 if arg.is_a?(Array) && arg.size >= 2
501 if arg.is_a?(Array) && arg.size >= 2
499 link_to(*arg)
502 link_to(*arg)
500 else
503 else
501 h(arg.to_s)
504 h(arg.to_s)
502 end
505 end
503 end
506 end
504 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
507 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
505 content_tag('h2', strings.join(' &#187; ').html_safe)
508 content_tag('h2', strings.join(' &#187; ').html_safe)
506 end
509 end
507
510
508 # Sets the html title
511 # Sets the html title
509 # Returns the html title when called without arguments
512 # Returns the html title when called without arguments
510 # Current project name and app_title and automatically appended
513 # Current project name and app_title and automatically appended
511 # Exemples:
514 # Exemples:
512 # html_title 'Foo', 'Bar'
515 # html_title 'Foo', 'Bar'
513 # html_title # => 'Foo - Bar - My Project - Redmine'
516 # html_title # => 'Foo - Bar - My Project - Redmine'
514 def html_title(*args)
517 def html_title(*args)
515 if args.empty?
518 if args.empty?
516 title = @html_title || []
519 title = @html_title || []
517 title << @project.name if @project
520 title << @project.name if @project
518 title << Setting.app_title unless Setting.app_title == title.last
521 title << Setting.app_title unless Setting.app_title == title.last
519 title.reject(&:blank?).join(' - ')
522 title.reject(&:blank?).join(' - ')
520 else
523 else
521 @html_title ||= []
524 @html_title ||= []
522 @html_title += args
525 @html_title += args
523 end
526 end
524 end
527 end
525
528
526 # Returns the theme, controller name, and action as css classes for the
529 # Returns the theme, controller name, and action as css classes for the
527 # HTML body.
530 # HTML body.
528 def body_css_classes
531 def body_css_classes
529 css = []
532 css = []
530 if theme = Redmine::Themes.theme(Setting.ui_theme)
533 if theme = Redmine::Themes.theme(Setting.ui_theme)
531 css << 'theme-' + theme.name
534 css << 'theme-' + theme.name
532 end
535 end
533
536
534 css << 'project-' + @project.identifier if @project && @project.identifier.present?
537 css << 'project-' + @project.identifier if @project && @project.identifier.present?
535 css << 'controller-' + controller_name
538 css << 'controller-' + controller_name
536 css << 'action-' + action_name
539 css << 'action-' + action_name
537 css.join(' ')
540 css.join(' ')
538 end
541 end
539
542
540 def accesskey(s)
543 def accesskey(s)
541 @used_accesskeys ||= []
544 @used_accesskeys ||= []
542 key = Redmine::AccessKeys.key_for(s)
545 key = Redmine::AccessKeys.key_for(s)
543 return nil if @used_accesskeys.include?(key)
546 return nil if @used_accesskeys.include?(key)
544 @used_accesskeys << key
547 @used_accesskeys << key
545 key
548 key
546 end
549 end
547
550
548 # Formats text according to system settings.
551 # Formats text according to system settings.
549 # 2 ways to call this method:
552 # 2 ways to call this method:
550 # * with a String: textilizable(text, options)
553 # * with a String: textilizable(text, options)
551 # * with an object and one of its attribute: textilizable(issue, :description, options)
554 # * with an object and one of its attribute: textilizable(issue, :description, options)
552 def textilizable(*args)
555 def textilizable(*args)
553 options = args.last.is_a?(Hash) ? args.pop : {}
556 options = args.last.is_a?(Hash) ? args.pop : {}
554 case args.size
557 case args.size
555 when 1
558 when 1
556 obj = options[:object]
559 obj = options[:object]
557 text = args.shift
560 text = args.shift
558 when 2
561 when 2
559 obj = args.shift
562 obj = args.shift
560 attr = args.shift
563 attr = args.shift
561 text = obj.send(attr).to_s
564 text = obj.send(attr).to_s
562 else
565 else
563 raise ArgumentError, 'invalid arguments to textilizable'
566 raise ArgumentError, 'invalid arguments to textilizable'
564 end
567 end
565 return '' if text.blank?
568 return '' if text.blank?
566 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
569 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
567 @only_path = only_path = options.delete(:only_path) == false ? false : true
570 @only_path = only_path = options.delete(:only_path) == false ? false : true
568
571
569 text = text.dup
572 text = text.dup
570 macros = catch_macros(text)
573 macros = catch_macros(text)
571 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
574 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
572
575
573 @parsed_headings = []
576 @parsed_headings = []
574 @heading_anchors = {}
577 @heading_anchors = {}
575 @current_section = 0 if options[:edit_section_links]
578 @current_section = 0 if options[:edit_section_links]
576
579
577 parse_sections(text, project, obj, attr, only_path, options)
580 parse_sections(text, project, obj, attr, only_path, options)
578 text = parse_non_pre_blocks(text, obj, macros) do |text|
581 text = parse_non_pre_blocks(text, obj, macros) do |text|
579 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
582 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
580 send method_name, text, project, obj, attr, only_path, options
583 send method_name, text, project, obj, attr, only_path, options
581 end
584 end
582 end
585 end
583 parse_headings(text, project, obj, attr, only_path, options)
586 parse_headings(text, project, obj, attr, only_path, options)
584
587
585 if @parsed_headings.any?
588 if @parsed_headings.any?
586 replace_toc(text, @parsed_headings)
589 replace_toc(text, @parsed_headings)
587 end
590 end
588
591
589 text.html_safe
592 text.html_safe
590 end
593 end
591
594
592 def parse_non_pre_blocks(text, obj, macros)
595 def parse_non_pre_blocks(text, obj, macros)
593 s = StringScanner.new(text)
596 s = StringScanner.new(text)
594 tags = []
597 tags = []
595 parsed = ''
598 parsed = ''
596 while !s.eos?
599 while !s.eos?
597 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
600 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
598 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
601 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
599 if tags.empty?
602 if tags.empty?
600 yield text
603 yield text
601 inject_macros(text, obj, macros) if macros.any?
604 inject_macros(text, obj, macros) if macros.any?
602 else
605 else
603 inject_macros(text, obj, macros, false) if macros.any?
606 inject_macros(text, obj, macros, false) if macros.any?
604 end
607 end
605 parsed << text
608 parsed << text
606 if tag
609 if tag
607 if closing
610 if closing
608 if tags.last == tag.downcase
611 if tags.last == tag.downcase
609 tags.pop
612 tags.pop
610 end
613 end
611 else
614 else
612 tags << tag.downcase
615 tags << tag.downcase
613 end
616 end
614 parsed << full_tag
617 parsed << full_tag
615 end
618 end
616 end
619 end
617 # Close any non closing tags
620 # Close any non closing tags
618 while tag = tags.pop
621 while tag = tags.pop
619 parsed << "</#{tag}>"
622 parsed << "</#{tag}>"
620 end
623 end
621 parsed
624 parsed
622 end
625 end
623
626
624 def parse_inline_attachments(text, project, obj, attr, only_path, options)
627 def parse_inline_attachments(text, project, obj, attr, only_path, options)
625 return if options[:inline_attachments] == false
628 return if options[:inline_attachments] == false
626
629
627 # when using an image link, try to use an attachment, if possible
630 # when using an image link, try to use an attachment, if possible
628 attachments = options[:attachments] || []
631 attachments = options[:attachments] || []
629 attachments += obj.attachments if obj.respond_to?(:attachments)
632 attachments += obj.attachments if obj.respond_to?(:attachments)
630 if attachments.present?
633 if attachments.present?
631 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
634 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
632 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
635 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
633 # search for the picture in attachments
636 # search for the picture in attachments
634 if found = Attachment.latest_attach(attachments, filename)
637 if found = Attachment.latest_attach(attachments, filename)
635 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
638 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
636 desc = found.description.to_s.gsub('"', '')
639 desc = found.description.to_s.gsub('"', '')
637 if !desc.blank? && alttext.blank?
640 if !desc.blank? && alttext.blank?
638 alt = " title=\"#{desc}\" alt=\"#{desc}\""
641 alt = " title=\"#{desc}\" alt=\"#{desc}\""
639 end
642 end
640 "src=\"#{image_url}\"#{alt}"
643 "src=\"#{image_url}\"#{alt}"
641 else
644 else
642 m
645 m
643 end
646 end
644 end
647 end
645 end
648 end
646 end
649 end
647
650
648 # Wiki links
651 # Wiki links
649 #
652 #
650 # Examples:
653 # Examples:
651 # [[mypage]]
654 # [[mypage]]
652 # [[mypage|mytext]]
655 # [[mypage|mytext]]
653 # wiki links can refer other project wikis, using project name or identifier:
656 # wiki links can refer other project wikis, using project name or identifier:
654 # [[project:]] -> wiki starting page
657 # [[project:]] -> wiki starting page
655 # [[project:|mytext]]
658 # [[project:|mytext]]
656 # [[project:mypage]]
659 # [[project:mypage]]
657 # [[project:mypage|mytext]]
660 # [[project:mypage|mytext]]
658 def parse_wiki_links(text, project, obj, attr, only_path, options)
661 def parse_wiki_links(text, project, obj, attr, only_path, options)
659 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
662 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
660 link_project = project
663 link_project = project
661 esc, all, page, title = $1, $2, $3, $5
664 esc, all, page, title = $1, $2, $3, $5
662 if esc.nil?
665 if esc.nil?
663 if page =~ /^([^\:]+)\:(.*)$/
666 if page =~ /^([^\:]+)\:(.*)$/
664 identifier, page = $1, $2
667 identifier, page = $1, $2
665 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
668 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
666 title ||= identifier if page.blank?
669 title ||= identifier if page.blank?
667 end
670 end
668
671
669 if link_project && link_project.wiki
672 if link_project && link_project.wiki
670 # extract anchor
673 # extract anchor
671 anchor = nil
674 anchor = nil
672 if page =~ /^(.+?)\#(.+)$/
675 if page =~ /^(.+?)\#(.+)$/
673 page, anchor = $1, $2
676 page, anchor = $1, $2
674 end
677 end
675 anchor = sanitize_anchor_name(anchor) if anchor.present?
678 anchor = sanitize_anchor_name(anchor) if anchor.present?
676 # check if page exists
679 # check if page exists
677 wiki_page = link_project.wiki.find_page(page)
680 wiki_page = link_project.wiki.find_page(page)
678 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
681 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
679 "##{anchor}"
682 "##{anchor}"
680 else
683 else
681 case options[:wiki_links]
684 case options[:wiki_links]
682 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
685 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
683 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
686 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
684 else
687 else
685 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
688 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
686 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
689 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
687 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
690 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
688 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
691 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
689 end
692 end
690 end
693 end
691 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
694 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
692 else
695 else
693 # project or wiki doesn't exist
696 # project or wiki doesn't exist
694 all
697 all
695 end
698 end
696 else
699 else
697 all
700 all
698 end
701 end
699 end
702 end
700 end
703 end
701
704
702 # Redmine links
705 # Redmine links
703 #
706 #
704 # Examples:
707 # Examples:
705 # Issues:
708 # Issues:
706 # #52 -> Link to issue #52
709 # #52 -> Link to issue #52
707 # Changesets:
710 # Changesets:
708 # r52 -> Link to revision 52
711 # r52 -> Link to revision 52
709 # commit:a85130f -> Link to scmid starting with a85130f
712 # commit:a85130f -> Link to scmid starting with a85130f
710 # Documents:
713 # Documents:
711 # document#17 -> Link to document with id 17
714 # document#17 -> Link to document with id 17
712 # document:Greetings -> Link to the document with title "Greetings"
715 # document:Greetings -> Link to the document with title "Greetings"
713 # document:"Some document" -> Link to the document with title "Some document"
716 # document:"Some document" -> Link to the document with title "Some document"
714 # Versions:
717 # Versions:
715 # version#3 -> Link to version with id 3
718 # version#3 -> Link to version with id 3
716 # version:1.0.0 -> Link to version named "1.0.0"
719 # version:1.0.0 -> Link to version named "1.0.0"
717 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
720 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
718 # Attachments:
721 # Attachments:
719 # attachment:file.zip -> Link to the attachment of the current object named file.zip
722 # attachment:file.zip -> Link to the attachment of the current object named file.zip
720 # Source files:
723 # Source files:
721 # source:some/file -> Link to the file located at /some/file in the project's repository
724 # source:some/file -> Link to the file located at /some/file in the project's repository
722 # source:some/file@52 -> Link to the file's revision 52
725 # source:some/file@52 -> Link to the file's revision 52
723 # source:some/file#L120 -> Link to line 120 of the file
726 # source:some/file#L120 -> Link to line 120 of the file
724 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
727 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
725 # export:some/file -> Force the download of the file
728 # export:some/file -> Force the download of the file
726 # Forum messages:
729 # Forum messages:
727 # message#1218 -> Link to message with id 1218
730 # message#1218 -> Link to message with id 1218
728 # Projects:
731 # Projects:
729 # project:someproject -> Link to project named "someproject"
732 # project:someproject -> Link to project named "someproject"
730 # project#3 -> Link to project with id 3
733 # project#3 -> Link to project with id 3
731 #
734 #
732 # Links can refer other objects from other projects, using project identifier:
735 # Links can refer other objects from other projects, using project identifier:
733 # identifier:r52
736 # identifier:r52
734 # identifier:document:"Some document"
737 # identifier:document:"Some document"
735 # identifier:version:1.0.0
738 # identifier:version:1.0.0
736 # identifier:source:some/file
739 # identifier:source:some/file
737 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
740 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
738 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|
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 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
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 if tag_content
743 if tag_content
741 $&
744 $&
742 else
745 else
743 link = nil
746 link = nil
744 project = default_project
747 project = default_project
745 if project_identifier
748 if project_identifier
746 project = Project.visible.find_by_identifier(project_identifier)
749 project = Project.visible.find_by_identifier(project_identifier)
747 end
750 end
748 if esc.nil?
751 if esc.nil?
749 if prefix.nil? && sep == 'r'
752 if prefix.nil? && sep == 'r'
750 if project
753 if project
751 repository = nil
754 repository = nil
752 if repo_identifier
755 if repo_identifier
753 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
756 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
754 else
757 else
755 repository = project.repository
758 repository = project.repository
756 end
759 end
757 # project.changesets.visible raises an SQL error because of a double join on repositories
760 # project.changesets.visible raises an SQL error because of a double join on repositories
758 if repository &&
761 if repository &&
759 (changeset = Changeset.visible.
762 (changeset = Changeset.visible.
760 find_by_repository_id_and_revision(repository.id, identifier))
763 find_by_repository_id_and_revision(repository.id, identifier))
761 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
764 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
762 {:only_path => only_path, :controller => 'repositories',
765 {:only_path => only_path, :controller => 'repositories',
763 :action => 'revision', :id => project,
766 :action => 'revision', :id => project,
764 :repository_id => repository.identifier_param,
767 :repository_id => repository.identifier_param,
765 :rev => changeset.revision},
768 :rev => changeset.revision},
766 :class => 'changeset',
769 :class => 'changeset',
767 :title => truncate_single_line_raw(changeset.comments, 100))
770 :title => truncate_single_line_raw(changeset.comments, 100))
768 end
771 end
769 end
772 end
770 elsif sep == '#'
773 elsif sep == '#'
771 oid = identifier.to_i
774 oid = identifier.to_i
772 case prefix
775 case prefix
773 when nil
776 when nil
774 if oid.to_s == identifier &&
777 if oid.to_s == identifier &&
775 issue = Issue.visible.find_by_id(oid)
778 issue = Issue.visible.find_by_id(oid)
776 anchor = comment_id ? "note-#{comment_id}" : nil
779 anchor = comment_id ? "note-#{comment_id}" : nil
777 link = link_to("##{oid}#{comment_suffix}",
780 link = link_to("##{oid}#{comment_suffix}",
778 issue_url(issue, :only_path => only_path, :anchor => anchor),
781 issue_url(issue, :only_path => only_path, :anchor => anchor),
779 :class => issue.css_classes,
782 :class => issue.css_classes,
780 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
783 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
781 end
784 end
782 when 'document'
785 when 'document'
783 if document = Document.visible.find_by_id(oid)
786 if document = Document.visible.find_by_id(oid)
784 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
787 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
785 end
788 end
786 when 'version'
789 when 'version'
787 if version = Version.visible.find_by_id(oid)
790 if version = Version.visible.find_by_id(oid)
788 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
791 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
789 end
792 end
790 when 'message'
793 when 'message'
791 if message = Message.visible.find_by_id(oid)
794 if message = Message.visible.find_by_id(oid)
792 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
795 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
793 end
796 end
794 when 'forum'
797 when 'forum'
795 if board = Board.visible.find_by_id(oid)
798 if board = Board.visible.find_by_id(oid)
796 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
799 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
797 end
800 end
798 when 'news'
801 when 'news'
799 if news = News.visible.find_by_id(oid)
802 if news = News.visible.find_by_id(oid)
800 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
803 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
801 end
804 end
802 when 'project'
805 when 'project'
803 if p = Project.visible.find_by_id(oid)
806 if p = Project.visible.find_by_id(oid)
804 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
807 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
805 end
808 end
806 end
809 end
807 elsif sep == ':'
810 elsif sep == ':'
808 # removes the double quotes if any
811 # removes the double quotes if any
809 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
812 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
810 name = CGI.unescapeHTML(name)
813 name = CGI.unescapeHTML(name)
811 case prefix
814 case prefix
812 when 'document'
815 when 'document'
813 if project && document = project.documents.visible.find_by_title(name)
816 if project && document = project.documents.visible.find_by_title(name)
814 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
815 end
818 end
816 when 'version'
819 when 'version'
817 if project && version = project.versions.visible.find_by_name(name)
820 if project && version = project.versions.visible.find_by_name(name)
818 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
819 end
822 end
820 when 'forum'
823 when 'forum'
821 if project && board = project.boards.visible.find_by_name(name)
824 if project && board = project.boards.visible.find_by_name(name)
822 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
825 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
823 end
826 end
824 when 'news'
827 when 'news'
825 if project && news = project.news.visible.find_by_title(name)
828 if project && news = project.news.visible.find_by_title(name)
826 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
829 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
827 end
830 end
828 when 'commit', 'source', 'export'
831 when 'commit', 'source', 'export'
829 if project
832 if project
830 repository = nil
833 repository = nil
831 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
834 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
832 repo_prefix, repo_identifier, name = $1, $2, $3
835 repo_prefix, repo_identifier, name = $1, $2, $3
833 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
836 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
834 else
837 else
835 repository = project.repository
838 repository = project.repository
836 end
839 end
837 if prefix == 'commit'
840 if prefix == 'commit'
838 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
841 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
839 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},
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 :class => 'changeset',
843 :class => 'changeset',
841 :title => truncate_single_line_raw(changeset.comments, 100)
844 :title => truncate_single_line_raw(changeset.comments, 100)
842 end
845 end
843 else
846 else
844 if repository && User.current.allowed_to?(:browse_repository, project)
847 if repository && User.current.allowed_to?(:browse_repository, project)
845 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
848 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
846 path, rev, anchor = $1, $3, $5
849 path, rev, anchor = $1, $3, $5
847 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,
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 :path => to_path_param(path),
851 :path => to_path_param(path),
849 :rev => rev,
852 :rev => rev,
850 :anchor => anchor},
853 :anchor => anchor},
851 :class => (prefix == 'export' ? 'source download' : 'source')
854 :class => (prefix == 'export' ? 'source download' : 'source')
852 end
855 end
853 end
856 end
854 repo_prefix = nil
857 repo_prefix = nil
855 end
858 end
856 when 'attachment'
859 when 'attachment'
857 attachments = options[:attachments] || []
860 attachments = options[:attachments] || []
858 attachments += obj.attachments if obj.respond_to?(:attachments)
861 attachments += obj.attachments if obj.respond_to?(:attachments)
859 if attachments && attachment = Attachment.latest_attach(attachments, name)
862 if attachments && attachment = Attachment.latest_attach(attachments, name)
860 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
863 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
861 end
864 end
862 when 'project'
865 when 'project'
863 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
866 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
864 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
867 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
865 end
868 end
866 end
869 end
867 end
870 end
868 end
871 end
869 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
872 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
870 end
873 end
871 end
874 end
872 end
875 end
873
876
874 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
877 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
875
878
876 def parse_sections(text, project, obj, attr, only_path, options)
879 def parse_sections(text, project, obj, attr, only_path, options)
877 return unless options[:edit_section_links]
880 return unless options[:edit_section_links]
878 text.gsub!(HEADING_RE) do
881 text.gsub!(HEADING_RE) do
879 heading = $1
882 heading = $1
880 @current_section += 1
883 @current_section += 1
881 if @current_section > 1
884 if @current_section > 1
882 content_tag('div',
885 content_tag('div',
883 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
886 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
884 :class => 'contextual',
887 :class => 'contextual',
885 :title => l(:button_edit_section),
888 :title => l(:button_edit_section),
886 :id => "section-#{@current_section}") + heading.html_safe
889 :id => "section-#{@current_section}") + heading.html_safe
887 else
890 else
888 heading
891 heading
889 end
892 end
890 end
893 end
891 end
894 end
892
895
893 # Headings and TOC
896 # Headings and TOC
894 # Adds ids and links to headings unless options[:headings] is set to false
897 # Adds ids and links to headings unless options[:headings] is set to false
895 def parse_headings(text, project, obj, attr, only_path, options)
898 def parse_headings(text, project, obj, attr, only_path, options)
896 return if options[:headings] == false
899 return if options[:headings] == false
897
900
898 text.gsub!(HEADING_RE) do
901 text.gsub!(HEADING_RE) do
899 level, attrs, content = $2.to_i, $3, $4
902 level, attrs, content = $2.to_i, $3, $4
900 item = strip_tags(content).strip
903 item = strip_tags(content).strip
901 anchor = sanitize_anchor_name(item)
904 anchor = sanitize_anchor_name(item)
902 # used for single-file wiki export
905 # used for single-file wiki export
903 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
906 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
904 @heading_anchors[anchor] ||= 0
907 @heading_anchors[anchor] ||= 0
905 idx = (@heading_anchors[anchor] += 1)
908 idx = (@heading_anchors[anchor] += 1)
906 if idx > 1
909 if idx > 1
907 anchor = "#{anchor}-#{idx}"
910 anchor = "#{anchor}-#{idx}"
908 end
911 end
909 @parsed_headings << [level, anchor, item]
912 @parsed_headings << [level, anchor, item]
910 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
913 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
911 end
914 end
912 end
915 end
913
916
914 MACROS_RE = /(
917 MACROS_RE = /(
915 (!)? # escaping
918 (!)? # escaping
916 (
919 (
917 \{\{ # opening tag
920 \{\{ # opening tag
918 ([\w]+) # macro name
921 ([\w]+) # macro name
919 (\(([^\n\r]*?)\))? # optional arguments
922 (\(([^\n\r]*?)\))? # optional arguments
920 ([\n\r].*?[\n\r])? # optional block of text
923 ([\n\r].*?[\n\r])? # optional block of text
921 \}\} # closing tag
924 \}\} # closing tag
922 )
925 )
923 )/mx unless const_defined?(:MACROS_RE)
926 )/mx unless const_defined?(:MACROS_RE)
924
927
925 MACRO_SUB_RE = /(
928 MACRO_SUB_RE = /(
926 \{\{
929 \{\{
927 macro\((\d+)\)
930 macro\((\d+)\)
928 \}\}
931 \}\}
929 )/x unless const_defined?(:MACRO_SUB_RE)
932 )/x unless const_defined?(:MACRO_SUB_RE)
930
933
931 # Extracts macros from text
934 # Extracts macros from text
932 def catch_macros(text)
935 def catch_macros(text)
933 macros = {}
936 macros = {}
934 text.gsub!(MACROS_RE) do
937 text.gsub!(MACROS_RE) do
935 all, macro = $1, $4.downcase
938 all, macro = $1, $4.downcase
936 if macro_exists?(macro) || all =~ MACRO_SUB_RE
939 if macro_exists?(macro) || all =~ MACRO_SUB_RE
937 index = macros.size
940 index = macros.size
938 macros[index] = all
941 macros[index] = all
939 "{{macro(#{index})}}"
942 "{{macro(#{index})}}"
940 else
943 else
941 all
944 all
942 end
945 end
943 end
946 end
944 macros
947 macros
945 end
948 end
946
949
947 # Executes and replaces macros in text
950 # Executes and replaces macros in text
948 def inject_macros(text, obj, macros, execute=true)
951 def inject_macros(text, obj, macros, execute=true)
949 text.gsub!(MACRO_SUB_RE) do
952 text.gsub!(MACRO_SUB_RE) do
950 all, index = $1, $2.to_i
953 all, index = $1, $2.to_i
951 orig = macros.delete(index)
954 orig = macros.delete(index)
952 if execute && orig && orig =~ MACROS_RE
955 if execute && orig && orig =~ MACROS_RE
953 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
956 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
954 if esc.nil?
957 if esc.nil?
955 h(exec_macro(macro, obj, args, block) || all)
958 h(exec_macro(macro, obj, args, block) || all)
956 else
959 else
957 h(all)
960 h(all)
958 end
961 end
959 elsif orig
962 elsif orig
960 h(orig)
963 h(orig)
961 else
964 else
962 h(all)
965 h(all)
963 end
966 end
964 end
967 end
965 end
968 end
966
969
967 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
970 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
968
971
969 # Renders the TOC with given headings
972 # Renders the TOC with given headings
970 def replace_toc(text, headings)
973 def replace_toc(text, headings)
971 text.gsub!(TOC_RE) do
974 text.gsub!(TOC_RE) do
972 left_align, right_align = $2, $3
975 left_align, right_align = $2, $3
973 # Keep only the 4 first levels
976 # Keep only the 4 first levels
974 headings = headings.select{|level, anchor, item| level <= 4}
977 headings = headings.select{|level, anchor, item| level <= 4}
975 if headings.empty?
978 if headings.empty?
976 ''
979 ''
977 else
980 else
978 div_class = 'toc'
981 div_class = 'toc'
979 div_class << ' right' if right_align
982 div_class << ' right' if right_align
980 div_class << ' left' if left_align
983 div_class << ' left' if left_align
981 out = "<ul class=\"#{div_class}\"><li>"
984 out = "<ul class=\"#{div_class}\"><li>"
982 root = headings.map(&:first).min
985 root = headings.map(&:first).min
983 current = root
986 current = root
984 started = false
987 started = false
985 headings.each do |level, anchor, item|
988 headings.each do |level, anchor, item|
986 if level > current
989 if level > current
987 out << '<ul><li>' * (level - current)
990 out << '<ul><li>' * (level - current)
988 elsif level < current
991 elsif level < current
989 out << "</li></ul>\n" * (current - level) + "</li><li>"
992 out << "</li></ul>\n" * (current - level) + "</li><li>"
990 elsif started
993 elsif started
991 out << '</li><li>'
994 out << '</li><li>'
992 end
995 end
993 out << "<a href=\"##{anchor}\">#{item}</a>"
996 out << "<a href=\"##{anchor}\">#{item}</a>"
994 current = level
997 current = level
995 started = true
998 started = true
996 end
999 end
997 out << '</li></ul>' * (current - root)
1000 out << '</li></ul>' * (current - root)
998 out << '</li></ul>'
1001 out << '</li></ul>'
999 end
1002 end
1000 end
1003 end
1001 end
1004 end
1002
1005
1003 # Same as Rails' simple_format helper without using paragraphs
1006 # Same as Rails' simple_format helper without using paragraphs
1004 def simple_format_without_paragraph(text)
1007 def simple_format_without_paragraph(text)
1005 text.to_s.
1008 text.to_s.
1006 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1009 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1007 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1010 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1008 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1011 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1009 html_safe
1012 html_safe
1010 end
1013 end
1011
1014
1012 def lang_options_for_select(blank=true)
1015 def lang_options_for_select(blank=true)
1013 (blank ? [["(auto)", ""]] : []) + languages_options
1016 (blank ? [["(auto)", ""]] : []) + languages_options
1014 end
1017 end
1015
1018
1016 def labelled_form_for(*args, &proc)
1019 def labelled_form_for(*args, &proc)
1017 args << {} unless args.last.is_a?(Hash)
1020 args << {} unless args.last.is_a?(Hash)
1018 options = args.last
1021 options = args.last
1019 if args.first.is_a?(Symbol)
1022 if args.first.is_a?(Symbol)
1020 options.merge!(:as => args.shift)
1023 options.merge!(:as => args.shift)
1021 end
1024 end
1022 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1025 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1023 form_for(*args, &proc)
1026 form_for(*args, &proc)
1024 end
1027 end
1025
1028
1026 def labelled_fields_for(*args, &proc)
1029 def labelled_fields_for(*args, &proc)
1027 args << {} unless args.last.is_a?(Hash)
1030 args << {} unless args.last.is_a?(Hash)
1028 options = args.last
1031 options = args.last
1029 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1032 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1030 fields_for(*args, &proc)
1033 fields_for(*args, &proc)
1031 end
1034 end
1032
1035
1033 def error_messages_for(*objects)
1036 def error_messages_for(*objects)
1034 html = ""
1037 html = ""
1035 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1038 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1036 errors = objects.map {|o| o.errors.full_messages}.flatten
1039 errors = objects.map {|o| o.errors.full_messages}.flatten
1037 if errors.any?
1040 if errors.any?
1038 html << "<div id='errorExplanation'><ul>\n"
1041 html << "<div id='errorExplanation'><ul>\n"
1039 errors.each do |error|
1042 errors.each do |error|
1040 html << "<li>#{h error}</li>\n"
1043 html << "<li>#{h error}</li>\n"
1041 end
1044 end
1042 html << "</ul></div>\n"
1045 html << "</ul></div>\n"
1043 end
1046 end
1044 html.html_safe
1047 html.html_safe
1045 end
1048 end
1046
1049
1047 def delete_link(url, options={})
1050 def delete_link(url, options={})
1048 options = {
1051 options = {
1049 :method => :delete,
1052 :method => :delete,
1050 :data => {:confirm => l(:text_are_you_sure)},
1053 :data => {:confirm => l(:text_are_you_sure)},
1051 :class => 'icon icon-del'
1054 :class => 'icon icon-del'
1052 }.merge(options)
1055 }.merge(options)
1053
1056
1054 link_to l(:button_delete), url, options
1057 link_to l(:button_delete), url, options
1055 end
1058 end
1056
1059
1057 def preview_link(url, form, target='preview', options={})
1060 def preview_link(url, form, target='preview', options={})
1058 content_tag 'a', l(:label_preview), {
1061 content_tag 'a', l(:label_preview), {
1059 :href => "#",
1062 :href => "#",
1060 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1063 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1061 :accesskey => accesskey(:preview)
1064 :accesskey => accesskey(:preview)
1062 }.merge(options)
1065 }.merge(options)
1063 end
1066 end
1064
1067
1065 def link_to_function(name, function, html_options={})
1068 def link_to_function(name, function, html_options={})
1066 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1069 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1067 end
1070 end
1068
1071
1069 # Helper to render JSON in views
1072 # Helper to render JSON in views
1070 def raw_json(arg)
1073 def raw_json(arg)
1071 arg.to_json.to_s.gsub('/', '\/').html_safe
1074 arg.to_json.to_s.gsub('/', '\/').html_safe
1072 end
1075 end
1073
1076
1074 def back_url
1077 def back_url
1075 url = params[:back_url]
1078 url = params[:back_url]
1076 if url.nil? && referer = request.env['HTTP_REFERER']
1079 if url.nil? && referer = request.env['HTTP_REFERER']
1077 url = CGI.unescape(referer.to_s)
1080 url = CGI.unescape(referer.to_s)
1078 end
1081 end
1079 url
1082 url
1080 end
1083 end
1081
1084
1082 def back_url_hidden_field_tag
1085 def back_url_hidden_field_tag
1083 url = back_url
1086 url = back_url
1084 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1087 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1085 end
1088 end
1086
1089
1087 def check_all_links(form_name)
1090 def check_all_links(form_name)
1088 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1091 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1089 " | ".html_safe +
1092 " | ".html_safe +
1090 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1093 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1091 end
1094 end
1092
1095
1093 def toggle_checkboxes_link(selector)
1096 def toggle_checkboxes_link(selector)
1094 link_to_function image_tag('toggle_check.png'),
1097 link_to_function image_tag('toggle_check.png'),
1095 "toggleCheckboxesBySelector('#{selector}')",
1098 "toggleCheckboxesBySelector('#{selector}')",
1096 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1099 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1097 end
1100 end
1098
1101
1099 def progress_bar(pcts, options={})
1102 def progress_bar(pcts, options={})
1100 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1103 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1101 pcts = pcts.collect(&:round)
1104 pcts = pcts.collect(&:round)
1102 pcts[1] = pcts[1] - pcts[0]
1105 pcts[1] = pcts[1] - pcts[0]
1103 pcts << (100 - pcts[1] - pcts[0])
1106 pcts << (100 - pcts[1] - pcts[0])
1104 width = options[:width] || '100px;'
1107 width = options[:width] || '100px;'
1105 legend = options[:legend] || ''
1108 legend = options[:legend] || ''
1106 content_tag('table',
1109 content_tag('table',
1107 content_tag('tr',
1110 content_tag('tr',
1108 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1111 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1109 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1112 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1110 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1113 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1111 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1114 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1112 content_tag('p', legend, :class => 'percent').html_safe
1115 content_tag('p', legend, :class => 'percent').html_safe
1113 end
1116 end
1114
1117
1115 def checked_image(checked=true)
1118 def checked_image(checked=true)
1116 if checked
1119 if checked
1117 image_tag 'toggle_check.png'
1120 image_tag 'toggle_check.png'
1118 end
1121 end
1119 end
1122 end
1120
1123
1121 def context_menu(url)
1124 def context_menu(url)
1122 unless @context_menu_included
1125 unless @context_menu_included
1123 content_for :header_tags do
1126 content_for :header_tags do
1124 javascript_include_tag('context_menu') +
1127 javascript_include_tag('context_menu') +
1125 stylesheet_link_tag('context_menu')
1128 stylesheet_link_tag('context_menu')
1126 end
1129 end
1127 if l(:direction) == 'rtl'
1130 if l(:direction) == 'rtl'
1128 content_for :header_tags do
1131 content_for :header_tags do
1129 stylesheet_link_tag('context_menu_rtl')
1132 stylesheet_link_tag('context_menu_rtl')
1130 end
1133 end
1131 end
1134 end
1132 @context_menu_included = true
1135 @context_menu_included = true
1133 end
1136 end
1134 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1137 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1135 end
1138 end
1136
1139
1137 def calendar_for(field_id)
1140 def calendar_for(field_id)
1138 include_calendar_headers_tags
1141 include_calendar_headers_tags
1139 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1142 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1140 end
1143 end
1141
1144
1142 def include_calendar_headers_tags
1145 def include_calendar_headers_tags
1143 unless @calendar_headers_tags_included
1146 unless @calendar_headers_tags_included
1144 tags = ''.html_safe
1147 tags = ''.html_safe
1145 @calendar_headers_tags_included = true
1148 @calendar_headers_tags_included = true
1146 content_for :header_tags do
1149 content_for :header_tags do
1147 start_of_week = Setting.start_of_week
1150 start_of_week = Setting.start_of_week
1148 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1151 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1149 # Redmine uses 1..7 (monday..sunday) in settings and locales
1152 # Redmine uses 1..7 (monday..sunday) in settings and locales
1150 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1153 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1151 start_of_week = start_of_week.to_i % 7
1154 start_of_week = start_of_week.to_i % 7
1152 tags << javascript_tag(
1155 tags << javascript_tag(
1153 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1156 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1154 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1157 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1155 path_to_image('/images/calendar.png') +
1158 path_to_image('/images/calendar.png') +
1156 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1159 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1157 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1160 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1158 "beforeShow: beforeShowDatePicker};")
1161 "beforeShow: beforeShowDatePicker};")
1159 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1162 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1160 unless jquery_locale == 'en'
1163 unless jquery_locale == 'en'
1161 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1164 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1162 end
1165 end
1163 tags
1166 tags
1164 end
1167 end
1165 end
1168 end
1166 end
1169 end
1167
1170
1168 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1171 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1169 # Examples:
1172 # Examples:
1170 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1173 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1171 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1174 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1172 #
1175 #
1173 def stylesheet_link_tag(*sources)
1176 def stylesheet_link_tag(*sources)
1174 options = sources.last.is_a?(Hash) ? sources.pop : {}
1177 options = sources.last.is_a?(Hash) ? sources.pop : {}
1175 plugin = options.delete(:plugin)
1178 plugin = options.delete(:plugin)
1176 sources = sources.map do |source|
1179 sources = sources.map do |source|
1177 if plugin
1180 if plugin
1178 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1181 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1179 elsif current_theme && current_theme.stylesheets.include?(source)
1182 elsif current_theme && current_theme.stylesheets.include?(source)
1180 current_theme.stylesheet_path(source)
1183 current_theme.stylesheet_path(source)
1181 else
1184 else
1182 source
1185 source
1183 end
1186 end
1184 end
1187 end
1185 super *sources, options
1188 super *sources, options
1186 end
1189 end
1187
1190
1188 # Overrides Rails' image_tag with themes and plugins support.
1191 # Overrides Rails' image_tag with themes and plugins support.
1189 # Examples:
1192 # Examples:
1190 # image_tag('image.png') # => picks image.png from the current theme or defaults
1193 # image_tag('image.png') # => picks image.png from the current theme or defaults
1191 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1194 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1192 #
1195 #
1193 def image_tag(source, options={})
1196 def image_tag(source, options={})
1194 if plugin = options.delete(:plugin)
1197 if plugin = options.delete(:plugin)
1195 source = "/plugin_assets/#{plugin}/images/#{source}"
1198 source = "/plugin_assets/#{plugin}/images/#{source}"
1196 elsif current_theme && current_theme.images.include?(source)
1199 elsif current_theme && current_theme.images.include?(source)
1197 source = current_theme.image_path(source)
1200 source = current_theme.image_path(source)
1198 end
1201 end
1199 super source, options
1202 super source, options
1200 end
1203 end
1201
1204
1202 # Overrides Rails' javascript_include_tag with plugins support
1205 # Overrides Rails' javascript_include_tag with plugins support
1203 # Examples:
1206 # Examples:
1204 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1207 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1205 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1208 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1206 #
1209 #
1207 def javascript_include_tag(*sources)
1210 def javascript_include_tag(*sources)
1208 options = sources.last.is_a?(Hash) ? sources.pop : {}
1211 options = sources.last.is_a?(Hash) ? sources.pop : {}
1209 if plugin = options.delete(:plugin)
1212 if plugin = options.delete(:plugin)
1210 sources = sources.map do |source|
1213 sources = sources.map do |source|
1211 if plugin
1214 if plugin
1212 "/plugin_assets/#{plugin}/javascripts/#{source}"
1215 "/plugin_assets/#{plugin}/javascripts/#{source}"
1213 else
1216 else
1214 source
1217 source
1215 end
1218 end
1216 end
1219 end
1217 end
1220 end
1218 super *sources, options
1221 super *sources, options
1219 end
1222 end
1220
1223
1221 def sidebar_content?
1224 def sidebar_content?
1222 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1225 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1223 end
1226 end
1224
1227
1225 def view_layouts_base_sidebar_hook_response
1228 def view_layouts_base_sidebar_hook_response
1226 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1229 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1227 end
1230 end
1228
1231
1229 def email_delivery_enabled?
1232 def email_delivery_enabled?
1230 !!ActionMailer::Base.perform_deliveries
1233 !!ActionMailer::Base.perform_deliveries
1231 end
1234 end
1232
1235
1233 # Returns the avatar image tag for the given +user+ if avatars are enabled
1236 # Returns the avatar image tag for the given +user+ if avatars are enabled
1234 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1237 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1235 def avatar(user, options = { })
1238 def avatar(user, options = { })
1236 if Setting.gravatar_enabled?
1239 if Setting.gravatar_enabled?
1237 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1240 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1238 email = nil
1241 email = nil
1239 if user.respond_to?(:mail)
1242 if user.respond_to?(:mail)
1240 email = user.mail
1243 email = user.mail
1241 elsif user.to_s =~ %r{<(.+?)>}
1244 elsif user.to_s =~ %r{<(.+?)>}
1242 email = $1
1245 email = $1
1243 end
1246 end
1244 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1247 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1245 else
1248 else
1246 ''
1249 ''
1247 end
1250 end
1248 end
1251 end
1249
1252
1250 def sanitize_anchor_name(anchor)
1253 def sanitize_anchor_name(anchor)
1251 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1254 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1252 end
1255 end
1253
1256
1254 # Returns the javascript tags that are included in the html layout head
1257 # Returns the javascript tags that are included in the html layout head
1255 def javascript_heads
1258 def javascript_heads
1256 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1259 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1257 unless User.current.pref.warn_on_leaving_unsaved == '0'
1260 unless User.current.pref.warn_on_leaving_unsaved == '0'
1258 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1261 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1259 end
1262 end
1260 tags
1263 tags
1261 end
1264 end
1262
1265
1263 def favicon
1266 def favicon
1264 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1267 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1265 end
1268 end
1266
1269
1267 # Returns the path to the favicon
1270 # Returns the path to the favicon
1268 def favicon_path
1271 def favicon_path
1269 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1272 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1270 image_path(icon)
1273 image_path(icon)
1271 end
1274 end
1272
1275
1273 # Returns the full URL to the favicon
1276 # Returns the full URL to the favicon
1274 def favicon_url
1277 def favicon_url
1275 # TODO: use #image_url introduced in Rails4
1278 # TODO: use #image_url introduced in Rails4
1276 path = favicon_path
1279 path = favicon_path
1277 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1280 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1278 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1281 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1279 end
1282 end
1280
1283
1281 def robot_exclusion_tag
1284 def robot_exclusion_tag
1282 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1285 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1283 end
1286 end
1284
1287
1285 # Returns true if arg is expected in the API response
1288 # Returns true if arg is expected in the API response
1286 def include_in_api_response?(arg)
1289 def include_in_api_response?(arg)
1287 unless @included_in_api_response
1290 unless @included_in_api_response
1288 param = params[:include]
1291 param = params[:include]
1289 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1292 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1290 @included_in_api_response.collect!(&:strip)
1293 @included_in_api_response.collect!(&:strip)
1291 end
1294 end
1292 @included_in_api_response.include?(arg.to_s)
1295 @included_in_api_response.include?(arg.to_s)
1293 end
1296 end
1294
1297
1295 # Returns options or nil if nometa param or X-Redmine-Nometa header
1298 # Returns options or nil if nometa param or X-Redmine-Nometa header
1296 # was set in the request
1299 # was set in the request
1297 def api_meta(options)
1300 def api_meta(options)
1298 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1301 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1299 # compatibility mode for activeresource clients that raise
1302 # compatibility mode for activeresource clients that raise
1300 # an error when deserializing an array with attributes
1303 # an error when deserializing an array with attributes
1301 nil
1304 nil
1302 else
1305 else
1303 options
1306 options
1304 end
1307 end
1305 end
1308 end
1306
1309
1307 private
1310 private
1308
1311
1309 def wiki_helper
1312 def wiki_helper
1310 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1313 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1311 extend helper
1314 extend helper
1312 return self
1315 return self
1313 end
1316 end
1314
1317
1315 def link_to_content_update(text, url_params = {}, html_options = {})
1318 def link_to_content_update(text, url_params = {}, html_options = {})
1316 link_to(text, url_params, html_options)
1319 link_to(text, url_params, html_options)
1317 end
1320 end
1318 end
1321 end
@@ -1,1562 +1,1565
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21 include Redmine::I18n
21 include Redmine::I18n
22 before_save :set_parent_id
22 before_save :set_parent_id
23 include Redmine::NestedSet::IssueNestedSet
23 include Redmine::NestedSet::IssueNestedSet
24
24
25 belongs_to :project
25 belongs_to :project
26 belongs_to :tracker
26 belongs_to :tracker
27 belongs_to :status, :class_name => 'IssueStatus'
27 belongs_to :status, :class_name => 'IssueStatus'
28 belongs_to :author, :class_name => 'User'
28 belongs_to :author, :class_name => 'User'
29 belongs_to :assigned_to, :class_name => 'Principal'
29 belongs_to :assigned_to, :class_name => 'Principal'
30 belongs_to :fixed_version, :class_name => 'Version'
30 belongs_to :fixed_version, :class_name => 'Version'
31 belongs_to :priority, :class_name => 'IssuePriority'
31 belongs_to :priority, :class_name => 'IssuePriority'
32 belongs_to :category, :class_name => 'IssueCategory'
32 belongs_to :category, :class_name => 'IssueCategory'
33
33
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 has_many :visible_journals,
35 has_many :visible_journals,
36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 :class_name => 'Journal',
37 :class_name => 'Journal',
38 :as => :journalized
38 :as => :journalized
39
39
40 has_many :time_entries, :dependent => :destroy
40 has_many :time_entries, :dependent => :destroy
41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42
42
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45
45
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 acts_as_customizable
47 acts_as_customizable
48 acts_as_watchable
48 acts_as_watchable
49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 :preload => [:project, :status, :tracker],
50 :preload => [:project, :status, :tracker],
51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52
52
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56
56
57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
58 :author_key => :author_id
58 :author_key => :author_id
59
59
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61
61
62 attr_reader :current_journal
62 attr_reader :current_journal
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64
64
65 validates_presence_of :subject, :project, :tracker
65 validates_presence_of :subject, :project, :tracker
66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69
69
70 validates_length_of :subject, :maximum => 255
70 validates_length_of :subject, :maximum => 255
71 validates_inclusion_of :done_ratio, :in => 0..100
71 validates_inclusion_of :done_ratio, :in => 0..100
72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 validates :start_date, :date => true
73 validates :start_date, :date => true
74 validates :due_date, :date => true
74 validates :due_date, :date => true
75 validate :validate_issue, :validate_required_fields
75 validate :validate_issue, :validate_required_fields
76 attr_protected :id
76 attr_protected :id
77
77
78 scope :visible, lambda {|*args|
78 scope :visible, lambda {|*args|
79 joins(:project).
79 joins(:project).
80 where(Issue.visible_condition(args.shift || User.current, *args))
80 where(Issue.visible_condition(args.shift || User.current, *args))
81 }
81 }
82
82
83 scope :open, lambda {|*args|
83 scope :open, lambda {|*args|
84 is_closed = args.size > 0 ? !args.first : false
84 is_closed = args.size > 0 ? !args.first : false
85 joins(:status).
85 joins(:status).
86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 }
87 }
88
88
89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 scope :on_active_project, lambda {
90 scope :on_active_project, lambda {
91 joins(:project).
91 joins(:project).
92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 }
93 }
94 scope :fixed_version, lambda {|versions|
94 scope :fixed_version, lambda {|versions|
95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 }
97 }
98
98
99 before_create :default_assign
99 before_create :default_assign
100 before_save :close_duplicates, :update_done_ratio_from_issue_status,
100 before_save :close_duplicates, :update_done_ratio_from_issue_status,
101 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
101 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
102 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
102 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
103 after_save :reschedule_following_issues, :update_nested_set_attributes,
103 after_save :reschedule_following_issues, :update_nested_set_attributes,
104 :update_parent_attributes, :create_journal
104 :update_parent_attributes, :create_journal
105 # Should be after_create but would be called before previous after_save callbacks
105 # Should be after_create but would be called before previous after_save callbacks
106 after_save :after_create_from_copy
106 after_save :after_create_from_copy
107 after_destroy :update_parent_attributes
107 after_destroy :update_parent_attributes
108 after_create :send_notification
108 after_create :send_notification
109 # Keep it at the end of after_save callbacks
109 # Keep it at the end of after_save callbacks
110 after_save :clear_assigned_to_was
110 after_save :clear_assigned_to_was
111
111
112 # Returns a SQL conditions string used to find all issues visible by the specified user
112 # Returns a SQL conditions string used to find all issues visible by the specified user
113 def self.visible_condition(user, options={})
113 def self.visible_condition(user, options={})
114 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
114 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
115 if user.id && user.logged?
115 if user.id && user.logged?
116 case role.issues_visibility
116 case role.issues_visibility
117 when 'all'
117 when 'all'
118 nil
118 nil
119 when 'default'
119 when 'default'
120 user_ids = [user.id] + user.groups.map(&:id).compact
120 user_ids = [user.id] + user.groups.map(&:id).compact
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(',')}))"
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 when 'own'
122 when 'own'
123 user_ids = [user.id] + user.groups.map(&:id).compact
123 user_ids = [user.id] + user.groups.map(&:id).compact
124 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
124 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
125 else
125 else
126 '1=0'
126 '1=0'
127 end
127 end
128 else
128 else
129 "(#{table_name}.is_private = #{connection.quoted_false})"
129 "(#{table_name}.is_private = #{connection.quoted_false})"
130 end
130 end
131 end
131 end
132 end
132 end
133
133
134 # Returns true if usr or current user is allowed to view the issue
134 # Returns true if usr or current user is allowed to view the issue
135 def visible?(usr=nil)
135 def visible?(usr=nil)
136 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
136 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
137 if user.logged?
137 if user.logged?
138 case role.issues_visibility
138 case role.issues_visibility
139 when 'all'
139 when 'all'
140 true
140 true
141 when 'default'
141 when 'default'
142 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
142 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
143 when 'own'
143 when 'own'
144 self.author == user || user.is_or_belongs_to?(assigned_to)
144 self.author == user || user.is_or_belongs_to?(assigned_to)
145 else
145 else
146 false
146 false
147 end
147 end
148 else
148 else
149 !self.is_private?
149 !self.is_private?
150 end
150 end
151 end
151 end
152 end
152 end
153
153
154 # Returns true if user or current user is allowed to edit or add a note to the issue
154 # Returns true if user or current user is allowed to edit or add a note to the issue
155 def editable?(user=User.current)
155 def editable?(user=User.current)
156 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
156 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
157 end
157 end
158
158
159 def initialize(attributes=nil, *args)
159 def initialize(attributes=nil, *args)
160 super
160 super
161 if new_record?
161 if new_record?
162 # set default values for new records only
162 # set default values for new records only
163 self.priority ||= IssuePriority.default
163 self.priority ||= IssuePriority.default
164 self.watcher_user_ids = []
164 self.watcher_user_ids = []
165 end
165 end
166 end
166 end
167
167
168 def create_or_update
168 def create_or_update
169 super
169 super
170 ensure
170 ensure
171 @status_was = nil
171 @status_was = nil
172 end
172 end
173 private :create_or_update
173 private :create_or_update
174
174
175 # AR#Persistence#destroy would raise and RecordNotFound exception
175 # AR#Persistence#destroy would raise and RecordNotFound exception
176 # if the issue was already deleted or updated (non matching lock_version).
176 # if the issue was already deleted or updated (non matching lock_version).
177 # This is a problem when bulk deleting issues or deleting a project
177 # This is a problem when bulk deleting issues or deleting a project
178 # (because an issue may already be deleted if its parent was deleted
178 # (because an issue may already be deleted if its parent was deleted
179 # first).
179 # first).
180 # The issue is reloaded by the nested_set before being deleted so
180 # The issue is reloaded by the nested_set before being deleted so
181 # the lock_version condition should not be an issue but we handle it.
181 # the lock_version condition should not be an issue but we handle it.
182 def destroy
182 def destroy
183 super
183 super
184 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
184 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
185 # Stale or already deleted
185 # Stale or already deleted
186 begin
186 begin
187 reload
187 reload
188 rescue ActiveRecord::RecordNotFound
188 rescue ActiveRecord::RecordNotFound
189 # The issue was actually already deleted
189 # The issue was actually already deleted
190 @destroyed = true
190 @destroyed = true
191 return freeze
191 return freeze
192 end
192 end
193 # The issue was stale, retry to destroy
193 # The issue was stale, retry to destroy
194 super
194 super
195 end
195 end
196
196
197 alias :base_reload :reload
197 alias :base_reload :reload
198 def reload(*args)
198 def reload(*args)
199 @workflow_rule_by_attribute = nil
199 @workflow_rule_by_attribute = nil
200 @assignable_versions = nil
200 @assignable_versions = nil
201 @relations = nil
201 @relations = nil
202 @spent_hours = nil
202 @spent_hours = nil
203 base_reload(*args)
203 base_reload(*args)
204 end
204 end
205
205
206 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
206 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
207 def available_custom_fields
207 def available_custom_fields
208 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
208 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
209 end
209 end
210
210
211 def visible_custom_field_values(user=nil)
211 def visible_custom_field_values(user=nil)
212 user_real = user || User.current
212 user_real = user || User.current
213 custom_field_values.select do |value|
213 custom_field_values.select do |value|
214 value.custom_field.visible_by?(project, user_real)
214 value.custom_field.visible_by?(project, user_real)
215 end
215 end
216 end
216 end
217
217
218 # Copies attributes from another issue, arg can be an id or an Issue
218 # Copies attributes from another issue, arg can be an id or an Issue
219 def copy_from(arg, options={})
219 def copy_from(arg, options={})
220 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
220 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
221 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
221 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
222 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
222 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
223 self.status = issue.status
223 self.status = issue.status
224 self.author = User.current
224 self.author = User.current
225 unless options[:attachments] == false
225 unless options[:attachments] == false
226 self.attachments = issue.attachments.map do |attachement|
226 self.attachments = issue.attachments.map do |attachement|
227 attachement.copy(:container => self)
227 attachement.copy(:container => self)
228 end
228 end
229 end
229 end
230 @copied_from = issue
230 @copied_from = issue
231 @copy_options = options
231 @copy_options = options
232 self
232 self
233 end
233 end
234
234
235 # Returns an unsaved copy of the issue
235 # Returns an unsaved copy of the issue
236 def copy(attributes=nil, copy_options={})
236 def copy(attributes=nil, copy_options={})
237 copy = self.class.new.copy_from(self, copy_options)
237 copy = self.class.new.copy_from(self, copy_options)
238 copy.attributes = attributes if attributes
238 copy.attributes = attributes if attributes
239 copy
239 copy
240 end
240 end
241
241
242 # Returns true if the issue is a copy
242 # Returns true if the issue is a copy
243 def copy?
243 def copy?
244 @copied_from.present?
244 @copied_from.present?
245 end
245 end
246
246
247 def status_id=(status_id)
247 def status_id=(status_id)
248 if status_id.to_s != self.status_id.to_s
248 if status_id.to_s != self.status_id.to_s
249 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
249 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
250 end
250 end
251 self.status_id
251 self.status_id
252 end
252 end
253
253
254 # Sets the status.
254 # Sets the status.
255 def status=(status)
255 def status=(status)
256 if status != self.status
256 if status != self.status
257 @workflow_rule_by_attribute = nil
257 @workflow_rule_by_attribute = nil
258 end
258 end
259 association(:status).writer(status)
259 association(:status).writer(status)
260 end
260 end
261
261
262 def priority_id=(pid)
262 def priority_id=(pid)
263 self.priority = nil
263 self.priority = nil
264 write_attribute(:priority_id, pid)
264 write_attribute(:priority_id, pid)
265 end
265 end
266
266
267 def category_id=(cid)
267 def category_id=(cid)
268 self.category = nil
268 self.category = nil
269 write_attribute(:category_id, cid)
269 write_attribute(:category_id, cid)
270 end
270 end
271
271
272 def fixed_version_id=(vid)
272 def fixed_version_id=(vid)
273 self.fixed_version = nil
273 self.fixed_version = nil
274 write_attribute(:fixed_version_id, vid)
274 write_attribute(:fixed_version_id, vid)
275 end
275 end
276
276
277 def tracker_id=(tracker_id)
277 def tracker_id=(tracker_id)
278 if tracker_id.to_s != self.tracker_id.to_s
278 if tracker_id.to_s != self.tracker_id.to_s
279 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
279 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
280 end
280 end
281 self.tracker_id
281 self.tracker_id
282 end
282 end
283
283
284 # Sets the tracker.
284 # Sets the tracker.
285 # This will set the status to the default status of the new tracker if:
285 # This will set the status to the default status of the new tracker if:
286 # * the status was the default for the previous tracker
286 # * the status was the default for the previous tracker
287 # * or if the status was not part of the new tracker statuses
287 # * or if the status was not part of the new tracker statuses
288 # * or the status was nil
288 # * or the status was nil
289 def tracker=(tracker)
289 def tracker=(tracker)
290 if tracker != self.tracker
290 if tracker != self.tracker
291 if status == default_status
291 if status == default_status
292 self.status = nil
292 self.status = nil
293 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
293 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
294 self.status = nil
294 self.status = nil
295 end
295 end
296 @custom_field_values = nil
296 @custom_field_values = nil
297 @workflow_rule_by_attribute = nil
297 @workflow_rule_by_attribute = nil
298 end
298 end
299 association(:tracker).writer(tracker)
299 association(:tracker).writer(tracker)
300 self.status ||= default_status
300 self.status ||= default_status
301 self.tracker
301 self.tracker
302 end
302 end
303
303
304 def project_id=(project_id)
304 def project_id=(project_id)
305 if project_id.to_s != self.project_id.to_s
305 if project_id.to_s != self.project_id.to_s
306 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
306 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
307 end
307 end
308 self.project_id
308 self.project_id
309 end
309 end
310
310
311 # Sets the project.
311 # Sets the project.
312 # Unless keep_tracker argument is set to true, this will change the tracker
312 # Unless keep_tracker argument is set to true, this will change the tracker
313 # to the first tracker of the new project if the previous tracker is not part
313 # to the first tracker of the new project if the previous tracker is not part
314 # of the new project trackers.
314 # of the new project trackers.
315 # This will clear the fixed_version is it's no longer valid for the new project.
315 # This will clear the fixed_version is it's no longer valid for the new project.
316 # This will clear the parent issue if it's no longer valid for the new project.
316 # This will clear the parent issue if it's no longer valid for the new project.
317 # This will set the category to the category with the same name in the new
317 # This will set the category to the category with the same name in the new
318 # project if it exists, or clear it if it doesn't.
318 # project if it exists, or clear it if it doesn't.
319 def project=(project, keep_tracker=false)
319 def project=(project, keep_tracker=false)
320 project_was = self.project
320 project_was = self.project
321 association(:project).writer(project)
321 association(:project).writer(project)
322 if project_was && project && project_was != project
322 if project_was && project && project_was != project
323 @assignable_versions = nil
323 @assignable_versions = nil
324
324
325 unless keep_tracker || project.trackers.include?(tracker)
325 unless keep_tracker || project.trackers.include?(tracker)
326 self.tracker = project.trackers.first
326 self.tracker = project.trackers.first
327 end
327 end
328 # Reassign to the category with same name if any
328 # Reassign to the category with same name if any
329 if category
329 if category
330 self.category = project.issue_categories.find_by_name(category.name)
330 self.category = project.issue_categories.find_by_name(category.name)
331 end
331 end
332 # Keep the fixed_version if it's still valid in the new_project
332 # Keep the fixed_version if it's still valid in the new_project
333 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
333 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
334 self.fixed_version = nil
334 self.fixed_version = nil
335 end
335 end
336 # Clear the parent task if it's no longer valid
336 # Clear the parent task if it's no longer valid
337 unless valid_parent_project?
337 unless valid_parent_project?
338 self.parent_issue_id = nil
338 self.parent_issue_id = nil
339 end
339 end
340 @custom_field_values = nil
340 @custom_field_values = nil
341 @workflow_rule_by_attribute = nil
341 @workflow_rule_by_attribute = nil
342 end
342 end
343 self.project
343 self.project
344 end
344 end
345
345
346 def description=(arg)
346 def description=(arg)
347 if arg.is_a?(String)
347 if arg.is_a?(String)
348 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
348 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
349 end
349 end
350 write_attribute(:description, arg)
350 write_attribute(:description, arg)
351 end
351 end
352
352
353 # Overrides assign_attributes so that project and tracker get assigned first
353 # Overrides assign_attributes so that project and tracker get assigned first
354 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
354 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
355 return if new_attributes.nil?
355 return if new_attributes.nil?
356 attrs = new_attributes.dup
356 attrs = new_attributes.dup
357 attrs.stringify_keys!
357 attrs.stringify_keys!
358
358
359 %w(project project_id tracker tracker_id).each do |attr|
359 %w(project project_id tracker tracker_id).each do |attr|
360 if attrs.has_key?(attr)
360 if attrs.has_key?(attr)
361 send "#{attr}=", attrs.delete(attr)
361 send "#{attr}=", attrs.delete(attr)
362 end
362 end
363 end
363 end
364 send :assign_attributes_without_project_and_tracker_first, attrs, *args
364 send :assign_attributes_without_project_and_tracker_first, attrs, *args
365 end
365 end
366 # Do not redefine alias chain on reload (see #4838)
366 # Do not redefine alias chain on reload (see #4838)
367 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
367 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
368
368
369 def attributes=(new_attributes)
369 def attributes=(new_attributes)
370 assign_attributes new_attributes
370 assign_attributes new_attributes
371 end
371 end
372
372
373 def estimated_hours=(h)
373 def estimated_hours=(h)
374 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
374 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
375 end
375 end
376
376
377 safe_attributes 'project_id',
377 safe_attributes 'project_id',
378 :if => lambda {|issue, user|
378 :if => lambda {|issue, user|
379 if issue.new_record?
379 if issue.new_record?
380 issue.copy?
380 issue.copy?
381 else
381 else
382 user.allowed_to?(:edit_issues, issue.project)
382 user.allowed_to?(:edit_issues, issue.project)
383 end
383 end
384 }
384 }
385
385
386 safe_attributes 'tracker_id',
386 safe_attributes 'tracker_id',
387 'status_id',
387 'status_id',
388 'category_id',
388 'category_id',
389 'assigned_to_id',
389 'assigned_to_id',
390 'priority_id',
390 'priority_id',
391 'fixed_version_id',
391 'fixed_version_id',
392 'subject',
392 'subject',
393 'description',
393 'description',
394 'start_date',
394 'start_date',
395 'due_date',
395 'due_date',
396 'done_ratio',
396 'done_ratio',
397 'estimated_hours',
397 'estimated_hours',
398 'custom_field_values',
398 'custom_field_values',
399 'custom_fields',
399 'custom_fields',
400 'lock_version',
400 'lock_version',
401 'notes',
401 'notes',
402 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
402 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
403
403
404 safe_attributes 'notes',
404 safe_attributes 'notes',
405 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
405 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
406
406
407 safe_attributes 'private_notes',
407 safe_attributes 'private_notes',
408 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
408 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
409
409
410 safe_attributes 'watcher_user_ids',
410 safe_attributes 'watcher_user_ids',
411 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
411 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
412
412
413 safe_attributes 'is_private',
413 safe_attributes 'is_private',
414 :if => lambda {|issue, user|
414 :if => lambda {|issue, user|
415 user.allowed_to?(:set_issues_private, issue.project) ||
415 user.allowed_to?(:set_issues_private, issue.project) ||
416 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
416 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
417 }
417 }
418
418
419 safe_attributes 'parent_issue_id',
419 safe_attributes 'parent_issue_id',
420 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
420 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
421 user.allowed_to?(:manage_subtasks, issue.project)}
421 user.allowed_to?(:manage_subtasks, issue.project)}
422
422
423 def safe_attribute_names(user=nil)
423 def safe_attribute_names(user=nil)
424 names = super
424 names = super
425 names -= disabled_core_fields
425 names -= disabled_core_fields
426 names -= read_only_attribute_names(user)
426 names -= read_only_attribute_names(user)
427 if new_record? && copy?
428 names |= %w(project_id)
429 end
427 names
430 names
428 end
431 end
429
432
430 # Safely sets attributes
433 # Safely sets attributes
431 # Should be called from controllers instead of #attributes=
434 # Should be called from controllers instead of #attributes=
432 # attr_accessible is too rough because we still want things like
435 # attr_accessible is too rough because we still want things like
433 # Issue.new(:project => foo) to work
436 # Issue.new(:project => foo) to work
434 def safe_attributes=(attrs, user=User.current)
437 def safe_attributes=(attrs, user=User.current)
435 return unless attrs.is_a?(Hash)
438 return unless attrs.is_a?(Hash)
436
439
437 attrs = attrs.deep_dup
440 attrs = attrs.deep_dup
438
441
439 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
442 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
440 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
443 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
441 if allowed_target_projects(user).where(:id => p.to_i).exists?
444 if allowed_target_projects(user).where(:id => p.to_i).exists?
442 self.project_id = p
445 self.project_id = p
443 end
446 end
444 end
447 end
445
448
446 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
449 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
447 self.tracker_id = t
450 self.tracker_id = t
448 end
451 end
449
452
450 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
453 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
451 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
454 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
452 self.status_id = s
455 self.status_id = s
453 end
456 end
454 end
457 end
455
458
456 attrs = delete_unsafe_attributes(attrs, user)
459 attrs = delete_unsafe_attributes(attrs, user)
457 return if attrs.empty?
460 return if attrs.empty?
458
461
459 unless leaf?
462 unless leaf?
460 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
463 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
461 end
464 end
462
465
463 if attrs['parent_issue_id'].present?
466 if attrs['parent_issue_id'].present?
464 s = attrs['parent_issue_id'].to_s
467 s = attrs['parent_issue_id'].to_s
465 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
468 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
466 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
469 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
467 end
470 end
468 end
471 end
469
472
470 if attrs['custom_field_values'].present?
473 if attrs['custom_field_values'].present?
471 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
474 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
472 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
475 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
473 end
476 end
474
477
475 if attrs['custom_fields'].present?
478 if attrs['custom_fields'].present?
476 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
479 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
477 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
480 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
478 end
481 end
479
482
480 # mass-assignment security bypass
483 # mass-assignment security bypass
481 assign_attributes attrs, :without_protection => true
484 assign_attributes attrs, :without_protection => true
482 end
485 end
483
486
484 def disabled_core_fields
487 def disabled_core_fields
485 tracker ? tracker.disabled_core_fields : []
488 tracker ? tracker.disabled_core_fields : []
486 end
489 end
487
490
488 # Returns the custom_field_values that can be edited by the given user
491 # Returns the custom_field_values that can be edited by the given user
489 def editable_custom_field_values(user=nil)
492 def editable_custom_field_values(user=nil)
490 visible_custom_field_values(user).reject do |value|
493 visible_custom_field_values(user).reject do |value|
491 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
494 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
492 end
495 end
493 end
496 end
494
497
495 # Returns the custom fields that can be edited by the given user
498 # Returns the custom fields that can be edited by the given user
496 def editable_custom_fields(user=nil)
499 def editable_custom_fields(user=nil)
497 editable_custom_field_values(user).map(&:custom_field).uniq
500 editable_custom_field_values(user).map(&:custom_field).uniq
498 end
501 end
499
502
500 # Returns the names of attributes that are read-only for user or the current user
503 # Returns the names of attributes that are read-only for user or the current user
501 # For users with multiple roles, the read-only fields are the intersection of
504 # For users with multiple roles, the read-only fields are the intersection of
502 # read-only fields of each role
505 # read-only fields of each role
503 # The result is an array of strings where sustom fields are represented with their ids
506 # The result is an array of strings where sustom fields are represented with their ids
504 #
507 #
505 # Examples:
508 # Examples:
506 # issue.read_only_attribute_names # => ['due_date', '2']
509 # issue.read_only_attribute_names # => ['due_date', '2']
507 # issue.read_only_attribute_names(user) # => []
510 # issue.read_only_attribute_names(user) # => []
508 def read_only_attribute_names(user=nil)
511 def read_only_attribute_names(user=nil)
509 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
512 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
510 end
513 end
511
514
512 # Returns the names of required attributes for user or the current user
515 # Returns the names of required attributes for user or the current user
513 # For users with multiple roles, the required fields are the intersection of
516 # For users with multiple roles, the required fields are the intersection of
514 # required fields of each role
517 # required fields of each role
515 # The result is an array of strings where sustom fields are represented with their ids
518 # The result is an array of strings where sustom fields are represented with their ids
516 #
519 #
517 # Examples:
520 # Examples:
518 # issue.required_attribute_names # => ['due_date', '2']
521 # issue.required_attribute_names # => ['due_date', '2']
519 # issue.required_attribute_names(user) # => []
522 # issue.required_attribute_names(user) # => []
520 def required_attribute_names(user=nil)
523 def required_attribute_names(user=nil)
521 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
524 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
522 end
525 end
523
526
524 # Returns true if the attribute is required for user
527 # Returns true if the attribute is required for user
525 def required_attribute?(name, user=nil)
528 def required_attribute?(name, user=nil)
526 required_attribute_names(user).include?(name.to_s)
529 required_attribute_names(user).include?(name.to_s)
527 end
530 end
528
531
529 # Returns a hash of the workflow rule by attribute for the given user
532 # Returns a hash of the workflow rule by attribute for the given user
530 #
533 #
531 # Examples:
534 # Examples:
532 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
535 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
533 def workflow_rule_by_attribute(user=nil)
536 def workflow_rule_by_attribute(user=nil)
534 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
537 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
535
538
536 user_real = user || User.current
539 user_real = user || User.current
537 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
540 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
538 roles = roles.select(&:consider_workflow?)
541 roles = roles.select(&:consider_workflow?)
539 return {} if roles.empty?
542 return {} if roles.empty?
540
543
541 result = {}
544 result = {}
542 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
545 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
543 if workflow_permissions.any?
546 if workflow_permissions.any?
544 workflow_rules = workflow_permissions.inject({}) do |h, wp|
547 workflow_rules = workflow_permissions.inject({}) do |h, wp|
545 h[wp.field_name] ||= []
548 h[wp.field_name] ||= []
546 h[wp.field_name] << wp.rule
549 h[wp.field_name] << wp.rule
547 h
550 h
548 end
551 end
549 workflow_rules.each do |attr, rules|
552 workflow_rules.each do |attr, rules|
550 next if rules.size < roles.size
553 next if rules.size < roles.size
551 uniq_rules = rules.uniq
554 uniq_rules = rules.uniq
552 if uniq_rules.size == 1
555 if uniq_rules.size == 1
553 result[attr] = uniq_rules.first
556 result[attr] = uniq_rules.first
554 else
557 else
555 result[attr] = 'required'
558 result[attr] = 'required'
556 end
559 end
557 end
560 end
558 end
561 end
559 @workflow_rule_by_attribute = result if user.nil?
562 @workflow_rule_by_attribute = result if user.nil?
560 result
563 result
561 end
564 end
562 private :workflow_rule_by_attribute
565 private :workflow_rule_by_attribute
563
566
564 def done_ratio
567 def done_ratio
565 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
568 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
566 status.default_done_ratio
569 status.default_done_ratio
567 else
570 else
568 read_attribute(:done_ratio)
571 read_attribute(:done_ratio)
569 end
572 end
570 end
573 end
571
574
572 def self.use_status_for_done_ratio?
575 def self.use_status_for_done_ratio?
573 Setting.issue_done_ratio == 'issue_status'
576 Setting.issue_done_ratio == 'issue_status'
574 end
577 end
575
578
576 def self.use_field_for_done_ratio?
579 def self.use_field_for_done_ratio?
577 Setting.issue_done_ratio == 'issue_field'
580 Setting.issue_done_ratio == 'issue_field'
578 end
581 end
579
582
580 def validate_issue
583 def validate_issue
581 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
584 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
582 errors.add :due_date, :greater_than_start_date
585 errors.add :due_date, :greater_than_start_date
583 end
586 end
584
587
585 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
588 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
586 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
589 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
587 end
590 end
588
591
589 if fixed_version
592 if fixed_version
590 if !assignable_versions.include?(fixed_version)
593 if !assignable_versions.include?(fixed_version)
591 errors.add :fixed_version_id, :inclusion
594 errors.add :fixed_version_id, :inclusion
592 elsif reopening? && fixed_version.closed?
595 elsif reopening? && fixed_version.closed?
593 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
596 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
594 end
597 end
595 end
598 end
596
599
597 # Checks that the issue can not be added/moved to a disabled tracker
600 # Checks that the issue can not be added/moved to a disabled tracker
598 if project && (tracker_id_changed? || project_id_changed?)
601 if project && (tracker_id_changed? || project_id_changed?)
599 unless project.trackers.include?(tracker)
602 unless project.trackers.include?(tracker)
600 errors.add :tracker_id, :inclusion
603 errors.add :tracker_id, :inclusion
601 end
604 end
602 end
605 end
603
606
604 # Checks parent issue assignment
607 # Checks parent issue assignment
605 if @invalid_parent_issue_id.present?
608 if @invalid_parent_issue_id.present?
606 errors.add :parent_issue_id, :invalid
609 errors.add :parent_issue_id, :invalid
607 elsif @parent_issue
610 elsif @parent_issue
608 if !valid_parent_project?(@parent_issue)
611 if !valid_parent_project?(@parent_issue)
609 errors.add :parent_issue_id, :invalid
612 errors.add :parent_issue_id, :invalid
610 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
613 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
611 errors.add :parent_issue_id, :invalid
614 errors.add :parent_issue_id, :invalid
612 elsif !new_record?
615 elsif !new_record?
613 # moving an existing issue
616 # moving an existing issue
614 if move_possible?(@parent_issue)
617 if move_possible?(@parent_issue)
615 # move accepted
618 # move accepted
616 else
619 else
617 errors.add :parent_issue_id, :invalid
620 errors.add :parent_issue_id, :invalid
618 end
621 end
619 end
622 end
620 end
623 end
621 end
624 end
622
625
623 # Validates the issue against additional workflow requirements
626 # Validates the issue against additional workflow requirements
624 def validate_required_fields
627 def validate_required_fields
625 user = new_record? ? author : current_journal.try(:user)
628 user = new_record? ? author : current_journal.try(:user)
626
629
627 required_attribute_names(user).each do |attribute|
630 required_attribute_names(user).each do |attribute|
628 if attribute =~ /^\d+$/
631 if attribute =~ /^\d+$/
629 attribute = attribute.to_i
632 attribute = attribute.to_i
630 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
633 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
631 if v && v.value.blank?
634 if v && v.value.blank?
632 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
635 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
633 end
636 end
634 else
637 else
635 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
638 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
636 errors.add attribute, :blank
639 errors.add attribute, :blank
637 end
640 end
638 end
641 end
639 end
642 end
640 end
643 end
641
644
642 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
645 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
643 # even if the user turns off the setting later
646 # even if the user turns off the setting later
644 def update_done_ratio_from_issue_status
647 def update_done_ratio_from_issue_status
645 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
648 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
646 self.done_ratio = status.default_done_ratio
649 self.done_ratio = status.default_done_ratio
647 end
650 end
648 end
651 end
649
652
650 def init_journal(user, notes = "")
653 def init_journal(user, notes = "")
651 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
654 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
652 end
655 end
653
656
654 # Returns the current journal or nil if it's not initialized
657 # Returns the current journal or nil if it's not initialized
655 def current_journal
658 def current_journal
656 @current_journal
659 @current_journal
657 end
660 end
658
661
659 # Returns the names of attributes that are journalized when updating the issue
662 # Returns the names of attributes that are journalized when updating the issue
660 def journalized_attribute_names
663 def journalized_attribute_names
661 Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
664 Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
662 end
665 end
663
666
664 # Returns the id of the last journal or nil
667 # Returns the id of the last journal or nil
665 def last_journal_id
668 def last_journal_id
666 if new_record?
669 if new_record?
667 nil
670 nil
668 else
671 else
669 journals.maximum(:id)
672 journals.maximum(:id)
670 end
673 end
671 end
674 end
672
675
673 # Returns a scope for journals that have an id greater than journal_id
676 # Returns a scope for journals that have an id greater than journal_id
674 def journals_after(journal_id)
677 def journals_after(journal_id)
675 scope = journals.reorder("#{Journal.table_name}.id ASC")
678 scope = journals.reorder("#{Journal.table_name}.id ASC")
676 if journal_id.present?
679 if journal_id.present?
677 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
680 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
678 end
681 end
679 scope
682 scope
680 end
683 end
681
684
682 # Returns the initial status of the issue
685 # Returns the initial status of the issue
683 # Returns nil for a new issue
686 # Returns nil for a new issue
684 def status_was
687 def status_was
685 if status_id_changed?
688 if status_id_changed?
686 if status_id_was.to_i > 0
689 if status_id_was.to_i > 0
687 @status_was ||= IssueStatus.find_by_id(status_id_was)
690 @status_was ||= IssueStatus.find_by_id(status_id_was)
688 end
691 end
689 else
692 else
690 @status_was ||= status
693 @status_was ||= status
691 end
694 end
692 end
695 end
693
696
694 # Return true if the issue is closed, otherwise false
697 # Return true if the issue is closed, otherwise false
695 def closed?
698 def closed?
696 status.present? && status.is_closed?
699 status.present? && status.is_closed?
697 end
700 end
698
701
699 # Returns true if the issue was closed when loaded
702 # Returns true if the issue was closed when loaded
700 def was_closed?
703 def was_closed?
701 status_was.present? && status_was.is_closed?
704 status_was.present? && status_was.is_closed?
702 end
705 end
703
706
704 # Return true if the issue is being reopened
707 # Return true if the issue is being reopened
705 def reopening?
708 def reopening?
706 if new_record?
709 if new_record?
707 false
710 false
708 else
711 else
709 status_id_changed? && !closed? && was_closed?
712 status_id_changed? && !closed? && was_closed?
710 end
713 end
711 end
714 end
712 alias :reopened? :reopening?
715 alias :reopened? :reopening?
713
716
714 # Return true if the issue is being closed
717 # Return true if the issue is being closed
715 def closing?
718 def closing?
716 if new_record?
719 if new_record?
717 closed?
720 closed?
718 else
721 else
719 status_id_changed? && closed? && !was_closed?
722 status_id_changed? && closed? && !was_closed?
720 end
723 end
721 end
724 end
722
725
723 # Returns true if the issue is overdue
726 # Returns true if the issue is overdue
724 def overdue?
727 def overdue?
725 due_date.present? && (due_date < Date.today) && !closed?
728 due_date.present? && (due_date < Date.today) && !closed?
726 end
729 end
727
730
728 # Is the amount of work done less than it should for the due date
731 # Is the amount of work done less than it should for the due date
729 def behind_schedule?
732 def behind_schedule?
730 return false if start_date.nil? || due_date.nil?
733 return false if start_date.nil? || due_date.nil?
731 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
734 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
732 return done_date <= Date.today
735 return done_date <= Date.today
733 end
736 end
734
737
735 # Does this issue have children?
738 # Does this issue have children?
736 def children?
739 def children?
737 !leaf?
740 !leaf?
738 end
741 end
739
742
740 # Users the issue can be assigned to
743 # Users the issue can be assigned to
741 def assignable_users
744 def assignable_users
742 users = project.assignable_users.to_a
745 users = project.assignable_users.to_a
743 users << author if author
746 users << author if author
744 users << assigned_to if assigned_to
747 users << assigned_to if assigned_to
745 users.uniq.sort
748 users.uniq.sort
746 end
749 end
747
750
748 # Versions that the issue can be assigned to
751 # Versions that the issue can be assigned to
749 def assignable_versions
752 def assignable_versions
750 return @assignable_versions if @assignable_versions
753 return @assignable_versions if @assignable_versions
751
754
752 versions = project.shared_versions.open.to_a
755 versions = project.shared_versions.open.to_a
753 if fixed_version
756 if fixed_version
754 if fixed_version_id_changed?
757 if fixed_version_id_changed?
755 # nothing to do
758 # nothing to do
756 elsif project_id_changed?
759 elsif project_id_changed?
757 if project.shared_versions.include?(fixed_version)
760 if project.shared_versions.include?(fixed_version)
758 versions << fixed_version
761 versions << fixed_version
759 end
762 end
760 else
763 else
761 versions << fixed_version
764 versions << fixed_version
762 end
765 end
763 end
766 end
764 @assignable_versions = versions.uniq.sort
767 @assignable_versions = versions.uniq.sort
765 end
768 end
766
769
767 # Returns true if this issue is blocked by another issue that is still open
770 # Returns true if this issue is blocked by another issue that is still open
768 def blocked?
771 def blocked?
769 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
772 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
770 end
773 end
771
774
772 # Returns the default status of the issue based on its tracker
775 # Returns the default status of the issue based on its tracker
773 # Returns nil if tracker is nil
776 # Returns nil if tracker is nil
774 def default_status
777 def default_status
775 tracker.try(:default_status)
778 tracker.try(:default_status)
776 end
779 end
777
780
778 # Returns an array of statuses that user is able to apply
781 # Returns an array of statuses that user is able to apply
779 def new_statuses_allowed_to(user=User.current, include_default=false)
782 def new_statuses_allowed_to(user=User.current, include_default=false)
780 if new_record? && @copied_from
783 if new_record? && @copied_from
781 [default_status, @copied_from.status].compact.uniq.sort
784 [default_status, @copied_from.status].compact.uniq.sort
782 else
785 else
783 initial_status = nil
786 initial_status = nil
784 if new_record?
787 if new_record?
785 initial_status = default_status
788 initial_status = default_status
786 elsif tracker_id_changed?
789 elsif tracker_id_changed?
787 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
790 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
788 initial_status = default_status
791 initial_status = default_status
789 elsif tracker.issue_status_ids.include?(status_id_was)
792 elsif tracker.issue_status_ids.include?(status_id_was)
790 initial_status = IssueStatus.find_by_id(status_id_was)
793 initial_status = IssueStatus.find_by_id(status_id_was)
791 else
794 else
792 initial_status = default_status
795 initial_status = default_status
793 end
796 end
794 else
797 else
795 initial_status = status_was
798 initial_status = status_was
796 end
799 end
797
800
798 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
801 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
799 assignee_transitions_allowed = initial_assigned_to_id.present? &&
802 assignee_transitions_allowed = initial_assigned_to_id.present? &&
800 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
803 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
801
804
802 statuses = []
805 statuses = []
803 if initial_status
806 if initial_status
804 statuses += initial_status.find_new_statuses_allowed_to(
807 statuses += initial_status.find_new_statuses_allowed_to(
805 user.admin ? Role.all.to_a : user.roles_for_project(project),
808 user.admin ? Role.all.to_a : user.roles_for_project(project),
806 tracker,
809 tracker,
807 author == user,
810 author == user,
808 assignee_transitions_allowed
811 assignee_transitions_allowed
809 )
812 )
810 end
813 end
811 statuses << initial_status unless statuses.empty?
814 statuses << initial_status unless statuses.empty?
812 statuses << default_status if include_default
815 statuses << default_status if include_default
813 statuses = statuses.compact.uniq.sort
816 statuses = statuses.compact.uniq.sort
814 if blocked?
817 if blocked?
815 statuses.reject!(&:is_closed?)
818 statuses.reject!(&:is_closed?)
816 end
819 end
817 statuses
820 statuses
818 end
821 end
819 end
822 end
820
823
821 # Returns the previous assignee if changed
824 # Returns the previous assignee if changed
822 def assigned_to_was
825 def assigned_to_was
823 # assigned_to_id_was is reset before after_save callbacks
826 # assigned_to_id_was is reset before after_save callbacks
824 user_id = @previous_assigned_to_id || assigned_to_id_was
827 user_id = @previous_assigned_to_id || assigned_to_id_was
825 if user_id && user_id != assigned_to_id
828 if user_id && user_id != assigned_to_id
826 @assigned_to_was ||= User.find_by_id(user_id)
829 @assigned_to_was ||= User.find_by_id(user_id)
827 end
830 end
828 end
831 end
829
832
830 # Returns the users that should be notified
833 # Returns the users that should be notified
831 def notified_users
834 def notified_users
832 notified = []
835 notified = []
833 # Author and assignee are always notified unless they have been
836 # Author and assignee are always notified unless they have been
834 # locked or don't want to be notified
837 # locked or don't want to be notified
835 notified << author if author
838 notified << author if author
836 if assigned_to
839 if assigned_to
837 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
840 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
838 end
841 end
839 if assigned_to_was
842 if assigned_to_was
840 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
843 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
841 end
844 end
842 notified = notified.select {|u| u.active? && u.notify_about?(self)}
845 notified = notified.select {|u| u.active? && u.notify_about?(self)}
843
846
844 notified += project.notified_users
847 notified += project.notified_users
845 notified.uniq!
848 notified.uniq!
846 # Remove users that can not view the issue
849 # Remove users that can not view the issue
847 notified.reject! {|user| !visible?(user)}
850 notified.reject! {|user| !visible?(user)}
848 notified
851 notified
849 end
852 end
850
853
851 # Returns the email addresses that should be notified
854 # Returns the email addresses that should be notified
852 def recipients
855 def recipients
853 notified_users.collect(&:mail)
856 notified_users.collect(&:mail)
854 end
857 end
855
858
856 def each_notification(users, &block)
859 def each_notification(users, &block)
857 if users.any?
860 if users.any?
858 if custom_field_values.detect {|value| !value.custom_field.visible?}
861 if custom_field_values.detect {|value| !value.custom_field.visible?}
859 users_by_custom_field_visibility = users.group_by do |user|
862 users_by_custom_field_visibility = users.group_by do |user|
860 visible_custom_field_values(user).map(&:custom_field_id).sort
863 visible_custom_field_values(user).map(&:custom_field_id).sort
861 end
864 end
862 users_by_custom_field_visibility.values.each do |users|
865 users_by_custom_field_visibility.values.each do |users|
863 yield(users)
866 yield(users)
864 end
867 end
865 else
868 else
866 yield(users)
869 yield(users)
867 end
870 end
868 end
871 end
869 end
872 end
870
873
871 # Returns the number of hours spent on this issue
874 # Returns the number of hours spent on this issue
872 def spent_hours
875 def spent_hours
873 @spent_hours ||= time_entries.sum(:hours) || 0
876 @spent_hours ||= time_entries.sum(:hours) || 0
874 end
877 end
875
878
876 # Returns the total number of hours spent on this issue and its descendants
879 # Returns the total number of hours spent on this issue and its descendants
877 #
880 #
878 # Example:
881 # Example:
879 # spent_hours => 0.0
882 # spent_hours => 0.0
880 # spent_hours => 50.2
883 # spent_hours => 50.2
881 def total_spent_hours
884 def total_spent_hours
882 @total_spent_hours ||=
885 @total_spent_hours ||=
883 self_and_descendants.
886 self_and_descendants.
884 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
887 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
885 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
888 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
886 end
889 end
887
890
888 def relations
891 def relations
889 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
892 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
890 end
893 end
891
894
892 # Preloads relations for a collection of issues
895 # Preloads relations for a collection of issues
893 def self.load_relations(issues)
896 def self.load_relations(issues)
894 if issues.any?
897 if issues.any?
895 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
898 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
896 issues.each do |issue|
899 issues.each do |issue|
897 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
900 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
898 end
901 end
899 end
902 end
900 end
903 end
901
904
902 # Preloads visible spent time for a collection of issues
905 # Preloads visible spent time for a collection of issues
903 def self.load_visible_spent_hours(issues, user=User.current)
906 def self.load_visible_spent_hours(issues, user=User.current)
904 if issues.any?
907 if issues.any?
905 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
908 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
906 issues.each do |issue|
909 issues.each do |issue|
907 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
910 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
908 end
911 end
909 end
912 end
910 end
913 end
911
914
912 # Preloads visible relations for a collection of issues
915 # Preloads visible relations for a collection of issues
913 def self.load_visible_relations(issues, user=User.current)
916 def self.load_visible_relations(issues, user=User.current)
914 if issues.any?
917 if issues.any?
915 issue_ids = issues.map(&:id)
918 issue_ids = issues.map(&:id)
916 # Relations with issue_from in given issues and visible issue_to
919 # Relations with issue_from in given issues and visible issue_to
917 relations_from = IssueRelation.joins(:issue_to => :project).
920 relations_from = IssueRelation.joins(:issue_to => :project).
918 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
921 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
919 # Relations with issue_to in given issues and visible issue_from
922 # Relations with issue_to in given issues and visible issue_from
920 relations_to = IssueRelation.joins(:issue_from => :project).
923 relations_to = IssueRelation.joins(:issue_from => :project).
921 where(visible_condition(user)).
924 where(visible_condition(user)).
922 where(:issue_to_id => issue_ids).to_a
925 where(:issue_to_id => issue_ids).to_a
923 issues.each do |issue|
926 issues.each do |issue|
924 relations =
927 relations =
925 relations_from.select {|relation| relation.issue_from_id == issue.id} +
928 relations_from.select {|relation| relation.issue_from_id == issue.id} +
926 relations_to.select {|relation| relation.issue_to_id == issue.id}
929 relations_to.select {|relation| relation.issue_to_id == issue.id}
927
930
928 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
931 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
929 end
932 end
930 end
933 end
931 end
934 end
932
935
933 # Finds an issue relation given its id.
936 # Finds an issue relation given its id.
934 def find_relation(relation_id)
937 def find_relation(relation_id)
935 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
938 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
936 end
939 end
937
940
938 # Returns all the other issues that depend on the issue
941 # Returns all the other issues that depend on the issue
939 # The algorithm is a modified breadth first search (bfs)
942 # The algorithm is a modified breadth first search (bfs)
940 def all_dependent_issues(except=[])
943 def all_dependent_issues(except=[])
941 # The found dependencies
944 # The found dependencies
942 dependencies = []
945 dependencies = []
943
946
944 # The visited flag for every node (issue) used by the breadth first search
947 # The visited flag for every node (issue) used by the breadth first search
945 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
948 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
946
949
947 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
950 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
948 # the issue when it is processed.
951 # the issue when it is processed.
949
952
950 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
953 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
951 # but its children will not be added to the queue when it is processed.
954 # but its children will not be added to the queue when it is processed.
952
955
953 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
956 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
954 # the queue, but its children have not been added.
957 # the queue, but its children have not been added.
955
958
956 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
959 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
957 # the children still need to be processed.
960 # the children still need to be processed.
958
961
959 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
962 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
960 # added as dependent issues. It needs no further processing.
963 # added as dependent issues. It needs no further processing.
961
964
962 issue_status = Hash.new(eNOT_DISCOVERED)
965 issue_status = Hash.new(eNOT_DISCOVERED)
963
966
964 # The queue
967 # The queue
965 queue = []
968 queue = []
966
969
967 # Initialize the bfs, add start node (self) to the queue
970 # Initialize the bfs, add start node (self) to the queue
968 queue << self
971 queue << self
969 issue_status[self] = ePROCESS_ALL
972 issue_status[self] = ePROCESS_ALL
970
973
971 while (!queue.empty?) do
974 while (!queue.empty?) do
972 current_issue = queue.shift
975 current_issue = queue.shift
973 current_issue_status = issue_status[current_issue]
976 current_issue_status = issue_status[current_issue]
974 dependencies << current_issue
977 dependencies << current_issue
975
978
976 # Add parent to queue, if not already in it.
979 # Add parent to queue, if not already in it.
977 parent = current_issue.parent
980 parent = current_issue.parent
978 parent_status = issue_status[parent]
981 parent_status = issue_status[parent]
979
982
980 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
983 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
981 queue << parent
984 queue << parent
982 issue_status[parent] = ePROCESS_RELATIONS_ONLY
985 issue_status[parent] = ePROCESS_RELATIONS_ONLY
983 end
986 end
984
987
985 # Add children to queue, but only if they are not already in it and
988 # Add children to queue, but only if they are not already in it and
986 # the children of the current node need to be processed.
989 # the children of the current node need to be processed.
987 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
990 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
988 current_issue.children.each do |child|
991 current_issue.children.each do |child|
989 next if except.include?(child)
992 next if except.include?(child)
990
993
991 if (issue_status[child] == eNOT_DISCOVERED)
994 if (issue_status[child] == eNOT_DISCOVERED)
992 queue << child
995 queue << child
993 issue_status[child] = ePROCESS_ALL
996 issue_status[child] = ePROCESS_ALL
994 elsif (issue_status[child] == eRELATIONS_PROCESSED)
997 elsif (issue_status[child] == eRELATIONS_PROCESSED)
995 queue << child
998 queue << child
996 issue_status[child] = ePROCESS_CHILDREN_ONLY
999 issue_status[child] = ePROCESS_CHILDREN_ONLY
997 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1000 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
998 queue << child
1001 queue << child
999 issue_status[child] = ePROCESS_ALL
1002 issue_status[child] = ePROCESS_ALL
1000 end
1003 end
1001 end
1004 end
1002 end
1005 end
1003
1006
1004 # Add related issues to the queue, if they are not already in it.
1007 # Add related issues to the queue, if they are not already in it.
1005 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1008 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1006 next if except.include?(related_issue)
1009 next if except.include?(related_issue)
1007
1010
1008 if (issue_status[related_issue] == eNOT_DISCOVERED)
1011 if (issue_status[related_issue] == eNOT_DISCOVERED)
1009 queue << related_issue
1012 queue << related_issue
1010 issue_status[related_issue] = ePROCESS_ALL
1013 issue_status[related_issue] = ePROCESS_ALL
1011 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1014 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1012 queue << related_issue
1015 queue << related_issue
1013 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1016 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1014 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1017 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1015 queue << related_issue
1018 queue << related_issue
1016 issue_status[related_issue] = ePROCESS_ALL
1019 issue_status[related_issue] = ePROCESS_ALL
1017 end
1020 end
1018 end
1021 end
1019
1022
1020 # Set new status for current issue
1023 # Set new status for current issue
1021 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1024 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1022 issue_status[current_issue] = eALL_PROCESSED
1025 issue_status[current_issue] = eALL_PROCESSED
1023 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1026 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1024 issue_status[current_issue] = eRELATIONS_PROCESSED
1027 issue_status[current_issue] = eRELATIONS_PROCESSED
1025 end
1028 end
1026 end # while
1029 end # while
1027
1030
1028 # Remove the issues from the "except" parameter from the result array
1031 # Remove the issues from the "except" parameter from the result array
1029 dependencies -= except
1032 dependencies -= except
1030 dependencies.delete(self)
1033 dependencies.delete(self)
1031
1034
1032 dependencies
1035 dependencies
1033 end
1036 end
1034
1037
1035 # Returns an array of issues that duplicate this one
1038 # Returns an array of issues that duplicate this one
1036 def duplicates
1039 def duplicates
1037 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1040 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1038 end
1041 end
1039
1042
1040 # Returns the due date or the target due date if any
1043 # Returns the due date or the target due date if any
1041 # Used on gantt chart
1044 # Used on gantt chart
1042 def due_before
1045 def due_before
1043 due_date || (fixed_version ? fixed_version.effective_date : nil)
1046 due_date || (fixed_version ? fixed_version.effective_date : nil)
1044 end
1047 end
1045
1048
1046 # Returns the time scheduled for this issue.
1049 # Returns the time scheduled for this issue.
1047 #
1050 #
1048 # Example:
1051 # Example:
1049 # Start Date: 2/26/09, End Date: 3/04/09
1052 # Start Date: 2/26/09, End Date: 3/04/09
1050 # duration => 6
1053 # duration => 6
1051 def duration
1054 def duration
1052 (start_date && due_date) ? due_date - start_date : 0
1055 (start_date && due_date) ? due_date - start_date : 0
1053 end
1056 end
1054
1057
1055 # Returns the duration in working days
1058 # Returns the duration in working days
1056 def working_duration
1059 def working_duration
1057 (start_date && due_date) ? working_days(start_date, due_date) : 0
1060 (start_date && due_date) ? working_days(start_date, due_date) : 0
1058 end
1061 end
1059
1062
1060 def soonest_start(reload=false)
1063 def soonest_start(reload=false)
1061 @soonest_start = nil if reload
1064 @soonest_start = nil if reload
1062 @soonest_start ||= (
1065 @soonest_start ||= (
1063 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1066 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1064 [(@parent_issue || parent).try(:soonest_start)]
1067 [(@parent_issue || parent).try(:soonest_start)]
1065 ).compact.max
1068 ).compact.max
1066 end
1069 end
1067
1070
1068 # Sets start_date on the given date or the next working day
1071 # Sets start_date on the given date or the next working day
1069 # and changes due_date to keep the same working duration.
1072 # and changes due_date to keep the same working duration.
1070 def reschedule_on(date)
1073 def reschedule_on(date)
1071 wd = working_duration
1074 wd = working_duration
1072 date = next_working_date(date)
1075 date = next_working_date(date)
1073 self.start_date = date
1076 self.start_date = date
1074 self.due_date = add_working_days(date, wd)
1077 self.due_date = add_working_days(date, wd)
1075 end
1078 end
1076
1079
1077 # Reschedules the issue on the given date or the next working day and saves the record.
1080 # Reschedules the issue on the given date or the next working day and saves the record.
1078 # If the issue is a parent task, this is done by rescheduling its subtasks.
1081 # If the issue is a parent task, this is done by rescheduling its subtasks.
1079 def reschedule_on!(date)
1082 def reschedule_on!(date)
1080 return if date.nil?
1083 return if date.nil?
1081 if leaf?
1084 if leaf?
1082 if start_date.nil? || start_date != date
1085 if start_date.nil? || start_date != date
1083 if start_date && start_date > date
1086 if start_date && start_date > date
1084 # Issue can not be moved earlier than its soonest start date
1087 # Issue can not be moved earlier than its soonest start date
1085 date = [soonest_start(true), date].compact.max
1088 date = [soonest_start(true), date].compact.max
1086 end
1089 end
1087 reschedule_on(date)
1090 reschedule_on(date)
1088 begin
1091 begin
1089 save
1092 save
1090 rescue ActiveRecord::StaleObjectError
1093 rescue ActiveRecord::StaleObjectError
1091 reload
1094 reload
1092 reschedule_on(date)
1095 reschedule_on(date)
1093 save
1096 save
1094 end
1097 end
1095 end
1098 end
1096 else
1099 else
1097 leaves.each do |leaf|
1100 leaves.each do |leaf|
1098 if leaf.start_date
1101 if leaf.start_date
1099 # Only move subtask if it starts at the same date as the parent
1102 # Only move subtask if it starts at the same date as the parent
1100 # or if it starts before the given date
1103 # or if it starts before the given date
1101 if start_date == leaf.start_date || date > leaf.start_date
1104 if start_date == leaf.start_date || date > leaf.start_date
1102 leaf.reschedule_on!(date)
1105 leaf.reschedule_on!(date)
1103 end
1106 end
1104 else
1107 else
1105 leaf.reschedule_on!(date)
1108 leaf.reschedule_on!(date)
1106 end
1109 end
1107 end
1110 end
1108 end
1111 end
1109 end
1112 end
1110
1113
1111 def <=>(issue)
1114 def <=>(issue)
1112 if issue.nil?
1115 if issue.nil?
1113 -1
1116 -1
1114 elsif root_id != issue.root_id
1117 elsif root_id != issue.root_id
1115 (root_id || 0) <=> (issue.root_id || 0)
1118 (root_id || 0) <=> (issue.root_id || 0)
1116 else
1119 else
1117 (lft || 0) <=> (issue.lft || 0)
1120 (lft || 0) <=> (issue.lft || 0)
1118 end
1121 end
1119 end
1122 end
1120
1123
1121 def to_s
1124 def to_s
1122 "#{tracker} ##{id}: #{subject}"
1125 "#{tracker} ##{id}: #{subject}"
1123 end
1126 end
1124
1127
1125 # Returns a string of css classes that apply to the issue
1128 # Returns a string of css classes that apply to the issue
1126 def css_classes(user=User.current)
1129 def css_classes(user=User.current)
1127 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1130 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1128 s << ' closed' if closed?
1131 s << ' closed' if closed?
1129 s << ' overdue' if overdue?
1132 s << ' overdue' if overdue?
1130 s << ' child' if child?
1133 s << ' child' if child?
1131 s << ' parent' unless leaf?
1134 s << ' parent' unless leaf?
1132 s << ' private' if is_private?
1135 s << ' private' if is_private?
1133 if user.logged?
1136 if user.logged?
1134 s << ' created-by-me' if author_id == user.id
1137 s << ' created-by-me' if author_id == user.id
1135 s << ' assigned-to-me' if assigned_to_id == user.id
1138 s << ' assigned-to-me' if assigned_to_id == user.id
1136 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1139 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1137 end
1140 end
1138 s
1141 s
1139 end
1142 end
1140
1143
1141 # Unassigns issues from +version+ if it's no longer shared with issue's project
1144 # Unassigns issues from +version+ if it's no longer shared with issue's project
1142 def self.update_versions_from_sharing_change(version)
1145 def self.update_versions_from_sharing_change(version)
1143 # Update issues assigned to the version
1146 # Update issues assigned to the version
1144 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1147 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1145 end
1148 end
1146
1149
1147 # Unassigns issues from versions that are no longer shared
1150 # Unassigns issues from versions that are no longer shared
1148 # after +project+ was moved
1151 # after +project+ was moved
1149 def self.update_versions_from_hierarchy_change(project)
1152 def self.update_versions_from_hierarchy_change(project)
1150 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1153 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1151 # Update issues of the moved projects and issues assigned to a version of a moved project
1154 # Update issues of the moved projects and issues assigned to a version of a moved project
1152 Issue.update_versions(
1155 Issue.update_versions(
1153 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1156 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1154 moved_project_ids, moved_project_ids]
1157 moved_project_ids, moved_project_ids]
1155 )
1158 )
1156 end
1159 end
1157
1160
1158 def parent_issue_id=(arg)
1161 def parent_issue_id=(arg)
1159 s = arg.to_s.strip.presence
1162 s = arg.to_s.strip.presence
1160 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1163 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1161 @invalid_parent_issue_id = nil
1164 @invalid_parent_issue_id = nil
1162 elsif s.blank?
1165 elsif s.blank?
1163 @parent_issue = nil
1166 @parent_issue = nil
1164 @invalid_parent_issue_id = nil
1167 @invalid_parent_issue_id = nil
1165 else
1168 else
1166 @parent_issue = nil
1169 @parent_issue = nil
1167 @invalid_parent_issue_id = arg
1170 @invalid_parent_issue_id = arg
1168 end
1171 end
1169 end
1172 end
1170
1173
1171 def parent_issue_id
1174 def parent_issue_id
1172 if @invalid_parent_issue_id
1175 if @invalid_parent_issue_id
1173 @invalid_parent_issue_id
1176 @invalid_parent_issue_id
1174 elsif instance_variable_defined? :@parent_issue
1177 elsif instance_variable_defined? :@parent_issue
1175 @parent_issue.nil? ? nil : @parent_issue.id
1178 @parent_issue.nil? ? nil : @parent_issue.id
1176 else
1179 else
1177 parent_id
1180 parent_id
1178 end
1181 end
1179 end
1182 end
1180
1183
1181 def set_parent_id
1184 def set_parent_id
1182 self.parent_id = parent_issue_id
1185 self.parent_id = parent_issue_id
1183 end
1186 end
1184
1187
1185 # Returns true if issue's project is a valid
1188 # Returns true if issue's project is a valid
1186 # parent issue project
1189 # parent issue project
1187 def valid_parent_project?(issue=parent)
1190 def valid_parent_project?(issue=parent)
1188 return true if issue.nil? || issue.project_id == project_id
1191 return true if issue.nil? || issue.project_id == project_id
1189
1192
1190 case Setting.cross_project_subtasks
1193 case Setting.cross_project_subtasks
1191 when 'system'
1194 when 'system'
1192 true
1195 true
1193 when 'tree'
1196 when 'tree'
1194 issue.project.root == project.root
1197 issue.project.root == project.root
1195 when 'hierarchy'
1198 when 'hierarchy'
1196 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1199 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1197 when 'descendants'
1200 when 'descendants'
1198 issue.project.is_or_is_ancestor_of?(project)
1201 issue.project.is_or_is_ancestor_of?(project)
1199 else
1202 else
1200 false
1203 false
1201 end
1204 end
1202 end
1205 end
1203
1206
1204 # Returns an issue scope based on project and scope
1207 # Returns an issue scope based on project and scope
1205 def self.cross_project_scope(project, scope=nil)
1208 def self.cross_project_scope(project, scope=nil)
1206 if project.nil?
1209 if project.nil?
1207 return Issue
1210 return Issue
1208 end
1211 end
1209 case scope
1212 case scope
1210 when 'all', 'system'
1213 when 'all', 'system'
1211 Issue
1214 Issue
1212 when 'tree'
1215 when 'tree'
1213 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1216 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1214 :lft => project.root.lft, :rgt => project.root.rgt)
1217 :lft => project.root.lft, :rgt => project.root.rgt)
1215 when 'hierarchy'
1218 when 'hierarchy'
1216 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)",
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 :lft => project.lft, :rgt => project.rgt)
1220 :lft => project.lft, :rgt => project.rgt)
1218 when 'descendants'
1221 when 'descendants'
1219 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1222 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1220 :lft => project.lft, :rgt => project.rgt)
1223 :lft => project.lft, :rgt => project.rgt)
1221 else
1224 else
1222 Issue.where(:project_id => project.id)
1225 Issue.where(:project_id => project.id)
1223 end
1226 end
1224 end
1227 end
1225
1228
1226 def self.by_tracker(project)
1229 def self.by_tracker(project)
1227 count_and_group_by(:project => project, :association => :tracker)
1230 count_and_group_by(:project => project, :association => :tracker)
1228 end
1231 end
1229
1232
1230 def self.by_version(project)
1233 def self.by_version(project)
1231 count_and_group_by(:project => project, :association => :fixed_version)
1234 count_and_group_by(:project => project, :association => :fixed_version)
1232 end
1235 end
1233
1236
1234 def self.by_priority(project)
1237 def self.by_priority(project)
1235 count_and_group_by(:project => project, :association => :priority)
1238 count_and_group_by(:project => project, :association => :priority)
1236 end
1239 end
1237
1240
1238 def self.by_category(project)
1241 def self.by_category(project)
1239 count_and_group_by(:project => project, :association => :category)
1242 count_and_group_by(:project => project, :association => :category)
1240 end
1243 end
1241
1244
1242 def self.by_assigned_to(project)
1245 def self.by_assigned_to(project)
1243 count_and_group_by(:project => project, :association => :assigned_to)
1246 count_and_group_by(:project => project, :association => :assigned_to)
1244 end
1247 end
1245
1248
1246 def self.by_author(project)
1249 def self.by_author(project)
1247 count_and_group_by(:project => project, :association => :author)
1250 count_and_group_by(:project => project, :association => :author)
1248 end
1251 end
1249
1252
1250 def self.by_subproject(project)
1253 def self.by_subproject(project)
1251 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1254 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1252 r.reject {|r| r["project_id"] == project.id.to_s}
1255 r.reject {|r| r["project_id"] == project.id.to_s}
1253 end
1256 end
1254
1257
1255 # Query generator for selecting groups of issue counts for a project
1258 # Query generator for selecting groups of issue counts for a project
1256 # based on specific criteria
1259 # based on specific criteria
1257 #
1260 #
1258 # Options
1261 # Options
1259 # * project - Project to search in.
1262 # * project - Project to search in.
1260 # * with_subprojects - Includes subprojects issues if set to true.
1263 # * with_subprojects - Includes subprojects issues if set to true.
1261 # * association - Symbol. Association for grouping.
1264 # * association - Symbol. Association for grouping.
1262 def self.count_and_group_by(options)
1265 def self.count_and_group_by(options)
1263 assoc = reflect_on_association(options[:association])
1266 assoc = reflect_on_association(options[:association])
1264 select_field = assoc.foreign_key
1267 select_field = assoc.foreign_key
1265
1268
1266 Issue.
1269 Issue.
1267 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1270 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1268 joins(:status, assoc.name).
1271 joins(:status, assoc.name).
1269 group(:status_id, :is_closed, select_field).
1272 group(:status_id, :is_closed, select_field).
1270 count.
1273 count.
1271 map do |columns, total|
1274 map do |columns, total|
1272 status_id, is_closed, field_value = columns
1275 status_id, is_closed, field_value = columns
1273 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1276 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1274 {
1277 {
1275 "status_id" => status_id.to_s,
1278 "status_id" => status_id.to_s,
1276 "closed" => is_closed,
1279 "closed" => is_closed,
1277 select_field => field_value.to_s,
1280 select_field => field_value.to_s,
1278 "total" => total.to_s
1281 "total" => total.to_s
1279 }
1282 }
1280 end
1283 end
1281 end
1284 end
1282
1285
1283 # Returns a scope of projects that user can assign the issue to
1286 # Returns a scope of projects that user can assign the issue to
1284 def allowed_target_projects(user=User.current)
1287 def allowed_target_projects(user=User.current)
1285 current_project = new_record? ? nil : project
1288 current_project = new_record? ? nil : project
1286 self.class.allowed_target_projects(user, current_project)
1289 self.class.allowed_target_projects(user, current_project)
1287 end
1290 end
1288
1291
1289 # Returns a scope of projects that user can assign issues to
1292 # Returns a scope of projects that user can assign issues to
1290 # If current_project is given, it will be included in the scope
1293 # If current_project is given, it will be included in the scope
1291 def self.allowed_target_projects(user=User.current, current_project=nil)
1294 def self.allowed_target_projects(user=User.current, current_project=nil)
1292 condition = Project.allowed_to_condition(user, :add_issues)
1295 condition = Project.allowed_to_condition(user, :add_issues)
1293 if current_project
1296 if current_project
1294 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1297 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1295 end
1298 end
1296 Project.where(condition)
1299 Project.where(condition)
1297 end
1300 end
1298
1301
1299 private
1302 private
1300
1303
1301 def after_project_change
1304 def after_project_change
1302 # Update project_id on related time entries
1305 # Update project_id on related time entries
1303 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1306 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1304
1307
1305 # Delete issue relations
1308 # Delete issue relations
1306 unless Setting.cross_project_issue_relations?
1309 unless Setting.cross_project_issue_relations?
1307 relations_from.clear
1310 relations_from.clear
1308 relations_to.clear
1311 relations_to.clear
1309 end
1312 end
1310
1313
1311 # Move subtasks that were in the same project
1314 # Move subtasks that were in the same project
1312 children.each do |child|
1315 children.each do |child|
1313 next unless child.project_id == project_id_was
1316 next unless child.project_id == project_id_was
1314 # Change project and keep project
1317 # Change project and keep project
1315 child.send :project=, project, true
1318 child.send :project=, project, true
1316 unless child.save
1319 unless child.save
1317 raise ActiveRecord::Rollback
1320 raise ActiveRecord::Rollback
1318 end
1321 end
1319 end
1322 end
1320 end
1323 end
1321
1324
1322 # Callback for after the creation of an issue by copy
1325 # Callback for after the creation of an issue by copy
1323 # * adds a "copied to" relation with the copied issue
1326 # * adds a "copied to" relation with the copied issue
1324 # * copies subtasks from the copied issue
1327 # * copies subtasks from the copied issue
1325 def after_create_from_copy
1328 def after_create_from_copy
1326 return unless copy? && !@after_create_from_copy_handled
1329 return unless copy? && !@after_create_from_copy_handled
1327
1330
1328 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1331 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1329 if @current_journal
1332 if @current_journal
1330 @copied_from.init_journal(@current_journal.user)
1333 @copied_from.init_journal(@current_journal.user)
1331 end
1334 end
1332 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1335 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1333 unless relation.save
1336 unless relation.save
1334 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
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 end
1338 end
1336 end
1339 end
1337
1340
1338 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1341 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1339 copy_options = (@copy_options || {}).merge(:subtasks => false)
1342 copy_options = (@copy_options || {}).merge(:subtasks => false)
1340 copied_issue_ids = {@copied_from.id => self.id}
1343 copied_issue_ids = {@copied_from.id => self.id}
1341 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1344 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1342 # Do not copy self when copying an issue as a descendant of the copied issue
1345 # Do not copy self when copying an issue as a descendant of the copied issue
1343 next if child == self
1346 next if child == self
1344 # Do not copy subtasks of issues that were not copied
1347 # Do not copy subtasks of issues that were not copied
1345 next unless copied_issue_ids[child.parent_id]
1348 next unless copied_issue_ids[child.parent_id]
1346 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1349 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1347 unless child.visible?
1350 unless child.visible?
1348 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
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 next
1352 next
1350 end
1353 end
1351 copy = Issue.new.copy_from(child, copy_options)
1354 copy = Issue.new.copy_from(child, copy_options)
1352 if @current_journal
1355 if @current_journal
1353 copy.init_journal(@current_journal.user)
1356 copy.init_journal(@current_journal.user)
1354 end
1357 end
1355 copy.author = author
1358 copy.author = author
1356 copy.project = project
1359 copy.project = project
1357 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1360 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1358 unless copy.save
1361 unless copy.save
1359 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
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 next
1363 next
1361 end
1364 end
1362 copied_issue_ids[child.id] = copy.id
1365 copied_issue_ids[child.id] = copy.id
1363 end
1366 end
1364 end
1367 end
1365 @after_create_from_copy_handled = true
1368 @after_create_from_copy_handled = true
1366 end
1369 end
1367
1370
1368 def update_nested_set_attributes
1371 def update_nested_set_attributes
1369 if parent_id_changed?
1372 if parent_id_changed?
1370 update_nested_set_attributes_on_parent_change
1373 update_nested_set_attributes_on_parent_change
1371 end
1374 end
1372 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1375 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1373 end
1376 end
1374
1377
1375 # Updates the nested set for when an existing issue is moved
1378 # Updates the nested set for when an existing issue is moved
1376 def update_nested_set_attributes_on_parent_change
1379 def update_nested_set_attributes_on_parent_change
1377 former_parent_id = parent_id_was
1380 former_parent_id = parent_id_was
1378 # delete invalid relations of all descendants
1381 # delete invalid relations of all descendants
1379 self_and_descendants.each do |issue|
1382 self_and_descendants.each do |issue|
1380 issue.relations.each do |relation|
1383 issue.relations.each do |relation|
1381 relation.destroy unless relation.valid?
1384 relation.destroy unless relation.valid?
1382 end
1385 end
1383 end
1386 end
1384 # update former parent
1387 # update former parent
1385 recalculate_attributes_for(former_parent_id) if former_parent_id
1388 recalculate_attributes_for(former_parent_id) if former_parent_id
1386 end
1389 end
1387
1390
1388 def update_parent_attributes
1391 def update_parent_attributes
1389 if parent_id
1392 if parent_id
1390 recalculate_attributes_for(parent_id)
1393 recalculate_attributes_for(parent_id)
1391 association(:parent).reset
1394 association(:parent).reset
1392 end
1395 end
1393 end
1396 end
1394
1397
1395 def recalculate_attributes_for(issue_id)
1398 def recalculate_attributes_for(issue_id)
1396 if issue_id && p = Issue.find_by_id(issue_id)
1399 if issue_id && p = Issue.find_by_id(issue_id)
1397 # priority = highest priority of children
1400 # priority = highest priority of children
1398 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1401 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1399 p.priority = IssuePriority.find_by_position(priority_position)
1402 p.priority = IssuePriority.find_by_position(priority_position)
1400 end
1403 end
1401
1404
1402 # start/due dates = lowest/highest dates of children
1405 # start/due dates = lowest/highest dates of children
1403 p.start_date = p.children.minimum(:start_date)
1406 p.start_date = p.children.minimum(:start_date)
1404 p.due_date = p.children.maximum(:due_date)
1407 p.due_date = p.children.maximum(:due_date)
1405 if p.start_date && p.due_date && p.due_date < p.start_date
1408 if p.start_date && p.due_date && p.due_date < p.start_date
1406 p.start_date, p.due_date = p.due_date, p.start_date
1409 p.start_date, p.due_date = p.due_date, p.start_date
1407 end
1410 end
1408
1411
1409 # done ratio = weighted average ratio of leaves
1412 # done ratio = weighted average ratio of leaves
1410 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1413 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1411 leaves_count = p.leaves.count
1414 leaves_count = p.leaves.count
1412 if leaves_count > 0
1415 if leaves_count > 0
1413 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1416 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1414 if average == 0
1417 if average == 0
1415 average = 1
1418 average = 1
1416 end
1419 end
1417 done = p.leaves.joins(:status).
1420 done = p.leaves.joins(:status).
1418 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1421 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1419 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1422 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1420 progress = done / (average * leaves_count)
1423 progress = done / (average * leaves_count)
1421 p.done_ratio = progress.round
1424 p.done_ratio = progress.round
1422 end
1425 end
1423 end
1426 end
1424
1427
1425 # estimate = sum of leaves estimates
1428 # estimate = sum of leaves estimates
1426 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1429 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1427 p.estimated_hours = nil if p.estimated_hours == 0.0
1430 p.estimated_hours = nil if p.estimated_hours == 0.0
1428
1431
1429 # ancestors will be recursively updated
1432 # ancestors will be recursively updated
1430 p.save(:validate => false)
1433 p.save(:validate => false)
1431 end
1434 end
1432 end
1435 end
1433
1436
1434 # Update issues so their versions are not pointing to a
1437 # Update issues so their versions are not pointing to a
1435 # fixed_version that is not shared with the issue's project
1438 # fixed_version that is not shared with the issue's project
1436 def self.update_versions(conditions=nil)
1439 def self.update_versions(conditions=nil)
1437 # Only need to update issues with a fixed_version from
1440 # Only need to update issues with a fixed_version from
1438 # a different project and that is not systemwide shared
1441 # a different project and that is not systemwide shared
1439 Issue.joins(:project, :fixed_version).
1442 Issue.joins(:project, :fixed_version).
1440 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1443 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1441 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1444 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1442 " AND #{Version.table_name}.sharing <> 'system'").
1445 " AND #{Version.table_name}.sharing <> 'system'").
1443 where(conditions).each do |issue|
1446 where(conditions).each do |issue|
1444 next if issue.project.nil? || issue.fixed_version.nil?
1447 next if issue.project.nil? || issue.fixed_version.nil?
1445 unless issue.project.shared_versions.include?(issue.fixed_version)
1448 unless issue.project.shared_versions.include?(issue.fixed_version)
1446 issue.init_journal(User.current)
1449 issue.init_journal(User.current)
1447 issue.fixed_version = nil
1450 issue.fixed_version = nil
1448 issue.save
1451 issue.save
1449 end
1452 end
1450 end
1453 end
1451 end
1454 end
1452
1455
1453 # Callback on file attachment
1456 # Callback on file attachment
1454 def attachment_added(attachment)
1457 def attachment_added(attachment)
1455 if current_journal && !attachment.new_record?
1458 if current_journal && !attachment.new_record?
1456 current_journal.journalize_attachment(attachment, :added)
1459 current_journal.journalize_attachment(attachment, :added)
1457 end
1460 end
1458 end
1461 end
1459
1462
1460 # Callback on attachment deletion
1463 # Callback on attachment deletion
1461 def attachment_removed(attachment)
1464 def attachment_removed(attachment)
1462 if current_journal && !attachment.new_record?
1465 if current_journal && !attachment.new_record?
1463 current_journal.journalize_attachment(attachment, :removed)
1466 current_journal.journalize_attachment(attachment, :removed)
1464 current_journal.save
1467 current_journal.save
1465 end
1468 end
1466 end
1469 end
1467
1470
1468 # Called after a relation is added
1471 # Called after a relation is added
1469 def relation_added(relation)
1472 def relation_added(relation)
1470 if current_journal
1473 if current_journal
1471 current_journal.journalize_relation(relation, :added)
1474 current_journal.journalize_relation(relation, :added)
1472 current_journal.save
1475 current_journal.save
1473 end
1476 end
1474 end
1477 end
1475
1478
1476 # Called after a relation is removed
1479 # Called after a relation is removed
1477 def relation_removed(relation)
1480 def relation_removed(relation)
1478 if current_journal
1481 if current_journal
1479 current_journal.journalize_relation(relation, :removed)
1482 current_journal.journalize_relation(relation, :removed)
1480 current_journal.save
1483 current_journal.save
1481 end
1484 end
1482 end
1485 end
1483
1486
1484 # Default assignment based on category
1487 # Default assignment based on category
1485 def default_assign
1488 def default_assign
1486 if assigned_to.nil? && category && category.assigned_to
1489 if assigned_to.nil? && category && category.assigned_to
1487 self.assigned_to = category.assigned_to
1490 self.assigned_to = category.assigned_to
1488 end
1491 end
1489 end
1492 end
1490
1493
1491 # Updates start/due dates of following issues
1494 # Updates start/due dates of following issues
1492 def reschedule_following_issues
1495 def reschedule_following_issues
1493 if start_date_changed? || due_date_changed?
1496 if start_date_changed? || due_date_changed?
1494 relations_from.each do |relation|
1497 relations_from.each do |relation|
1495 relation.set_issue_to_dates
1498 relation.set_issue_to_dates
1496 end
1499 end
1497 end
1500 end
1498 end
1501 end
1499
1502
1500 # Closes duplicates if the issue is being closed
1503 # Closes duplicates if the issue is being closed
1501 def close_duplicates
1504 def close_duplicates
1502 if closing?
1505 if closing?
1503 duplicates.each do |duplicate|
1506 duplicates.each do |duplicate|
1504 # Reload is needed in case the duplicate was updated by a previous duplicate
1507 # Reload is needed in case the duplicate was updated by a previous duplicate
1505 duplicate.reload
1508 duplicate.reload
1506 # Don't re-close it if it's already closed
1509 # Don't re-close it if it's already closed
1507 next if duplicate.closed?
1510 next if duplicate.closed?
1508 # Same user and notes
1511 # Same user and notes
1509 if @current_journal
1512 if @current_journal
1510 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1513 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1511 end
1514 end
1512 duplicate.update_attribute :status, self.status
1515 duplicate.update_attribute :status, self.status
1513 end
1516 end
1514 end
1517 end
1515 end
1518 end
1516
1519
1517 # Make sure updated_on is updated when adding a note and set updated_on now
1520 # Make sure updated_on is updated when adding a note and set updated_on now
1518 # so we can set closed_on with the same value on closing
1521 # so we can set closed_on with the same value on closing
1519 def force_updated_on_change
1522 def force_updated_on_change
1520 if @current_journal || changed?
1523 if @current_journal || changed?
1521 self.updated_on = current_time_from_proper_timezone
1524 self.updated_on = current_time_from_proper_timezone
1522 if new_record?
1525 if new_record?
1523 self.created_on = updated_on
1526 self.created_on = updated_on
1524 end
1527 end
1525 end
1528 end
1526 end
1529 end
1527
1530
1528 # Callback for setting closed_on when the issue is closed.
1531 # Callback for setting closed_on when the issue is closed.
1529 # The closed_on attribute stores the time of the last closing
1532 # The closed_on attribute stores the time of the last closing
1530 # and is preserved when the issue is reopened.
1533 # and is preserved when the issue is reopened.
1531 def update_closed_on
1534 def update_closed_on
1532 if closing?
1535 if closing?
1533 self.closed_on = updated_on
1536 self.closed_on = updated_on
1534 end
1537 end
1535 end
1538 end
1536
1539
1537 # Saves the changes in a Journal
1540 # Saves the changes in a Journal
1538 # Called after_save
1541 # Called after_save
1539 def create_journal
1542 def create_journal
1540 if current_journal
1543 if current_journal
1541 current_journal.save
1544 current_journal.save
1542 end
1545 end
1543 end
1546 end
1544
1547
1545 def send_notification
1548 def send_notification
1546 if Setting.notified_events.include?('issue_added')
1549 if Setting.notified_events.include?('issue_added')
1547 Mailer.deliver_issue_add(self)
1550 Mailer.deliver_issue_add(self)
1548 end
1551 end
1549 end
1552 end
1550
1553
1551 # Stores the previous assignee so we can still have access
1554 # Stores the previous assignee so we can still have access
1552 # to it during after_save callbacks (assigned_to_id_was is reset)
1555 # to it during after_save callbacks (assigned_to_id_was is reset)
1553 def set_assigned_to_was
1556 def set_assigned_to_was
1554 @previous_assigned_to_id = assigned_to_id_was
1557 @previous_assigned_to_id = assigned_to_id_was
1555 end
1558 end
1556
1559
1557 # Clears the previous assignee at the end of after_save callbacks
1560 # Clears the previous assignee at the end of after_save callbacks
1558 def clear_assigned_to_was
1561 def clear_assigned_to_was
1559 @assigned_to_was = nil
1562 @assigned_to_was = nil
1560 @previous_assigned_to_id = nil
1563 @previous_assigned_to_id = nil
1561 end
1564 end
1562 end
1565 end
@@ -1,7 +1,7
1 <div class="contextual">
1 <div class="contextual">
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? %>
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 <%= 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) %>
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 <%= watcher_link(@issue, User.current) %>
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 <%= 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) %>
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 </div>
7 </div>
@@ -1,206 +1,207
1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
2
2
3 <% if @saved_issues && @unsaved_issues.present? %>
3 <% if @saved_issues && @unsaved_issues.present? %>
4 <div id="errorExplanation">
4 <div id="errorExplanation">
5 <span>
5 <span>
6 <%= l(:notice_failed_to_save_issues,
6 <%= l(:notice_failed_to_save_issues,
7 :count => @unsaved_issues.size,
7 :count => @unsaved_issues.size,
8 :total => @saved_issues.size,
8 :total => @saved_issues.size,
9 :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %>
9 :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %>
10 </span>
10 </span>
11 <ul>
11 <ul>
12 <% bulk_edit_error_messages(@unsaved_issues).each do |message| %>
12 <% bulk_edit_error_messages(@unsaved_issues).each do |message| %>
13 <li><%= message %></li>
13 <li><%= message %></li>
14 <% end %>
14 <% end %>
15 </ul>
15 </ul>
16 </div>
16 </div>
17 <% end %>
17 <% end %>
18
18
19 <ul id="bulk-selection">
19 <ul id="bulk-selection">
20 <% @issues.each do |issue| %>
20 <% @issues.each do |issue| %>
21 <%= content_tag 'li', link_to_issue(issue) %>
21 <%= content_tag 'li', link_to_issue(issue) %>
22 <% end %>
22 <% end %>
23 </ul>
23 </ul>
24
24
25 <%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %>
25 <%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %>
26 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join("\n").html_safe %>
26 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join("\n").html_safe %>
27 <div class="box tabular">
27 <div class="box tabular">
28 <fieldset class="attributes">
28 <fieldset class="attributes">
29 <legend><%= l(:label_change_properties) %></legend>
29 <legend><%= l(:label_change_properties) %></legend>
30
30
31 <div class="splitcontentleft">
31 <div class="splitcontentleft">
32 <% if @allowed_projects.present? %>
32 <% if @allowed_projects.present? %>
33 <p>
33 <p>
34 <label for="issue_project_id"><%= l(:field_project) %></label>
34 <label for="issue_project_id"><%= l(:field_project) %></label>
35 <%= select_tag('issue[project_id]',
35 <%= select_tag('issue[project_id]',
36 content_tag('option', l(:label_no_change_option), :value => '') +
36 project_tree_options_for_select(@allowed_projects,
37 project_tree_options_for_select(@allowed_projects, :selected => @target_project),
37 :include_blank => ((!@copy || (@projects & @allowed_projects == @projects)) ? l(:label_no_change_option) : false),
38 :selected => @target_project),
38 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
39 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
39 </p>
40 </p>
40 <% end %>
41 <% end %>
41 <p>
42 <p>
42 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
43 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
43 <%= select_tag('issue[tracker_id]',
44 <%= select_tag('issue[tracker_id]',
44 content_tag('option', l(:label_no_change_option), :value => '') +
45 content_tag('option', l(:label_no_change_option), :value => '') +
45 options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %>
46 options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %>
46 </p>
47 </p>
47 <% if @available_statuses.any? %>
48 <% if @available_statuses.any? %>
48 <p>
49 <p>
49 <label for='issue_status_id'><%= l(:field_status) %></label>
50 <label for='issue_status_id'><%= l(:field_status) %></label>
50 <%= select_tag('issue[status_id]',
51 <%= select_tag('issue[status_id]',
51 content_tag('option', l(:label_no_change_option), :value => '') +
52 content_tag('option', l(:label_no_change_option), :value => '') +
52 options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %>
53 options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %>
53 </p>
54 </p>
54 <% end %>
55 <% end %>
55
56
56 <% if @safe_attributes.include?('priority_id') -%>
57 <% if @safe_attributes.include?('priority_id') -%>
57 <p>
58 <p>
58 <label for='issue_priority_id'><%= l(:field_priority) %></label>
59 <label for='issue_priority_id'><%= l(:field_priority) %></label>
59 <%= select_tag('issue[priority_id]',
60 <%= select_tag('issue[priority_id]',
60 content_tag('option', l(:label_no_change_option), :value => '') +
61 content_tag('option', l(:label_no_change_option), :value => '') +
61 options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %>
62 options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %>
62 </p>
63 </p>
63 <% end %>
64 <% end %>
64
65
65 <% if @safe_attributes.include?('assigned_to_id') -%>
66 <% if @safe_attributes.include?('assigned_to_id') -%>
66 <p>
67 <p>
67 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
68 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
68 <%= select_tag('issue[assigned_to_id]',
69 <%= select_tag('issue[assigned_to_id]',
69 content_tag('option', l(:label_no_change_option), :value => '') +
70 content_tag('option', l(:label_no_change_option), :value => '') +
70 content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) +
71 content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) +
71 principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %>
72 principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %>
72 </p>
73 </p>
73 <% end %>
74 <% end %>
74
75
75 <% if @safe_attributes.include?('category_id') -%>
76 <% if @safe_attributes.include?('category_id') -%>
76 <p>
77 <p>
77 <label for='issue_category_id'><%= l(:field_category) %></label>
78 <label for='issue_category_id'><%= l(:field_category) %></label>
78 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
79 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
79 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) +
80 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) +
80 options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %>
81 options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %>
81 </p>
82 </p>
82 <% end %>
83 <% end %>
83
84
84 <% if @safe_attributes.include?('fixed_version_id') -%>
85 <% if @safe_attributes.include?('fixed_version_id') -%>
85 <p>
86 <p>
86 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
87 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
87 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
88 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
88 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) +
89 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) +
89 version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %>
90 version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %>
90 </p>
91 </p>
91 <% end %>
92 <% end %>
92
93
93 <% @custom_fields.each do |custom_field| %>
94 <% @custom_fields.each do |custom_field| %>
94 <p>
95 <p>
95 <label><%= h(custom_field.name) %></label>
96 <label><%= h(custom_field.name) %></label>
96 <%= custom_field_tag_for_bulk_edit('issue', custom_field, @issues, @issue_params[:custom_field_values][custom_field.id.to_s]) %>
97 <%= custom_field_tag_for_bulk_edit('issue', custom_field, @issues, @issue_params[:custom_field_values][custom_field.id.to_s]) %>
97 </p>
98 </p>
98 <% end %>
99 <% end %>
99
100
100 <% if @copy && Setting.link_copied_issue == 'ask' %>
101 <% if @copy && Setting.link_copied_issue == 'ask' %>
101 <p>
102 <p>
102 <label for='link_copy'><%= l(:label_link_copied_issue) %></label>
103 <label for='link_copy'><%= l(:label_link_copied_issue) %></label>
103 <%= hidden_field_tag 'link_copy', '0' %>
104 <%= hidden_field_tag 'link_copy', '0' %>
104 <%= check_box_tag 'link_copy', '1', params[:link_copy] != 0 %>
105 <%= check_box_tag 'link_copy', '1', params[:link_copy] != 0 %>
105 </p>
106 </p>
106 <% end %>
107 <% end %>
107
108
108 <% if @copy && @attachments_present %>
109 <% if @copy && @attachments_present %>
109 <%= hidden_field_tag 'copy_attachments', '0' %>
110 <%= hidden_field_tag 'copy_attachments', '0' %>
110 <p>
111 <p>
111 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
112 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
112 <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %>
113 <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %>
113 </p>
114 </p>
114 <% end %>
115 <% end %>
115
116
116 <% if @copy && @subtasks_present %>
117 <% if @copy && @subtasks_present %>
117 <%= hidden_field_tag 'copy_subtasks', '0' %>
118 <%= hidden_field_tag 'copy_subtasks', '0' %>
118 <p>
119 <p>
119 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
120 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
120 <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %>
121 <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %>
121 </p>
122 </p>
122 <% end %>
123 <% end %>
123
124
124 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
125 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
125 </div>
126 </div>
126
127
127 <div class="splitcontentright">
128 <div class="splitcontentright">
128 <% if @safe_attributes.include?('is_private') %>
129 <% if @safe_attributes.include?('is_private') %>
129 <p>
130 <p>
130 <label for='issue_is_private'><%= l(:field_is_private) %></label>
131 <label for='issue_is_private'><%= l(:field_is_private) %></label>
131 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
132 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
132 content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) +
133 content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) +
133 content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %>
134 content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %>
134 </p>
135 </p>
135 <% end %>
136 <% end %>
136
137
137 <% if @safe_attributes.include?('parent_issue_id') && @project %>
138 <% if @safe_attributes.include?('parent_issue_id') && @project %>
138 <p>
139 <p>
139 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
140 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
140 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %>
141 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %>
141 <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 <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 </p>
143 </p>
143 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => Setting.cross_project_subtasks)}')" %>
144 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => Setting.cross_project_subtasks)}')" %>
144 <% end %>
145 <% end %>
145
146
146 <% if @safe_attributes.include?('start_date') %>
147 <% if @safe_attributes.include?('start_date') %>
147 <p>
148 <p>
148 <label for='issue_start_date'><%= l(:field_start_date) %></label>
149 <label for='issue_start_date'><%= l(:field_start_date) %></label>
149 <%= text_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
150 <%= text_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
150 <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 <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 </p>
152 </p>
152 <% end %>
153 <% end %>
153
154
154 <% if @safe_attributes.include?('due_date') %>
155 <% if @safe_attributes.include?('due_date') %>
155 <p>
156 <p>
156 <label for='issue_due_date'><%= l(:field_due_date) %></label>
157 <label for='issue_due_date'><%= l(:field_due_date) %></label>
157 <%= text_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
158 <%= text_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
158 <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 <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 </p>
160 </p>
160 <% end %>
161 <% end %>
161
162
162 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
163 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
163 <p>
164 <p>
164 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
165 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
165 <%= 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 <%= 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 </p>
167 </p>
167 <% end %>
168 <% end %>
168 </div>
169 </div>
169 </fieldset>
170 </fieldset>
170
171
171 <fieldset>
172 <fieldset>
172 <legend><%= l(:field_notes) %></legend>
173 <legend><%= l(:field_notes) %></legend>
173 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
174 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
174 <%= wikitoolbar_for 'notes' %>
175 <%= wikitoolbar_for 'notes' %>
175 </fieldset>
176 </fieldset>
176 </div>
177 </div>
177
178
178 <p>
179 <p>
179 <% if @copy %>
180 <% if @copy %>
180 <%= hidden_field_tag 'copy', '1' %>
181 <%= hidden_field_tag 'copy', '1' %>
181 <%= submit_tag l(:button_copy) %>
182 <%= submit_tag l(:button_copy) %>
182 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
183 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
183 <% elsif @target_project %>
184 <% elsif @target_project %>
184 <%= submit_tag l(:button_move) %>
185 <%= submit_tag l(:button_move) %>
185 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
186 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
186 <% else %>
187 <% else %>
187 <%= submit_tag l(:button_submit) %>
188 <%= submit_tag l(:button_submit) %>
188 <% end %>
189 <% end %>
189 </p>
190 </p>
190
191
191 <% end %>
192 <% end %>
192
193
193 <%= javascript_tag do %>
194 <%= javascript_tag do %>
194 $(window).load(function(){
195 $(window).load(function(){
195 $(document).on('change', 'input[data-disables]', function(){
196 $(document).on('change', 'input[data-disables]', function(){
196 if ($(this).prop('checked')){
197 if ($(this).prop('checked')){
197 $($(this).data('disables')).attr('disabled', true).val('');
198 $($(this).data('disables')).attr('disabled', true).val('');
198 } else {
199 } else {
199 $($(this).data('disables')).attr('disabled', false);
200 $($(this).data('disables')).attr('disabled', false);
200 }
201 }
201 });
202 });
202 });
203 });
203 $(document).ready(function(){
204 $(document).ready(function(){
204 $('input[data-disables]').trigger('change');
205 $('input[data-disables]').trigger('change');
205 });
206 });
206 <% end %>
207 <% end %>
@@ -1,1132 +1,1133
1 en:
1 en:
2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 direction: ltr
3 direction: ltr
4 date:
4 date:
5 formats:
5 formats:
6 # Use the strftime parameters for formats.
6 # Use the strftime parameters for formats.
7 # When no format has been given, it uses default.
7 # When no format has been given, it uses default.
8 # You can provide other formats here if you like!
8 # You can provide other formats here if you like!
9 default: "%m/%d/%Y"
9 default: "%m/%d/%Y"
10 short: "%b %d"
10 short: "%b %d"
11 long: "%B %d, %Y"
11 long: "%B %d, %Y"
12
12
13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15
15
16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 # Used in date_select and datime_select.
19 # Used in date_select and datime_select.
20 order:
20 order:
21 - :year
21 - :year
22 - :month
22 - :month
23 - :day
23 - :day
24
24
25 time:
25 time:
26 formats:
26 formats:
27 default: "%m/%d/%Y %I:%M %p"
27 default: "%m/%d/%Y %I:%M %p"
28 time: "%I:%M %p"
28 time: "%I:%M %p"
29 short: "%d %b %H:%M"
29 short: "%d %b %H:%M"
30 long: "%B %d, %Y %H:%M"
30 long: "%B %d, %Y %H:%M"
31 am: "am"
31 am: "am"
32 pm: "pm"
32 pm: "pm"
33
33
34 datetime:
34 datetime:
35 distance_in_words:
35 distance_in_words:
36 half_a_minute: "half a minute"
36 half_a_minute: "half a minute"
37 less_than_x_seconds:
37 less_than_x_seconds:
38 one: "less than 1 second"
38 one: "less than 1 second"
39 other: "less than %{count} seconds"
39 other: "less than %{count} seconds"
40 x_seconds:
40 x_seconds:
41 one: "1 second"
41 one: "1 second"
42 other: "%{count} seconds"
42 other: "%{count} seconds"
43 less_than_x_minutes:
43 less_than_x_minutes:
44 one: "less than a minute"
44 one: "less than a minute"
45 other: "less than %{count} minutes"
45 other: "less than %{count} minutes"
46 x_minutes:
46 x_minutes:
47 one: "1 minute"
47 one: "1 minute"
48 other: "%{count} minutes"
48 other: "%{count} minutes"
49 about_x_hours:
49 about_x_hours:
50 one: "about 1 hour"
50 one: "about 1 hour"
51 other: "about %{count} hours"
51 other: "about %{count} hours"
52 x_hours:
52 x_hours:
53 one: "1 hour"
53 one: "1 hour"
54 other: "%{count} hours"
54 other: "%{count} hours"
55 x_days:
55 x_days:
56 one: "1 day"
56 one: "1 day"
57 other: "%{count} days"
57 other: "%{count} days"
58 about_x_months:
58 about_x_months:
59 one: "about 1 month"
59 one: "about 1 month"
60 other: "about %{count} months"
60 other: "about %{count} months"
61 x_months:
61 x_months:
62 one: "1 month"
62 one: "1 month"
63 other: "%{count} months"
63 other: "%{count} months"
64 about_x_years:
64 about_x_years:
65 one: "about 1 year"
65 one: "about 1 year"
66 other: "about %{count} years"
66 other: "about %{count} years"
67 over_x_years:
67 over_x_years:
68 one: "over 1 year"
68 one: "over 1 year"
69 other: "over %{count} years"
69 other: "over %{count} years"
70 almost_x_years:
70 almost_x_years:
71 one: "almost 1 year"
71 one: "almost 1 year"
72 other: "almost %{count} years"
72 other: "almost %{count} years"
73
73
74 number:
74 number:
75 format:
75 format:
76 separator: "."
76 separator: "."
77 delimiter: ""
77 delimiter: ""
78 precision: 3
78 precision: 3
79
79
80 human:
80 human:
81 format:
81 format:
82 delimiter: ""
82 delimiter: ""
83 precision: 3
83 precision: 3
84 storage_units:
84 storage_units:
85 format: "%n %u"
85 format: "%n %u"
86 units:
86 units:
87 byte:
87 byte:
88 one: "Byte"
88 one: "Byte"
89 other: "Bytes"
89 other: "Bytes"
90 kb: "KB"
90 kb: "KB"
91 mb: "MB"
91 mb: "MB"
92 gb: "GB"
92 gb: "GB"
93 tb: "TB"
93 tb: "TB"
94
94
95 # Used in array.to_sentence.
95 # Used in array.to_sentence.
96 support:
96 support:
97 array:
97 array:
98 sentence_connector: "and"
98 sentence_connector: "and"
99 skip_last_comma: false
99 skip_last_comma: false
100
100
101 activerecord:
101 activerecord:
102 errors:
102 errors:
103 template:
103 template:
104 header:
104 header:
105 one: "1 error prohibited this %{model} from being saved"
105 one: "1 error prohibited this %{model} from being saved"
106 other: "%{count} errors prohibited this %{model} from being saved"
106 other: "%{count} errors prohibited this %{model} from being saved"
107 messages:
107 messages:
108 inclusion: "is not included in the list"
108 inclusion: "is not included in the list"
109 exclusion: "is reserved"
109 exclusion: "is reserved"
110 invalid: "is invalid"
110 invalid: "is invalid"
111 confirmation: "doesn't match confirmation"
111 confirmation: "doesn't match confirmation"
112 accepted: "must be accepted"
112 accepted: "must be accepted"
113 empty: "cannot be empty"
113 empty: "cannot be empty"
114 blank: "cannot be blank"
114 blank: "cannot be blank"
115 too_long: "is too long (maximum is %{count} characters)"
115 too_long: "is too long (maximum is %{count} characters)"
116 too_short: "is too short (minimum is %{count} characters)"
116 too_short: "is too short (minimum is %{count} characters)"
117 wrong_length: "is the wrong length (should be %{count} characters)"
117 wrong_length: "is the wrong length (should be %{count} characters)"
118 taken: "has already been taken"
118 taken: "has already been taken"
119 not_a_number: "is not a number"
119 not_a_number: "is not a number"
120 not_a_date: "is not a valid date"
120 not_a_date: "is not a valid date"
121 greater_than: "must be greater than %{count}"
121 greater_than: "must be greater than %{count}"
122 greater_than_or_equal_to: "must be greater than or equal to %{count}"
122 greater_than_or_equal_to: "must be greater than or equal to %{count}"
123 equal_to: "must be equal to %{count}"
123 equal_to: "must be equal to %{count}"
124 less_than: "must be less than %{count}"
124 less_than: "must be less than %{count}"
125 less_than_or_equal_to: "must be less than or equal to %{count}"
125 less_than_or_equal_to: "must be less than or equal to %{count}"
126 odd: "must be odd"
126 odd: "must be odd"
127 even: "must be even"
127 even: "must be even"
128 greater_than_start_date: "must be greater than start date"
128 greater_than_start_date: "must be greater than start date"
129 not_same_project: "doesn't belong to the same project"
129 not_same_project: "doesn't belong to the same project"
130 circular_dependency: "This relation would create a circular dependency"
130 circular_dependency: "This relation would create a circular dependency"
131 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
131 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
132 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
132 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
133
133
134 actionview_instancetag_blank_option: Please select
134 actionview_instancetag_blank_option: Please select
135
135
136 general_text_No: 'No'
136 general_text_No: 'No'
137 general_text_Yes: 'Yes'
137 general_text_Yes: 'Yes'
138 general_text_no: 'no'
138 general_text_no: 'no'
139 general_text_yes: 'yes'
139 general_text_yes: 'yes'
140 general_lang_name: 'English'
140 general_lang_name: 'English'
141 general_csv_separator: ','
141 general_csv_separator: ','
142 general_csv_decimal_separator: '.'
142 general_csv_decimal_separator: '.'
143 general_csv_encoding: ISO-8859-1
143 general_csv_encoding: ISO-8859-1
144 general_pdf_fontname: freesans
144 general_pdf_fontname: freesans
145 general_first_day_of_week: '7'
145 general_first_day_of_week: '7'
146
146
147 notice_account_updated: Account was successfully updated.
147 notice_account_updated: Account was successfully updated.
148 notice_account_invalid_creditentials: Invalid user or password
148 notice_account_invalid_creditentials: Invalid user or password
149 notice_account_password_updated: Password was successfully updated.
149 notice_account_password_updated: Password was successfully updated.
150 notice_account_wrong_password: Wrong password
150 notice_account_wrong_password: Wrong password
151 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
151 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
152 notice_account_unknown_email: Unknown user.
152 notice_account_unknown_email: Unknown user.
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>.
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 notice_account_locked: Your account is locked.
154 notice_account_locked: Your account is locked.
155 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
155 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
156 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
156 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
157 notice_account_activated: Your account has been activated. You can now log in.
157 notice_account_activated: Your account has been activated. You can now log in.
158 notice_successful_create: Successful creation.
158 notice_successful_create: Successful creation.
159 notice_successful_update: Successful update.
159 notice_successful_update: Successful update.
160 notice_successful_delete: Successful deletion.
160 notice_successful_delete: Successful deletion.
161 notice_successful_connection: Successful connection.
161 notice_successful_connection: Successful connection.
162 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
162 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
163 notice_locking_conflict: Data has been updated by another user.
163 notice_locking_conflict: Data has been updated by another user.
164 notice_not_authorized: You are not authorized to access this page.
164 notice_not_authorized: You are not authorized to access this page.
165 notice_not_authorized_archived_project: The project you're trying to access has been archived.
165 notice_not_authorized_archived_project: The project you're trying to access has been archived.
166 notice_email_sent: "An email was sent to %{value}"
166 notice_email_sent: "An email was sent to %{value}"
167 notice_email_error: "An error occurred while sending mail (%{value})"
167 notice_email_error: "An error occurred while sending mail (%{value})"
168 notice_feeds_access_key_reseted: Your Atom access key was reset.
168 notice_feeds_access_key_reseted: Your Atom access key was reset.
169 notice_api_access_key_reseted: Your API access key was reset.
169 notice_api_access_key_reseted: Your API access key was reset.
170 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
170 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
171 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
171 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
172 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
172 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
173 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
173 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
174 notice_account_pending: "Your account was created and is now pending administrator approval."
174 notice_account_pending: "Your account was created and is now pending administrator approval."
175 notice_default_data_loaded: Default configuration successfully loaded.
175 notice_default_data_loaded: Default configuration successfully loaded.
176 notice_unable_delete_version: Unable to delete version.
176 notice_unable_delete_version: Unable to delete version.
177 notice_unable_delete_time_entry: Unable to delete time log entry.
177 notice_unable_delete_time_entry: Unable to delete time log entry.
178 notice_issue_done_ratios_updated: Issue done ratios updated.
178 notice_issue_done_ratios_updated: Issue done ratios updated.
179 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
179 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
180 notice_issue_successful_create: "Issue %{id} created."
180 notice_issue_successful_create: "Issue %{id} created."
181 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
181 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
182 notice_account_deleted: "Your account has been permanently deleted."
182 notice_account_deleted: "Your account has been permanently deleted."
183 notice_user_successful_create: "User %{id} created."
183 notice_user_successful_create: "User %{id} created."
184 notice_new_password_must_be_different: The new password must be different from the current password
184 notice_new_password_must_be_different: The new password must be different from the current password
185
185
186 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
186 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
187 error_scm_not_found: "The entry or revision was not found in the repository."
187 error_scm_not_found: "The entry or revision was not found in the repository."
188 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
188 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
189 error_scm_annotate: "The entry does not exist or cannot be annotated."
189 error_scm_annotate: "The entry does not exist or cannot be annotated."
190 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
190 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
191 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
191 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
192 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
192 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
193 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
193 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
194 error_can_not_delete_custom_field: Unable to delete custom field
194 error_can_not_delete_custom_field: Unable to delete custom field
195 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
195 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
196 error_can_not_remove_role: "This role is in use and cannot be deleted."
196 error_can_not_remove_role: "This role is in use and cannot be deleted."
197 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
197 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
198 error_can_not_archive_project: This project cannot be archived
198 error_can_not_archive_project: This project cannot be archived
199 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
199 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
200 error_workflow_copy_source: 'Please select a source tracker or role'
200 error_workflow_copy_source: 'Please select a source tracker or role'
201 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
201 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
202 error_unable_delete_issue_status: 'Unable to delete issue status'
202 error_unable_delete_issue_status: 'Unable to delete issue status'
203 error_unable_to_connect: "Unable to connect (%{value})"
203 error_unable_to_connect: "Unable to connect (%{value})"
204 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
204 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
205 error_session_expired: "Your session has expired. Please login again."
205 error_session_expired: "Your session has expired. Please login again."
206 warning_attachments_not_saved: "%{count} file(s) could not be saved."
206 warning_attachments_not_saved: "%{count} file(s) could not be saved."
207
207
208 mail_subject_lost_password: "Your %{value} password"
208 mail_subject_lost_password: "Your %{value} password"
209 mail_body_lost_password: 'To change your password, click on the following link:'
209 mail_body_lost_password: 'To change your password, click on the following link:'
210 mail_subject_register: "Your %{value} account activation"
210 mail_subject_register: "Your %{value} account activation"
211 mail_body_register: 'To activate your account, click on the following link:'
211 mail_body_register: 'To activate your account, click on the following link:'
212 mail_body_account_information_external: "You can use your %{value} account to log in."
212 mail_body_account_information_external: "You can use your %{value} account to log in."
213 mail_body_account_information: Your account information
213 mail_body_account_information: Your account information
214 mail_subject_account_activation_request: "%{value} account activation request"
214 mail_subject_account_activation_request: "%{value} account activation request"
215 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
215 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
216 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
216 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
217 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
217 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
218 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
218 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
219 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
219 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
220 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
220 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
221 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
221 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
222
222
223 field_name: Name
223 field_name: Name
224 field_description: Description
224 field_description: Description
225 field_summary: Summary
225 field_summary: Summary
226 field_is_required: Required
226 field_is_required: Required
227 field_firstname: First name
227 field_firstname: First name
228 field_lastname: Last name
228 field_lastname: Last name
229 field_mail: Email
229 field_mail: Email
230 field_address: Email
230 field_address: Email
231 field_filename: File
231 field_filename: File
232 field_filesize: Size
232 field_filesize: Size
233 field_downloads: Downloads
233 field_downloads: Downloads
234 field_author: Author
234 field_author: Author
235 field_created_on: Created
235 field_created_on: Created
236 field_updated_on: Updated
236 field_updated_on: Updated
237 field_closed_on: Closed
237 field_closed_on: Closed
238 field_field_format: Format
238 field_field_format: Format
239 field_is_for_all: For all projects
239 field_is_for_all: For all projects
240 field_possible_values: Possible values
240 field_possible_values: Possible values
241 field_regexp: Regular expression
241 field_regexp: Regular expression
242 field_min_length: Minimum length
242 field_min_length: Minimum length
243 field_max_length: Maximum length
243 field_max_length: Maximum length
244 field_value: Value
244 field_value: Value
245 field_category: Category
245 field_category: Category
246 field_title: Title
246 field_title: Title
247 field_project: Project
247 field_project: Project
248 field_issue: Issue
248 field_issue: Issue
249 field_status: Status
249 field_status: Status
250 field_notes: Notes
250 field_notes: Notes
251 field_is_closed: Issue closed
251 field_is_closed: Issue closed
252 field_is_default: Default value
252 field_is_default: Default value
253 field_tracker: Tracker
253 field_tracker: Tracker
254 field_subject: Subject
254 field_subject: Subject
255 field_due_date: Due date
255 field_due_date: Due date
256 field_assigned_to: Assignee
256 field_assigned_to: Assignee
257 field_priority: Priority
257 field_priority: Priority
258 field_fixed_version: Target version
258 field_fixed_version: Target version
259 field_user: User
259 field_user: User
260 field_principal: Principal
260 field_principal: Principal
261 field_role: Role
261 field_role: Role
262 field_homepage: Homepage
262 field_homepage: Homepage
263 field_is_public: Public
263 field_is_public: Public
264 field_parent: Subproject of
264 field_parent: Subproject of
265 field_is_in_roadmap: Issues displayed in roadmap
265 field_is_in_roadmap: Issues displayed in roadmap
266 field_login: Login
266 field_login: Login
267 field_mail_notification: Email notifications
267 field_mail_notification: Email notifications
268 field_admin: Administrator
268 field_admin: Administrator
269 field_last_login_on: Last connection
269 field_last_login_on: Last connection
270 field_language: Language
270 field_language: Language
271 field_effective_date: Date
271 field_effective_date: Date
272 field_password: Password
272 field_password: Password
273 field_new_password: New password
273 field_new_password: New password
274 field_password_confirmation: Confirmation
274 field_password_confirmation: Confirmation
275 field_version: Version
275 field_version: Version
276 field_type: Type
276 field_type: Type
277 field_host: Host
277 field_host: Host
278 field_port: Port
278 field_port: Port
279 field_account: Account
279 field_account: Account
280 field_base_dn: Base DN
280 field_base_dn: Base DN
281 field_attr_login: Login attribute
281 field_attr_login: Login attribute
282 field_attr_firstname: Firstname attribute
282 field_attr_firstname: Firstname attribute
283 field_attr_lastname: Lastname attribute
283 field_attr_lastname: Lastname attribute
284 field_attr_mail: Email attribute
284 field_attr_mail: Email attribute
285 field_onthefly: On-the-fly user creation
285 field_onthefly: On-the-fly user creation
286 field_start_date: Start date
286 field_start_date: Start date
287 field_done_ratio: "% Done"
287 field_done_ratio: "% Done"
288 field_auth_source: Authentication mode
288 field_auth_source: Authentication mode
289 field_hide_mail: Hide my email address
289 field_hide_mail: Hide my email address
290 field_comments: Comment
290 field_comments: Comment
291 field_url: URL
291 field_url: URL
292 field_start_page: Start page
292 field_start_page: Start page
293 field_subproject: Subproject
293 field_subproject: Subproject
294 field_hours: Hours
294 field_hours: Hours
295 field_activity: Activity
295 field_activity: Activity
296 field_spent_on: Date
296 field_spent_on: Date
297 field_identifier: Identifier
297 field_identifier: Identifier
298 field_is_filter: Used as a filter
298 field_is_filter: Used as a filter
299 field_issue_to: Related issue
299 field_issue_to: Related issue
300 field_delay: Delay
300 field_delay: Delay
301 field_assignable: Issues can be assigned to this role
301 field_assignable: Issues can be assigned to this role
302 field_redirect_existing_links: Redirect existing links
302 field_redirect_existing_links: Redirect existing links
303 field_estimated_hours: Estimated time
303 field_estimated_hours: Estimated time
304 field_column_names: Columns
304 field_column_names: Columns
305 field_time_entries: Log time
305 field_time_entries: Log time
306 field_time_zone: Time zone
306 field_time_zone: Time zone
307 field_searchable: Searchable
307 field_searchable: Searchable
308 field_default_value: Default value
308 field_default_value: Default value
309 field_comments_sorting: Display comments
309 field_comments_sorting: Display comments
310 field_parent_title: Parent page
310 field_parent_title: Parent page
311 field_editable: Editable
311 field_editable: Editable
312 field_watcher: Watcher
312 field_watcher: Watcher
313 field_identity_url: OpenID URL
313 field_identity_url: OpenID URL
314 field_content: Content
314 field_content: Content
315 field_group_by: Group results by
315 field_group_by: Group results by
316 field_sharing: Sharing
316 field_sharing: Sharing
317 field_parent_issue: Parent task
317 field_parent_issue: Parent task
318 field_member_of_group: "Assignee's group"
318 field_member_of_group: "Assignee's group"
319 field_assigned_to_role: "Assignee's role"
319 field_assigned_to_role: "Assignee's role"
320 field_text: Text field
320 field_text: Text field
321 field_visible: Visible
321 field_visible: Visible
322 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
322 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
323 field_issues_visibility: Issues visibility
323 field_issues_visibility: Issues visibility
324 field_is_private: Private
324 field_is_private: Private
325 field_commit_logs_encoding: Commit messages encoding
325 field_commit_logs_encoding: Commit messages encoding
326 field_scm_path_encoding: Path encoding
326 field_scm_path_encoding: Path encoding
327 field_path_to_repository: Path to repository
327 field_path_to_repository: Path to repository
328 field_root_directory: Root directory
328 field_root_directory: Root directory
329 field_cvsroot: CVSROOT
329 field_cvsroot: CVSROOT
330 field_cvs_module: Module
330 field_cvs_module: Module
331 field_repository_is_default: Main repository
331 field_repository_is_default: Main repository
332 field_multiple: Multiple values
332 field_multiple: Multiple values
333 field_auth_source_ldap_filter: LDAP filter
333 field_auth_source_ldap_filter: LDAP filter
334 field_core_fields: Standard fields
334 field_core_fields: Standard fields
335 field_timeout: "Timeout (in seconds)"
335 field_timeout: "Timeout (in seconds)"
336 field_board_parent: Parent forum
336 field_board_parent: Parent forum
337 field_private_notes: Private notes
337 field_private_notes: Private notes
338 field_inherit_members: Inherit members
338 field_inherit_members: Inherit members
339 field_generate_password: Generate password
339 field_generate_password: Generate password
340 field_must_change_passwd: Must change password at next logon
340 field_must_change_passwd: Must change password at next logon
341 field_default_status: Default status
341 field_default_status: Default status
342 field_users_visibility: Users visibility
342 field_users_visibility: Users visibility
343
343
344 setting_app_title: Application title
344 setting_app_title: Application title
345 setting_app_subtitle: Application subtitle
345 setting_app_subtitle: Application subtitle
346 setting_welcome_text: Welcome text
346 setting_welcome_text: Welcome text
347 setting_default_language: Default language
347 setting_default_language: Default language
348 setting_login_required: Authentication required
348 setting_login_required: Authentication required
349 setting_self_registration: Self-registration
349 setting_self_registration: Self-registration
350 setting_attachment_max_size: Maximum attachment size
350 setting_attachment_max_size: Maximum attachment size
351 setting_issues_export_limit: Issues export limit
351 setting_issues_export_limit: Issues export limit
352 setting_mail_from: Emission email address
352 setting_mail_from: Emission email address
353 setting_bcc_recipients: Blind carbon copy recipients (bcc)
353 setting_bcc_recipients: Blind carbon copy recipients (bcc)
354 setting_plain_text_mail: Plain text mail (no HTML)
354 setting_plain_text_mail: Plain text mail (no HTML)
355 setting_host_name: Host name and path
355 setting_host_name: Host name and path
356 setting_text_formatting: Text formatting
356 setting_text_formatting: Text formatting
357 setting_wiki_compression: Wiki history compression
357 setting_wiki_compression: Wiki history compression
358 setting_feeds_limit: Maximum number of items in Atom feeds
358 setting_feeds_limit: Maximum number of items in Atom feeds
359 setting_default_projects_public: New projects are public by default
359 setting_default_projects_public: New projects are public by default
360 setting_autofetch_changesets: Fetch commits automatically
360 setting_autofetch_changesets: Fetch commits automatically
361 setting_sys_api_enabled: Enable WS for repository management
361 setting_sys_api_enabled: Enable WS for repository management
362 setting_commit_ref_keywords: Referencing keywords
362 setting_commit_ref_keywords: Referencing keywords
363 setting_commit_fix_keywords: Fixing keywords
363 setting_commit_fix_keywords: Fixing keywords
364 setting_autologin: Autologin
364 setting_autologin: Autologin
365 setting_date_format: Date format
365 setting_date_format: Date format
366 setting_time_format: Time format
366 setting_time_format: Time format
367 setting_cross_project_issue_relations: Allow cross-project issue relations
367 setting_cross_project_issue_relations: Allow cross-project issue relations
368 setting_cross_project_subtasks: Allow cross-project subtasks
368 setting_cross_project_subtasks: Allow cross-project subtasks
369 setting_issue_list_default_columns: Default columns displayed on the issue list
369 setting_issue_list_default_columns: Default columns displayed on the issue list
370 setting_repositories_encodings: Attachments and repositories encodings
370 setting_repositories_encodings: Attachments and repositories encodings
371 setting_emails_header: Email header
371 setting_emails_header: Email header
372 setting_emails_footer: Email footer
372 setting_emails_footer: Email footer
373 setting_protocol: Protocol
373 setting_protocol: Protocol
374 setting_per_page_options: Objects per page options
374 setting_per_page_options: Objects per page options
375 setting_user_format: Users display format
375 setting_user_format: Users display format
376 setting_activity_days_default: Days displayed on project activity
376 setting_activity_days_default: Days displayed on project activity
377 setting_display_subprojects_issues: Display subprojects issues on main projects by default
377 setting_display_subprojects_issues: Display subprojects issues on main projects by default
378 setting_enabled_scm: Enabled SCM
378 setting_enabled_scm: Enabled SCM
379 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
379 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
380 setting_mail_handler_api_enabled: Enable WS for incoming emails
380 setting_mail_handler_api_enabled: Enable WS for incoming emails
381 setting_mail_handler_api_key: API key
381 setting_mail_handler_api_key: API key
382 setting_sequential_project_identifiers: Generate sequential project identifiers
382 setting_sequential_project_identifiers: Generate sequential project identifiers
383 setting_gravatar_enabled: Use Gravatar user icons
383 setting_gravatar_enabled: Use Gravatar user icons
384 setting_gravatar_default: Default Gravatar image
384 setting_gravatar_default: Default Gravatar image
385 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
385 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
386 setting_file_max_size_displayed: Maximum size of text files displayed inline
386 setting_file_max_size_displayed: Maximum size of text files displayed inline
387 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
387 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
388 setting_openid: Allow OpenID login and registration
388 setting_openid: Allow OpenID login and registration
389 setting_password_min_length: Minimum password length
389 setting_password_min_length: Minimum password length
390 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
390 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
391 setting_default_projects_modules: Default enabled modules for new projects
391 setting_default_projects_modules: Default enabled modules for new projects
392 setting_issue_done_ratio: Calculate the issue done ratio with
392 setting_issue_done_ratio: Calculate the issue done ratio with
393 setting_issue_done_ratio_issue_field: Use the issue field
393 setting_issue_done_ratio_issue_field: Use the issue field
394 setting_issue_done_ratio_issue_status: Use the issue status
394 setting_issue_done_ratio_issue_status: Use the issue status
395 setting_start_of_week: Start calendars on
395 setting_start_of_week: Start calendars on
396 setting_rest_api_enabled: Enable REST web service
396 setting_rest_api_enabled: Enable REST web service
397 setting_cache_formatted_text: Cache formatted text
397 setting_cache_formatted_text: Cache formatted text
398 setting_default_notification_option: Default notification option
398 setting_default_notification_option: Default notification option
399 setting_commit_logtime_enabled: Enable time logging
399 setting_commit_logtime_enabled: Enable time logging
400 setting_commit_logtime_activity_id: Activity for logged time
400 setting_commit_logtime_activity_id: Activity for logged time
401 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
401 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
402 setting_issue_group_assignment: Allow issue assignment to groups
402 setting_issue_group_assignment: Allow issue assignment to groups
403 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
403 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
404 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
404 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
405 setting_unsubscribe: Allow users to delete their own account
405 setting_unsubscribe: Allow users to delete their own account
406 setting_session_lifetime: Session maximum lifetime
406 setting_session_lifetime: Session maximum lifetime
407 setting_session_timeout: Session inactivity timeout
407 setting_session_timeout: Session inactivity timeout
408 setting_thumbnails_enabled: Display attachment thumbnails
408 setting_thumbnails_enabled: Display attachment thumbnails
409 setting_thumbnails_size: Thumbnails size (in pixels)
409 setting_thumbnails_size: Thumbnails size (in pixels)
410 setting_non_working_week_days: Non-working days
410 setting_non_working_week_days: Non-working days
411 setting_jsonp_enabled: Enable JSONP support
411 setting_jsonp_enabled: Enable JSONP support
412 setting_default_projects_tracker_ids: Default trackers for new projects
412 setting_default_projects_tracker_ids: Default trackers for new projects
413 setting_mail_handler_excluded_filenames: Exclude attachments by name
413 setting_mail_handler_excluded_filenames: Exclude attachments by name
414 setting_force_default_language_for_anonymous: Force default language for anonymous users
414 setting_force_default_language_for_anonymous: Force default language for anonymous users
415 setting_force_default_language_for_loggedin: Force default language for logged-in users
415 setting_force_default_language_for_loggedin: Force default language for logged-in users
416 setting_link_copied_issue: Link issues on copy
416 setting_link_copied_issue: Link issues on copy
417 setting_max_additional_emails: Maximum number of additional email addresses
417 setting_max_additional_emails: Maximum number of additional email addresses
418 setting_search_results_per_page: Search results per page
418 setting_search_results_per_page: Search results per page
419
419
420 permission_add_project: Create project
420 permission_add_project: Create project
421 permission_add_subprojects: Create subprojects
421 permission_add_subprojects: Create subprojects
422 permission_edit_project: Edit project
422 permission_edit_project: Edit project
423 permission_close_project: Close / reopen the project
423 permission_close_project: Close / reopen the project
424 permission_select_project_modules: Select project modules
424 permission_select_project_modules: Select project modules
425 permission_manage_members: Manage members
425 permission_manage_members: Manage members
426 permission_manage_project_activities: Manage project activities
426 permission_manage_project_activities: Manage project activities
427 permission_manage_versions: Manage versions
427 permission_manage_versions: Manage versions
428 permission_manage_categories: Manage issue categories
428 permission_manage_categories: Manage issue categories
429 permission_view_issues: View Issues
429 permission_view_issues: View Issues
430 permission_add_issues: Add issues
430 permission_add_issues: Add issues
431 permission_edit_issues: Edit issues
431 permission_edit_issues: Edit issues
432 permission_copy_issues: Copy issues
432 permission_manage_issue_relations: Manage issue relations
433 permission_manage_issue_relations: Manage issue relations
433 permission_set_issues_private: Set issues public or private
434 permission_set_issues_private: Set issues public or private
434 permission_set_own_issues_private: Set own issues public or private
435 permission_set_own_issues_private: Set own issues public or private
435 permission_add_issue_notes: Add notes
436 permission_add_issue_notes: Add notes
436 permission_edit_issue_notes: Edit notes
437 permission_edit_issue_notes: Edit notes
437 permission_edit_own_issue_notes: Edit own notes
438 permission_edit_own_issue_notes: Edit own notes
438 permission_view_private_notes: View private notes
439 permission_view_private_notes: View private notes
439 permission_set_notes_private: Set notes as private
440 permission_set_notes_private: Set notes as private
440 permission_move_issues: Move issues
441 permission_move_issues: Move issues
441 permission_delete_issues: Delete issues
442 permission_delete_issues: Delete issues
442 permission_manage_public_queries: Manage public queries
443 permission_manage_public_queries: Manage public queries
443 permission_save_queries: Save queries
444 permission_save_queries: Save queries
444 permission_view_gantt: View gantt chart
445 permission_view_gantt: View gantt chart
445 permission_view_calendar: View calendar
446 permission_view_calendar: View calendar
446 permission_view_issue_watchers: View watchers list
447 permission_view_issue_watchers: View watchers list
447 permission_add_issue_watchers: Add watchers
448 permission_add_issue_watchers: Add watchers
448 permission_delete_issue_watchers: Delete watchers
449 permission_delete_issue_watchers: Delete watchers
449 permission_log_time: Log spent time
450 permission_log_time: Log spent time
450 permission_view_time_entries: View spent time
451 permission_view_time_entries: View spent time
451 permission_edit_time_entries: Edit time logs
452 permission_edit_time_entries: Edit time logs
452 permission_edit_own_time_entries: Edit own time logs
453 permission_edit_own_time_entries: Edit own time logs
453 permission_manage_news: Manage news
454 permission_manage_news: Manage news
454 permission_comment_news: Comment news
455 permission_comment_news: Comment news
455 permission_view_documents: View documents
456 permission_view_documents: View documents
456 permission_add_documents: Add documents
457 permission_add_documents: Add documents
457 permission_edit_documents: Edit documents
458 permission_edit_documents: Edit documents
458 permission_delete_documents: Delete documents
459 permission_delete_documents: Delete documents
459 permission_manage_files: Manage files
460 permission_manage_files: Manage files
460 permission_view_files: View files
461 permission_view_files: View files
461 permission_manage_wiki: Manage wiki
462 permission_manage_wiki: Manage wiki
462 permission_rename_wiki_pages: Rename wiki pages
463 permission_rename_wiki_pages: Rename wiki pages
463 permission_delete_wiki_pages: Delete wiki pages
464 permission_delete_wiki_pages: Delete wiki pages
464 permission_view_wiki_pages: View wiki
465 permission_view_wiki_pages: View wiki
465 permission_view_wiki_edits: View wiki history
466 permission_view_wiki_edits: View wiki history
466 permission_edit_wiki_pages: Edit wiki pages
467 permission_edit_wiki_pages: Edit wiki pages
467 permission_delete_wiki_pages_attachments: Delete attachments
468 permission_delete_wiki_pages_attachments: Delete attachments
468 permission_protect_wiki_pages: Protect wiki pages
469 permission_protect_wiki_pages: Protect wiki pages
469 permission_manage_repository: Manage repository
470 permission_manage_repository: Manage repository
470 permission_browse_repository: Browse repository
471 permission_browse_repository: Browse repository
471 permission_view_changesets: View changesets
472 permission_view_changesets: View changesets
472 permission_commit_access: Commit access
473 permission_commit_access: Commit access
473 permission_manage_boards: Manage forums
474 permission_manage_boards: Manage forums
474 permission_view_messages: View messages
475 permission_view_messages: View messages
475 permission_add_messages: Post messages
476 permission_add_messages: Post messages
476 permission_edit_messages: Edit messages
477 permission_edit_messages: Edit messages
477 permission_edit_own_messages: Edit own messages
478 permission_edit_own_messages: Edit own messages
478 permission_delete_messages: Delete messages
479 permission_delete_messages: Delete messages
479 permission_delete_own_messages: Delete own messages
480 permission_delete_own_messages: Delete own messages
480 permission_export_wiki_pages: Export wiki pages
481 permission_export_wiki_pages: Export wiki pages
481 permission_manage_subtasks: Manage subtasks
482 permission_manage_subtasks: Manage subtasks
482 permission_manage_related_issues: Manage related issues
483 permission_manage_related_issues: Manage related issues
483
484
484 project_module_issue_tracking: Issue tracking
485 project_module_issue_tracking: Issue tracking
485 project_module_time_tracking: Time tracking
486 project_module_time_tracking: Time tracking
486 project_module_news: News
487 project_module_news: News
487 project_module_documents: Documents
488 project_module_documents: Documents
488 project_module_files: Files
489 project_module_files: Files
489 project_module_wiki: Wiki
490 project_module_wiki: Wiki
490 project_module_repository: Repository
491 project_module_repository: Repository
491 project_module_boards: Forums
492 project_module_boards: Forums
492 project_module_calendar: Calendar
493 project_module_calendar: Calendar
493 project_module_gantt: Gantt
494 project_module_gantt: Gantt
494
495
495 label_user: User
496 label_user: User
496 label_user_plural: Users
497 label_user_plural: Users
497 label_user_new: New user
498 label_user_new: New user
498 label_user_anonymous: Anonymous
499 label_user_anonymous: Anonymous
499 label_project: Project
500 label_project: Project
500 label_project_new: New project
501 label_project_new: New project
501 label_project_plural: Projects
502 label_project_plural: Projects
502 label_x_projects:
503 label_x_projects:
503 zero: no projects
504 zero: no projects
504 one: 1 project
505 one: 1 project
505 other: "%{count} projects"
506 other: "%{count} projects"
506 label_project_all: All Projects
507 label_project_all: All Projects
507 label_project_latest: Latest projects
508 label_project_latest: Latest projects
508 label_issue: Issue
509 label_issue: Issue
509 label_issue_new: New issue
510 label_issue_new: New issue
510 label_issue_plural: Issues
511 label_issue_plural: Issues
511 label_issue_view_all: View all issues
512 label_issue_view_all: View all issues
512 label_issues_by: "Issues by %{value}"
513 label_issues_by: "Issues by %{value}"
513 label_issue_added: Issue added
514 label_issue_added: Issue added
514 label_issue_updated: Issue updated
515 label_issue_updated: Issue updated
515 label_issue_note_added: Note added
516 label_issue_note_added: Note added
516 label_issue_status_updated: Status updated
517 label_issue_status_updated: Status updated
517 label_issue_assigned_to_updated: Assignee updated
518 label_issue_assigned_to_updated: Assignee updated
518 label_issue_priority_updated: Priority updated
519 label_issue_priority_updated: Priority updated
519 label_document: Document
520 label_document: Document
520 label_document_new: New document
521 label_document_new: New document
521 label_document_plural: Documents
522 label_document_plural: Documents
522 label_document_added: Document added
523 label_document_added: Document added
523 label_role: Role
524 label_role: Role
524 label_role_plural: Roles
525 label_role_plural: Roles
525 label_role_new: New role
526 label_role_new: New role
526 label_role_and_permissions: Roles and permissions
527 label_role_and_permissions: Roles and permissions
527 label_role_anonymous: Anonymous
528 label_role_anonymous: Anonymous
528 label_role_non_member: Non member
529 label_role_non_member: Non member
529 label_member: Member
530 label_member: Member
530 label_member_new: New member
531 label_member_new: New member
531 label_member_plural: Members
532 label_member_plural: Members
532 label_tracker: Tracker
533 label_tracker: Tracker
533 label_tracker_plural: Trackers
534 label_tracker_plural: Trackers
534 label_tracker_new: New tracker
535 label_tracker_new: New tracker
535 label_workflow: Workflow
536 label_workflow: Workflow
536 label_issue_status: Issue status
537 label_issue_status: Issue status
537 label_issue_status_plural: Issue statuses
538 label_issue_status_plural: Issue statuses
538 label_issue_status_new: New status
539 label_issue_status_new: New status
539 label_issue_category: Issue category
540 label_issue_category: Issue category
540 label_issue_category_plural: Issue categories
541 label_issue_category_plural: Issue categories
541 label_issue_category_new: New category
542 label_issue_category_new: New category
542 label_custom_field: Custom field
543 label_custom_field: Custom field
543 label_custom_field_plural: Custom fields
544 label_custom_field_plural: Custom fields
544 label_custom_field_new: New custom field
545 label_custom_field_new: New custom field
545 label_enumerations: Enumerations
546 label_enumerations: Enumerations
546 label_enumeration_new: New value
547 label_enumeration_new: New value
547 label_information: Information
548 label_information: Information
548 label_information_plural: Information
549 label_information_plural: Information
549 label_please_login: Please log in
550 label_please_login: Please log in
550 label_register: Register
551 label_register: Register
551 label_login_with_open_id_option: or login with OpenID
552 label_login_with_open_id_option: or login with OpenID
552 label_password_lost: Lost password
553 label_password_lost: Lost password
553 label_home: Home
554 label_home: Home
554 label_my_page: My page
555 label_my_page: My page
555 label_my_account: My account
556 label_my_account: My account
556 label_my_projects: My projects
557 label_my_projects: My projects
557 label_my_page_block: My page block
558 label_my_page_block: My page block
558 label_administration: Administration
559 label_administration: Administration
559 label_login: Sign in
560 label_login: Sign in
560 label_logout: Sign out
561 label_logout: Sign out
561 label_help: Help
562 label_help: Help
562 label_reported_issues: Reported issues
563 label_reported_issues: Reported issues
563 label_assigned_to_me_issues: Issues assigned to me
564 label_assigned_to_me_issues: Issues assigned to me
564 label_last_login: Last connection
565 label_last_login: Last connection
565 label_registered_on: Registered on
566 label_registered_on: Registered on
566 label_activity: Activity
567 label_activity: Activity
567 label_overall_activity: Overall activity
568 label_overall_activity: Overall activity
568 label_user_activity: "%{value}'s activity"
569 label_user_activity: "%{value}'s activity"
569 label_new: New
570 label_new: New
570 label_logged_as: Logged in as
571 label_logged_as: Logged in as
571 label_environment: Environment
572 label_environment: Environment
572 label_authentication: Authentication
573 label_authentication: Authentication
573 label_auth_source: Authentication mode
574 label_auth_source: Authentication mode
574 label_auth_source_new: New authentication mode
575 label_auth_source_new: New authentication mode
575 label_auth_source_plural: Authentication modes
576 label_auth_source_plural: Authentication modes
576 label_subproject_plural: Subprojects
577 label_subproject_plural: Subprojects
577 label_subproject_new: New subproject
578 label_subproject_new: New subproject
578 label_and_its_subprojects: "%{value} and its subprojects"
579 label_and_its_subprojects: "%{value} and its subprojects"
579 label_min_max_length: Min - Max length
580 label_min_max_length: Min - Max length
580 label_list: List
581 label_list: List
581 label_date: Date
582 label_date: Date
582 label_integer: Integer
583 label_integer: Integer
583 label_float: Float
584 label_float: Float
584 label_boolean: Boolean
585 label_boolean: Boolean
585 label_string: Text
586 label_string: Text
586 label_text: Long text
587 label_text: Long text
587 label_attribute: Attribute
588 label_attribute: Attribute
588 label_attribute_plural: Attributes
589 label_attribute_plural: Attributes
589 label_no_data: No data to display
590 label_no_data: No data to display
590 label_change_status: Change status
591 label_change_status: Change status
591 label_history: History
592 label_history: History
592 label_attachment: File
593 label_attachment: File
593 label_attachment_new: New file
594 label_attachment_new: New file
594 label_attachment_delete: Delete file
595 label_attachment_delete: Delete file
595 label_attachment_plural: Files
596 label_attachment_plural: Files
596 label_file_added: File added
597 label_file_added: File added
597 label_report: Report
598 label_report: Report
598 label_report_plural: Reports
599 label_report_plural: Reports
599 label_news: News
600 label_news: News
600 label_news_new: Add news
601 label_news_new: Add news
601 label_news_plural: News
602 label_news_plural: News
602 label_news_latest: Latest news
603 label_news_latest: Latest news
603 label_news_view_all: View all news
604 label_news_view_all: View all news
604 label_news_added: News added
605 label_news_added: News added
605 label_news_comment_added: Comment added to a news
606 label_news_comment_added: Comment added to a news
606 label_settings: Settings
607 label_settings: Settings
607 label_overview: Overview
608 label_overview: Overview
608 label_version: Version
609 label_version: Version
609 label_version_new: New version
610 label_version_new: New version
610 label_version_plural: Versions
611 label_version_plural: Versions
611 label_close_versions: Close completed versions
612 label_close_versions: Close completed versions
612 label_confirmation: Confirmation
613 label_confirmation: Confirmation
613 label_export_to: 'Also available in:'
614 label_export_to: 'Also available in:'
614 label_read: Read...
615 label_read: Read...
615 label_public_projects: Public projects
616 label_public_projects: Public projects
616 label_open_issues: open
617 label_open_issues: open
617 label_open_issues_plural: open
618 label_open_issues_plural: open
618 label_closed_issues: closed
619 label_closed_issues: closed
619 label_closed_issues_plural: closed
620 label_closed_issues_plural: closed
620 label_x_open_issues_abbr_on_total:
621 label_x_open_issues_abbr_on_total:
621 zero: 0 open / %{total}
622 zero: 0 open / %{total}
622 one: 1 open / %{total}
623 one: 1 open / %{total}
623 other: "%{count} open / %{total}"
624 other: "%{count} open / %{total}"
624 label_x_open_issues_abbr:
625 label_x_open_issues_abbr:
625 zero: 0 open
626 zero: 0 open
626 one: 1 open
627 one: 1 open
627 other: "%{count} open"
628 other: "%{count} open"
628 label_x_closed_issues_abbr:
629 label_x_closed_issues_abbr:
629 zero: 0 closed
630 zero: 0 closed
630 one: 1 closed
631 one: 1 closed
631 other: "%{count} closed"
632 other: "%{count} closed"
632 label_x_issues:
633 label_x_issues:
633 zero: 0 issues
634 zero: 0 issues
634 one: 1 issue
635 one: 1 issue
635 other: "%{count} issues"
636 other: "%{count} issues"
636 label_total: Total
637 label_total: Total
637 label_total_time: Total time
638 label_total_time: Total time
638 label_permissions: Permissions
639 label_permissions: Permissions
639 label_current_status: Current status
640 label_current_status: Current status
640 label_new_statuses_allowed: New statuses allowed
641 label_new_statuses_allowed: New statuses allowed
641 label_all: all
642 label_all: all
642 label_any: any
643 label_any: any
643 label_none: none
644 label_none: none
644 label_nobody: nobody
645 label_nobody: nobody
645 label_next: Next
646 label_next: Next
646 label_previous: Previous
647 label_previous: Previous
647 label_used_by: Used by
648 label_used_by: Used by
648 label_details: Details
649 label_details: Details
649 label_add_note: Add a note
650 label_add_note: Add a note
650 label_calendar: Calendar
651 label_calendar: Calendar
651 label_months_from: months from
652 label_months_from: months from
652 label_gantt: Gantt
653 label_gantt: Gantt
653 label_internal: Internal
654 label_internal: Internal
654 label_last_changes: "last %{count} changes"
655 label_last_changes: "last %{count} changes"
655 label_change_view_all: View all changes
656 label_change_view_all: View all changes
656 label_personalize_page: Personalize this page
657 label_personalize_page: Personalize this page
657 label_comment: Comment
658 label_comment: Comment
658 label_comment_plural: Comments
659 label_comment_plural: Comments
659 label_x_comments:
660 label_x_comments:
660 zero: no comments
661 zero: no comments
661 one: 1 comment
662 one: 1 comment
662 other: "%{count} comments"
663 other: "%{count} comments"
663 label_comment_add: Add a comment
664 label_comment_add: Add a comment
664 label_comment_added: Comment added
665 label_comment_added: Comment added
665 label_comment_delete: Delete comments
666 label_comment_delete: Delete comments
666 label_query: Custom query
667 label_query: Custom query
667 label_query_plural: Custom queries
668 label_query_plural: Custom queries
668 label_query_new: New query
669 label_query_new: New query
669 label_my_queries: My custom queries
670 label_my_queries: My custom queries
670 label_filter_add: Add filter
671 label_filter_add: Add filter
671 label_filter_plural: Filters
672 label_filter_plural: Filters
672 label_equals: is
673 label_equals: is
673 label_not_equals: is not
674 label_not_equals: is not
674 label_in_less_than: in less than
675 label_in_less_than: in less than
675 label_in_more_than: in more than
676 label_in_more_than: in more than
676 label_in_the_next_days: in the next
677 label_in_the_next_days: in the next
677 label_in_the_past_days: in the past
678 label_in_the_past_days: in the past
678 label_greater_or_equal: '>='
679 label_greater_or_equal: '>='
679 label_less_or_equal: '<='
680 label_less_or_equal: '<='
680 label_between: between
681 label_between: between
681 label_in: in
682 label_in: in
682 label_today: today
683 label_today: today
683 label_all_time: all time
684 label_all_time: all time
684 label_yesterday: yesterday
685 label_yesterday: yesterday
685 label_this_week: this week
686 label_this_week: this week
686 label_last_week: last week
687 label_last_week: last week
687 label_last_n_weeks: "last %{count} weeks"
688 label_last_n_weeks: "last %{count} weeks"
688 label_last_n_days: "last %{count} days"
689 label_last_n_days: "last %{count} days"
689 label_this_month: this month
690 label_this_month: this month
690 label_last_month: last month
691 label_last_month: last month
691 label_this_year: this year
692 label_this_year: this year
692 label_date_range: Date range
693 label_date_range: Date range
693 label_less_than_ago: less than days ago
694 label_less_than_ago: less than days ago
694 label_more_than_ago: more than days ago
695 label_more_than_ago: more than days ago
695 label_ago: days ago
696 label_ago: days ago
696 label_contains: contains
697 label_contains: contains
697 label_not_contains: doesn't contain
698 label_not_contains: doesn't contain
698 label_any_issues_in_project: any issues in project
699 label_any_issues_in_project: any issues in project
699 label_any_issues_not_in_project: any issues not in project
700 label_any_issues_not_in_project: any issues not in project
700 label_no_issues_in_project: no issues in project
701 label_no_issues_in_project: no issues in project
701 label_day_plural: days
702 label_day_plural: days
702 label_repository: Repository
703 label_repository: Repository
703 label_repository_new: New repository
704 label_repository_new: New repository
704 label_repository_plural: Repositories
705 label_repository_plural: Repositories
705 label_browse: Browse
706 label_browse: Browse
706 label_branch: Branch
707 label_branch: Branch
707 label_tag: Tag
708 label_tag: Tag
708 label_revision: Revision
709 label_revision: Revision
709 label_revision_plural: Revisions
710 label_revision_plural: Revisions
710 label_revision_id: "Revision %{value}"
711 label_revision_id: "Revision %{value}"
711 label_associated_revisions: Associated revisions
712 label_associated_revisions: Associated revisions
712 label_added: added
713 label_added: added
713 label_modified: modified
714 label_modified: modified
714 label_copied: copied
715 label_copied: copied
715 label_renamed: renamed
716 label_renamed: renamed
716 label_deleted: deleted
717 label_deleted: deleted
717 label_latest_revision: Latest revision
718 label_latest_revision: Latest revision
718 label_latest_revision_plural: Latest revisions
719 label_latest_revision_plural: Latest revisions
719 label_view_revisions: View revisions
720 label_view_revisions: View revisions
720 label_view_all_revisions: View all revisions
721 label_view_all_revisions: View all revisions
721 label_max_size: Maximum size
722 label_max_size: Maximum size
722 label_sort_highest: Move to top
723 label_sort_highest: Move to top
723 label_sort_higher: Move up
724 label_sort_higher: Move up
724 label_sort_lower: Move down
725 label_sort_lower: Move down
725 label_sort_lowest: Move to bottom
726 label_sort_lowest: Move to bottom
726 label_roadmap: Roadmap
727 label_roadmap: Roadmap
727 label_roadmap_due_in: "Due in %{value}"
728 label_roadmap_due_in: "Due in %{value}"
728 label_roadmap_overdue: "%{value} late"
729 label_roadmap_overdue: "%{value} late"
729 label_roadmap_no_issues: No issues for this version
730 label_roadmap_no_issues: No issues for this version
730 label_search: Search
731 label_search: Search
731 label_result_plural: Results
732 label_result_plural: Results
732 label_all_words: All words
733 label_all_words: All words
733 label_wiki: Wiki
734 label_wiki: Wiki
734 label_wiki_edit: Wiki edit
735 label_wiki_edit: Wiki edit
735 label_wiki_edit_plural: Wiki edits
736 label_wiki_edit_plural: Wiki edits
736 label_wiki_page: Wiki page
737 label_wiki_page: Wiki page
737 label_wiki_page_plural: Wiki pages
738 label_wiki_page_plural: Wiki pages
738 label_index_by_title: Index by title
739 label_index_by_title: Index by title
739 label_index_by_date: Index by date
740 label_index_by_date: Index by date
740 label_current_version: Current version
741 label_current_version: Current version
741 label_preview: Preview
742 label_preview: Preview
742 label_feed_plural: Feeds
743 label_feed_plural: Feeds
743 label_changes_details: Details of all changes
744 label_changes_details: Details of all changes
744 label_issue_tracking: Issue tracking
745 label_issue_tracking: Issue tracking
745 label_spent_time: Spent time
746 label_spent_time: Spent time
746 label_overall_spent_time: Overall spent time
747 label_overall_spent_time: Overall spent time
747 label_f_hour: "%{value} hour"
748 label_f_hour: "%{value} hour"
748 label_f_hour_plural: "%{value} hours"
749 label_f_hour_plural: "%{value} hours"
749 label_time_tracking: Time tracking
750 label_time_tracking: Time tracking
750 label_change_plural: Changes
751 label_change_plural: Changes
751 label_statistics: Statistics
752 label_statistics: Statistics
752 label_commits_per_month: Commits per month
753 label_commits_per_month: Commits per month
753 label_commits_per_author: Commits per author
754 label_commits_per_author: Commits per author
754 label_diff: diff
755 label_diff: diff
755 label_view_diff: View differences
756 label_view_diff: View differences
756 label_diff_inline: inline
757 label_diff_inline: inline
757 label_diff_side_by_side: side by side
758 label_diff_side_by_side: side by side
758 label_options: Options
759 label_options: Options
759 label_copy_workflow_from: Copy workflow from
760 label_copy_workflow_from: Copy workflow from
760 label_permissions_report: Permissions report
761 label_permissions_report: Permissions report
761 label_watched_issues: Watched issues
762 label_watched_issues: Watched issues
762 label_related_issues: Related issues
763 label_related_issues: Related issues
763 label_applied_status: Applied status
764 label_applied_status: Applied status
764 label_loading: Loading...
765 label_loading: Loading...
765 label_relation_new: New relation
766 label_relation_new: New relation
766 label_relation_delete: Delete relation
767 label_relation_delete: Delete relation
767 label_relates_to: Related to
768 label_relates_to: Related to
768 label_duplicates: Duplicates
769 label_duplicates: Duplicates
769 label_duplicated_by: Duplicated by
770 label_duplicated_by: Duplicated by
770 label_blocks: Blocks
771 label_blocks: Blocks
771 label_blocked_by: Blocked by
772 label_blocked_by: Blocked by
772 label_precedes: Precedes
773 label_precedes: Precedes
773 label_follows: Follows
774 label_follows: Follows
774 label_copied_to: Copied to
775 label_copied_to: Copied to
775 label_copied_from: Copied from
776 label_copied_from: Copied from
776 label_end_to_start: end to start
777 label_end_to_start: end to start
777 label_end_to_end: end to end
778 label_end_to_end: end to end
778 label_start_to_start: start to start
779 label_start_to_start: start to start
779 label_start_to_end: start to end
780 label_start_to_end: start to end
780 label_stay_logged_in: Stay logged in
781 label_stay_logged_in: Stay logged in
781 label_disabled: disabled
782 label_disabled: disabled
782 label_show_completed_versions: Show completed versions
783 label_show_completed_versions: Show completed versions
783 label_me: me
784 label_me: me
784 label_board: Forum
785 label_board: Forum
785 label_board_new: New forum
786 label_board_new: New forum
786 label_board_plural: Forums
787 label_board_plural: Forums
787 label_board_locked: Locked
788 label_board_locked: Locked
788 label_board_sticky: Sticky
789 label_board_sticky: Sticky
789 label_topic_plural: Topics
790 label_topic_plural: Topics
790 label_message_plural: Messages
791 label_message_plural: Messages
791 label_message_last: Last message
792 label_message_last: Last message
792 label_message_new: New message
793 label_message_new: New message
793 label_message_posted: Message added
794 label_message_posted: Message added
794 label_reply_plural: Replies
795 label_reply_plural: Replies
795 label_send_information: Send account information to the user
796 label_send_information: Send account information to the user
796 label_year: Year
797 label_year: Year
797 label_month: Month
798 label_month: Month
798 label_week: Week
799 label_week: Week
799 label_date_from: From
800 label_date_from: From
800 label_date_to: To
801 label_date_to: To
801 label_language_based: Based on user's language
802 label_language_based: Based on user's language
802 label_sort_by: "Sort by %{value}"
803 label_sort_by: "Sort by %{value}"
803 label_send_test_email: Send a test email
804 label_send_test_email: Send a test email
804 label_feeds_access_key: Atom access key
805 label_feeds_access_key: Atom access key
805 label_missing_feeds_access_key: Missing a Atom access key
806 label_missing_feeds_access_key: Missing a Atom access key
806 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
807 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
807 label_module_plural: Modules
808 label_module_plural: Modules
808 label_added_time_by: "Added by %{author} %{age} ago"
809 label_added_time_by: "Added by %{author} %{age} ago"
809 label_updated_time_by: "Updated by %{author} %{age} ago"
810 label_updated_time_by: "Updated by %{author} %{age} ago"
810 label_updated_time: "Updated %{value} ago"
811 label_updated_time: "Updated %{value} ago"
811 label_jump_to_a_project: Jump to a project...
812 label_jump_to_a_project: Jump to a project...
812 label_file_plural: Files
813 label_file_plural: Files
813 label_changeset_plural: Changesets
814 label_changeset_plural: Changesets
814 label_default_columns: Default columns
815 label_default_columns: Default columns
815 label_no_change_option: (No change)
816 label_no_change_option: (No change)
816 label_bulk_edit_selected_issues: Bulk edit selected issues
817 label_bulk_edit_selected_issues: Bulk edit selected issues
817 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
818 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
818 label_theme: Theme
819 label_theme: Theme
819 label_default: Default
820 label_default: Default
820 label_search_titles_only: Search titles only
821 label_search_titles_only: Search titles only
821 label_user_mail_option_all: "For any event on all my projects"
822 label_user_mail_option_all: "For any event on all my projects"
822 label_user_mail_option_selected: "For any event on the selected projects only..."
823 label_user_mail_option_selected: "For any event on the selected projects only..."
823 label_user_mail_option_none: "No events"
824 label_user_mail_option_none: "No events"
824 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
825 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
825 label_user_mail_option_only_assigned: "Only for things I am assigned to"
826 label_user_mail_option_only_assigned: "Only for things I am assigned to"
826 label_user_mail_option_only_owner: "Only for things I am the owner of"
827 label_user_mail_option_only_owner: "Only for things I am the owner of"
827 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
828 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
828 label_registration_activation_by_email: account activation by email
829 label_registration_activation_by_email: account activation by email
829 label_registration_manual_activation: manual account activation
830 label_registration_manual_activation: manual account activation
830 label_registration_automatic_activation: automatic account activation
831 label_registration_automatic_activation: automatic account activation
831 label_display_per_page: "Per page: %{value}"
832 label_display_per_page: "Per page: %{value}"
832 label_age: Age
833 label_age: Age
833 label_change_properties: Change properties
834 label_change_properties: Change properties
834 label_general: General
835 label_general: General
835 label_more: More
836 label_more: More
836 label_scm: SCM
837 label_scm: SCM
837 label_plugins: Plugins
838 label_plugins: Plugins
838 label_ldap_authentication: LDAP authentication
839 label_ldap_authentication: LDAP authentication
839 label_downloads_abbr: D/L
840 label_downloads_abbr: D/L
840 label_optional_description: Optional description
841 label_optional_description: Optional description
841 label_add_another_file: Add another file
842 label_add_another_file: Add another file
842 label_preferences: Preferences
843 label_preferences: Preferences
843 label_chronological_order: In chronological order
844 label_chronological_order: In chronological order
844 label_reverse_chronological_order: In reverse chronological order
845 label_reverse_chronological_order: In reverse chronological order
845 label_planning: Planning
846 label_planning: Planning
846 label_incoming_emails: Incoming emails
847 label_incoming_emails: Incoming emails
847 label_generate_key: Generate a key
848 label_generate_key: Generate a key
848 label_issue_watchers: Watchers
849 label_issue_watchers: Watchers
849 label_example: Example
850 label_example: Example
850 label_display: Display
851 label_display: Display
851 label_sort: Sort
852 label_sort: Sort
852 label_ascending: Ascending
853 label_ascending: Ascending
853 label_descending: Descending
854 label_descending: Descending
854 label_date_from_to: From %{start} to %{end}
855 label_date_from_to: From %{start} to %{end}
855 label_wiki_content_added: Wiki page added
856 label_wiki_content_added: Wiki page added
856 label_wiki_content_updated: Wiki page updated
857 label_wiki_content_updated: Wiki page updated
857 label_group: Group
858 label_group: Group
858 label_group_plural: Groups
859 label_group_plural: Groups
859 label_group_new: New group
860 label_group_new: New group
860 label_group_anonymous: Anonymous users
861 label_group_anonymous: Anonymous users
861 label_group_non_member: Non member users
862 label_group_non_member: Non member users
862 label_time_entry_plural: Spent time
863 label_time_entry_plural: Spent time
863 label_version_sharing_none: Not shared
864 label_version_sharing_none: Not shared
864 label_version_sharing_descendants: With subprojects
865 label_version_sharing_descendants: With subprojects
865 label_version_sharing_hierarchy: With project hierarchy
866 label_version_sharing_hierarchy: With project hierarchy
866 label_version_sharing_tree: With project tree
867 label_version_sharing_tree: With project tree
867 label_version_sharing_system: With all projects
868 label_version_sharing_system: With all projects
868 label_update_issue_done_ratios: Update issue done ratios
869 label_update_issue_done_ratios: Update issue done ratios
869 label_copy_source: Source
870 label_copy_source: Source
870 label_copy_target: Target
871 label_copy_target: Target
871 label_copy_same_as_target: Same as target
872 label_copy_same_as_target: Same as target
872 label_display_used_statuses_only: Only display statuses that are used by this tracker
873 label_display_used_statuses_only: Only display statuses that are used by this tracker
873 label_api_access_key: API access key
874 label_api_access_key: API access key
874 label_missing_api_access_key: Missing an API access key
875 label_missing_api_access_key: Missing an API access key
875 label_api_access_key_created_on: "API access key created %{value} ago"
876 label_api_access_key_created_on: "API access key created %{value} ago"
876 label_profile: Profile
877 label_profile: Profile
877 label_subtask_plural: Subtasks
878 label_subtask_plural: Subtasks
878 label_project_copy_notifications: Send email notifications during the project copy
879 label_project_copy_notifications: Send email notifications during the project copy
879 label_principal_search: "Search for user or group:"
880 label_principal_search: "Search for user or group:"
880 label_user_search: "Search for user:"
881 label_user_search: "Search for user:"
881 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
882 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
882 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
883 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
883 label_issues_visibility_all: All issues
884 label_issues_visibility_all: All issues
884 label_issues_visibility_public: All non private issues
885 label_issues_visibility_public: All non private issues
885 label_issues_visibility_own: Issues created by or assigned to the user
886 label_issues_visibility_own: Issues created by or assigned to the user
886 label_git_report_last_commit: Report last commit for files and directories
887 label_git_report_last_commit: Report last commit for files and directories
887 label_parent_revision: Parent
888 label_parent_revision: Parent
888 label_child_revision: Child
889 label_child_revision: Child
889 label_export_options: "%{export_format} export options"
890 label_export_options: "%{export_format} export options"
890 label_copy_attachments: Copy attachments
891 label_copy_attachments: Copy attachments
891 label_copy_subtasks: Copy subtasks
892 label_copy_subtasks: Copy subtasks
892 label_item_position: "%{position} of %{count}"
893 label_item_position: "%{position} of %{count}"
893 label_completed_versions: Completed versions
894 label_completed_versions: Completed versions
894 label_search_for_watchers: Search for watchers to add
895 label_search_for_watchers: Search for watchers to add
895 label_session_expiration: Session expiration
896 label_session_expiration: Session expiration
896 label_show_closed_projects: View closed projects
897 label_show_closed_projects: View closed projects
897 label_status_transitions: Status transitions
898 label_status_transitions: Status transitions
898 label_fields_permissions: Fields permissions
899 label_fields_permissions: Fields permissions
899 label_readonly: Read-only
900 label_readonly: Read-only
900 label_required: Required
901 label_required: Required
901 label_hidden: Hidden
902 label_hidden: Hidden
902 label_attribute_of_project: "Project's %{name}"
903 label_attribute_of_project: "Project's %{name}"
903 label_attribute_of_issue: "Issue's %{name}"
904 label_attribute_of_issue: "Issue's %{name}"
904 label_attribute_of_author: "Author's %{name}"
905 label_attribute_of_author: "Author's %{name}"
905 label_attribute_of_assigned_to: "Assignee's %{name}"
906 label_attribute_of_assigned_to: "Assignee's %{name}"
906 label_attribute_of_user: "User's %{name}"
907 label_attribute_of_user: "User's %{name}"
907 label_attribute_of_fixed_version: "Target version's %{name}"
908 label_attribute_of_fixed_version: "Target version's %{name}"
908 label_cross_project_descendants: With subprojects
909 label_cross_project_descendants: With subprojects
909 label_cross_project_tree: With project tree
910 label_cross_project_tree: With project tree
910 label_cross_project_hierarchy: With project hierarchy
911 label_cross_project_hierarchy: With project hierarchy
911 label_cross_project_system: With all projects
912 label_cross_project_system: With all projects
912 label_gantt_progress_line: Progress line
913 label_gantt_progress_line: Progress line
913 label_visibility_private: to me only
914 label_visibility_private: to me only
914 label_visibility_roles: to these roles only
915 label_visibility_roles: to these roles only
915 label_visibility_public: to any users
916 label_visibility_public: to any users
916 label_link: Link
917 label_link: Link
917 label_only: only
918 label_only: only
918 label_drop_down_list: drop-down list
919 label_drop_down_list: drop-down list
919 label_checkboxes: checkboxes
920 label_checkboxes: checkboxes
920 label_radio_buttons: radio buttons
921 label_radio_buttons: radio buttons
921 label_link_values_to: Link values to URL
922 label_link_values_to: Link values to URL
922 label_custom_field_select_type: Select the type of object to which the custom field is to be attached
923 label_custom_field_select_type: Select the type of object to which the custom field is to be attached
923 label_check_for_updates: Check for updates
924 label_check_for_updates: Check for updates
924 label_latest_compatible_version: Latest compatible version
925 label_latest_compatible_version: Latest compatible version
925 label_unknown_plugin: Unknown plugin
926 label_unknown_plugin: Unknown plugin
926 label_add_projects: Add projects
927 label_add_projects: Add projects
927 label_users_visibility_all: All active users
928 label_users_visibility_all: All active users
928 label_users_visibility_members_of_visible_projects: Members of visible projects
929 label_users_visibility_members_of_visible_projects: Members of visible projects
929 label_edit_attachments: Edit attached files
930 label_edit_attachments: Edit attached files
930 label_link_copied_issue: Link copied issue
931 label_link_copied_issue: Link copied issue
931 label_ask: Ask
932 label_ask: Ask
932 label_search_attachments_yes: Search attachment filenames and descriptions
933 label_search_attachments_yes: Search attachment filenames and descriptions
933 label_search_attachments_no: Do not search attachments
934 label_search_attachments_no: Do not search attachments
934 label_search_attachments_only: Search attachments only
935 label_search_attachments_only: Search attachments only
935 label_search_open_issues_only: Open issues only
936 label_search_open_issues_only: Open issues only
936 label_email_address_plural: Emails
937 label_email_address_plural: Emails
937 label_email_address_add: Add email address
938 label_email_address_add: Add email address
938 label_enable_notifications: Enable notifications
939 label_enable_notifications: Enable notifications
939 label_disable_notifications: Disable notifications
940 label_disable_notifications: Disable notifications
940 label_blank_value: blank
941 label_blank_value: blank
941
942
942 button_login: Login
943 button_login: Login
943 button_submit: Submit
944 button_submit: Submit
944 button_save: Save
945 button_save: Save
945 button_check_all: Check all
946 button_check_all: Check all
946 button_uncheck_all: Uncheck all
947 button_uncheck_all: Uncheck all
947 button_collapse_all: Collapse all
948 button_collapse_all: Collapse all
948 button_expand_all: Expand all
949 button_expand_all: Expand all
949 button_delete: Delete
950 button_delete: Delete
950 button_create: Create
951 button_create: Create
951 button_create_and_continue: Create and continue
952 button_create_and_continue: Create and continue
952 button_test: Test
953 button_test: Test
953 button_edit: Edit
954 button_edit: Edit
954 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
955 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
955 button_add: Add
956 button_add: Add
956 button_change: Change
957 button_change: Change
957 button_apply: Apply
958 button_apply: Apply
958 button_clear: Clear
959 button_clear: Clear
959 button_lock: Lock
960 button_lock: Lock
960 button_unlock: Unlock
961 button_unlock: Unlock
961 button_download: Download
962 button_download: Download
962 button_list: List
963 button_list: List
963 button_view: View
964 button_view: View
964 button_move: Move
965 button_move: Move
965 button_move_and_follow: Move and follow
966 button_move_and_follow: Move and follow
966 button_back: Back
967 button_back: Back
967 button_cancel: Cancel
968 button_cancel: Cancel
968 button_activate: Activate
969 button_activate: Activate
969 button_sort: Sort
970 button_sort: Sort
970 button_log_time: Log time
971 button_log_time: Log time
971 button_rollback: Rollback to this version
972 button_rollback: Rollback to this version
972 button_watch: Watch
973 button_watch: Watch
973 button_unwatch: Unwatch
974 button_unwatch: Unwatch
974 button_reply: Reply
975 button_reply: Reply
975 button_archive: Archive
976 button_archive: Archive
976 button_unarchive: Unarchive
977 button_unarchive: Unarchive
977 button_reset: Reset
978 button_reset: Reset
978 button_rename: Rename
979 button_rename: Rename
979 button_change_password: Change password
980 button_change_password: Change password
980 button_copy: Copy
981 button_copy: Copy
981 button_copy_and_follow: Copy and follow
982 button_copy_and_follow: Copy and follow
982 button_annotate: Annotate
983 button_annotate: Annotate
983 button_update: Update
984 button_update: Update
984 button_configure: Configure
985 button_configure: Configure
985 button_quote: Quote
986 button_quote: Quote
986 button_duplicate: Duplicate
987 button_duplicate: Duplicate
987 button_show: Show
988 button_show: Show
988 button_hide: Hide
989 button_hide: Hide
989 button_edit_section: Edit this section
990 button_edit_section: Edit this section
990 button_export: Export
991 button_export: Export
991 button_delete_my_account: Delete my account
992 button_delete_my_account: Delete my account
992 button_close: Close
993 button_close: Close
993 button_reopen: Reopen
994 button_reopen: Reopen
994
995
995 status_active: active
996 status_active: active
996 status_registered: registered
997 status_registered: registered
997 status_locked: locked
998 status_locked: locked
998
999
999 project_status_active: active
1000 project_status_active: active
1000 project_status_closed: closed
1001 project_status_closed: closed
1001 project_status_archived: archived
1002 project_status_archived: archived
1002
1003
1003 version_status_open: open
1004 version_status_open: open
1004 version_status_locked: locked
1005 version_status_locked: locked
1005 version_status_closed: closed
1006 version_status_closed: closed
1006
1007
1007 field_active: Active
1008 field_active: Active
1008
1009
1009 text_select_mail_notifications: Select actions for which email notifications should be sent.
1010 text_select_mail_notifications: Select actions for which email notifications should be sent.
1010 text_regexp_info: eg. ^[A-Z0-9]+$
1011 text_regexp_info: eg. ^[A-Z0-9]+$
1011 text_min_max_length_info: 0 means no restriction
1012 text_min_max_length_info: 0 means no restriction
1012 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
1013 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
1013 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
1014 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
1014 text_workflow_edit: Select a role and a tracker to edit the workflow
1015 text_workflow_edit: Select a role and a tracker to edit the workflow
1015 text_are_you_sure: Are you sure?
1016 text_are_you_sure: Are you sure?
1016 text_journal_changed: "%{label} changed from %{old} to %{new}"
1017 text_journal_changed: "%{label} changed from %{old} to %{new}"
1017 text_journal_changed_no_detail: "%{label} updated"
1018 text_journal_changed_no_detail: "%{label} updated"
1018 text_journal_set_to: "%{label} set to %{value}"
1019 text_journal_set_to: "%{label} set to %{value}"
1019 text_journal_deleted: "%{label} deleted (%{old})"
1020 text_journal_deleted: "%{label} deleted (%{old})"
1020 text_journal_added: "%{label} %{value} added"
1021 text_journal_added: "%{label} %{value} added"
1021 text_tip_issue_begin_day: issue beginning this day
1022 text_tip_issue_begin_day: issue beginning this day
1022 text_tip_issue_end_day: issue ending this day
1023 text_tip_issue_end_day: issue ending this day
1023 text_tip_issue_begin_end_day: issue beginning and ending this day
1024 text_tip_issue_begin_end_day: issue beginning and ending this day
1024 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 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 text_caracters_maximum: "%{count} characters maximum."
1026 text_caracters_maximum: "%{count} characters maximum."
1026 text_caracters_minimum: "Must be at least %{count} characters long."
1027 text_caracters_minimum: "Must be at least %{count} characters long."
1027 text_length_between: "Length between %{min} and %{max} characters."
1028 text_length_between: "Length between %{min} and %{max} characters."
1028 text_tracker_no_workflow: No workflow defined for this tracker
1029 text_tracker_no_workflow: No workflow defined for this tracker
1029 text_unallowed_characters: Unallowed characters
1030 text_unallowed_characters: Unallowed characters
1030 text_comma_separated: Multiple values allowed (comma separated).
1031 text_comma_separated: Multiple values allowed (comma separated).
1031 text_line_separated: Multiple values allowed (one line for each value).
1032 text_line_separated: Multiple values allowed (one line for each value).
1032 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
1033 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
1033 text_issue_added: "Issue %{id} has been reported by %{author}."
1034 text_issue_added: "Issue %{id} has been reported by %{author}."
1034 text_issue_updated: "Issue %{id} has been updated by %{author}."
1035 text_issue_updated: "Issue %{id} has been updated by %{author}."
1035 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
1036 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
1036 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
1037 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
1037 text_issue_category_destroy_assignments: Remove category assignments
1038 text_issue_category_destroy_assignments: Remove category assignments
1038 text_issue_category_reassign_to: Reassign issues to this category
1039 text_issue_category_reassign_to: Reassign issues to this category
1039 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 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 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 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 text_load_default_configuration: Load the default configuration
1042 text_load_default_configuration: Load the default configuration
1042 text_status_changed_by_changeset: "Applied in changeset %{value}."
1043 text_status_changed_by_changeset: "Applied in changeset %{value}."
1043 text_time_logged_by_changeset: "Applied in changeset %{value}."
1044 text_time_logged_by_changeset: "Applied in changeset %{value}."
1044 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1045 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1045 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1046 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1046 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1047 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1047 text_select_project_modules: 'Select modules to enable for this project:'
1048 text_select_project_modules: 'Select modules to enable for this project:'
1048 text_default_administrator_account_changed: Default administrator account changed
1049 text_default_administrator_account_changed: Default administrator account changed
1049 text_file_repository_writable: Attachments directory writable
1050 text_file_repository_writable: Attachments directory writable
1050 text_plugin_assets_writable: Plugin assets directory writable
1051 text_plugin_assets_writable: Plugin assets directory writable
1051 text_rmagick_available: RMagick available (optional)
1052 text_rmagick_available: RMagick available (optional)
1052 text_convert_available: ImageMagick convert available (optional)
1053 text_convert_available: ImageMagick convert available (optional)
1053 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
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 text_destroy_time_entries: Delete reported hours
1055 text_destroy_time_entries: Delete reported hours
1055 text_assign_time_entries_to_project: Assign reported hours to the project
1056 text_assign_time_entries_to_project: Assign reported hours to the project
1056 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1057 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1057 text_user_wrote: "%{value} wrote:"
1058 text_user_wrote: "%{value} wrote:"
1058 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1059 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1059 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1060 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1060 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 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 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 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 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1063 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1063 text_custom_field_possible_values_info: 'One line for each value'
1064 text_custom_field_possible_values_info: 'One line for each value'
1064 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1065 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1065 text_wiki_page_nullify_children: "Keep child pages as root pages"
1066 text_wiki_page_nullify_children: "Keep child pages as root pages"
1066 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1067 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1067 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1068 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1068 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 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 text_zoom_in: Zoom in
1070 text_zoom_in: Zoom in
1070 text_zoom_out: Zoom out
1071 text_zoom_out: Zoom out
1071 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1072 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1072 text_scm_path_encoding_note: "Default: UTF-8"
1073 text_scm_path_encoding_note: "Default: UTF-8"
1073 text_subversion_repository_note: "Examples: file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1074 text_subversion_repository_note: "Examples: file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1074 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1075 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1075 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1076 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1076 text_scm_command: Command
1077 text_scm_command: Command
1077 text_scm_command_version: Version
1078 text_scm_command_version: Version
1078 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1079 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1079 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1080 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1080 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1081 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1081 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1082 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1082 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1083 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1083 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
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 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1085 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1085 text_project_closed: This project is closed and read-only.
1086 text_project_closed: This project is closed and read-only.
1086 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
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 default_role_manager: Manager
1089 default_role_manager: Manager
1089 default_role_developer: Developer
1090 default_role_developer: Developer
1090 default_role_reporter: Reporter
1091 default_role_reporter: Reporter
1091 default_tracker_bug: Bug
1092 default_tracker_bug: Bug
1092 default_tracker_feature: Feature
1093 default_tracker_feature: Feature
1093 default_tracker_support: Support
1094 default_tracker_support: Support
1094 default_issue_status_new: New
1095 default_issue_status_new: New
1095 default_issue_status_in_progress: In Progress
1096 default_issue_status_in_progress: In Progress
1096 default_issue_status_resolved: Resolved
1097 default_issue_status_resolved: Resolved
1097 default_issue_status_feedback: Feedback
1098 default_issue_status_feedback: Feedback
1098 default_issue_status_closed: Closed
1099 default_issue_status_closed: Closed
1099 default_issue_status_rejected: Rejected
1100 default_issue_status_rejected: Rejected
1100 default_doc_category_user: User documentation
1101 default_doc_category_user: User documentation
1101 default_doc_category_tech: Technical documentation
1102 default_doc_category_tech: Technical documentation
1102 default_priority_low: Low
1103 default_priority_low: Low
1103 default_priority_normal: Normal
1104 default_priority_normal: Normal
1104 default_priority_high: High
1105 default_priority_high: High
1105 default_priority_urgent: Urgent
1106 default_priority_urgent: Urgent
1106 default_priority_immediate: Immediate
1107 default_priority_immediate: Immediate
1107 default_activity_design: Design
1108 default_activity_design: Design
1108 default_activity_development: Development
1109 default_activity_development: Development
1109
1110
1110 enumeration_issue_priorities: Issue priorities
1111 enumeration_issue_priorities: Issue priorities
1111 enumeration_doc_categories: Document categories
1112 enumeration_doc_categories: Document categories
1112 enumeration_activities: Activities (time tracking)
1113 enumeration_activities: Activities (time tracking)
1113 enumeration_system_activity: System Activity
1114 enumeration_system_activity: System Activity
1114 description_filter: Filter
1115 description_filter: Filter
1115 description_search: Searchfield
1116 description_search: Searchfield
1116 description_choose_project: Projects
1117 description_choose_project: Projects
1117 description_project_scope: Search scope
1118 description_project_scope: Search scope
1118 description_notes: Notes
1119 description_notes: Notes
1119 description_message_content: Message content
1120 description_message_content: Message content
1120 description_query_sort_criteria_attribute: Sort attribute
1121 description_query_sort_criteria_attribute: Sort attribute
1121 description_query_sort_criteria_direction: Sort direction
1122 description_query_sort_criteria_direction: Sort direction
1122 description_user_mail_notification: Mail notification settings
1123 description_user_mail_notification: Mail notification settings
1123 description_available_columns: Available Columns
1124 description_available_columns: Available Columns
1124 description_selected_columns: Selected Columns
1125 description_selected_columns: Selected Columns
1125 description_all_columns: All Columns
1126 description_all_columns: All Columns
1126 description_issue_category_reassign: Choose issue category
1127 description_issue_category_reassign: Choose issue category
1127 description_wiki_subpages_reassign: Choose new parent page
1128 description_wiki_subpages_reassign: Choose new parent page
1128 description_date_range_list: Choose range from list
1129 description_date_range_list: Choose range from list
1129 description_date_range_interval: Choose range by selecting start and end date
1130 description_date_range_interval: Choose range by selecting start and end date
1130 description_date_from: Enter start date
1131 description_date_from: Enter start date
1131 description_date_to: Enter end date
1132 description_date_to: Enter end date
1132 text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
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 # French translations for Ruby on Rails
1 # French translations for Ruby on Rails
2 # by Christian Lescuyer (christian@flyingcoders.com)
2 # by Christian Lescuyer (christian@flyingcoders.com)
3 # contributor: Sebastien Grosjean - ZenCocoon.com
3 # contributor: Sebastien Grosjean - ZenCocoon.com
4 # contributor: Thibaut Cuvelier - Developpez.com
4 # contributor: Thibaut Cuvelier - Developpez.com
5
5
6 fr:
6 fr:
7 direction: ltr
7 direction: ltr
8 date:
8 date:
9 formats:
9 formats:
10 default: "%d/%m/%Y"
10 default: "%d/%m/%Y"
11 short: "%e %b"
11 short: "%e %b"
12 long: "%e %B %Y"
12 long: "%e %B %Y"
13 long_ordinal: "%e %B %Y"
13 long_ordinal: "%e %B %Y"
14 only_day: "%e"
14 only_day: "%e"
15
15
16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
18
18
19 # Don't forget the nil at the beginning; there's no such thing as a 0th month
19 # Don't forget the nil at the beginning; there's no such thing as a 0th month
20 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
20 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
21 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
21 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
22 # Used in date_select and datime_select.
22 # Used in date_select and datime_select.
23 order:
23 order:
24 - :day
24 - :day
25 - :month
25 - :month
26 - :year
26 - :year
27
27
28 time:
28 time:
29 formats:
29 formats:
30 default: "%d/%m/%Y %H:%M"
30 default: "%d/%m/%Y %H:%M"
31 time: "%H:%M"
31 time: "%H:%M"
32 short: "%d %b %H:%M"
32 short: "%d %b %H:%M"
33 long: "%A %d %B %Y %H:%M:%S %Z"
33 long: "%A %d %B %Y %H:%M:%S %Z"
34 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
34 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
35 only_second: "%S"
35 only_second: "%S"
36 am: 'am'
36 am: 'am'
37 pm: 'pm'
37 pm: 'pm'
38
38
39 datetime:
39 datetime:
40 distance_in_words:
40 distance_in_words:
41 half_a_minute: "30 secondes"
41 half_a_minute: "30 secondes"
42 less_than_x_seconds:
42 less_than_x_seconds:
43 zero: "moins d'une seconde"
43 zero: "moins d'une seconde"
44 one: "moins d'une seconde"
44 one: "moins d'une seconde"
45 other: "moins de %{count} secondes"
45 other: "moins de %{count} secondes"
46 x_seconds:
46 x_seconds:
47 one: "1 seconde"
47 one: "1 seconde"
48 other: "%{count} secondes"
48 other: "%{count} secondes"
49 less_than_x_minutes:
49 less_than_x_minutes:
50 zero: "moins d'une minute"
50 zero: "moins d'une minute"
51 one: "moins d'une minute"
51 one: "moins d'une minute"
52 other: "moins de %{count} minutes"
52 other: "moins de %{count} minutes"
53 x_minutes:
53 x_minutes:
54 one: "1 minute"
54 one: "1 minute"
55 other: "%{count} minutes"
55 other: "%{count} minutes"
56 about_x_hours:
56 about_x_hours:
57 one: "environ une heure"
57 one: "environ une heure"
58 other: "environ %{count} heures"
58 other: "environ %{count} heures"
59 x_hours:
59 x_hours:
60 one: "une heure"
60 one: "une heure"
61 other: "%{count} heures"
61 other: "%{count} heures"
62 x_days:
62 x_days:
63 one: "un jour"
63 one: "un jour"
64 other: "%{count} jours"
64 other: "%{count} jours"
65 about_x_months:
65 about_x_months:
66 one: "environ un mois"
66 one: "environ un mois"
67 other: "environ %{count} mois"
67 other: "environ %{count} mois"
68 x_months:
68 x_months:
69 one: "un mois"
69 one: "un mois"
70 other: "%{count} mois"
70 other: "%{count} mois"
71 about_x_years:
71 about_x_years:
72 one: "environ un an"
72 one: "environ un an"
73 other: "environ %{count} ans"
73 other: "environ %{count} ans"
74 over_x_years:
74 over_x_years:
75 one: "plus d'un an"
75 one: "plus d'un an"
76 other: "plus de %{count} ans"
76 other: "plus de %{count} ans"
77 almost_x_years:
77 almost_x_years:
78 one: "presqu'un an"
78 one: "presqu'un an"
79 other: "presque %{count} ans"
79 other: "presque %{count} ans"
80 prompts:
80 prompts:
81 year: "Année"
81 year: "Année"
82 month: "Mois"
82 month: "Mois"
83 day: "Jour"
83 day: "Jour"
84 hour: "Heure"
84 hour: "Heure"
85 minute: "Minute"
85 minute: "Minute"
86 second: "Seconde"
86 second: "Seconde"
87
87
88 number:
88 number:
89 format:
89 format:
90 precision: 3
90 precision: 3
91 separator: ','
91 separator: ','
92 delimiter: ' '
92 delimiter: ' '
93 currency:
93 currency:
94 format:
94 format:
95 unit: '€'
95 unit: '€'
96 precision: 2
96 precision: 2
97 format: '%n %u'
97 format: '%n %u'
98 human:
98 human:
99 format:
99 format:
100 precision: 3
100 precision: 3
101 storage_units:
101 storage_units:
102 format: "%n %u"
102 format: "%n %u"
103 units:
103 units:
104 byte:
104 byte:
105 one: "octet"
105 one: "octet"
106 other: "octets"
106 other: "octets"
107 kb: "ko"
107 kb: "ko"
108 mb: "Mo"
108 mb: "Mo"
109 gb: "Go"
109 gb: "Go"
110 tb: "To"
110 tb: "To"
111
111
112 support:
112 support:
113 array:
113 array:
114 sentence_connector: 'et'
114 sentence_connector: 'et'
115 skip_last_comma: true
115 skip_last_comma: true
116 word_connector: ", "
116 word_connector: ", "
117 two_words_connector: " et "
117 two_words_connector: " et "
118 last_word_connector: " et "
118 last_word_connector: " et "
119
119
120 activerecord:
120 activerecord:
121 errors:
121 errors:
122 template:
122 template:
123 header:
123 header:
124 one: "Impossible d'enregistrer %{model} : une erreur"
124 one: "Impossible d'enregistrer %{model} : une erreur"
125 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
125 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
126 body: "Veuillez vérifier les champs suivants :"
126 body: "Veuillez vérifier les champs suivants :"
127 messages:
127 messages:
128 inclusion: "n'est pas inclus(e) dans la liste"
128 inclusion: "n'est pas inclus(e) dans la liste"
129 exclusion: "n'est pas disponible"
129 exclusion: "n'est pas disponible"
130 invalid: "n'est pas valide"
130 invalid: "n'est pas valide"
131 confirmation: "ne concorde pas avec la confirmation"
131 confirmation: "ne concorde pas avec la confirmation"
132 accepted: "doit être accepté(e)"
132 accepted: "doit être accepté(e)"
133 empty: "doit être renseigné(e)"
133 empty: "doit être renseigné(e)"
134 blank: "doit être renseigné(e)"
134 blank: "doit être renseigné(e)"
135 too_long: "est trop long (pas plus de %{count} caractères)"
135 too_long: "est trop long (pas plus de %{count} caractères)"
136 too_short: "est trop court (au moins %{count} caractères)"
136 too_short: "est trop court (au moins %{count} caractères)"
137 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
137 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
138 taken: "est déjà utilisé"
138 taken: "est déjà utilisé"
139 not_a_number: "n'est pas un nombre"
139 not_a_number: "n'est pas un nombre"
140 not_a_date: "n'est pas une date valide"
140 not_a_date: "n'est pas une date valide"
141 greater_than: "doit être supérieur à %{count}"
141 greater_than: "doit être supérieur à %{count}"
142 greater_than_or_equal_to: "doit être supérieur ou égal à %{count}"
142 greater_than_or_equal_to: "doit être supérieur ou égal à %{count}"
143 equal_to: "doit être égal à %{count}"
143 equal_to: "doit être égal à %{count}"
144 less_than: "doit être inférieur à %{count}"
144 less_than: "doit être inférieur à %{count}"
145 less_than_or_equal_to: "doit être inférieur ou égal à %{count}"
145 less_than_or_equal_to: "doit être inférieur ou égal à %{count}"
146 odd: "doit être impair"
146 odd: "doit être impair"
147 even: "doit être pair"
147 even: "doit être pair"
148 greater_than_start_date: "doit être postérieure à la date de début"
148 greater_than_start_date: "doit être postérieure à la date de début"
149 not_same_project: "n'appartient pas au même projet"
149 not_same_project: "n'appartient pas au même projet"
150 circular_dependency: "Cette relation créerait une dépendance circulaire"
150 circular_dependency: "Cette relation créerait une dépendance circulaire"
151 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
151 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
152 earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précèdent"
152 earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précèdent"
153
153
154 actionview_instancetag_blank_option: Choisir
154 actionview_instancetag_blank_option: Choisir
155
155
156 general_text_No: 'Non'
156 general_text_No: 'Non'
157 general_text_Yes: 'Oui'
157 general_text_Yes: 'Oui'
158 general_text_no: 'non'
158 general_text_no: 'non'
159 general_text_yes: 'oui'
159 general_text_yes: 'oui'
160 general_lang_name: 'French (Français)'
160 general_lang_name: 'French (Français)'
161 general_csv_separator: ';'
161 general_csv_separator: ';'
162 general_csv_decimal_separator: ','
162 general_csv_decimal_separator: ','
163 general_csv_encoding: ISO-8859-1
163 general_csv_encoding: ISO-8859-1
164 general_pdf_fontname: freesans
164 general_pdf_fontname: freesans
165 general_first_day_of_week: '1'
165 general_first_day_of_week: '1'
166
166
167 notice_account_updated: Le compte a été mis à jour avec succès.
167 notice_account_updated: Le compte a été mis à jour avec succès.
168 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
168 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
169 notice_account_password_updated: Mot de passe mis à jour avec succès.
169 notice_account_password_updated: Mot de passe mis à jour avec succès.
170 notice_account_wrong_password: Mot de passe incorrect
170 notice_account_wrong_password: Mot de passe incorrect
171 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}.
171 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}.
172 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
172 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
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>.
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 notice_account_locked: Votre compte est verrouillé.
174 notice_account_locked: Votre compte est verrouillé.
175 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
175 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
176 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
176 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
177 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
177 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
178 notice_successful_create: Création effectuée avec succès.
178 notice_successful_create: Création effectuée avec succès.
179 notice_successful_update: Mise à jour effectuée avec succès.
179 notice_successful_update: Mise à jour effectuée avec succès.
180 notice_successful_delete: Suppression effectuée avec succès.
180 notice_successful_delete: Suppression effectuée avec succès.
181 notice_successful_connection: Connexion réussie.
181 notice_successful_connection: Connexion réussie.
182 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
182 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
183 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
183 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
184 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
184 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
185 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
185 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
186 notice_email_sent: "Un email a été envoyé à %{value}"
186 notice_email_sent: "Un email a été envoyé à %{value}"
187 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
187 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
188 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée."
188 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée."
189 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
189 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
190 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
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 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
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 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
192 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
193 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
193 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
194 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
194 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
195 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
195 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
196 notice_unable_delete_version: Impossible de supprimer cette version.
196 notice_unable_delete_version: Impossible de supprimer cette version.
197 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
197 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
198 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
198 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
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})"
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 notice_issue_successful_create: "Demande %{id} créée."
200 notice_issue_successful_create: "Demande %{id} créée."
201 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
201 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
202 notice_account_deleted: "Votre compte a été définitivement supprimé."
202 notice_account_deleted: "Votre compte a été définitivement supprimé."
203 notice_user_successful_create: "Utilisateur %{id} créé."
203 notice_user_successful_create: "Utilisateur %{id} créé."
204 notice_new_password_must_be_different: Votre nouveau mot de passe doit être différent de votre mot de passe actuel
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 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
206 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
207 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
207 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
208 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
208 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
209 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
209 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
210 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
210 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
211 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
211 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
212 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
212 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
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)."
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 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
214 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
215 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
215 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
216 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
216 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
217 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
217 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
218 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
218 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
219 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
219 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
220 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
220 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
221 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
221 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
222 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
222 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
223 error_unable_to_connect: Connexion impossible (%{value})
223 error_unable_to_connect: Connexion impossible (%{value})
224 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
224 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
225 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
225 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
226 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
226 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
227
227
228 mail_subject_lost_password: "Votre mot de passe %{value}"
228 mail_subject_lost_password: "Votre mot de passe %{value}"
229 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
229 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
230 mail_subject_register: "Activation de votre compte %{value}"
230 mail_subject_register: "Activation de votre compte %{value}"
231 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
231 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
232 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
232 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
233 mail_body_account_information: Paramètres de connexion de votre compte
233 mail_body_account_information: Paramètres de connexion de votre compte
234 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
234 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
235 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
235 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
236 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
236 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
237 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
237 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
238 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
238 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
239 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
239 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
240 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
240 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
241 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
241 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
242
242
243 field_name: Nom
243 field_name: Nom
244 field_description: Description
244 field_description: Description
245 field_summary: Résumé
245 field_summary: Résumé
246 field_is_required: Obligatoire
246 field_is_required: Obligatoire
247 field_firstname: Prénom
247 field_firstname: Prénom
248 field_lastname: Nom
248 field_lastname: Nom
249 field_mail: Email
249 field_mail: Email
250 field_address: Email
250 field_address: Email
251 field_filename: Fichier
251 field_filename: Fichier
252 field_filesize: Taille
252 field_filesize: Taille
253 field_downloads: Téléchargements
253 field_downloads: Téléchargements
254 field_author: Auteur
254 field_author: Auteur
255 field_created_on: Créé
255 field_created_on: Créé
256 field_updated_on: Mis-à-jour
256 field_updated_on: Mis-à-jour
257 field_closed_on: Fermé
257 field_closed_on: Fermé
258 field_field_format: Format
258 field_field_format: Format
259 field_is_for_all: Pour tous les projets
259 field_is_for_all: Pour tous les projets
260 field_possible_values: Valeurs possibles
260 field_possible_values: Valeurs possibles
261 field_regexp: Expression régulière
261 field_regexp: Expression régulière
262 field_min_length: Longueur minimum
262 field_min_length: Longueur minimum
263 field_max_length: Longueur maximum
263 field_max_length: Longueur maximum
264 field_value: Valeur
264 field_value: Valeur
265 field_category: Catégorie
265 field_category: Catégorie
266 field_title: Titre
266 field_title: Titre
267 field_project: Projet
267 field_project: Projet
268 field_issue: Demande
268 field_issue: Demande
269 field_status: Statut
269 field_status: Statut
270 field_notes: Notes
270 field_notes: Notes
271 field_is_closed: Demande fermée
271 field_is_closed: Demande fermée
272 field_is_default: Valeur par défaut
272 field_is_default: Valeur par défaut
273 field_tracker: Tracker
273 field_tracker: Tracker
274 field_subject: Sujet
274 field_subject: Sujet
275 field_due_date: Echéance
275 field_due_date: Echéance
276 field_assigned_to: Assigné à
276 field_assigned_to: Assigné à
277 field_priority: Priorité
277 field_priority: Priorité
278 field_fixed_version: Version cible
278 field_fixed_version: Version cible
279 field_user: Utilisateur
279 field_user: Utilisateur
280 field_principal: Principal
280 field_principal: Principal
281 field_role: Rôle
281 field_role: Rôle
282 field_homepage: Site web
282 field_homepage: Site web
283 field_is_public: Public
283 field_is_public: Public
284 field_parent: Sous-projet de
284 field_parent: Sous-projet de
285 field_is_in_roadmap: Demandes affichées dans la roadmap
285 field_is_in_roadmap: Demandes affichées dans la roadmap
286 field_login: Identifiant
286 field_login: Identifiant
287 field_mail_notification: Notifications par mail
287 field_mail_notification: Notifications par mail
288 field_admin: Administrateur
288 field_admin: Administrateur
289 field_last_login_on: Dernière connexion
289 field_last_login_on: Dernière connexion
290 field_language: Langue
290 field_language: Langue
291 field_effective_date: Date
291 field_effective_date: Date
292 field_password: Mot de passe
292 field_password: Mot de passe
293 field_new_password: Nouveau mot de passe
293 field_new_password: Nouveau mot de passe
294 field_password_confirmation: Confirmation
294 field_password_confirmation: Confirmation
295 field_version: Version
295 field_version: Version
296 field_type: Type
296 field_type: Type
297 field_host: Hôte
297 field_host: Hôte
298 field_port: Port
298 field_port: Port
299 field_account: Compte
299 field_account: Compte
300 field_base_dn: Base DN
300 field_base_dn: Base DN
301 field_attr_login: Attribut Identifiant
301 field_attr_login: Attribut Identifiant
302 field_attr_firstname: Attribut Prénom
302 field_attr_firstname: Attribut Prénom
303 field_attr_lastname: Attribut Nom
303 field_attr_lastname: Attribut Nom
304 field_attr_mail: Attribut Email
304 field_attr_mail: Attribut Email
305 field_onthefly: Création des utilisateurs à la volée
305 field_onthefly: Création des utilisateurs à la volée
306 field_start_date: Début
306 field_start_date: Début
307 field_done_ratio: "% réalisé"
307 field_done_ratio: "% réalisé"
308 field_auth_source: Mode d'authentification
308 field_auth_source: Mode d'authentification
309 field_hide_mail: Cacher mon adresse mail
309 field_hide_mail: Cacher mon adresse mail
310 field_comments: Commentaire
310 field_comments: Commentaire
311 field_url: URL
311 field_url: URL
312 field_start_page: Page de démarrage
312 field_start_page: Page de démarrage
313 field_subproject: Sous-projet
313 field_subproject: Sous-projet
314 field_hours: Heures
314 field_hours: Heures
315 field_activity: Activité
315 field_activity: Activité
316 field_spent_on: Date
316 field_spent_on: Date
317 field_identifier: Identifiant
317 field_identifier: Identifiant
318 field_is_filter: Utilisé comme filtre
318 field_is_filter: Utilisé comme filtre
319 field_issue_to: Demande liée
319 field_issue_to: Demande liée
320 field_delay: Retard
320 field_delay: Retard
321 field_assignable: Demandes assignables à ce rôle
321 field_assignable: Demandes assignables à ce rôle
322 field_redirect_existing_links: Rediriger les liens existants
322 field_redirect_existing_links: Rediriger les liens existants
323 field_estimated_hours: Temps estimé
323 field_estimated_hours: Temps estimé
324 field_column_names: Colonnes
324 field_column_names: Colonnes
325 field_time_entries: Temps passé
325 field_time_entries: Temps passé
326 field_time_zone: Fuseau horaire
326 field_time_zone: Fuseau horaire
327 field_searchable: Utilisé pour les recherches
327 field_searchable: Utilisé pour les recherches
328 field_default_value: Valeur par défaut
328 field_default_value: Valeur par défaut
329 field_comments_sorting: Afficher les commentaires
329 field_comments_sorting: Afficher les commentaires
330 field_parent_title: Page parent
330 field_parent_title: Page parent
331 field_editable: Modifiable
331 field_editable: Modifiable
332 field_watcher: Observateur
332 field_watcher: Observateur
333 field_identity_url: URL OpenID
333 field_identity_url: URL OpenID
334 field_content: Contenu
334 field_content: Contenu
335 field_group_by: Grouper par
335 field_group_by: Grouper par
336 field_sharing: Partage
336 field_sharing: Partage
337 field_parent_issue: Tâche parente
337 field_parent_issue: Tâche parente
338 field_member_of_group: Groupe de l'assigné
338 field_member_of_group: Groupe de l'assigné
339 field_assigned_to_role: Rôle de l'assigné
339 field_assigned_to_role: Rôle de l'assigné
340 field_text: Champ texte
340 field_text: Champ texte
341 field_visible: Visible
341 field_visible: Visible
342 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
342 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
343 field_issues_visibility: Visibilité des demandes
343 field_issues_visibility: Visibilité des demandes
344 field_is_private: Privée
344 field_is_private: Privée
345 field_commit_logs_encoding: Encodage des messages de commit
345 field_commit_logs_encoding: Encodage des messages de commit
346 field_scm_path_encoding: Encodage des chemins
346 field_scm_path_encoding: Encodage des chemins
347 field_path_to_repository: Chemin du dépôt
347 field_path_to_repository: Chemin du dépôt
348 field_root_directory: Répertoire racine
348 field_root_directory: Répertoire racine
349 field_cvsroot: CVSROOT
349 field_cvsroot: CVSROOT
350 field_cvs_module: Module
350 field_cvs_module: Module
351 field_repository_is_default: Dépôt principal
351 field_repository_is_default: Dépôt principal
352 field_multiple: Valeurs multiples
352 field_multiple: Valeurs multiples
353 field_auth_source_ldap_filter: Filtre LDAP
353 field_auth_source_ldap_filter: Filtre LDAP
354 field_core_fields: Champs standards
354 field_core_fields: Champs standards
355 field_timeout: "Timeout (en secondes)"
355 field_timeout: "Timeout (en secondes)"
356 field_board_parent: Forum parent
356 field_board_parent: Forum parent
357 field_private_notes: Notes privées
357 field_private_notes: Notes privées
358 field_inherit_members: Hériter les membres
358 field_inherit_members: Hériter les membres
359 field_generate_password: Générer un mot de passe
359 field_generate_password: Générer un mot de passe
360 field_must_change_passwd: Doit changer de mot de passe à la prochaine connexion
360 field_must_change_passwd: Doit changer de mot de passe à la prochaine connexion
361 field_default_status: Statut par défaut
361 field_default_status: Statut par défaut
362 field_users_visibility: Visibilité des utilisateurs
362 field_users_visibility: Visibilité des utilisateurs
363
363
364 setting_app_title: Titre de l'application
364 setting_app_title: Titre de l'application
365 setting_app_subtitle: Sous-titre de l'application
365 setting_app_subtitle: Sous-titre de l'application
366 setting_welcome_text: Texte d'accueil
366 setting_welcome_text: Texte d'accueil
367 setting_default_language: Langue par défaut
367 setting_default_language: Langue par défaut
368 setting_login_required: Authentification obligatoire
368 setting_login_required: Authentification obligatoire
369 setting_self_registration: Inscription des nouveaux utilisateurs
369 setting_self_registration: Inscription des nouveaux utilisateurs
370 setting_attachment_max_size: Taille maximale des fichiers
370 setting_attachment_max_size: Taille maximale des fichiers
371 setting_issues_export_limit: Limite d'exportation des demandes
371 setting_issues_export_limit: Limite d'exportation des demandes
372 setting_mail_from: Adresse d'émission
372 setting_mail_from: Adresse d'émission
373 setting_bcc_recipients: Destinataires en copie cachée (cci)
373 setting_bcc_recipients: Destinataires en copie cachée (cci)
374 setting_plain_text_mail: Mail en texte brut (non HTML)
374 setting_plain_text_mail: Mail en texte brut (non HTML)
375 setting_host_name: Nom d'hôte et chemin
375 setting_host_name: Nom d'hôte et chemin
376 setting_text_formatting: Formatage du texte
376 setting_text_formatting: Formatage du texte
377 setting_wiki_compression: Compression de l'historique des pages wiki
377 setting_wiki_compression: Compression de l'historique des pages wiki
378 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
378 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
379 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
379 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
380 setting_autofetch_changesets: Récupération automatique des commits
380 setting_autofetch_changesets: Récupération automatique des commits
381 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
381 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
382 setting_commit_ref_keywords: Mots-clés de référencement
382 setting_commit_ref_keywords: Mots-clés de référencement
383 setting_commit_fix_keywords: Mots-clés de résolution
383 setting_commit_fix_keywords: Mots-clés de résolution
384 setting_autologin: Durée maximale de connexion automatique
384 setting_autologin: Durée maximale de connexion automatique
385 setting_date_format: Format de date
385 setting_date_format: Format de date
386 setting_time_format: Format d'heure
386 setting_time_format: Format d'heure
387 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
387 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
388 setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
388 setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
389 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
389 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
390 setting_repositories_encodings: Encodages des fichiers et des dépôts
390 setting_repositories_encodings: Encodages des fichiers et des dépôts
391 setting_emails_header: En-tête des emails
391 setting_emails_header: En-tête des emails
392 setting_emails_footer: Pied-de-page des emails
392 setting_emails_footer: Pied-de-page des emails
393 setting_protocol: Protocole
393 setting_protocol: Protocole
394 setting_per_page_options: Options d'objets affichés par page
394 setting_per_page_options: Options d'objets affichés par page
395 setting_user_format: Format d'affichage des utilisateurs
395 setting_user_format: Format d'affichage des utilisateurs
396 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
396 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
397 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
397 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
398 setting_enabled_scm: SCM activés
398 setting_enabled_scm: SCM activés
399 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
399 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
400 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
400 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
401 setting_mail_handler_api_key: Clé de protection de l'API
401 setting_mail_handler_api_key: Clé de protection de l'API
402 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
402 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
403 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
403 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
404 setting_gravatar_default: Image Gravatar par défaut
404 setting_gravatar_default: Image Gravatar par défaut
405 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
405 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
406 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
406 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
407 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
407 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
408 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
408 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
409 setting_password_min_length: Longueur minimum des mots de passe
409 setting_password_min_length: Longueur minimum des mots de passe
410 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
410 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
411 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
411 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
412 setting_issue_done_ratio: Calcul de l'avancement des demandes
412 setting_issue_done_ratio: Calcul de l'avancement des demandes
413 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
413 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
414 setting_issue_done_ratio_issue_status: Utiliser le statut
414 setting_issue_done_ratio_issue_status: Utiliser le statut
415 setting_start_of_week: Jour de début des calendriers
415 setting_start_of_week: Jour de début des calendriers
416 setting_rest_api_enabled: Activer l'API REST
416 setting_rest_api_enabled: Activer l'API REST
417 setting_cache_formatted_text: Mettre en cache le texte formaté
417 setting_cache_formatted_text: Mettre en cache le texte formaté
418 setting_default_notification_option: Option de notification par défaut
418 setting_default_notification_option: Option de notification par défaut
419 setting_commit_logtime_enabled: Permettre la saisie de temps
419 setting_commit_logtime_enabled: Permettre la saisie de temps
420 setting_commit_logtime_activity_id: Activité pour le temps saisi
420 setting_commit_logtime_activity_id: Activité pour le temps saisi
421 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
421 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
422 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
422 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
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
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 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
424 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
425 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
425 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
426 setting_session_lifetime: Durée de vie maximale des sessions
426 setting_session_lifetime: Durée de vie maximale des sessions
427 setting_session_timeout: Durée maximale d'inactivité
427 setting_session_timeout: Durée maximale d'inactivité
428 setting_thumbnails_enabled: Afficher les vignettes des images
428 setting_thumbnails_enabled: Afficher les vignettes des images
429 setting_thumbnails_size: Taille des vignettes (en pixels)
429 setting_thumbnails_size: Taille des vignettes (en pixels)
430 setting_non_working_week_days: Jours non travaillés
430 setting_non_working_week_days: Jours non travaillés
431 setting_jsonp_enabled: Activer le support JSONP
431 setting_jsonp_enabled: Activer le support JSONP
432 setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets
432 setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets
433 setting_mail_handler_excluded_filenames: Exclure les fichiers attachés par leur nom
433 setting_mail_handler_excluded_filenames: Exclure les fichiers attachés par leur nom
434 setting_force_default_language_for_anonymous: Forcer la langue par défault pour les utilisateurs anonymes
434 setting_force_default_language_for_anonymous: Forcer la langue par défault pour les utilisateurs anonymes
435 setting_force_default_language_for_loggedin: Forcer la langue par défault pour les utilisateurs identifiés
435 setting_force_default_language_for_loggedin: Forcer la langue par défault pour les utilisateurs identifiés
436 setting_link_copied_issue: Lier les demandes lors de la copie
436 setting_link_copied_issue: Lier les demandes lors de la copie
437 setting_max_additional_emails: Nombre maximal d'adresses email additionnelles
437 setting_max_additional_emails: Nombre maximal d'adresses email additionnelles
438 setting_search_results_per_page: Résultats de recherche affichés par page
438 setting_search_results_per_page: Résultats de recherche affichés par page
439
439
440 permission_add_project: Créer un projet
440 permission_add_project: Créer un projet
441 permission_add_subprojects: Créer des sous-projets
441 permission_add_subprojects: Créer des sous-projets
442 permission_edit_project: Modifier le projet
442 permission_edit_project: Modifier le projet
443 permission_close_project: Fermer / réouvrir le projet
443 permission_close_project: Fermer / réouvrir le projet
444 permission_select_project_modules: Choisir les modules
444 permission_select_project_modules: Choisir les modules
445 permission_manage_members: Gérer les membres
445 permission_manage_members: Gérer les membres
446 permission_manage_project_activities: Gérer les activités
446 permission_manage_project_activities: Gérer les activités
447 permission_manage_versions: Gérer les versions
447 permission_manage_versions: Gérer les versions
448 permission_manage_categories: Gérer les catégories de demandes
448 permission_manage_categories: Gérer les catégories de demandes
449 permission_view_issues: Voir les demandes
449 permission_view_issues: Voir les demandes
450 permission_add_issues: Créer des demandes
450 permission_add_issues: Créer des demandes
451 permission_edit_issues: Modifier les demandes
451 permission_edit_issues: Modifier les demandes
452 permission_copy_issues: Copier les demandes
452 permission_manage_issue_relations: Gérer les relations
453 permission_manage_issue_relations: Gérer les relations
453 permission_set_issues_private: Rendre les demandes publiques ou privées
454 permission_set_issues_private: Rendre les demandes publiques ou privées
454 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
455 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
455 permission_add_issue_notes: Ajouter des notes
456 permission_add_issue_notes: Ajouter des notes
456 permission_edit_issue_notes: Modifier les notes
457 permission_edit_issue_notes: Modifier les notes
457 permission_edit_own_issue_notes: Modifier ses propres notes
458 permission_edit_own_issue_notes: Modifier ses propres notes
458 permission_view_private_notes: Voir les notes privées
459 permission_view_private_notes: Voir les notes privées
459 permission_set_notes_private: Rendre les notes privées
460 permission_set_notes_private: Rendre les notes privées
460 permission_move_issues: Déplacer les demandes
461 permission_move_issues: Déplacer les demandes
461 permission_delete_issues: Supprimer les demandes
462 permission_delete_issues: Supprimer les demandes
462 permission_manage_public_queries: Gérer les requêtes publiques
463 permission_manage_public_queries: Gérer les requêtes publiques
463 permission_save_queries: Sauvegarder les requêtes
464 permission_save_queries: Sauvegarder les requêtes
464 permission_view_gantt: Voir le gantt
465 permission_view_gantt: Voir le gantt
465 permission_view_calendar: Voir le calendrier
466 permission_view_calendar: Voir le calendrier
466 permission_view_issue_watchers: Voir la liste des observateurs
467 permission_view_issue_watchers: Voir la liste des observateurs
467 permission_add_issue_watchers: Ajouter des observateurs
468 permission_add_issue_watchers: Ajouter des observateurs
468 permission_delete_issue_watchers: Supprimer des observateurs
469 permission_delete_issue_watchers: Supprimer des observateurs
469 permission_log_time: Saisir le temps passé
470 permission_log_time: Saisir le temps passé
470 permission_view_time_entries: Voir le temps passé
471 permission_view_time_entries: Voir le temps passé
471 permission_edit_time_entries: Modifier les temps passés
472 permission_edit_time_entries: Modifier les temps passés
472 permission_edit_own_time_entries: Modifier son propre temps passé
473 permission_edit_own_time_entries: Modifier son propre temps passé
473 permission_manage_news: Gérer les annonces
474 permission_manage_news: Gérer les annonces
474 permission_comment_news: Commenter les annonces
475 permission_comment_news: Commenter les annonces
475 permission_view_documents: Voir les documents
476 permission_view_documents: Voir les documents
476 permission_add_documents: Ajouter des documents
477 permission_add_documents: Ajouter des documents
477 permission_edit_documents: Modifier les documents
478 permission_edit_documents: Modifier les documents
478 permission_delete_documents: Supprimer les documents
479 permission_delete_documents: Supprimer les documents
479 permission_manage_files: Gérer les fichiers
480 permission_manage_files: Gérer les fichiers
480 permission_view_files: Voir les fichiers
481 permission_view_files: Voir les fichiers
481 permission_manage_wiki: Gérer le wiki
482 permission_manage_wiki: Gérer le wiki
482 permission_rename_wiki_pages: Renommer les pages
483 permission_rename_wiki_pages: Renommer les pages
483 permission_delete_wiki_pages: Supprimer les pages
484 permission_delete_wiki_pages: Supprimer les pages
484 permission_view_wiki_pages: Voir le wiki
485 permission_view_wiki_pages: Voir le wiki
485 permission_view_wiki_edits: "Voir l'historique des modifications"
486 permission_view_wiki_edits: "Voir l'historique des modifications"
486 permission_edit_wiki_pages: Modifier les pages
487 permission_edit_wiki_pages: Modifier les pages
487 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
488 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
488 permission_protect_wiki_pages: Protéger les pages
489 permission_protect_wiki_pages: Protéger les pages
489 permission_manage_repository: Gérer le dépôt de sources
490 permission_manage_repository: Gérer le dépôt de sources
490 permission_browse_repository: Parcourir les sources
491 permission_browse_repository: Parcourir les sources
491 permission_view_changesets: Voir les révisions
492 permission_view_changesets: Voir les révisions
492 permission_commit_access: Droit de commit
493 permission_commit_access: Droit de commit
493 permission_manage_boards: Gérer les forums
494 permission_manage_boards: Gérer les forums
494 permission_view_messages: Voir les messages
495 permission_view_messages: Voir les messages
495 permission_add_messages: Poster un message
496 permission_add_messages: Poster un message
496 permission_edit_messages: Modifier les messages
497 permission_edit_messages: Modifier les messages
497 permission_edit_own_messages: Modifier ses propres messages
498 permission_edit_own_messages: Modifier ses propres messages
498 permission_delete_messages: Supprimer les messages
499 permission_delete_messages: Supprimer les messages
499 permission_delete_own_messages: Supprimer ses propres messages
500 permission_delete_own_messages: Supprimer ses propres messages
500 permission_export_wiki_pages: Exporter les pages
501 permission_export_wiki_pages: Exporter les pages
501 permission_manage_subtasks: Gérer les sous-tâches
502 permission_manage_subtasks: Gérer les sous-tâches
502 permission_manage_related_issues: Gérer les demandes associées
503 permission_manage_related_issues: Gérer les demandes associées
503
504
504 project_module_issue_tracking: Suivi des demandes
505 project_module_issue_tracking: Suivi des demandes
505 project_module_time_tracking: Suivi du temps passé
506 project_module_time_tracking: Suivi du temps passé
506 project_module_news: Publication d'annonces
507 project_module_news: Publication d'annonces
507 project_module_documents: Publication de documents
508 project_module_documents: Publication de documents
508 project_module_files: Publication de fichiers
509 project_module_files: Publication de fichiers
509 project_module_wiki: Wiki
510 project_module_wiki: Wiki
510 project_module_repository: Dépôt de sources
511 project_module_repository: Dépôt de sources
511 project_module_boards: Forums de discussion
512 project_module_boards: Forums de discussion
512 project_module_calendar: Calendrier
513 project_module_calendar: Calendrier
513 project_module_gantt: Gantt
514 project_module_gantt: Gantt
514
515
515 label_user: Utilisateur
516 label_user: Utilisateur
516 label_user_plural: Utilisateurs
517 label_user_plural: Utilisateurs
517 label_user_new: Nouvel utilisateur
518 label_user_new: Nouvel utilisateur
518 label_user_anonymous: Anonyme
519 label_user_anonymous: Anonyme
519 label_project: Projet
520 label_project: Projet
520 label_project_new: Nouveau projet
521 label_project_new: Nouveau projet
521 label_project_plural: Projets
522 label_project_plural: Projets
522 label_x_projects:
523 label_x_projects:
523 zero: aucun projet
524 zero: aucun projet
524 one: un projet
525 one: un projet
525 other: "%{count} projets"
526 other: "%{count} projets"
526 label_project_all: Tous les projets
527 label_project_all: Tous les projets
527 label_project_latest: Derniers projets
528 label_project_latest: Derniers projets
528 label_issue: Demande
529 label_issue: Demande
529 label_issue_new: Nouvelle demande
530 label_issue_new: Nouvelle demande
530 label_issue_plural: Demandes
531 label_issue_plural: Demandes
531 label_issue_view_all: Voir toutes les demandes
532 label_issue_view_all: Voir toutes les demandes
532 label_issues_by: "Demandes par %{value}"
533 label_issues_by: "Demandes par %{value}"
533 label_issue_added: Demande ajoutée
534 label_issue_added: Demande ajoutée
534 label_issue_updated: Demande mise à jour
535 label_issue_updated: Demande mise à jour
535 label_issue_note_added: Note ajoutée
536 label_issue_note_added: Note ajoutée
536 label_issue_status_updated: Statut changé
537 label_issue_status_updated: Statut changé
537 label_issue_assigned_to_updated: Assigné changé
538 label_issue_assigned_to_updated: Assigné changé
538 label_issue_priority_updated: Priorité changée
539 label_issue_priority_updated: Priorité changée
539 label_document: Document
540 label_document: Document
540 label_document_new: Nouveau document
541 label_document_new: Nouveau document
541 label_document_plural: Documents
542 label_document_plural: Documents
542 label_document_added: Document ajouté
543 label_document_added: Document ajouté
543 label_role: Rôle
544 label_role: Rôle
544 label_role_plural: Rôles
545 label_role_plural: Rôles
545 label_role_new: Nouveau rôle
546 label_role_new: Nouveau rôle
546 label_role_and_permissions: Rôles et permissions
547 label_role_and_permissions: Rôles et permissions
547 label_role_anonymous: Anonyme
548 label_role_anonymous: Anonyme
548 label_role_non_member: Non membre
549 label_role_non_member: Non membre
549 label_member: Membre
550 label_member: Membre
550 label_member_new: Nouveau membre
551 label_member_new: Nouveau membre
551 label_member_plural: Membres
552 label_member_plural: Membres
552 label_tracker: Tracker
553 label_tracker: Tracker
553 label_tracker_plural: Trackers
554 label_tracker_plural: Trackers
554 label_tracker_new: Nouveau tracker
555 label_tracker_new: Nouveau tracker
555 label_workflow: Workflow
556 label_workflow: Workflow
556 label_issue_status: Statut de demandes
557 label_issue_status: Statut de demandes
557 label_issue_status_plural: Statuts de demandes
558 label_issue_status_plural: Statuts de demandes
558 label_issue_status_new: Nouveau statut
559 label_issue_status_new: Nouveau statut
559 label_issue_category: Catégorie de demandes
560 label_issue_category: Catégorie de demandes
560 label_issue_category_plural: Catégories de demandes
561 label_issue_category_plural: Catégories de demandes
561 label_issue_category_new: Nouvelle catégorie
562 label_issue_category_new: Nouvelle catégorie
562 label_custom_field: Champ personnalisé
563 label_custom_field: Champ personnalisé
563 label_custom_field_plural: Champs personnalisés
564 label_custom_field_plural: Champs personnalisés
564 label_custom_field_new: Nouveau champ personnalisé
565 label_custom_field_new: Nouveau champ personnalisé
565 label_enumerations: Listes de valeurs
566 label_enumerations: Listes de valeurs
566 label_enumeration_new: Nouvelle valeur
567 label_enumeration_new: Nouvelle valeur
567 label_information: Information
568 label_information: Information
568 label_information_plural: Informations
569 label_information_plural: Informations
569 label_please_login: Identification
570 label_please_login: Identification
570 label_register: S'enregistrer
571 label_register: S'enregistrer
571 label_login_with_open_id_option: S'authentifier avec OpenID
572 label_login_with_open_id_option: S'authentifier avec OpenID
572 label_password_lost: Mot de passe perdu
573 label_password_lost: Mot de passe perdu
573 label_home: Accueil
574 label_home: Accueil
574 label_my_page: Ma page
575 label_my_page: Ma page
575 label_my_account: Mon compte
576 label_my_account: Mon compte
576 label_my_projects: Mes projets
577 label_my_projects: Mes projets
577 label_my_page_block: Blocs disponibles
578 label_my_page_block: Blocs disponibles
578 label_administration: Administration
579 label_administration: Administration
579 label_login: Connexion
580 label_login: Connexion
580 label_logout: Déconnexion
581 label_logout: Déconnexion
581 label_help: Aide
582 label_help: Aide
582 label_reported_issues: Demandes soumises
583 label_reported_issues: Demandes soumises
583 label_assigned_to_me_issues: Demandes qui me sont assignées
584 label_assigned_to_me_issues: Demandes qui me sont assignées
584 label_last_login: Dernière connexion
585 label_last_login: Dernière connexion
585 label_registered_on: Inscrit le
586 label_registered_on: Inscrit le
586 label_activity: Activité
587 label_activity: Activité
587 label_overall_activity: Activité globale
588 label_overall_activity: Activité globale
588 label_user_activity: "Activité de %{value}"
589 label_user_activity: "Activité de %{value}"
589 label_new: Nouveau
590 label_new: Nouveau
590 label_logged_as: Connecté en tant que
591 label_logged_as: Connecté en tant que
591 label_environment: Environnement
592 label_environment: Environnement
592 label_authentication: Authentification
593 label_authentication: Authentification
593 label_auth_source: Mode d'authentification
594 label_auth_source: Mode d'authentification
594 label_auth_source_new: Nouveau mode d'authentification
595 label_auth_source_new: Nouveau mode d'authentification
595 label_auth_source_plural: Modes d'authentification
596 label_auth_source_plural: Modes d'authentification
596 label_subproject_plural: Sous-projets
597 label_subproject_plural: Sous-projets
597 label_subproject_new: Nouveau sous-projet
598 label_subproject_new: Nouveau sous-projet
598 label_and_its_subprojects: "%{value} et ses sous-projets"
599 label_and_its_subprojects: "%{value} et ses sous-projets"
599 label_min_max_length: Longueurs mini - maxi
600 label_min_max_length: Longueurs mini - maxi
600 label_list: Liste
601 label_list: Liste
601 label_date: Date
602 label_date: Date
602 label_integer: Entier
603 label_integer: Entier
603 label_float: Nombre décimal
604 label_float: Nombre décimal
604 label_boolean: Booléen
605 label_boolean: Booléen
605 label_string: Texte
606 label_string: Texte
606 label_text: Texte long
607 label_text: Texte long
607 label_attribute: Attribut
608 label_attribute: Attribut
608 label_attribute_plural: Attributs
609 label_attribute_plural: Attributs
609 label_no_data: Aucune donnée à afficher
610 label_no_data: Aucune donnée à afficher
610 label_change_status: Changer le statut
611 label_change_status: Changer le statut
611 label_history: Historique
612 label_history: Historique
612 label_attachment: Fichier
613 label_attachment: Fichier
613 label_attachment_new: Nouveau fichier
614 label_attachment_new: Nouveau fichier
614 label_attachment_delete: Supprimer le fichier
615 label_attachment_delete: Supprimer le fichier
615 label_attachment_plural: Fichiers
616 label_attachment_plural: Fichiers
616 label_file_added: Fichier ajouté
617 label_file_added: Fichier ajouté
617 label_report: Rapport
618 label_report: Rapport
618 label_report_plural: Rapports
619 label_report_plural: Rapports
619 label_news: Annonce
620 label_news: Annonce
620 label_news_new: Nouvelle annonce
621 label_news_new: Nouvelle annonce
621 label_news_plural: Annonces
622 label_news_plural: Annonces
622 label_news_latest: Dernières annonces
623 label_news_latest: Dernières annonces
623 label_news_view_all: Voir toutes les annonces
624 label_news_view_all: Voir toutes les annonces
624 label_news_added: Annonce ajoutée
625 label_news_added: Annonce ajoutée
625 label_news_comment_added: Commentaire ajouté à une annonce
626 label_news_comment_added: Commentaire ajouté à une annonce
626 label_settings: Configuration
627 label_settings: Configuration
627 label_overview: Aperçu
628 label_overview: Aperçu
628 label_version: Version
629 label_version: Version
629 label_version_new: Nouvelle version
630 label_version_new: Nouvelle version
630 label_version_plural: Versions
631 label_version_plural: Versions
631 label_close_versions: Fermer les versions terminées
632 label_close_versions: Fermer les versions terminées
632 label_confirmation: Confirmation
633 label_confirmation: Confirmation
633 label_export_to: 'Formats disponibles :'
634 label_export_to: 'Formats disponibles :'
634 label_read: Lire...
635 label_read: Lire...
635 label_public_projects: Projets publics
636 label_public_projects: Projets publics
636 label_open_issues: ouvert
637 label_open_issues: ouvert
637 label_open_issues_plural: ouverts
638 label_open_issues_plural: ouverts
638 label_closed_issues: fermé
639 label_closed_issues: fermé
639 label_closed_issues_plural: fermés
640 label_closed_issues_plural: fermés
640 label_x_open_issues_abbr_on_total:
641 label_x_open_issues_abbr_on_total:
641 zero: 0 ouverte sur %{total}
642 zero: 0 ouverte sur %{total}
642 one: 1 ouverte sur %{total}
643 one: 1 ouverte sur %{total}
643 other: "%{count} ouvertes sur %{total}"
644 other: "%{count} ouvertes sur %{total}"
644 label_x_open_issues_abbr:
645 label_x_open_issues_abbr:
645 zero: 0 ouverte
646 zero: 0 ouverte
646 one: 1 ouverte
647 one: 1 ouverte
647 other: "%{count} ouvertes"
648 other: "%{count} ouvertes"
648 label_x_closed_issues_abbr:
649 label_x_closed_issues_abbr:
649 zero: 0 fermée
650 zero: 0 fermée
650 one: 1 fermée
651 one: 1 fermée
651 other: "%{count} fermées"
652 other: "%{count} fermées"
652 label_x_issues:
653 label_x_issues:
653 zero: 0 demande
654 zero: 0 demande
654 one: 1 demande
655 one: 1 demande
655 other: "%{count} demandes"
656 other: "%{count} demandes"
656 label_total: Total
657 label_total: Total
657 label_total_time: Temps total
658 label_total_time: Temps total
658 label_permissions: Permissions
659 label_permissions: Permissions
659 label_current_status: Statut actuel
660 label_current_status: Statut actuel
660 label_new_statuses_allowed: Nouveaux statuts autorisés
661 label_new_statuses_allowed: Nouveaux statuts autorisés
661 label_all: tous
662 label_all: tous
662 label_any: tous
663 label_any: tous
663 label_none: aucun
664 label_none: aucun
664 label_nobody: personne
665 label_nobody: personne
665 label_next: Suivant
666 label_next: Suivant
666 label_previous: Précédent
667 label_previous: Précédent
667 label_used_by: Utilisé par
668 label_used_by: Utilisé par
668 label_details: Détails
669 label_details: Détails
669 label_add_note: Ajouter une note
670 label_add_note: Ajouter une note
670 label_calendar: Calendrier
671 label_calendar: Calendrier
671 label_months_from: mois depuis
672 label_months_from: mois depuis
672 label_gantt: Gantt
673 label_gantt: Gantt
673 label_internal: Interne
674 label_internal: Interne
674 label_last_changes: "%{count} derniers changements"
675 label_last_changes: "%{count} derniers changements"
675 label_change_view_all: Voir tous les changements
676 label_change_view_all: Voir tous les changements
676 label_personalize_page: Personnaliser cette page
677 label_personalize_page: Personnaliser cette page
677 label_comment: Commentaire
678 label_comment: Commentaire
678 label_comment_plural: Commentaires
679 label_comment_plural: Commentaires
679 label_x_comments:
680 label_x_comments:
680 zero: aucun commentaire
681 zero: aucun commentaire
681 one: un commentaire
682 one: un commentaire
682 other: "%{count} commentaires"
683 other: "%{count} commentaires"
683 label_comment_add: Ajouter un commentaire
684 label_comment_add: Ajouter un commentaire
684 label_comment_added: Commentaire ajouté
685 label_comment_added: Commentaire ajouté
685 label_comment_delete: Supprimer les commentaires
686 label_comment_delete: Supprimer les commentaires
686 label_query: Rapport personnalisé
687 label_query: Rapport personnalisé
687 label_query_plural: Rapports personnalisés
688 label_query_plural: Rapports personnalisés
688 label_query_new: Nouveau rapport
689 label_query_new: Nouveau rapport
689 label_my_queries: Mes rapports personnalisés
690 label_my_queries: Mes rapports personnalisés
690 label_filter_add: Ajouter le filtre
691 label_filter_add: Ajouter le filtre
691 label_filter_plural: Filtres
692 label_filter_plural: Filtres
692 label_equals: égal
693 label_equals: égal
693 label_not_equals: différent
694 label_not_equals: différent
694 label_in_less_than: dans moins de
695 label_in_less_than: dans moins de
695 label_in_more_than: dans plus de
696 label_in_more_than: dans plus de
696 label_in_the_next_days: dans les prochains jours
697 label_in_the_next_days: dans les prochains jours
697 label_in_the_past_days: dans les derniers jours
698 label_in_the_past_days: dans les derniers jours
698 label_greater_or_equal: '>='
699 label_greater_or_equal: '>='
699 label_less_or_equal: '<='
700 label_less_or_equal: '<='
700 label_between: entre
701 label_between: entre
701 label_in: dans
702 label_in: dans
702 label_today: aujourd'hui
703 label_today: aujourd'hui
703 label_all_time: toute la période
704 label_all_time: toute la période
704 label_yesterday: hier
705 label_yesterday: hier
705 label_this_week: cette semaine
706 label_this_week: cette semaine
706 label_last_week: la semaine dernière
707 label_last_week: la semaine dernière
707 label_last_n_weeks: "les %{count} dernières semaines"
708 label_last_n_weeks: "les %{count} dernières semaines"
708 label_last_n_days: "les %{count} derniers jours"
709 label_last_n_days: "les %{count} derniers jours"
709 label_this_month: ce mois-ci
710 label_this_month: ce mois-ci
710 label_last_month: le mois dernier
711 label_last_month: le mois dernier
711 label_this_year: cette année
712 label_this_year: cette année
712 label_date_range: Période
713 label_date_range: Période
713 label_less_than_ago: il y a moins de
714 label_less_than_ago: il y a moins de
714 label_more_than_ago: il y a plus de
715 label_more_than_ago: il y a plus de
715 label_ago: il y a
716 label_ago: il y a
716 label_contains: contient
717 label_contains: contient
717 label_not_contains: ne contient pas
718 label_not_contains: ne contient pas
718 label_any_issues_in_project: une demande du projet
719 label_any_issues_in_project: une demande du projet
719 label_any_issues_not_in_project: une demande hors du projet
720 label_any_issues_not_in_project: une demande hors du projet
720 label_no_issues_in_project: aucune demande du projet
721 label_no_issues_in_project: aucune demande du projet
721 label_day_plural: jours
722 label_day_plural: jours
722 label_repository: Dépôt
723 label_repository: Dépôt
723 label_repository_new: Nouveau dépôt
724 label_repository_new: Nouveau dépôt
724 label_repository_plural: Dépôts
725 label_repository_plural: Dépôts
725 label_browse: Parcourir
726 label_browse: Parcourir
726 label_branch: Branche
727 label_branch: Branche
727 label_tag: Tag
728 label_tag: Tag
728 label_revision: Révision
729 label_revision: Révision
729 label_revision_plural: Révisions
730 label_revision_plural: Révisions
730 label_revision_id: "Révision %{value}"
731 label_revision_id: "Révision %{value}"
731 label_associated_revisions: Révisions associées
732 label_associated_revisions: Révisions associées
732 label_added: ajouté
733 label_added: ajouté
733 label_modified: modifié
734 label_modified: modifié
734 label_copied: copié
735 label_copied: copié
735 label_renamed: renommé
736 label_renamed: renommé
736 label_deleted: supprimé
737 label_deleted: supprimé
737 label_latest_revision: Dernière révision
738 label_latest_revision: Dernière révision
738 label_latest_revision_plural: Dernières révisions
739 label_latest_revision_plural: Dernières révisions
739 label_view_revisions: Voir les révisions
740 label_view_revisions: Voir les révisions
740 label_view_all_revisions: Voir toutes les révisions
741 label_view_all_revisions: Voir toutes les révisions
741 label_max_size: Taille maximale
742 label_max_size: Taille maximale
742 label_sort_highest: Remonter en premier
743 label_sort_highest: Remonter en premier
743 label_sort_higher: Remonter
744 label_sort_higher: Remonter
744 label_sort_lower: Descendre
745 label_sort_lower: Descendre
745 label_sort_lowest: Descendre en dernier
746 label_sort_lowest: Descendre en dernier
746 label_roadmap: Roadmap
747 label_roadmap: Roadmap
747 label_roadmap_due_in: "Échéance dans %{value}"
748 label_roadmap_due_in: "Échéance dans %{value}"
748 label_roadmap_overdue: "En retard de %{value}"
749 label_roadmap_overdue: "En retard de %{value}"
749 label_roadmap_no_issues: Aucune demande pour cette version
750 label_roadmap_no_issues: Aucune demande pour cette version
750 label_search: Recherche
751 label_search: Recherche
751 label_result_plural: Résultats
752 label_result_plural: Résultats
752 label_all_words: Tous les mots
753 label_all_words: Tous les mots
753 label_wiki: Wiki
754 label_wiki: Wiki
754 label_wiki_edit: Révision wiki
755 label_wiki_edit: Révision wiki
755 label_wiki_edit_plural: Révisions wiki
756 label_wiki_edit_plural: Révisions wiki
756 label_wiki_page: Page wiki
757 label_wiki_page: Page wiki
757 label_wiki_page_plural: Pages wiki
758 label_wiki_page_plural: Pages wiki
758 label_index_by_title: Index par titre
759 label_index_by_title: Index par titre
759 label_index_by_date: Index par date
760 label_index_by_date: Index par date
760 label_current_version: Version actuelle
761 label_current_version: Version actuelle
761 label_preview: Prévisualisation
762 label_preview: Prévisualisation
762 label_feed_plural: Flux Atom
763 label_feed_plural: Flux Atom
763 label_changes_details: Détails de tous les changements
764 label_changes_details: Détails de tous les changements
764 label_issue_tracking: Suivi des demandes
765 label_issue_tracking: Suivi des demandes
765 label_spent_time: Temps passé
766 label_spent_time: Temps passé
766 label_overall_spent_time: Temps passé global
767 label_overall_spent_time: Temps passé global
767 label_f_hour: "%{value} heure"
768 label_f_hour: "%{value} heure"
768 label_f_hour_plural: "%{value} heures"
769 label_f_hour_plural: "%{value} heures"
769 label_time_tracking: Suivi du temps
770 label_time_tracking: Suivi du temps
770 label_change_plural: Changements
771 label_change_plural: Changements
771 label_statistics: Statistiques
772 label_statistics: Statistiques
772 label_commits_per_month: Commits par mois
773 label_commits_per_month: Commits par mois
773 label_commits_per_author: Commits par auteur
774 label_commits_per_author: Commits par auteur
774 label_diff: diff
775 label_diff: diff
775 label_view_diff: Voir les différences
776 label_view_diff: Voir les différences
776 label_diff_inline: en ligne
777 label_diff_inline: en ligne
777 label_diff_side_by_side: côte à côte
778 label_diff_side_by_side: côte à côte
778 label_options: Options
779 label_options: Options
779 label_copy_workflow_from: Copier le workflow de
780 label_copy_workflow_from: Copier le workflow de
780 label_permissions_report: Synthèse des permissions
781 label_permissions_report: Synthèse des permissions
781 label_watched_issues: Demandes surveillées
782 label_watched_issues: Demandes surveillées
782 label_related_issues: Demandes liées
783 label_related_issues: Demandes liées
783 label_applied_status: Statut appliqué
784 label_applied_status: Statut appliqué
784 label_loading: Chargement...
785 label_loading: Chargement...
785 label_relation_new: Nouvelle relation
786 label_relation_new: Nouvelle relation
786 label_relation_delete: Supprimer la relation
787 label_relation_delete: Supprimer la relation
787 label_relates_to: Lié à
788 label_relates_to: Lié à
788 label_duplicates: Duplique
789 label_duplicates: Duplique
789 label_duplicated_by: Dupliqué par
790 label_duplicated_by: Dupliqué par
790 label_blocks: Bloque
791 label_blocks: Bloque
791 label_blocked_by: Bloqué par
792 label_blocked_by: Bloqué par
792 label_precedes: Précède
793 label_precedes: Précède
793 label_follows: Suit
794 label_follows: Suit
794 label_copied_to: Copié vers
795 label_copied_to: Copié vers
795 label_copied_from: Copié depuis
796 label_copied_from: Copié depuis
796 label_end_to_start: fin à début
797 label_end_to_start: fin à début
797 label_end_to_end: fin à fin
798 label_end_to_end: fin à fin
798 label_start_to_start: début à début
799 label_start_to_start: début à début
799 label_start_to_end: début à fin
800 label_start_to_end: début à fin
800 label_stay_logged_in: Rester connecté
801 label_stay_logged_in: Rester connecté
801 label_disabled: désactivé
802 label_disabled: désactivé
802 label_show_completed_versions: Voir les versions passées
803 label_show_completed_versions: Voir les versions passées
803 label_me: moi
804 label_me: moi
804 label_board: Forum
805 label_board: Forum
805 label_board_new: Nouveau forum
806 label_board_new: Nouveau forum
806 label_board_plural: Forums
807 label_board_plural: Forums
807 label_board_locked: Verrouillé
808 label_board_locked: Verrouillé
808 label_board_sticky: Sticky
809 label_board_sticky: Sticky
809 label_topic_plural: Discussions
810 label_topic_plural: Discussions
810 label_message_plural: Messages
811 label_message_plural: Messages
811 label_message_last: Dernier message
812 label_message_last: Dernier message
812 label_message_new: Nouveau message
813 label_message_new: Nouveau message
813 label_message_posted: Message ajouté
814 label_message_posted: Message ajouté
814 label_reply_plural: Réponses
815 label_reply_plural: Réponses
815 label_send_information: Envoyer les informations à l'utilisateur
816 label_send_information: Envoyer les informations à l'utilisateur
816 label_year: Année
817 label_year: Année
817 label_month: Mois
818 label_month: Mois
818 label_week: Semaine
819 label_week: Semaine
819 label_date_from: Du
820 label_date_from: Du
820 label_date_to: Au
821 label_date_to: Au
821 label_language_based: Basé sur la langue de l'utilisateur
822 label_language_based: Basé sur la langue de l'utilisateur
822 label_sort_by: "Trier par %{value}"
823 label_sort_by: "Trier par %{value}"
823 label_send_test_email: Envoyer un email de test
824 label_send_test_email: Envoyer un email de test
824 label_feeds_access_key: Clé d'accès Atom
825 label_feeds_access_key: Clé d'accès Atom
825 label_missing_feeds_access_key: Clé d'accès Atom manquante
826 label_missing_feeds_access_key: Clé d'accès Atom manquante
826 label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}"
827 label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}"
827 label_module_plural: Modules
828 label_module_plural: Modules
828 label_added_time_by: "Ajouté par %{author} il y a %{age}"
829 label_added_time_by: "Ajouté par %{author} il y a %{age}"
829 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
830 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
830 label_updated_time: "Mis à jour il y a %{value}"
831 label_updated_time: "Mis à jour il y a %{value}"
831 label_jump_to_a_project: Aller à un projet...
832 label_jump_to_a_project: Aller à un projet...
832 label_file_plural: Fichiers
833 label_file_plural: Fichiers
833 label_changeset_plural: Révisions
834 label_changeset_plural: Révisions
834 label_default_columns: Colonnes par défaut
835 label_default_columns: Colonnes par défaut
835 label_no_change_option: (Pas de changement)
836 label_no_change_option: (Pas de changement)
836 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
837 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
837 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
838 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
838 label_theme: Thème
839 label_theme: Thème
839 label_default: Défaut
840 label_default: Défaut
840 label_search_titles_only: Uniquement dans les titres
841 label_search_titles_only: Uniquement dans les titres
841 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
842 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
842 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
843 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
843 label_user_mail_option_none: Aucune notification
844 label_user_mail_option_none: Aucune notification
844 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
845 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
845 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
846 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
846 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
847 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
847 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
848 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
848 label_registration_activation_by_email: activation du compte par email
849 label_registration_activation_by_email: activation du compte par email
849 label_registration_manual_activation: activation manuelle du compte
850 label_registration_manual_activation: activation manuelle du compte
850 label_registration_automatic_activation: activation automatique du compte
851 label_registration_automatic_activation: activation automatique du compte
851 label_display_per_page: "Par page : %{value}"
852 label_display_per_page: "Par page : %{value}"
852 label_age: Âge
853 label_age: Âge
853 label_change_properties: Changer les propriétés
854 label_change_properties: Changer les propriétés
854 label_general: Général
855 label_general: Général
855 label_more: Plus
856 label_more: Plus
856 label_scm: SCM
857 label_scm: SCM
857 label_plugins: Plugins
858 label_plugins: Plugins
858 label_ldap_authentication: Authentification LDAP
859 label_ldap_authentication: Authentification LDAP
859 label_downloads_abbr: D/L
860 label_downloads_abbr: D/L
860 label_optional_description: Description facultative
861 label_optional_description: Description facultative
861 label_add_another_file: Ajouter un autre fichier
862 label_add_another_file: Ajouter un autre fichier
862 label_preferences: Préférences
863 label_preferences: Préférences
863 label_chronological_order: Dans l'ordre chronologique
864 label_chronological_order: Dans l'ordre chronologique
864 label_reverse_chronological_order: Dans l'ordre chronologique inverse
865 label_reverse_chronological_order: Dans l'ordre chronologique inverse
865 label_planning: Planning
866 label_planning: Planning
866 label_incoming_emails: Emails entrants
867 label_incoming_emails: Emails entrants
867 label_generate_key: Générer une clé
868 label_generate_key: Générer une clé
868 label_issue_watchers: Observateurs
869 label_issue_watchers: Observateurs
869 label_example: Exemple
870 label_example: Exemple
870 label_display: Affichage
871 label_display: Affichage
871 label_sort: Tri
872 label_sort: Tri
872 label_ascending: Croissant
873 label_ascending: Croissant
873 label_descending: Décroissant
874 label_descending: Décroissant
874 label_date_from_to: Du %{start} au %{end}
875 label_date_from_to: Du %{start} au %{end}
875 label_wiki_content_added: Page wiki ajoutée
876 label_wiki_content_added: Page wiki ajoutée
876 label_wiki_content_updated: Page wiki mise à jour
877 label_wiki_content_updated: Page wiki mise à jour
877 label_group: Groupe
878 label_group: Groupe
878 label_group_plural: Groupes
879 label_group_plural: Groupes
879 label_group_new: Nouveau groupe
880 label_group_new: Nouveau groupe
880 label_group_anonymous: Utilisateurs anonymes
881 label_group_anonymous: Utilisateurs anonymes
881 label_group_non_member: Utilisateurs non membres
882 label_group_non_member: Utilisateurs non membres
882 label_time_entry_plural: Temps passé
883 label_time_entry_plural: Temps passé
883 label_version_sharing_none: Non partagé
884 label_version_sharing_none: Non partagé
884 label_version_sharing_descendants: Avec les sous-projets
885 label_version_sharing_descendants: Avec les sous-projets
885 label_version_sharing_hierarchy: Avec toute la hiérarchie
886 label_version_sharing_hierarchy: Avec toute la hiérarchie
886 label_version_sharing_tree: Avec tout l'arbre
887 label_version_sharing_tree: Avec tout l'arbre
887 label_version_sharing_system: Avec tous les projets
888 label_version_sharing_system: Avec tous les projets
888 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
889 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
889 label_copy_source: Source
890 label_copy_source: Source
890 label_copy_target: Cible
891 label_copy_target: Cible
891 label_copy_same_as_target: Comme la cible
892 label_copy_same_as_target: Comme la cible
892 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
893 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
893 label_api_access_key: Clé d'accès API
894 label_api_access_key: Clé d'accès API
894 label_missing_api_access_key: Clé d'accès API manquante
895 label_missing_api_access_key: Clé d'accès API manquante
895 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
896 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
896 label_profile: Profil
897 label_profile: Profil
897 label_subtask_plural: Sous-tâches
898 label_subtask_plural: Sous-tâches
898 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
899 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
899 label_principal_search: "Rechercher un utilisateur ou un groupe :"
900 label_principal_search: "Rechercher un utilisateur ou un groupe :"
900 label_user_search: "Rechercher un utilisateur :"
901 label_user_search: "Rechercher un utilisateur :"
901 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
902 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
902 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
903 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
903 label_issues_visibility_all: Toutes les demandes
904 label_issues_visibility_all: Toutes les demandes
904 label_issues_visibility_public: Toutes les demandes non privées
905 label_issues_visibility_public: Toutes les demandes non privées
905 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
906 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
906 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
907 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
907 label_parent_revision: Parent
908 label_parent_revision: Parent
908 label_child_revision: Enfant
909 label_child_revision: Enfant
909 label_export_options: Options d'exportation %{export_format}
910 label_export_options: Options d'exportation %{export_format}
910 label_copy_attachments: Copier les fichiers
911 label_copy_attachments: Copier les fichiers
911 label_copy_subtasks: Copier les sous-tâches
912 label_copy_subtasks: Copier les sous-tâches
912 label_item_position: "%{position} sur %{count}"
913 label_item_position: "%{position} sur %{count}"
913 label_completed_versions: Versions passées
914 label_completed_versions: Versions passées
914 label_search_for_watchers: Rechercher des observateurs
915 label_search_for_watchers: Rechercher des observateurs
915 label_session_expiration: Expiration des sessions
916 label_session_expiration: Expiration des sessions
916 label_show_closed_projects: Voir les projets fermés
917 label_show_closed_projects: Voir les projets fermés
917 label_status_transitions: Changements de statut
918 label_status_transitions: Changements de statut
918 label_fields_permissions: Permissions sur les champs
919 label_fields_permissions: Permissions sur les champs
919 label_readonly: Lecture
920 label_readonly: Lecture
920 label_required: Obligatoire
921 label_required: Obligatoire
921 label_hidden: Caché
922 label_hidden: Caché
922 label_attribute_of_project: "%{name} du projet"
923 label_attribute_of_project: "%{name} du projet"
923 label_attribute_of_issue: "%{name} de la demande"
924 label_attribute_of_issue: "%{name} de la demande"
924 label_attribute_of_author: "%{name} de l'auteur"
925 label_attribute_of_author: "%{name} de l'auteur"
925 label_attribute_of_assigned_to: "%{name} de l'assigné"
926 label_attribute_of_assigned_to: "%{name} de l'assigné"
926 label_attribute_of_user: "%{name} de l'utilisateur"
927 label_attribute_of_user: "%{name} de l'utilisateur"
927 label_attribute_of_fixed_version: "%{name} de la version cible"
928 label_attribute_of_fixed_version: "%{name} de la version cible"
928 label_cross_project_descendants: Avec les sous-projets
929 label_cross_project_descendants: Avec les sous-projets
929 label_cross_project_tree: Avec tout l'arbre
930 label_cross_project_tree: Avec tout l'arbre
930 label_cross_project_hierarchy: Avec toute la hiérarchie
931 label_cross_project_hierarchy: Avec toute la hiérarchie
931 label_cross_project_system: Avec tous les projets
932 label_cross_project_system: Avec tous les projets
932 label_gantt_progress_line: Ligne de progression
933 label_gantt_progress_line: Ligne de progression
933 label_visibility_private: par moi uniquement
934 label_visibility_private: par moi uniquement
934 label_visibility_roles: par ces rôles uniquement
935 label_visibility_roles: par ces rôles uniquement
935 label_visibility_public: par tout le monde
936 label_visibility_public: par tout le monde
936 label_link: Lien
937 label_link: Lien
937 label_only: seulement
938 label_only: seulement
938 label_drop_down_list: liste déroulante
939 label_drop_down_list: liste déroulante
939 label_checkboxes: cases à cocher
940 label_checkboxes: cases à cocher
940 label_radio_buttons: boutons radio
941 label_radio_buttons: boutons radio
941 label_link_values_to: Lier les valeurs vers l'URL
942 label_link_values_to: Lier les valeurs vers l'URL
942 label_custom_field_select_type: Selectionner le type d'objet auquel attacher le champ personnalisé
943 label_custom_field_select_type: Selectionner le type d'objet auquel attacher le champ personnalisé
943 label_check_for_updates: Vérifier les mises à jour
944 label_check_for_updates: Vérifier les mises à jour
944 label_latest_compatible_version: Dernière version compatible
945 label_latest_compatible_version: Dernière version compatible
945 label_unknown_plugin: Plugin inconnu
946 label_unknown_plugin: Plugin inconnu
946 label_add_projects: Ajouter des projets
947 label_add_projects: Ajouter des projets
947 label_users_visibility_all: Tous les utilisateurs actifs
948 label_users_visibility_all: Tous les utilisateurs actifs
948 label_users_visibility_members_of_visible_projects: Membres des projets visibles
949 label_users_visibility_members_of_visible_projects: Membres des projets visibles
949 label_edit_attachments: Modifier les fichiers attachés
950 label_edit_attachments: Modifier les fichiers attachés
950 label_link_copied_issue: Lier la demande copiée
951 label_link_copied_issue: Lier la demande copiée
951 label_ask: Demander
952 label_ask: Demander
952 label_search_attachments_yes: Rechercher les noms et descriptions de fichiers
953 label_search_attachments_yes: Rechercher les noms et descriptions de fichiers
953 label_search_attachments_no: Ne pas rechercher les fichiers
954 label_search_attachments_no: Ne pas rechercher les fichiers
954 label_search_attachments_only: Rechercher les fichiers uniquement
955 label_search_attachments_only: Rechercher les fichiers uniquement
955 label_search_open_issues_only: Demandes ouvertes uniquement
956 label_search_open_issues_only: Demandes ouvertes uniquement
956 label_email_address_plural: Emails
957 label_email_address_plural: Emails
957 label_email_address_add: Ajouter une adresse email
958 label_email_address_add: Ajouter une adresse email
958 label_enable_notifications: Activer les notifications
959 label_enable_notifications: Activer les notifications
959 label_disable_notifications: Désactiver les notifications
960 label_disable_notifications: Désactiver les notifications
960 label_blank_value: non renseigné
961 label_blank_value: non renseigné
961
962
962 button_login: Connexion
963 button_login: Connexion
963 button_submit: Soumettre
964 button_submit: Soumettre
964 button_save: Sauvegarder
965 button_save: Sauvegarder
965 button_check_all: Tout cocher
966 button_check_all: Tout cocher
966 button_uncheck_all: Tout décocher
967 button_uncheck_all: Tout décocher
967 button_collapse_all: Plier tout
968 button_collapse_all: Plier tout
968 button_expand_all: Déplier tout
969 button_expand_all: Déplier tout
969 button_delete: Supprimer
970 button_delete: Supprimer
970 button_create: Créer
971 button_create: Créer
971 button_create_and_continue: Créer et continuer
972 button_create_and_continue: Créer et continuer
972 button_test: Tester
973 button_test: Tester
973 button_edit: Modifier
974 button_edit: Modifier
974 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
975 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
975 button_add: Ajouter
976 button_add: Ajouter
976 button_change: Changer
977 button_change: Changer
977 button_apply: Appliquer
978 button_apply: Appliquer
978 button_clear: Effacer
979 button_clear: Effacer
979 button_lock: Verrouiller
980 button_lock: Verrouiller
980 button_unlock: Déverrouiller
981 button_unlock: Déverrouiller
981 button_download: Télécharger
982 button_download: Télécharger
982 button_list: Lister
983 button_list: Lister
983 button_view: Voir
984 button_view: Voir
984 button_move: Déplacer
985 button_move: Déplacer
985 button_move_and_follow: Déplacer et suivre
986 button_move_and_follow: Déplacer et suivre
986 button_back: Retour
987 button_back: Retour
987 button_cancel: Annuler
988 button_cancel: Annuler
988 button_activate: Activer
989 button_activate: Activer
989 button_sort: Trier
990 button_sort: Trier
990 button_log_time: Saisir temps
991 button_log_time: Saisir temps
991 button_rollback: Revenir à cette version
992 button_rollback: Revenir à cette version
992 button_watch: Surveiller
993 button_watch: Surveiller
993 button_unwatch: Ne plus surveiller
994 button_unwatch: Ne plus surveiller
994 button_reply: Répondre
995 button_reply: Répondre
995 button_archive: Archiver
996 button_archive: Archiver
996 button_unarchive: Désarchiver
997 button_unarchive: Désarchiver
997 button_reset: Réinitialiser
998 button_reset: Réinitialiser
998 button_rename: Renommer
999 button_rename: Renommer
999 button_change_password: Changer de mot de passe
1000 button_change_password: Changer de mot de passe
1000 button_copy: Copier
1001 button_copy: Copier
1001 button_copy_and_follow: Copier et suivre
1002 button_copy_and_follow: Copier et suivre
1002 button_annotate: Annoter
1003 button_annotate: Annoter
1003 button_update: Mettre à jour
1004 button_update: Mettre à jour
1004 button_configure: Configurer
1005 button_configure: Configurer
1005 button_quote: Citer
1006 button_quote: Citer
1006 button_duplicate: Dupliquer
1007 button_duplicate: Dupliquer
1007 button_show: Afficher
1008 button_show: Afficher
1008 button_hide: Cacher
1009 button_hide: Cacher
1009 button_edit_section: Modifier cette section
1010 button_edit_section: Modifier cette section
1010 button_export: Exporter
1011 button_export: Exporter
1011 button_delete_my_account: Supprimer mon compte
1012 button_delete_my_account: Supprimer mon compte
1012 button_close: Fermer
1013 button_close: Fermer
1013 button_reopen: Réouvrir
1014 button_reopen: Réouvrir
1014
1015
1015 status_active: actif
1016 status_active: actif
1016 status_registered: enregistré
1017 status_registered: enregistré
1017 status_locked: verrouillé
1018 status_locked: verrouillé
1018
1019
1019 project_status_active: actif
1020 project_status_active: actif
1020 project_status_closed: fermé
1021 project_status_closed: fermé
1021 project_status_archived: archivé
1022 project_status_archived: archivé
1022
1023
1023 version_status_open: ouvert
1024 version_status_open: ouvert
1024 version_status_locked: verrouillé
1025 version_status_locked: verrouillé
1025 version_status_closed: fermé
1026 version_status_closed: fermé
1026
1027
1027 field_active: Actif
1028 field_active: Actif
1028
1029
1029 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
1030 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
1030 text_regexp_info: ex. ^[A-Z0-9]+$
1031 text_regexp_info: ex. ^[A-Z0-9]+$
1031 text_min_max_length_info: 0 pour aucune restriction
1032 text_min_max_length_info: 0 pour aucune restriction
1032 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
1033 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
1033 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
1034 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
1034 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
1035 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
1035 text_are_you_sure: Êtes-vous sûr ?
1036 text_are_you_sure: Êtes-vous sûr ?
1036 text_journal_changed: "%{label} changé de %{old} à %{new}"
1037 text_journal_changed: "%{label} changé de %{old} à %{new}"
1037 text_journal_changed_no_detail: "%{label} mis à jour"
1038 text_journal_changed_no_detail: "%{label} mis à jour"
1038 text_journal_set_to: "%{label} mis à %{value}"
1039 text_journal_set_to: "%{label} mis à %{value}"
1039 text_journal_deleted: "%{label} %{old} supprimé"
1040 text_journal_deleted: "%{label} %{old} supprimé"
1040 text_journal_added: "%{label} %{value} ajouté"
1041 text_journal_added: "%{label} %{value} ajouté"
1041 text_tip_issue_begin_day: tâche commençant ce jour
1042 text_tip_issue_begin_day: tâche commençant ce jour
1042 text_tip_issue_end_day: tâche finissant ce jour
1043 text_tip_issue_end_day: tâche finissant ce jour
1043 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
1044 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
1044 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 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 text_caracters_maximum: "%{count} caractères maximum."
1046 text_caracters_maximum: "%{count} caractères maximum."
1046 text_caracters_minimum: "%{count} caractères minimum."
1047 text_caracters_minimum: "%{count} caractères minimum."
1047 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
1048 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
1048 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
1049 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
1049 text_unallowed_characters: Caractères non autorisés
1050 text_unallowed_characters: Caractères non autorisés
1050 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
1051 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
1051 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
1052 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
1052 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
1053 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
1053 text_issue_added: "La demande %{id} a été soumise par %{author}."
1054 text_issue_added: "La demande %{id} a été soumise par %{author}."
1054 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
1055 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
1055 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
1056 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
1056 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
1057 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
1057 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
1058 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
1058 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
1059 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
1059 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 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 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 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 text_load_default_configuration: Charger le paramétrage par défaut
1062 text_load_default_configuration: Charger le paramétrage par défaut
1062 text_status_changed_by_changeset: "Appliqué par commit %{value}."
1063 text_status_changed_by_changeset: "Appliqué par commit %{value}."
1063 text_time_logged_by_changeset: "Appliqué par commit %{value}"
1064 text_time_logged_by_changeset: "Appliqué par commit %{value}"
1064 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
1065 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
1065 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
1066 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
1066 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1067 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1067 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
1068 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
1068 text_default_administrator_account_changed: Compte administrateur par défaut changé
1069 text_default_administrator_account_changed: Compte administrateur par défaut changé
1069 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
1070 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
1070 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
1071 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
1071 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
1072 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
1072 text_convert_available: Binaire convert de ImageMagick présent (optionel)
1073 text_convert_available: Binaire convert de ImageMagick présent (optionel)
1073 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
1074 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
1074 text_destroy_time_entries: Supprimer les heures
1075 text_destroy_time_entries: Supprimer les heures
1075 text_assign_time_entries_to_project: Reporter les heures sur le projet
1076 text_assign_time_entries_to_project: Reporter les heures sur le projet
1076 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
1077 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
1077 text_user_wrote: "%{value} a écrit :"
1078 text_user_wrote: "%{value} a écrit :"
1078 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
1079 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
1079 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
1080 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
1080 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 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 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 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 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
1083 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
1083 text_custom_field_possible_values_info: 'Une ligne par valeur'
1084 text_custom_field_possible_values_info: 'Une ligne par valeur'
1084 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
1085 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
1085 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
1086 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
1086 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
1087 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
1087 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
1088 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
1088 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 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 text_zoom_in: Zoom avant
1090 text_zoom_in: Zoom avant
1090 text_zoom_out: Zoom arrière
1091 text_zoom_out: Zoom arrière
1091 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
1092 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
1092 text_scm_path_encoding_note: "Défaut : UTF-8"
1093 text_scm_path_encoding_note: "Défaut : UTF-8"
1093 text_subversion_repository_note: "Exemples (en fonction des protocoles supportés) : file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1094 text_subversion_repository_note: "Exemples (en fonction des protocoles supportés) : file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1094 text_git_repository_note: "Chemin vers un dépôt vide et local (exemples : /gitrepo, c:\\gitrepo)"
1095 text_git_repository_note: "Chemin vers un dépôt vide et local (exemples : /gitrepo, c:\\gitrepo)"
1095 text_mercurial_repository_note: "Chemin vers un dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1096 text_mercurial_repository_note: "Chemin vers un dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1096 text_scm_command: Commande
1097 text_scm_command: Commande
1097 text_scm_command_version: Version
1098 text_scm_command_version: Version
1098 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1099 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1099 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1100 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1100 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 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 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
1102 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
1102 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
1103 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
1103 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 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 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 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 text_project_closed: Ce projet est fermé et accessible en lecture seule.
1106 text_project_closed: Ce projet est fermé et accessible en lecture seule.
1106 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 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 default_role_manager: Manager
1109 default_role_manager: Manager
1109 default_role_developer: Développeur
1110 default_role_developer: Développeur
1110 default_role_reporter: Rapporteur
1111 default_role_reporter: Rapporteur
1111 default_tracker_bug: Anomalie
1112 default_tracker_bug: Anomalie
1112 default_tracker_feature: Evolution
1113 default_tracker_feature: Evolution
1113 default_tracker_support: Assistance
1114 default_tracker_support: Assistance
1114 default_issue_status_new: Nouveau
1115 default_issue_status_new: Nouveau
1115 default_issue_status_in_progress: En cours
1116 default_issue_status_in_progress: En cours
1116 default_issue_status_resolved: Résolu
1117 default_issue_status_resolved: Résolu
1117 default_issue_status_feedback: Commentaire
1118 default_issue_status_feedback: Commentaire
1118 default_issue_status_closed: Fermé
1119 default_issue_status_closed: Fermé
1119 default_issue_status_rejected: Rejeté
1120 default_issue_status_rejected: Rejeté
1120 default_doc_category_user: Documentation utilisateur
1121 default_doc_category_user: Documentation utilisateur
1121 default_doc_category_tech: Documentation technique
1122 default_doc_category_tech: Documentation technique
1122 default_priority_low: Bas
1123 default_priority_low: Bas
1123 default_priority_normal: Normal
1124 default_priority_normal: Normal
1124 default_priority_high: Haut
1125 default_priority_high: Haut
1125 default_priority_urgent: Urgent
1126 default_priority_urgent: Urgent
1126 default_priority_immediate: Immédiat
1127 default_priority_immediate: Immédiat
1127 default_activity_design: Conception
1128 default_activity_design: Conception
1128 default_activity_development: Développement
1129 default_activity_development: Développement
1129
1130
1130 enumeration_issue_priorities: Priorités des demandes
1131 enumeration_issue_priorities: Priorités des demandes
1131 enumeration_doc_categories: Catégories des documents
1132 enumeration_doc_categories: Catégories des documents
1132 enumeration_activities: Activités (suivi du temps)
1133 enumeration_activities: Activités (suivi du temps)
1133 enumeration_system_activity: Activité système
1134 enumeration_system_activity: Activité système
1134 description_filter: Filtre
1135 description_filter: Filtre
1135 description_search: Champ de recherche
1136 description_search: Champ de recherche
1136 description_choose_project: Projets
1137 description_choose_project: Projets
1137 description_project_scope: Périmètre de recherche
1138 description_project_scope: Périmètre de recherche
1138 description_notes: Notes
1139 description_notes: Notes
1139 description_message_content: Contenu du message
1140 description_message_content: Contenu du message
1140 description_query_sort_criteria_attribute: Critère de tri
1141 description_query_sort_criteria_attribute: Critère de tri
1141 description_query_sort_criteria_direction: Ordre de tri
1142 description_query_sort_criteria_direction: Ordre de tri
1142 description_user_mail_notification: Option de notification
1143 description_user_mail_notification: Option de notification
1143 description_available_columns: Colonnes disponibles
1144 description_available_columns: Colonnes disponibles
1144 description_selected_columns: Colonnes sélectionnées
1145 description_selected_columns: Colonnes sélectionnées
1145 description_all_columns: Toutes les colonnes
1146 description_all_columns: Toutes les colonnes
1146 description_issue_category_reassign: Choisir une catégorie
1147 description_issue_category_reassign: Choisir une catégorie
1147 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1148 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1148 description_date_range_list: Choisir une période prédéfinie
1149 description_date_range_list: Choisir une période prédéfinie
1149 description_date_range_interval: Choisir une période
1150 description_date_range_interval: Choisir une période
1150 description_date_from: Date de début
1151 description_date_from: Date de début
1151 description_date_to: Date de fin
1152 description_date_to: Date de fin
1152 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é.'
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 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/core_ext'
18 require 'redmine/core_ext'
19
19
20 begin
20 begin
21 require 'RMagick' unless Object.const_defined?(:Magick)
21 require 'RMagick' unless Object.const_defined?(:Magick)
22 rescue LoadError
22 rescue LoadError
23 # RMagick is not available
23 # RMagick is not available
24 end
24 end
25 begin
25 begin
26 require 'redcarpet' unless Object.const_defined?(:Redcarpet)
26 require 'redcarpet' unless Object.const_defined?(:Redcarpet)
27 rescue LoadError
27 rescue LoadError
28 # Redcarpet is not available
28 # Redcarpet is not available
29 end
29 end
30
30
31 require 'redmine/scm/base'
31 require 'redmine/scm/base'
32 require 'redmine/access_control'
32 require 'redmine/access_control'
33 require 'redmine/access_keys'
33 require 'redmine/access_keys'
34 require 'redmine/activity'
34 require 'redmine/activity'
35 require 'redmine/activity/fetcher'
35 require 'redmine/activity/fetcher'
36 require 'redmine/ciphering'
36 require 'redmine/ciphering'
37 require 'redmine/codeset_util'
37 require 'redmine/codeset_util'
38 require 'redmine/field_format'
38 require 'redmine/field_format'
39 require 'redmine/i18n'
39 require 'redmine/i18n'
40 require 'redmine/menu_manager'
40 require 'redmine/menu_manager'
41 require 'redmine/notifiable'
41 require 'redmine/notifiable'
42 require 'redmine/platform'
42 require 'redmine/platform'
43 require 'redmine/mime_type'
43 require 'redmine/mime_type'
44 require 'redmine/notifiable'
44 require 'redmine/notifiable'
45 require 'redmine/search'
45 require 'redmine/search'
46 require 'redmine/syntax_highlighting'
46 require 'redmine/syntax_highlighting'
47 require 'redmine/thumbnail'
47 require 'redmine/thumbnail'
48 require 'redmine/unified_diff'
48 require 'redmine/unified_diff'
49 require 'redmine/utils'
49 require 'redmine/utils'
50 require 'redmine/version'
50 require 'redmine/version'
51 require 'redmine/wiki_formatting'
51 require 'redmine/wiki_formatting'
52
52
53 require 'redmine/default_data/loader'
53 require 'redmine/default_data/loader'
54 require 'redmine/helpers/calendar'
54 require 'redmine/helpers/calendar'
55 require 'redmine/helpers/diff'
55 require 'redmine/helpers/diff'
56 require 'redmine/helpers/gantt'
56 require 'redmine/helpers/gantt'
57 require 'redmine/helpers/time_report'
57 require 'redmine/helpers/time_report'
58 require 'redmine/views/other_formats_builder'
58 require 'redmine/views/other_formats_builder'
59 require 'redmine/views/labelled_form_builder'
59 require 'redmine/views/labelled_form_builder'
60 require 'redmine/views/builders'
60 require 'redmine/views/builders'
61
61
62 require 'redmine/themes'
62 require 'redmine/themes'
63 require 'redmine/hook'
63 require 'redmine/hook'
64 require 'redmine/plugin'
64 require 'redmine/plugin'
65
65
66 require 'csv'
66 require 'csv'
67
67
68 Redmine::Scm::Base.add "Subversion"
68 Redmine::Scm::Base.add "Subversion"
69 Redmine::Scm::Base.add "Darcs"
69 Redmine::Scm::Base.add "Darcs"
70 Redmine::Scm::Base.add "Mercurial"
70 Redmine::Scm::Base.add "Mercurial"
71 Redmine::Scm::Base.add "Cvs"
71 Redmine::Scm::Base.add "Cvs"
72 Redmine::Scm::Base.add "Bazaar"
72 Redmine::Scm::Base.add "Bazaar"
73 Redmine::Scm::Base.add "Git"
73 Redmine::Scm::Base.add "Git"
74 Redmine::Scm::Base.add "Filesystem"
74 Redmine::Scm::Base.add "Filesystem"
75
75
76 # Permissions
76 # Permissions
77 Redmine::AccessControl.map do |map|
77 Redmine::AccessControl.map do |map|
78 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
78 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
79 map.permission :search_project, {:search => :index}, :public => true, :read => true
79 map.permission :search_project, {:search => :index}, :public => true, :read => true
80 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
80 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
81 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
81 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
82 map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
82 map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
83 map.permission :select_project_modules, {:projects => :modules}, :require => :member
83 map.permission :select_project_modules, {:projects => :modules}, :require => :member
84 map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true
84 map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true
85 map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :update, :destroy, :autocomplete]}, :require => :member
85 map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :update, :destroy, :autocomplete]}, :require => :member
86 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
86 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
87 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
87 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
88
88
89 map.project_module :issue_tracking do |map|
89 map.project_module :issue_tracking do |map|
90 # Issue categories
90 # Issue categories
91 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
91 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
92 # Issues
92 # Issues
93 map.permission :view_issues, {:issues => [:index, :show],
93 map.permission :view_issues, {:issues => [:index, :show],
94 :auto_complete => [:issues],
94 :auto_complete => [:issues],
95 :context_menus => [:issues],
95 :context_menus => [:issues],
96 :versions => [:index, :show, :status_by],
96 :versions => [:index, :show, :status_by],
97 :journals => [:index, :diff],
97 :journals => [:index, :diff],
98 :queries => :index,
98 :queries => :index,
99 :reports => [:issue_report, :issue_report_details]},
99 :reports => [:issue_report, :issue_report_details]},
100 :read => true
100 :read => true
101 map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
101 map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
102 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
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 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
104 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
104 map.permission :manage_subtasks, {}
105 map.permission :manage_subtasks, {}
105 map.permission :set_issues_private, {}
106 map.permission :set_issues_private, {}
106 map.permission :set_own_issues_private, {}, :require => :loggedin
107 map.permission :set_own_issues_private, {}, :require => :loggedin
107 map.permission :add_issue_notes, {:issues => [:edit, :update, :update_form], :journals => [:new], :attachments => :upload}
108 map.permission :add_issue_notes, {:issues => [:edit, :update, :update_form], :journals => [:new], :attachments => :upload}
108 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
109 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
109 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
110 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
110 map.permission :view_private_notes, {}, :read => true, :require => :member
111 map.permission :view_private_notes, {}, :read => true, :require => :member
111 map.permission :set_notes_private, {}, :require => :member
112 map.permission :set_notes_private, {}, :require => :member
112 map.permission :delete_issues, {:issues => :destroy}, :require => :member
113 map.permission :delete_issues, {:issues => :destroy}, :require => :member
113 # Queries
114 # Queries
114 map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
115 map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
115 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
116 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
116 # Watchers
117 # Watchers
117 map.permission :view_issue_watchers, {}, :read => true
118 map.permission :view_issue_watchers, {}, :read => true
118 map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]}
119 map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]}
119 map.permission :delete_issue_watchers, {:watchers => :destroy}
120 map.permission :delete_issue_watchers, {:watchers => :destroy}
120 end
121 end
121
122
122 map.project_module :time_tracking do |map|
123 map.project_module :time_tracking do |map|
123 map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
124 map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
124 map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
125 map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
125 map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
126 map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
126 map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
127 map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
127 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
128 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
128 end
129 end
129
130
130 map.project_module :news do |map|
131 map.project_module :news do |map|
131 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member
132 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member
132 map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
133 map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
133 map.permission :comment_news, {:comments => :create}
134 map.permission :comment_news, {:comments => :create}
134 end
135 end
135
136
136 map.project_module :documents do |map|
137 map.project_module :documents do |map|
137 map.permission :add_documents, {:documents => [:new, :create, :add_attachment], :attachments => :upload}, :require => :loggedin
138 map.permission :add_documents, {:documents => [:new, :create, :add_attachment], :attachments => :upload}, :require => :loggedin
138 map.permission :edit_documents, {:documents => [:edit, :update, :add_attachment], :attachments => :upload}, :require => :loggedin
139 map.permission :edit_documents, {:documents => [:edit, :update, :add_attachment], :attachments => :upload}, :require => :loggedin
139 map.permission :delete_documents, {:documents => [:destroy]}, :require => :loggedin
140 map.permission :delete_documents, {:documents => [:destroy]}, :require => :loggedin
140 map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
141 map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
141 end
142 end
142
143
143 map.project_module :files do |map|
144 map.project_module :files do |map|
144 map.permission :manage_files, {:files => [:new, :create], :attachments => :upload}, :require => :loggedin
145 map.permission :manage_files, {:files => [:new, :create], :attachments => :upload}, :require => :loggedin
145 map.permission :view_files, {:files => :index, :versions => :download}, :read => true
146 map.permission :view_files, {:files => :index, :versions => :download}, :read => true
146 end
147 end
147
148
148 map.project_module :wiki do |map|
149 map.project_module :wiki do |map|
149 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
150 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
150 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
151 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
151 map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
152 map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
152 map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
153 map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
153 map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
154 map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
154 map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
155 map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
155 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment], :attachments => :upload
156 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment], :attachments => :upload
156 map.permission :delete_wiki_pages_attachments, {}
157 map.permission :delete_wiki_pages_attachments, {}
157 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
158 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
158 end
159 end
159
160
160 map.project_module :repository do |map|
161 map.project_module :repository do |map|
161 map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
162 map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
162 map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
163 map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
163 map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
164 map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
164 map.permission :commit_access, {}
165 map.permission :commit_access, {}
165 map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
166 map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
166 end
167 end
167
168
168 map.project_module :boards do |map|
169 map.project_module :boards do |map|
169 map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
170 map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
170 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
171 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
171 map.permission :add_messages, {:messages => [:new, :reply, :quote], :attachments => :upload}
172 map.permission :add_messages, {:messages => [:new, :reply, :quote], :attachments => :upload}
172 map.permission :edit_messages, {:messages => :edit, :attachments => :upload}, :require => :member
173 map.permission :edit_messages, {:messages => :edit, :attachments => :upload}, :require => :member
173 map.permission :edit_own_messages, {:messages => :edit, :attachments => :upload}, :require => :loggedin
174 map.permission :edit_own_messages, {:messages => :edit, :attachments => :upload}, :require => :loggedin
174 map.permission :delete_messages, {:messages => :destroy}, :require => :member
175 map.permission :delete_messages, {:messages => :destroy}, :require => :member
175 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
176 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
176 end
177 end
177
178
178 map.project_module :calendar do |map|
179 map.project_module :calendar do |map|
179 map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
180 map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
180 end
181 end
181
182
182 map.project_module :gantt do |map|
183 map.project_module :gantt do |map|
183 map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
184 map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
184 end
185 end
185 end
186 end
186
187
187 Redmine::MenuManager.map :top_menu do |menu|
188 Redmine::MenuManager.map :top_menu do |menu|
188 menu.push :home, :home_path
189 menu.push :home, :home_path
189 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
190 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
190 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
191 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
191 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
192 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
192 menu.push :help, Redmine::Info.help_url, :last => true
193 menu.push :help, Redmine::Info.help_url, :last => true
193 end
194 end
194
195
195 Redmine::MenuManager.map :account_menu do |menu|
196 Redmine::MenuManager.map :account_menu do |menu|
196 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
197 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
197 menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
198 menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
198 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
199 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
199 menu.push :logout, :signout_path, :html => {:method => 'post'}, :if => Proc.new { User.current.logged? }
200 menu.push :logout, :signout_path, :html => {:method => 'post'}, :if => Proc.new { User.current.logged? }
200 end
201 end
201
202
202 Redmine::MenuManager.map :application_menu do |menu|
203 Redmine::MenuManager.map :application_menu do |menu|
203 # Empty
204 # Empty
204 end
205 end
205
206
206 Redmine::MenuManager.map :admin_menu do |menu|
207 Redmine::MenuManager.map :admin_menu do |menu|
207 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
208 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
208 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
209 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
209 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
210 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
210 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
211 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
211 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
212 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
212 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
213 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
213 :html => {:class => 'issue_statuses'}
214 :html => {:class => 'issue_statuses'}
214 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
215 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
215 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
216 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
216 :html => {:class => 'custom_fields'}
217 :html => {:class => 'custom_fields'}
217 menu.push :enumerations, {:controller => 'enumerations'}
218 menu.push :enumerations, {:controller => 'enumerations'}
218 menu.push :settings, {:controller => 'settings'}
219 menu.push :settings, {:controller => 'settings'}
219 menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
220 menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
220 :html => {:class => 'server_authentication'}
221 :html => {:class => 'server_authentication'}
221 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
222 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
222 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
223 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
223 end
224 end
224
225
225 Redmine::MenuManager.map :project_menu do |menu|
226 Redmine::MenuManager.map :project_menu do |menu|
226 menu.push :overview, { :controller => 'projects', :action => 'show' }
227 menu.push :overview, { :controller => 'projects', :action => 'show' }
227 menu.push :activity, { :controller => 'activities', :action => 'index' }
228 menu.push :activity, { :controller => 'activities', :action => 'index' }
228 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
229 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
229 :if => Proc.new { |p| p.shared_versions.any? }
230 :if => Proc.new { |p| p.shared_versions.any? }
230 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
231 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
231 menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new,
232 menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new,
232 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) },
233 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) },
233 :if => Proc.new { |p| p.trackers.any? }
234 :if => Proc.new { |p| p.trackers.any? }
234 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
235 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
235 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
236 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
236 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
237 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
237 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
238 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
238 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
239 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
239 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
240 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
240 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
241 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
241 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
242 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
242 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
243 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
243 menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
244 menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
244 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
245 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
245 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
246 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
246 end
247 end
247
248
248 Redmine::Activity.map do |activity|
249 Redmine::Activity.map do |activity|
249 activity.register :issues, :class_name => %w(Issue Journal)
250 activity.register :issues, :class_name => %w(Issue Journal)
250 activity.register :changesets
251 activity.register :changesets
251 activity.register :news
252 activity.register :news
252 activity.register :documents, :class_name => %w(Document Attachment)
253 activity.register :documents, :class_name => %w(Document Attachment)
253 activity.register :files, :class_name => 'Attachment'
254 activity.register :files, :class_name => 'Attachment'
254 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
255 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
255 activity.register :messages, :default => false
256 activity.register :messages, :default => false
256 activity.register :time_entries, :default => false
257 activity.register :time_entries, :default => false
257 end
258 end
258
259
259 Redmine::Search.map do |search|
260 Redmine::Search.map do |search|
260 search.register :issues
261 search.register :issues
261 search.register :news
262 search.register :news
262 search.register :documents
263 search.register :documents
263 search.register :changesets
264 search.register :changesets
264 search.register :wiki_pages
265 search.register :wiki_pages
265 search.register :messages
266 search.register :messages
266 search.register :projects
267 search.register :projects
267 end
268 end
268
269
269 Redmine::WikiFormatting.map do |format|
270 Redmine::WikiFormatting.map do |format|
270 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
271 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
271 if Object.const_defined?(:Redcarpet)
272 if Object.const_defined?(:Redcarpet)
272 format.register :markdown, Redmine::WikiFormatting::Markdown::Formatter, Redmine::WikiFormatting::Markdown::Helper,
273 format.register :markdown, Redmine::WikiFormatting::Markdown::Formatter, Redmine::WikiFormatting::Markdown::Helper,
273 :label => 'Markdown (experimental)'
274 :label => 'Markdown (experimental)'
274 end
275 end
275 end
276 end
276
277
277 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
278 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
@@ -1,204 +1,206
1 ---
1 ---
2 roles_001:
2 roles_001:
3 name: Manager
3 name: Manager
4 id: 1
4 id: 1
5 builtin: 0
5 builtin: 0
6 issues_visibility: all
6 issues_visibility: all
7 users_visibility: all
7 users_visibility: all
8 permissions: |
8 permissions: |
9 ---
9 ---
10 - :add_project
10 - :add_project
11 - :edit_project
11 - :edit_project
12 - :close_project
12 - :close_project
13 - :select_project_modules
13 - :select_project_modules
14 - :manage_members
14 - :manage_members
15 - :manage_versions
15 - :manage_versions
16 - :manage_categories
16 - :manage_categories
17 - :view_issues
17 - :view_issues
18 - :add_issues
18 - :add_issues
19 - :edit_issues
19 - :edit_issues
20 - :copy_issues
20 - :manage_issue_relations
21 - :manage_issue_relations
21 - :manage_subtasks
22 - :manage_subtasks
22 - :add_issue_notes
23 - :add_issue_notes
23 - :delete_issues
24 - :delete_issues
24 - :view_issue_watchers
25 - :view_issue_watchers
25 - :add_issue_watchers
26 - :add_issue_watchers
26 - :set_issues_private
27 - :set_issues_private
27 - :set_notes_private
28 - :set_notes_private
28 - :view_private_notes
29 - :view_private_notes
29 - :delete_issue_watchers
30 - :delete_issue_watchers
30 - :manage_public_queries
31 - :manage_public_queries
31 - :save_queries
32 - :save_queries
32 - :view_gantt
33 - :view_gantt
33 - :view_calendar
34 - :view_calendar
34 - :log_time
35 - :log_time
35 - :view_time_entries
36 - :view_time_entries
36 - :edit_time_entries
37 - :edit_time_entries
37 - :delete_time_entries
38 - :delete_time_entries
38 - :manage_news
39 - :manage_news
39 - :comment_news
40 - :comment_news
40 - :view_documents
41 - :view_documents
41 - :add_documents
42 - :add_documents
42 - :edit_documents
43 - :edit_documents
43 - :delete_documents
44 - :delete_documents
44 - :view_wiki_pages
45 - :view_wiki_pages
45 - :export_wiki_pages
46 - :export_wiki_pages
46 - :view_wiki_edits
47 - :view_wiki_edits
47 - :edit_wiki_pages
48 - :edit_wiki_pages
48 - :delete_wiki_pages_attachments
49 - :delete_wiki_pages_attachments
49 - :protect_wiki_pages
50 - :protect_wiki_pages
50 - :delete_wiki_pages
51 - :delete_wiki_pages
51 - :rename_wiki_pages
52 - :rename_wiki_pages
52 - :add_messages
53 - :add_messages
53 - :edit_messages
54 - :edit_messages
54 - :delete_messages
55 - :delete_messages
55 - :manage_boards
56 - :manage_boards
56 - :view_files
57 - :view_files
57 - :manage_files
58 - :manage_files
58 - :browse_repository
59 - :browse_repository
59 - :manage_repository
60 - :manage_repository
60 - :view_changesets
61 - :view_changesets
61 - :manage_related_issues
62 - :manage_related_issues
62 - :manage_project_activities
63 - :manage_project_activities
63
64
64 position: 1
65 position: 1
65 roles_002:
66 roles_002:
66 name: Developer
67 name: Developer
67 id: 2
68 id: 2
68 builtin: 0
69 builtin: 0
69 issues_visibility: default
70 issues_visibility: default
70 users_visibility: all
71 users_visibility: all
71 permissions: |
72 permissions: |
72 ---
73 ---
73 - :edit_project
74 - :edit_project
74 - :manage_members
75 - :manage_members
75 - :manage_versions
76 - :manage_versions
76 - :manage_categories
77 - :manage_categories
77 - :view_issues
78 - :view_issues
78 - :add_issues
79 - :add_issues
79 - :edit_issues
80 - :edit_issues
81 - :copy_issues
80 - :manage_issue_relations
82 - :manage_issue_relations
81 - :manage_subtasks
83 - :manage_subtasks
82 - :add_issue_notes
84 - :add_issue_notes
83 - :delete_issues
85 - :delete_issues
84 - :view_issue_watchers
86 - :view_issue_watchers
85 - :save_queries
87 - :save_queries
86 - :view_gantt
88 - :view_gantt
87 - :view_calendar
89 - :view_calendar
88 - :log_time
90 - :log_time
89 - :view_time_entries
91 - :view_time_entries
90 - :edit_own_time_entries
92 - :edit_own_time_entries
91 - :manage_news
93 - :manage_news
92 - :comment_news
94 - :comment_news
93 - :view_documents
95 - :view_documents
94 - :add_documents
96 - :add_documents
95 - :edit_documents
97 - :edit_documents
96 - :delete_documents
98 - :delete_documents
97 - :view_wiki_pages
99 - :view_wiki_pages
98 - :view_wiki_edits
100 - :view_wiki_edits
99 - :edit_wiki_pages
101 - :edit_wiki_pages
100 - :protect_wiki_pages
102 - :protect_wiki_pages
101 - :delete_wiki_pages
103 - :delete_wiki_pages
102 - :add_messages
104 - :add_messages
103 - :edit_own_messages
105 - :edit_own_messages
104 - :delete_own_messages
106 - :delete_own_messages
105 - :manage_boards
107 - :manage_boards
106 - :view_files
108 - :view_files
107 - :manage_files
109 - :manage_files
108 - :browse_repository
110 - :browse_repository
109 - :view_changesets
111 - :view_changesets
110
112
111 position: 2
113 position: 2
112 roles_003:
114 roles_003:
113 name: Reporter
115 name: Reporter
114 id: 3
116 id: 3
115 builtin: 0
117 builtin: 0
116 issues_visibility: default
118 issues_visibility: default
117 users_visibility: all
119 users_visibility: all
118 permissions: |
120 permissions: |
119 ---
121 ---
120 - :edit_project
122 - :edit_project
121 - :manage_members
123 - :manage_members
122 - :manage_versions
124 - :manage_versions
123 - :manage_categories
125 - :manage_categories
124 - :view_issues
126 - :view_issues
125 - :add_issues
127 - :add_issues
126 - :edit_issues
128 - :edit_issues
127 - :manage_issue_relations
129 - :manage_issue_relations
128 - :add_issue_notes
130 - :add_issue_notes
129 - :view_issue_watchers
131 - :view_issue_watchers
130 - :save_queries
132 - :save_queries
131 - :view_gantt
133 - :view_gantt
132 - :view_calendar
134 - :view_calendar
133 - :log_time
135 - :log_time
134 - :view_time_entries
136 - :view_time_entries
135 - :manage_news
137 - :manage_news
136 - :comment_news
138 - :comment_news
137 - :view_documents
139 - :view_documents
138 - :add_documents
140 - :add_documents
139 - :edit_documents
141 - :edit_documents
140 - :delete_documents
142 - :delete_documents
141 - :view_wiki_pages
143 - :view_wiki_pages
142 - :view_wiki_edits
144 - :view_wiki_edits
143 - :edit_wiki_pages
145 - :edit_wiki_pages
144 - :delete_wiki_pages
146 - :delete_wiki_pages
145 - :add_messages
147 - :add_messages
146 - :manage_boards
148 - :manage_boards
147 - :view_files
149 - :view_files
148 - :manage_files
150 - :manage_files
149 - :browse_repository
151 - :browse_repository
150 - :view_changesets
152 - :view_changesets
151
153
152 position: 3
154 position: 3
153 roles_004:
155 roles_004:
154 name: Non member
156 name: Non member
155 id: 4
157 id: 4
156 builtin: 1
158 builtin: 1
157 issues_visibility: default
159 issues_visibility: default
158 users_visibility: all
160 users_visibility: all
159 permissions: |
161 permissions: |
160 ---
162 ---
161 - :view_issues
163 - :view_issues
162 - :add_issues
164 - :add_issues
163 - :edit_issues
165 - :edit_issues
164 - :manage_issue_relations
166 - :manage_issue_relations
165 - :add_issue_notes
167 - :add_issue_notes
166 - :save_queries
168 - :save_queries
167 - :view_gantt
169 - :view_gantt
168 - :view_calendar
170 - :view_calendar
169 - :log_time
171 - :log_time
170 - :view_time_entries
172 - :view_time_entries
171 - :comment_news
173 - :comment_news
172 - :view_documents
174 - :view_documents
173 - :view_wiki_pages
175 - :view_wiki_pages
174 - :view_wiki_edits
176 - :view_wiki_edits
175 - :edit_wiki_pages
177 - :edit_wiki_pages
176 - :add_messages
178 - :add_messages
177 - :view_files
179 - :view_files
178 - :manage_files
180 - :manage_files
179 - :browse_repository
181 - :browse_repository
180 - :view_changesets
182 - :view_changesets
181
183
182 position: 4
184 position: 4
183 roles_005:
185 roles_005:
184 name: Anonymous
186 name: Anonymous
185 id: 5
187 id: 5
186 builtin: 2
188 builtin: 2
187 issues_visibility: default
189 issues_visibility: default
188 users_visibility: all
190 users_visibility: all
189 permissions: |
191 permissions: |
190 ---
192 ---
191 - :view_issues
193 - :view_issues
192 - :add_issue_notes
194 - :add_issue_notes
193 - :view_gantt
195 - :view_gantt
194 - :view_calendar
196 - :view_calendar
195 - :view_time_entries
197 - :view_time_entries
196 - :view_documents
198 - :view_documents
197 - :view_wiki_pages
199 - :view_wiki_pages
198 - :view_wiki_edits
200 - :view_wiki_edits
199 - :view_files
201 - :view_files
200 - :browse_repository
202 - :browse_repository
201 - :view_changesets
203 - :view_changesets
202
204
203 position: 5
205 position: 5
204
206
1 NO CONTENT: modified file
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