##// END OF EJS Templates
Adds support for multiple repositories per project (#779)....
Jean-Philippe Lang -
r8530:1bd5e58c8478
parent child
Show More
@@ -0,0 +1,9
1 class AddRepositoriesIdentifier < ActiveRecord::Migration
2 def self.up
3 add_column :repositories, :identifier, :string
4 end
5
6 def self.down
7 remove_column :repositories, :identifier
8 end
9 end
@@ -0,0 +1,9
1 class AddRepositoriesIsDefault < ActiveRecord::Migration
2 def self.up
3 add_column :repositories, :is_default, :boolean, :default => false
4 end
5
6 def self.down
7 remove_column :repositories, :is_default
8 end
9 end
@@ -0,0 +1,14
1 class SetDefaultRepositories < ActiveRecord::Migration
2 def self.up
3 Repository.update_all(["is_default = ?", false])
4 # Sets the last repository as default in case multiple repositories exist for the same project
5 Repository.connection.select_values("SELECT r.id FROM #{Repository.table_name} r" +
6 " WHERE r.id = (SELECT max(r1.id) FROM #{Repository.table_name} r1 WHERE r1.project_id = r.project_id)").each do |i|
7 Repository.update_all(["is_default = ?", true], ["id = ?", i])
8 end
9 end
10
11 def self.down
12 Repository.update_all(["is_default = ?", false])
13 end
14 end
@@ -1,380 +1,378
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 'SVG/Graph/Bar'
18 require 'SVG/Graph/Bar'
19 require 'SVG/Graph/BarHorizontal'
19 require 'SVG/Graph/BarHorizontal'
20 require 'digest/sha1'
20 require 'digest/sha1'
21
21
22 class ChangesetNotFound < Exception; end
22 class ChangesetNotFound < Exception; end
23 class InvalidRevisionParam < Exception; end
23 class InvalidRevisionParam < Exception; end
24
24
25 class RepositoriesController < ApplicationController
25 class RepositoriesController < ApplicationController
26 menu_item :repository
26 menu_item :repository
27 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
27 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
28 default_search_scope :changesets
28 default_search_scope :changesets
29
29
30 before_filter :find_project_by_project_id, :only => [:new, :create]
30 before_filter :find_project_by_project_id, :only => [:new, :create]
31 before_filter :check_repository_uniqueness, :only => [:new, :create]
32 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
31 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
33 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
32 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 before_filter :authorize
33 before_filter :authorize
35 accept_rss_auth :revisions
34 accept_rss_auth :revisions
36
35
37 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
36 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
38
37
39 def new
38 def new
40 scm = params[:repository_scm] || Redmine::Scm::Base.all.first
39 scm = params[:repository_scm] || Redmine::Scm::Base.all.first
41 @repository = Repository.factory(scm)
40 @repository = Repository.factory(scm)
41 @repository.is_default = @project.repository.nil?
42 @repository.project = @project
42 @repository.project = @project
43 render :layout => !request.xhr?
43 render :layout => !request.xhr?
44 end
44 end
45
45
46 def create
46 def create
47 @repository = Repository.factory(params[:repository_scm], params[:repository])
47 @repository = Repository.factory(params[:repository_scm], params[:repository])
48 @repository.project = @project
48 @repository.project = @project
49 if request.post? && @repository.save
49 if request.post? && @repository.save
50 redirect_to settings_project_path(@project, :tab => 'repositories')
50 redirect_to settings_project_path(@project, :tab => 'repositories')
51 else
51 else
52 render :action => 'new'
52 render :action => 'new'
53 end
53 end
54 end
54 end
55
55
56 def edit
56 def edit
57 end
57 end
58
58
59 def update
59 def update
60 @repository.attributes = params[:repository]
60 @repository.attributes = params[:repository]
61 @repository.project = @project
61 @repository.project = @project
62 if request.put? && @repository.save
62 if request.put? && @repository.save
63 redirect_to settings_project_path(@project, :tab => 'repositories')
63 redirect_to settings_project_path(@project, :tab => 'repositories')
64 else
64 else
65 render :action => 'edit'
65 render :action => 'edit'
66 end
66 end
67 end
67 end
68
68
69 def committers
69 def committers
70 @committers = @repository.committers
70 @committers = @repository.committers
71 @users = @project.users
71 @users = @project.users
72 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
72 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
73 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
73 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
74 @users.compact!
74 @users.compact!
75 @users.sort!
75 @users.sort!
76 if request.post? && params[:committers].is_a?(Hash)
76 if request.post? && params[:committers].is_a?(Hash)
77 # Build a hash with repository usernames as keys and corresponding user ids as values
77 # Build a hash with repository usernames as keys and corresponding user ids as values
78 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
78 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
79 flash[:notice] = l(:notice_successful_update)
79 flash[:notice] = l(:notice_successful_update)
80 redirect_to settings_project_path(@project, :tab => 'repositories')
80 redirect_to settings_project_path(@project, :tab => 'repositories')
81 end
81 end
82 end
82 end
83
83
84 def destroy
84 def destroy
85 @repository.destroy if request.delete?
85 @repository.destroy if request.delete?
86 redirect_to settings_project_path(@project, :tab => 'repositories')
86 redirect_to settings_project_path(@project, :tab => 'repositories')
87 end
87 end
88
88
89 def show
89 def show
90 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
90 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
91
91
92 @entries = @repository.entries(@path, @rev)
92 @entries = @repository.entries(@path, @rev)
93 @changeset = @repository.find_changeset_by_name(@rev)
93 @changeset = @repository.find_changeset_by_name(@rev)
94 if request.xhr?
94 if request.xhr?
95 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
95 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
96 else
96 else
97 (show_error_not_found; return) unless @entries
97 (show_error_not_found; return) unless @entries
98 @changesets = @repository.latest_changesets(@path, @rev)
98 @changesets = @repository.latest_changesets(@path, @rev)
99 @properties = @repository.properties(@path, @rev)
99 @properties = @repository.properties(@path, @rev)
100 @repositories = @project.repositories
100 render :action => 'show'
101 render :action => 'show'
101 end
102 end
102 end
103 end
103
104
104 alias_method :browse, :show
105 alias_method :browse, :show
105
106
106 def changes
107 def changes
107 @entry = @repository.entry(@path, @rev)
108 @entry = @repository.entry(@path, @rev)
108 (show_error_not_found; return) unless @entry
109 (show_error_not_found; return) unless @entry
109 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
110 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
110 @properties = @repository.properties(@path, @rev)
111 @properties = @repository.properties(@path, @rev)
111 @changeset = @repository.find_changeset_by_name(@rev)
112 @changeset = @repository.find_changeset_by_name(@rev)
112 end
113 end
113
114
114 def revisions
115 def revisions
115 @changeset_count = @repository.changesets.count
116 @changeset_count = @repository.changesets.count
116 @changeset_pages = Paginator.new self, @changeset_count,
117 @changeset_pages = Paginator.new self, @changeset_count,
117 per_page_option,
118 per_page_option,
118 params['page']
119 params['page']
119 @changesets = @repository.changesets.find(:all,
120 @changesets = @repository.changesets.find(:all,
120 :limit => @changeset_pages.items_per_page,
121 :limit => @changeset_pages.items_per_page,
121 :offset => @changeset_pages.current.offset,
122 :offset => @changeset_pages.current.offset,
122 :include => [:user, :repository, :parents])
123 :include => [:user, :repository, :parents])
123
124
124 respond_to do |format|
125 respond_to do |format|
125 format.html { render :layout => false if request.xhr? }
126 format.html { render :layout => false if request.xhr? }
126 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
127 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
127 end
128 end
128 end
129 end
129
130
130 def entry
131 def entry
131 @entry = @repository.entry(@path, @rev)
132 @entry = @repository.entry(@path, @rev)
132 (show_error_not_found; return) unless @entry
133 (show_error_not_found; return) unless @entry
133
134
134 # If the entry is a dir, show the browser
135 # If the entry is a dir, show the browser
135 (show; return) if @entry.is_dir?
136 (show; return) if @entry.is_dir?
136
137
137 @content = @repository.cat(@path, @rev)
138 @content = @repository.cat(@path, @rev)
138 (show_error_not_found; return) unless @content
139 (show_error_not_found; return) unless @content
139 if 'raw' == params[:format] ||
140 if 'raw' == params[:format] ||
140 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
141 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
141 ! is_entry_text_data?(@content, @path)
142 ! is_entry_text_data?(@content, @path)
142 # Force the download
143 # Force the download
143 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
144 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
144 send_type = Redmine::MimeType.of(@path)
145 send_type = Redmine::MimeType.of(@path)
145 send_opt[:type] = send_type.to_s if send_type
146 send_opt[:type] = send_type.to_s if send_type
146 send_data @content, send_opt
147 send_data @content, send_opt
147 else
148 else
148 # Prevent empty lines when displaying a file with Windows style eol
149 # Prevent empty lines when displaying a file with Windows style eol
149 # TODO: UTF-16
150 # TODO: UTF-16
150 # Is this needs? AttachmentsController reads file simply.
151 # Is this needs? AttachmentsController reads file simply.
151 @content.gsub!("\r\n", "\n")
152 @content.gsub!("\r\n", "\n")
152 @changeset = @repository.find_changeset_by_name(@rev)
153 @changeset = @repository.find_changeset_by_name(@rev)
153 end
154 end
154 end
155 end
155
156
156 def is_entry_text_data?(ent, path)
157 def is_entry_text_data?(ent, path)
157 # UTF-16 contains "\x00".
158 # UTF-16 contains "\x00".
158 # It is very strict that file contains less than 30% of ascii symbols
159 # It is very strict that file contains less than 30% of ascii symbols
159 # in non Western Europe.
160 # in non Western Europe.
160 return true if Redmine::MimeType.is_type?('text', path)
161 return true if Redmine::MimeType.is_type?('text', path)
161 # Ruby 1.8.6 has a bug of integer divisions.
162 # Ruby 1.8.6 has a bug of integer divisions.
162 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
163 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
163 return false if ent.is_binary_data?
164 return false if ent.is_binary_data?
164 true
165 true
165 end
166 end
166 private :is_entry_text_data?
167 private :is_entry_text_data?
167
168
168 def annotate
169 def annotate
169 @entry = @repository.entry(@path, @rev)
170 @entry = @repository.entry(@path, @rev)
170 (show_error_not_found; return) unless @entry
171 (show_error_not_found; return) unless @entry
171
172
172 @annotate = @repository.scm.annotate(@path, @rev)
173 @annotate = @repository.scm.annotate(@path, @rev)
173 if @annotate.nil? || @annotate.empty?
174 if @annotate.nil? || @annotate.empty?
174 (render_error l(:error_scm_annotate); return)
175 (render_error l(:error_scm_annotate); return)
175 end
176 end
176 ann_buf_size = 0
177 ann_buf_size = 0
177 @annotate.lines.each do |buf|
178 @annotate.lines.each do |buf|
178 ann_buf_size += buf.size
179 ann_buf_size += buf.size
179 end
180 end
180 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
181 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
181 (render_error l(:error_scm_annotate_big_text_file); return)
182 (render_error l(:error_scm_annotate_big_text_file); return)
182 end
183 end
183 @changeset = @repository.find_changeset_by_name(@rev)
184 @changeset = @repository.find_changeset_by_name(@rev)
184 end
185 end
185
186
186 def revision
187 def revision
187 raise ChangesetNotFound if @rev.blank?
188 raise ChangesetNotFound if @rev.blank?
188 @changeset = @repository.find_changeset_by_name(@rev)
189 @changeset = @repository.find_changeset_by_name(@rev)
189 raise ChangesetNotFound unless @changeset
190 raise ChangesetNotFound unless @changeset
190
191
191 respond_to do |format|
192 respond_to do |format|
192 format.html
193 format.html
193 format.js {render :layout => false}
194 format.js {render :layout => false}
194 end
195 end
195 rescue ChangesetNotFound
196 rescue ChangesetNotFound
196 show_error_not_found
197 show_error_not_found
197 end
198 end
198
199
199 def diff
200 def diff
200 if params[:format] == 'diff'
201 if params[:format] == 'diff'
201 @diff = @repository.diff(@path, @rev, @rev_to)
202 @diff = @repository.diff(@path, @rev, @rev_to)
202 (show_error_not_found; return) unless @diff
203 (show_error_not_found; return) unless @diff
203 filename = "changeset_r#{@rev}"
204 filename = "changeset_r#{@rev}"
204 filename << "_r#{@rev_to}" if @rev_to
205 filename << "_r#{@rev_to}" if @rev_to
205 send_data @diff.join, :filename => "#{filename}.diff",
206 send_data @diff.join, :filename => "#{filename}.diff",
206 :type => 'text/x-patch',
207 :type => 'text/x-patch',
207 :disposition => 'attachment'
208 :disposition => 'attachment'
208 else
209 else
209 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
210 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
210 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
211 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
211
212
212 # Save diff type as user preference
213 # Save diff type as user preference
213 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
214 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
214 User.current.pref[:diff_type] = @diff_type
215 User.current.pref[:diff_type] = @diff_type
215 User.current.preference.save
216 User.current.preference.save
216 end
217 end
217 @cache_key = "repositories/diff/#{@repository.id}/" +
218 @cache_key = "repositories/diff/#{@repository.id}/" +
218 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
219 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
219 unless read_fragment(@cache_key)
220 unless read_fragment(@cache_key)
220 @diff = @repository.diff(@path, @rev, @rev_to)
221 @diff = @repository.diff(@path, @rev, @rev_to)
221 show_error_not_found unless @diff
222 show_error_not_found unless @diff
222 end
223 end
223
224
224 @changeset = @repository.find_changeset_by_name(@rev)
225 @changeset = @repository.find_changeset_by_name(@rev)
225 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
226 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
226 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
227 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
227 end
228 end
228 end
229 end
229
230
230 def stats
231 def stats
231 end
232 end
232
233
233 def graph
234 def graph
234 data = nil
235 data = nil
235 case params[:graph]
236 case params[:graph]
236 when "commits_per_month"
237 when "commits_per_month"
237 data = graph_commits_per_month(@repository)
238 data = graph_commits_per_month(@repository)
238 when "commits_per_author"
239 when "commits_per_author"
239 data = graph_commits_per_author(@repository)
240 data = graph_commits_per_author(@repository)
240 end
241 end
241 if data
242 if data
242 headers["Content-Type"] = "image/svg+xml"
243 headers["Content-Type"] = "image/svg+xml"
243 send_data(data, :type => "image/svg+xml", :disposition => "inline")
244 send_data(data, :type => "image/svg+xml", :disposition => "inline")
244 else
245 else
245 render_404
246 render_404
246 end
247 end
247 end
248 end
248
249
249 private
250 private
250
251
251 def find_repository
252 def find_repository
252 @repository = Repository.find(params[:id])
253 @repository = Repository.find(params[:id])
253 @project = @repository.project
254 @project = @repository.project
254 rescue ActiveRecord::RecordNotFound
255 rescue ActiveRecord::RecordNotFound
255 render_404
256 render_404
256 end
257 end
257
258
258 # TODO: remove it when multiple SCM support is added
259 def check_repository_uniqueness
260 if @project.repository
261 redirect_to settings_project_path(@project, :tab => 'repositories')
262 end
263 end
264
265 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
259 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
266
260
267 def find_project_repository
261 def find_project_repository
268 @project = Project.find(params[:id])
262 @project = Project.find(params[:id])
263 if params[:repository_id].present?
264 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
265 else
269 @repository = @project.repository
266 @repository = @project.repository
267 end
270 (render_404; return false) unless @repository
268 (render_404; return false) unless @repository
271 @path = params[:path].join('/') unless params[:path].nil?
269 @path = params[:path].join('/') unless params[:path].nil?
272 @path ||= ''
270 @path ||= ''
273 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
271 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
274 @rev_to = params[:rev_to]
272 @rev_to = params[:rev_to]
275
273
276 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
274 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
277 if @repository.branches.blank?
275 if @repository.branches.blank?
278 raise InvalidRevisionParam
276 raise InvalidRevisionParam
279 end
277 end
280 end
278 end
281 rescue ActiveRecord::RecordNotFound
279 rescue ActiveRecord::RecordNotFound
282 render_404
280 render_404
283 rescue InvalidRevisionParam
281 rescue InvalidRevisionParam
284 show_error_not_found
282 show_error_not_found
285 end
283 end
286
284
287 def show_error_not_found
285 def show_error_not_found
288 render_error :message => l(:error_scm_not_found), :status => 404
286 render_error :message => l(:error_scm_not_found), :status => 404
289 end
287 end
290
288
291 # Handler for Redmine::Scm::Adapters::CommandFailed exception
289 # Handler for Redmine::Scm::Adapters::CommandFailed exception
292 def show_error_command_failed(exception)
290 def show_error_command_failed(exception)
293 render_error l(:error_scm_command_failed, exception.message)
291 render_error l(:error_scm_command_failed, exception.message)
294 end
292 end
295
293
296 def graph_commits_per_month(repository)
294 def graph_commits_per_month(repository)
297 @date_to = Date.today
295 @date_to = Date.today
298 @date_from = @date_to << 11
296 @date_from = @date_to << 11
299 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
297 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
300 commits_by_day = repository.changesets.count(
298 commits_by_day = repository.changesets.count(
301 :all, :group => :commit_date,
299 :all, :group => :commit_date,
302 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
300 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
303 commits_by_month = [0] * 12
301 commits_by_month = [0] * 12
304 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
302 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
305
303
306 changes_by_day = repository.changes.count(
304 changes_by_day = repository.changes.count(
307 :all, :group => :commit_date,
305 :all, :group => :commit_date,
308 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
306 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
309 changes_by_month = [0] * 12
307 changes_by_month = [0] * 12
310 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
308 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
311
309
312 fields = []
310 fields = []
313 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
311 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
314
312
315 graph = SVG::Graph::Bar.new(
313 graph = SVG::Graph::Bar.new(
316 :height => 300,
314 :height => 300,
317 :width => 800,
315 :width => 800,
318 :fields => fields.reverse,
316 :fields => fields.reverse,
319 :stack => :side,
317 :stack => :side,
320 :scale_integers => true,
318 :scale_integers => true,
321 :step_x_labels => 2,
319 :step_x_labels => 2,
322 :show_data_values => false,
320 :show_data_values => false,
323 :graph_title => l(:label_commits_per_month),
321 :graph_title => l(:label_commits_per_month),
324 :show_graph_title => true
322 :show_graph_title => true
325 )
323 )
326
324
327 graph.add_data(
325 graph.add_data(
328 :data => commits_by_month[0..11].reverse,
326 :data => commits_by_month[0..11].reverse,
329 :title => l(:label_revision_plural)
327 :title => l(:label_revision_plural)
330 )
328 )
331
329
332 graph.add_data(
330 graph.add_data(
333 :data => changes_by_month[0..11].reverse,
331 :data => changes_by_month[0..11].reverse,
334 :title => l(:label_change_plural)
332 :title => l(:label_change_plural)
335 )
333 )
336
334
337 graph.burn
335 graph.burn
338 end
336 end
339
337
340 def graph_commits_per_author(repository)
338 def graph_commits_per_author(repository)
341 commits_by_author = repository.changesets.count(:all, :group => :committer)
339 commits_by_author = repository.changesets.count(:all, :group => :committer)
342 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
340 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
343
341
344 changes_by_author = repository.changes.count(:all, :group => :committer)
342 changes_by_author = repository.changes.count(:all, :group => :committer)
345 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
343 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
346
344
347 fields = commits_by_author.collect {|r| r.first}
345 fields = commits_by_author.collect {|r| r.first}
348 commits_data = commits_by_author.collect {|r| r.last}
346 commits_data = commits_by_author.collect {|r| r.last}
349 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
347 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
350
348
351 fields = fields + [""]*(10 - fields.length) if fields.length<10
349 fields = fields + [""]*(10 - fields.length) if fields.length<10
352 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
350 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
353 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
351 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
354
352
355 # Remove email adress in usernames
353 # Remove email adress in usernames
356 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
354 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
357
355
358 graph = SVG::Graph::BarHorizontal.new(
356 graph = SVG::Graph::BarHorizontal.new(
359 :height => 400,
357 :height => 400,
360 :width => 800,
358 :width => 800,
361 :fields => fields,
359 :fields => fields,
362 :stack => :side,
360 :stack => :side,
363 :scale_integers => true,
361 :scale_integers => true,
364 :show_data_values => false,
362 :show_data_values => false,
365 :rotate_y_labels => false,
363 :rotate_y_labels => false,
366 :graph_title => l(:label_commits_per_author),
364 :graph_title => l(:label_commits_per_author),
367 :show_graph_title => true
365 :show_graph_title => true
368 )
366 )
369 graph.add_data(
367 graph.add_data(
370 :data => commits_data,
368 :data => commits_data,
371 :title => l(:label_revision_plural)
369 :title => l(:label_revision_plural)
372 )
370 )
373 graph.add_data(
371 graph.add_data(
374 :data => changes_data,
372 :data => changes_data,
375 :title => l(:label_change_plural)
373 :title => l(:label_change_plural)
376 )
374 )
377 graph.burn
375 graph.burn
378 end
376 end
379 end
377 end
380
378
@@ -1,68 +1,68
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 SysController < ActionController::Base
18 class SysController < ActionController::Base
19 before_filter :check_enabled
19 before_filter :check_enabled
20
20
21 def projects
21 def projects
22 p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
22 p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => "#{Project.table_name}.identifier")
23 # extra_info attribute from repository breaks activeresource client
23 # extra_info attribute from repository breaks activeresource client
24 render :xml => p.to_xml(:only => [:id, :identifier, :name, :is_public, :status], :include => {:repository => {:only => [:id, :url]}})
24 render :xml => p.to_xml(:only => [:id, :identifier, :name, :is_public, :status], :include => {:repository => {:only => [:id, :url]}})
25 end
25 end
26
26
27 def create_project_repository
27 def create_project_repository
28 project = Project.find(params[:id])
28 project = Project.find(params[:id])
29 if project.repository
29 if project.repository
30 render :nothing => true, :status => 409
30 render :nothing => true, :status => 409
31 else
31 else
32 logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
32 logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
33 project.repository = Repository.factory(params[:vendor], params[:repository])
33 project.repository = Repository.factory(params[:vendor], params[:repository])
34 if project.repository && project.repository.save
34 if project.repository && project.repository.save
35 render :xml => project.repository.to_xml(:only => [:id, :url]), :status => 201
35 render :xml => project.repository.to_xml(:only => [:id, :url]), :status => 201
36 else
36 else
37 render :nothing => true, :status => 422
37 render :nothing => true, :status => 422
38 end
38 end
39 end
39 end
40 end
40 end
41
41
42 def fetch_changesets
42 def fetch_changesets
43 projects = []
43 projects = []
44 if params[:id]
44 if params[:id]
45 projects << Project.active.has_module(:repository).find(params[:id])
45 projects << Project.active.has_module(:repository).find(params[:id])
46 else
46 else
47 projects = Project.active.has_module(:repository).find(:all, :include => :repository)
47 projects = Project.active.has_module(:repository).all
48 end
48 end
49 projects.each do |project|
49 projects.each do |project|
50 if project.repository
50 project.repositories.each do |repository|
51 project.repository.fetch_changesets
51 repository.fetch_changesets
52 end
52 end
53 end
53 end
54 render :nothing => true, :status => 200
54 render :nothing => true, :status => 200
55 rescue ActiveRecord::RecordNotFound
55 rescue ActiveRecord::RecordNotFound
56 render :nothing => true, :status => 404
56 render :nothing => true, :status => 404
57 end
57 end
58
58
59 protected
59 protected
60
60
61 def check_enabled
61 def check_enabled
62 User.current = nil
62 User.current = nil
63 unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
63 unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
64 render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
64 render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
65 return false
65 return false
66 end
66 end
67 end
67 end
68 end
68 end
@@ -1,1086 +1,1089
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 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
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Display a link to remote if user is authorized
46 # Display a link to remote if user is authorized
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 url = options[:url] || {}
48 url = options[:url] || {}
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 end
50 end
51
51
52 # Displays a link to user's account page if active
52 # Displays a link to user's account page if active
53 def link_to_user(user, options={})
53 def link_to_user(user, options={})
54 if user.is_a?(User)
54 if user.is_a?(User)
55 name = h(user.name(options[:format]))
55 name = h(user.name(options[:format]))
56 if user.active?
56 if user.active?
57 link_to name, :controller => 'users', :action => 'show', :id => user
57 link_to name, :controller => 'users', :action => 'show', :id => user
58 else
58 else
59 name
59 name
60 end
60 end
61 else
61 else
62 h(user.to_s)
62 h(user.to_s)
63 end
63 end
64 end
64 end
65
65
66 # Displays a link to +issue+ with its subject.
66 # Displays a link to +issue+ with its subject.
67 # Examples:
67 # Examples:
68 #
68 #
69 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue) # => Defect #6: This is the subject
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :subject => false) # => Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 if options[:subject] == false
77 if options[:subject] == false
78 title = truncate(issue.subject, :length => 60)
78 title = truncate(issue.subject, :length => 60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if options[:truncate]
81 if options[:truncate]
82 subject = truncate(subject, :length => options[:truncate])
82 subject = truncate(subject, :length => options[:truncate])
83 end
83 end
84 end
84 end
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 :class => issue.css_classes,
86 :class => issue.css_classes,
87 :title => title
87 :title => title
88 s << h(": #{subject}") if subject
88 s << h(": #{subject}") if subject
89 s = h("#{issue.project} - ") + s if options[:project]
89 s = h("#{issue.project} - ") + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 action = options.delete(:download) ? 'download' : 'show'
99 action = options.delete(:download) ? 'download' : 'show'
100 link_to(h(text),
100 link_to(h(text),
101 {:controller => 'attachments', :action => action,
101 {:controller => 'attachments', :action => action,
102 :id => attachment, :filename => attachment.filename },
102 :id => attachment, :filename => attachment.filename },
103 options)
103 options)
104 end
104 end
105
105
106 # Generates a link to a SCM revision
106 # Generates a link to a SCM revision
107 # Options:
107 # Options:
108 # * :text - Link text (default to the formatted revision)
108 # * :text - Link text (default to the formatted revision)
109 def link_to_revision(revision, project, options={})
109 def link_to_revision(revision, repository, options={})
110 if repository.is_a?(Project)
111 repository = repository.repository
112 end
110 text = options.delete(:text) || format_revision(revision)
113 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 link_to(
115 link_to(
113 h(text),
116 h(text),
114 {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 :title => l(:label_revision_id, format_revision(revision))
118 :title => l(:label_revision_id, format_revision(revision))
116 )
119 )
117 end
120 end
118
121
119 # Generates a link to a message
122 # Generates a link to a message
120 def link_to_message(message, options={}, html_options = nil)
123 def link_to_message(message, options={}, html_options = nil)
121 link_to(
124 link_to(
122 h(truncate(message.subject, :length => 60)),
125 h(truncate(message.subject, :length => 60)),
123 { :controller => 'messages', :action => 'show',
126 { :controller => 'messages', :action => 'show',
124 :board_id => message.board_id,
127 :board_id => message.board_id,
125 :id => message.root,
128 :id => message.root,
126 :r => (message.parent_id && message.id),
129 :r => (message.parent_id && message.id),
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
128 }.merge(options),
131 }.merge(options),
129 html_options
132 html_options
130 )
133 )
131 end
134 end
132
135
133 # Generates a link to a project if active
136 # Generates a link to a project if active
134 # Examples:
137 # Examples:
135 #
138 #
136 # link_to_project(project) # => link to the specified project overview
139 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project, :action=>'settings') # => link to project settings
140 # link_to_project(project, :action=>'settings') # => link to project settings
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 #
143 #
141 def link_to_project(project, options={}, html_options = nil)
144 def link_to_project(project, options={}, html_options = nil)
142 if project.active?
145 if project.active?
143 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
144 link_to(h(project), url, html_options)
147 link_to(h(project), url, html_options)
145 else
148 else
146 h(project)
149 h(project)
147 end
150 end
148 end
151 end
149
152
150 def toggle_link(name, id, options={})
153 def toggle_link(name, id, options={})
151 onclick = "Element.toggle('#{id}'); "
154 onclick = "Element.toggle('#{id}'); "
152 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
155 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
153 onclick << "return false;"
156 onclick << "return false;"
154 link_to(name, "#", :onclick => onclick)
157 link_to(name, "#", :onclick => onclick)
155 end
158 end
156
159
157 def image_to_function(name, function, html_options = {})
160 def image_to_function(name, function, html_options = {})
158 html_options.symbolize_keys!
161 html_options.symbolize_keys!
159 tag(:input, html_options.merge({
162 tag(:input, html_options.merge({
160 :type => "image", :src => image_path(name),
163 :type => "image", :src => image_path(name),
161 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
164 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
162 }))
165 }))
163 end
166 end
164
167
165 def prompt_to_remote(name, text, param, url, html_options = {})
168 def prompt_to_remote(name, text, param, url, html_options = {})
166 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
169 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
167 link_to name, {}, html_options
170 link_to name, {}, html_options
168 end
171 end
169
172
170 def format_activity_title(text)
173 def format_activity_title(text)
171 h(truncate_single_line(text, :length => 100))
174 h(truncate_single_line(text, :length => 100))
172 end
175 end
173
176
174 def format_activity_day(date)
177 def format_activity_day(date)
175 date == Date.today ? l(:label_today).titleize : format_date(date)
178 date == Date.today ? l(:label_today).titleize : format_date(date)
176 end
179 end
177
180
178 def format_activity_description(text)
181 def format_activity_description(text)
179 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
180 ).gsub(/[\r\n]+/, "<br />").html_safe
183 ).gsub(/[\r\n]+/, "<br />").html_safe
181 end
184 end
182
185
183 def format_version_name(version)
186 def format_version_name(version)
184 if version.project == @project
187 if version.project == @project
185 h(version)
188 h(version)
186 else
189 else
187 h("#{version.project} - #{version}")
190 h("#{version.project} - #{version}")
188 end
191 end
189 end
192 end
190
193
191 def due_date_distance_in_words(date)
194 def due_date_distance_in_words(date)
192 if date
195 if date
193 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
194 end
197 end
195 end
198 end
196
199
197 def render_page_hierarchy(pages, node=nil, options={})
200 def render_page_hierarchy(pages, node=nil, options={})
198 content = ''
201 content = ''
199 if pages[node]
202 if pages[node]
200 content << "<ul class=\"pages-hierarchy\">\n"
203 content << "<ul class=\"pages-hierarchy\">\n"
201 pages[node].each do |page|
204 pages[node].each do |page|
202 content << "<li>"
205 content << "<li>"
203 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
206 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
204 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
207 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
205 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
208 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
206 content << "</li>\n"
209 content << "</li>\n"
207 end
210 end
208 content << "</ul>\n"
211 content << "</ul>\n"
209 end
212 end
210 content.html_safe
213 content.html_safe
211 end
214 end
212
215
213 # Renders flash messages
216 # Renders flash messages
214 def render_flash_messages
217 def render_flash_messages
215 s = ''
218 s = ''
216 flash.each do |k,v|
219 flash.each do |k,v|
217 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
220 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
218 end
221 end
219 s.html_safe
222 s.html_safe
220 end
223 end
221
224
222 # Renders tabs and their content
225 # Renders tabs and their content
223 def render_tabs(tabs)
226 def render_tabs(tabs)
224 if tabs.any?
227 if tabs.any?
225 render :partial => 'common/tabs', :locals => {:tabs => tabs}
228 render :partial => 'common/tabs', :locals => {:tabs => tabs}
226 else
229 else
227 content_tag 'p', l(:label_no_data), :class => "nodata"
230 content_tag 'p', l(:label_no_data), :class => "nodata"
228 end
231 end
229 end
232 end
230
233
231 # Renders the project quick-jump box
234 # Renders the project quick-jump box
232 def render_project_jump_box
235 def render_project_jump_box
233 return unless User.current.logged?
236 return unless User.current.logged?
234 projects = User.current.memberships.collect(&:project).compact.uniq
237 projects = User.current.memberships.collect(&:project).compact.uniq
235 if projects.any?
238 if projects.any?
236 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
239 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
237 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
240 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
238 '<option value="" disabled="disabled">---</option>'
241 '<option value="" disabled="disabled">---</option>'
239 s << project_tree_options_for_select(projects, :selected => @project) do |p|
242 s << project_tree_options_for_select(projects, :selected => @project) do |p|
240 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
243 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
241 end
244 end
242 s << '</select>'
245 s << '</select>'
243 s.html_safe
246 s.html_safe
244 end
247 end
245 end
248 end
246
249
247 def project_tree_options_for_select(projects, options = {})
250 def project_tree_options_for_select(projects, options = {})
248 s = ''
251 s = ''
249 project_tree(projects) do |project, level|
252 project_tree(projects) do |project, level|
250 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
253 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
251 tag_options = {:value => project.id}
254 tag_options = {:value => project.id}
252 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
255 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
253 tag_options[:selected] = 'selected'
256 tag_options[:selected] = 'selected'
254 else
257 else
255 tag_options[:selected] = nil
258 tag_options[:selected] = nil
256 end
259 end
257 tag_options.merge!(yield(project)) if block_given?
260 tag_options.merge!(yield(project)) if block_given?
258 s << content_tag('option', name_prefix + h(project), tag_options)
261 s << content_tag('option', name_prefix + h(project), tag_options)
259 end
262 end
260 s.html_safe
263 s.html_safe
261 end
264 end
262
265
263 # Yields the given block for each project with its level in the tree
266 # Yields the given block for each project with its level in the tree
264 #
267 #
265 # Wrapper for Project#project_tree
268 # Wrapper for Project#project_tree
266 def project_tree(projects, &block)
269 def project_tree(projects, &block)
267 Project.project_tree(projects, &block)
270 Project.project_tree(projects, &block)
268 end
271 end
269
272
270 def project_nested_ul(projects, &block)
273 def project_nested_ul(projects, &block)
271 s = ''
274 s = ''
272 if projects.any?
275 if projects.any?
273 ancestors = []
276 ancestors = []
274 projects.sort_by(&:lft).each do |project|
277 projects.sort_by(&:lft).each do |project|
275 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
278 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
276 s << "<ul>\n"
279 s << "<ul>\n"
277 else
280 else
278 ancestors.pop
281 ancestors.pop
279 s << "</li>"
282 s << "</li>"
280 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
283 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
281 ancestors.pop
284 ancestors.pop
282 s << "</ul></li>\n"
285 s << "</ul></li>\n"
283 end
286 end
284 end
287 end
285 s << "<li>"
288 s << "<li>"
286 s << yield(project).to_s
289 s << yield(project).to_s
287 ancestors << project
290 ancestors << project
288 end
291 end
289 s << ("</li></ul>\n" * ancestors.size)
292 s << ("</li></ul>\n" * ancestors.size)
290 end
293 end
291 s.html_safe
294 s.html_safe
292 end
295 end
293
296
294 def principals_check_box_tags(name, principals)
297 def principals_check_box_tags(name, principals)
295 s = ''
298 s = ''
296 principals.sort.each do |principal|
299 principals.sort.each do |principal|
297 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
300 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
298 end
301 end
299 s.html_safe
302 s.html_safe
300 end
303 end
301
304
302 # Returns a string for users/groups option tags
305 # Returns a string for users/groups option tags
303 def principals_options_for_select(collection, selected=nil)
306 def principals_options_for_select(collection, selected=nil)
304 s = ''
307 s = ''
305 groups = ''
308 groups = ''
306 collection.sort.each do |element|
309 collection.sort.each do |element|
307 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
310 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
308 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
311 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
309 end
312 end
310 unless groups.empty?
313 unless groups.empty?
311 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
314 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
312 end
315 end
313 s
316 s
314 end
317 end
315
318
316 # Truncates and returns the string as a single line
319 # Truncates and returns the string as a single line
317 def truncate_single_line(string, *args)
320 def truncate_single_line(string, *args)
318 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
321 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
319 end
322 end
320
323
321 # Truncates at line break after 250 characters or options[:length]
324 # Truncates at line break after 250 characters or options[:length]
322 def truncate_lines(string, options={})
325 def truncate_lines(string, options={})
323 length = options[:length] || 250
326 length = options[:length] || 250
324 if string.to_s =~ /\A(.{#{length}}.*?)$/m
327 if string.to_s =~ /\A(.{#{length}}.*?)$/m
325 "#{$1}..."
328 "#{$1}..."
326 else
329 else
327 string
330 string
328 end
331 end
329 end
332 end
330
333
331 def anchor(text)
334 def anchor(text)
332 text.to_s.gsub(' ', '_')
335 text.to_s.gsub(' ', '_')
333 end
336 end
334
337
335 def html_hours(text)
338 def html_hours(text)
336 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
339 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
337 end
340 end
338
341
339 def authoring(created, author, options={})
342 def authoring(created, author, options={})
340 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
343 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
341 end
344 end
342
345
343 def time_tag(time)
346 def time_tag(time)
344 text = distance_of_time_in_words(Time.now, time)
347 text = distance_of_time_in_words(Time.now, time)
345 if @project
348 if @project
346 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
349 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
347 else
350 else
348 content_tag('acronym', text, :title => format_time(time))
351 content_tag('acronym', text, :title => format_time(time))
349 end
352 end
350 end
353 end
351
354
352 def syntax_highlight(name, content)
355 def syntax_highlight(name, content)
353 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
356 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
354 end
357 end
355
358
356 def to_path_param(path)
359 def to_path_param(path)
357 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
360 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
358 end
361 end
359
362
360 def pagination_links_full(paginator, count=nil, options={})
363 def pagination_links_full(paginator, count=nil, options={})
361 page_param = options.delete(:page_param) || :page
364 page_param = options.delete(:page_param) || :page
362 per_page_links = options.delete(:per_page_links)
365 per_page_links = options.delete(:per_page_links)
363 url_param = params.dup
366 url_param = params.dup
364
367
365 html = ''
368 html = ''
366 if paginator.current.previous
369 if paginator.current.previous
367 # \xc2\xab(utf-8) = &#171;
370 # \xc2\xab(utf-8) = &#171;
368 html << link_to_content_update(
371 html << link_to_content_update(
369 "\xc2\xab " + l(:label_previous),
372 "\xc2\xab " + l(:label_previous),
370 url_param.merge(page_param => paginator.current.previous)) + ' '
373 url_param.merge(page_param => paginator.current.previous)) + ' '
371 end
374 end
372
375
373 html << (pagination_links_each(paginator, options) do |n|
376 html << (pagination_links_each(paginator, options) do |n|
374 link_to_content_update(n.to_s, url_param.merge(page_param => n))
377 link_to_content_update(n.to_s, url_param.merge(page_param => n))
375 end || '')
378 end || '')
376
379
377 if paginator.current.next
380 if paginator.current.next
378 # \xc2\xbb(utf-8) = &#187;
381 # \xc2\xbb(utf-8) = &#187;
379 html << ' ' + link_to_content_update(
382 html << ' ' + link_to_content_update(
380 (l(:label_next) + " \xc2\xbb"),
383 (l(:label_next) + " \xc2\xbb"),
381 url_param.merge(page_param => paginator.current.next))
384 url_param.merge(page_param => paginator.current.next))
382 end
385 end
383
386
384 unless count.nil?
387 unless count.nil?
385 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
388 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
386 if per_page_links != false && links = per_page_links(paginator.items_per_page)
389 if per_page_links != false && links = per_page_links(paginator.items_per_page)
387 html << " | #{links}"
390 html << " | #{links}"
388 end
391 end
389 end
392 end
390
393
391 html.html_safe
394 html.html_safe
392 end
395 end
393
396
394 def per_page_links(selected=nil)
397 def per_page_links(selected=nil)
395 links = Setting.per_page_options_array.collect do |n|
398 links = Setting.per_page_options_array.collect do |n|
396 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
399 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
397 end
400 end
398 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
401 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
399 end
402 end
400
403
401 def reorder_links(name, url, method = :post)
404 def reorder_links(name, url, method = :post)
402 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
405 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
403 url.merge({"#{name}[move_to]" => 'highest'}),
406 url.merge({"#{name}[move_to]" => 'highest'}),
404 :method => method, :title => l(:label_sort_highest)) +
407 :method => method, :title => l(:label_sort_highest)) +
405 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
408 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
406 url.merge({"#{name}[move_to]" => 'higher'}),
409 url.merge({"#{name}[move_to]" => 'higher'}),
407 :method => method, :title => l(:label_sort_higher)) +
410 :method => method, :title => l(:label_sort_higher)) +
408 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
411 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
409 url.merge({"#{name}[move_to]" => 'lower'}),
412 url.merge({"#{name}[move_to]" => 'lower'}),
410 :method => method, :title => l(:label_sort_lower)) +
413 :method => method, :title => l(:label_sort_lower)) +
411 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
414 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
412 url.merge({"#{name}[move_to]" => 'lowest'}),
415 url.merge({"#{name}[move_to]" => 'lowest'}),
413 :method => method, :title => l(:label_sort_lowest))
416 :method => method, :title => l(:label_sort_lowest))
414 end
417 end
415
418
416 def breadcrumb(*args)
419 def breadcrumb(*args)
417 elements = args.flatten
420 elements = args.flatten
418 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
421 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
419 end
422 end
420
423
421 def other_formats_links(&block)
424 def other_formats_links(&block)
422 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
425 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
423 yield Redmine::Views::OtherFormatsBuilder.new(self)
426 yield Redmine::Views::OtherFormatsBuilder.new(self)
424 concat('</p>'.html_safe)
427 concat('</p>'.html_safe)
425 end
428 end
426
429
427 def page_header_title
430 def page_header_title
428 if @project.nil? || @project.new_record?
431 if @project.nil? || @project.new_record?
429 h(Setting.app_title)
432 h(Setting.app_title)
430 else
433 else
431 b = []
434 b = []
432 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
435 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
433 if ancestors.any?
436 if ancestors.any?
434 root = ancestors.shift
437 root = ancestors.shift
435 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
438 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
436 if ancestors.size > 2
439 if ancestors.size > 2
437 b << "\xe2\x80\xa6"
440 b << "\xe2\x80\xa6"
438 ancestors = ancestors[-2, 2]
441 ancestors = ancestors[-2, 2]
439 end
442 end
440 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
443 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
441 end
444 end
442 b << h(@project)
445 b << h(@project)
443 b.join(" \xc2\xbb ").html_safe
446 b.join(" \xc2\xbb ").html_safe
444 end
447 end
445 end
448 end
446
449
447 def html_title(*args)
450 def html_title(*args)
448 if args.empty?
451 if args.empty?
449 title = @html_title || []
452 title = @html_title || []
450 title << @project.name if @project
453 title << @project.name if @project
451 title << Setting.app_title unless Setting.app_title == title.last
454 title << Setting.app_title unless Setting.app_title == title.last
452 title.select {|t| !t.blank? }.join(' - ')
455 title.select {|t| !t.blank? }.join(' - ')
453 else
456 else
454 @html_title ||= []
457 @html_title ||= []
455 @html_title += args
458 @html_title += args
456 end
459 end
457 end
460 end
458
461
459 # Returns the theme, controller name, and action as css classes for the
462 # Returns the theme, controller name, and action as css classes for the
460 # HTML body.
463 # HTML body.
461 def body_css_classes
464 def body_css_classes
462 css = []
465 css = []
463 if theme = Redmine::Themes.theme(Setting.ui_theme)
466 if theme = Redmine::Themes.theme(Setting.ui_theme)
464 css << 'theme-' + theme.name
467 css << 'theme-' + theme.name
465 end
468 end
466
469
467 css << 'controller-' + params[:controller]
470 css << 'controller-' + params[:controller]
468 css << 'action-' + params[:action]
471 css << 'action-' + params[:action]
469 css.join(' ')
472 css.join(' ')
470 end
473 end
471
474
472 def accesskey(s)
475 def accesskey(s)
473 Redmine::AccessKeys.key_for s
476 Redmine::AccessKeys.key_for s
474 end
477 end
475
478
476 # Formats text according to system settings.
479 # Formats text according to system settings.
477 # 2 ways to call this method:
480 # 2 ways to call this method:
478 # * with a String: textilizable(text, options)
481 # * with a String: textilizable(text, options)
479 # * with an object and one of its attribute: textilizable(issue, :description, options)
482 # * with an object and one of its attribute: textilizable(issue, :description, options)
480 def textilizable(*args)
483 def textilizable(*args)
481 options = args.last.is_a?(Hash) ? args.pop : {}
484 options = args.last.is_a?(Hash) ? args.pop : {}
482 case args.size
485 case args.size
483 when 1
486 when 1
484 obj = options[:object]
487 obj = options[:object]
485 text = args.shift
488 text = args.shift
486 when 2
489 when 2
487 obj = args.shift
490 obj = args.shift
488 attr = args.shift
491 attr = args.shift
489 text = obj.send(attr).to_s
492 text = obj.send(attr).to_s
490 else
493 else
491 raise ArgumentError, 'invalid arguments to textilizable'
494 raise ArgumentError, 'invalid arguments to textilizable'
492 end
495 end
493 return '' if text.blank?
496 return '' if text.blank?
494 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
497 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
495 only_path = options.delete(:only_path) == false ? false : true
498 only_path = options.delete(:only_path) == false ? false : true
496
499
497 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
500 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
498
501
499 @parsed_headings = []
502 @parsed_headings = []
500 @current_section = 0 if options[:edit_section_links]
503 @current_section = 0 if options[:edit_section_links]
501 text = parse_non_pre_blocks(text) do |text|
504 text = parse_non_pre_blocks(text) do |text|
502 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
505 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
503 send method_name, text, project, obj, attr, only_path, options
506 send method_name, text, project, obj, attr, only_path, options
504 end
507 end
505 end
508 end
506
509
507 if @parsed_headings.any?
510 if @parsed_headings.any?
508 replace_toc(text, @parsed_headings)
511 replace_toc(text, @parsed_headings)
509 end
512 end
510
513
511 text.html_safe
514 text.html_safe
512 end
515 end
513
516
514 def parse_non_pre_blocks(text)
517 def parse_non_pre_blocks(text)
515 s = StringScanner.new(text)
518 s = StringScanner.new(text)
516 tags = []
519 tags = []
517 parsed = ''
520 parsed = ''
518 while !s.eos?
521 while !s.eos?
519 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
522 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
520 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
523 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
521 if tags.empty?
524 if tags.empty?
522 yield text
525 yield text
523 end
526 end
524 parsed << text
527 parsed << text
525 if tag
528 if tag
526 if closing
529 if closing
527 if tags.last == tag.downcase
530 if tags.last == tag.downcase
528 tags.pop
531 tags.pop
529 end
532 end
530 else
533 else
531 tags << tag.downcase
534 tags << tag.downcase
532 end
535 end
533 parsed << full_tag
536 parsed << full_tag
534 end
537 end
535 end
538 end
536 # Close any non closing tags
539 # Close any non closing tags
537 while tag = tags.pop
540 while tag = tags.pop
538 parsed << "</#{tag}>"
541 parsed << "</#{tag}>"
539 end
542 end
540 parsed.html_safe
543 parsed.html_safe
541 end
544 end
542
545
543 def parse_inline_attachments(text, project, obj, attr, only_path, options)
546 def parse_inline_attachments(text, project, obj, attr, only_path, options)
544 # when using an image link, try to use an attachment, if possible
547 # when using an image link, try to use an attachment, if possible
545 if options[:attachments] || (obj && obj.respond_to?(:attachments))
548 if options[:attachments] || (obj && obj.respond_to?(:attachments))
546 attachments = options[:attachments] || obj.attachments
549 attachments = options[:attachments] || obj.attachments
547 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
550 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
548 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
551 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
549 # search for the picture in attachments
552 # search for the picture in attachments
550 if found = Attachment.latest_attach(attachments, filename)
553 if found = Attachment.latest_attach(attachments, filename)
551 image_url = url_for :only_path => only_path, :controller => 'attachments',
554 image_url = url_for :only_path => only_path, :controller => 'attachments',
552 :action => 'download', :id => found
555 :action => 'download', :id => found
553 desc = found.description.to_s.gsub('"', '')
556 desc = found.description.to_s.gsub('"', '')
554 if !desc.blank? && alttext.blank?
557 if !desc.blank? && alttext.blank?
555 alt = " title=\"#{desc}\" alt=\"#{desc}\""
558 alt = " title=\"#{desc}\" alt=\"#{desc}\""
556 end
559 end
557 "src=\"#{image_url}\"#{alt}".html_safe
560 "src=\"#{image_url}\"#{alt}".html_safe
558 else
561 else
559 m.html_safe
562 m.html_safe
560 end
563 end
561 end
564 end
562 end
565 end
563 end
566 end
564
567
565 # Wiki links
568 # Wiki links
566 #
569 #
567 # Examples:
570 # Examples:
568 # [[mypage]]
571 # [[mypage]]
569 # [[mypage|mytext]]
572 # [[mypage|mytext]]
570 # wiki links can refer other project wikis, using project name or identifier:
573 # wiki links can refer other project wikis, using project name or identifier:
571 # [[project:]] -> wiki starting page
574 # [[project:]] -> wiki starting page
572 # [[project:|mytext]]
575 # [[project:|mytext]]
573 # [[project:mypage]]
576 # [[project:mypage]]
574 # [[project:mypage|mytext]]
577 # [[project:mypage|mytext]]
575 def parse_wiki_links(text, project, obj, attr, only_path, options)
578 def parse_wiki_links(text, project, obj, attr, only_path, options)
576 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
579 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
577 link_project = project
580 link_project = project
578 esc, all, page, title = $1, $2, $3, $5
581 esc, all, page, title = $1, $2, $3, $5
579 if esc.nil?
582 if esc.nil?
580 if page =~ /^([^\:]+)\:(.*)$/
583 if page =~ /^([^\:]+)\:(.*)$/
581 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
584 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
582 page = $2
585 page = $2
583 title ||= $1 if page.blank?
586 title ||= $1 if page.blank?
584 end
587 end
585
588
586 if link_project && link_project.wiki
589 if link_project && link_project.wiki
587 # extract anchor
590 # extract anchor
588 anchor = nil
591 anchor = nil
589 if page =~ /^(.+?)\#(.+)$/
592 if page =~ /^(.+?)\#(.+)$/
590 page, anchor = $1, $2
593 page, anchor = $1, $2
591 end
594 end
592 anchor = sanitize_anchor_name(anchor) if anchor.present?
595 anchor = sanitize_anchor_name(anchor) if anchor.present?
593 # check if page exists
596 # check if page exists
594 wiki_page = link_project.wiki.find_page(page)
597 wiki_page = link_project.wiki.find_page(page)
595 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
598 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
596 "##{anchor}"
599 "##{anchor}"
597 else
600 else
598 case options[:wiki_links]
601 case options[:wiki_links]
599 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
602 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
600 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
603 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
601 else
604 else
602 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
605 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
603 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
606 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
604 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
607 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
605 :id => wiki_page_id, :anchor => anchor, :parent => parent)
608 :id => wiki_page_id, :anchor => anchor, :parent => parent)
606 end
609 end
607 end
610 end
608 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
611 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
609 else
612 else
610 # project or wiki doesn't exist
613 # project or wiki doesn't exist
611 all.html_safe
614 all.html_safe
612 end
615 end
613 else
616 else
614 all.html_safe
617 all.html_safe
615 end
618 end
616 end
619 end
617 end
620 end
618
621
619 # Redmine links
622 # Redmine links
620 #
623 #
621 # Examples:
624 # Examples:
622 # Issues:
625 # Issues:
623 # #52 -> Link to issue #52
626 # #52 -> Link to issue #52
624 # Changesets:
627 # Changesets:
625 # r52 -> Link to revision 52
628 # r52 -> Link to revision 52
626 # commit:a85130f -> Link to scmid starting with a85130f
629 # commit:a85130f -> Link to scmid starting with a85130f
627 # Documents:
630 # Documents:
628 # document#17 -> Link to document with id 17
631 # document#17 -> Link to document with id 17
629 # document:Greetings -> Link to the document with title "Greetings"
632 # document:Greetings -> Link to the document with title "Greetings"
630 # document:"Some document" -> Link to the document with title "Some document"
633 # document:"Some document" -> Link to the document with title "Some document"
631 # Versions:
634 # Versions:
632 # version#3 -> Link to version with id 3
635 # version#3 -> Link to version with id 3
633 # version:1.0.0 -> Link to version named "1.0.0"
636 # version:1.0.0 -> Link to version named "1.0.0"
634 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
637 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
635 # Attachments:
638 # Attachments:
636 # attachment:file.zip -> Link to the attachment of the current object named file.zip
639 # attachment:file.zip -> Link to the attachment of the current object named file.zip
637 # Source files:
640 # Source files:
638 # source:some/file -> Link to the file located at /some/file in the project's repository
641 # source:some/file -> Link to the file located at /some/file in the project's repository
639 # source:some/file@52 -> Link to the file's revision 52
642 # source:some/file@52 -> Link to the file's revision 52
640 # source:some/file#L120 -> Link to line 120 of the file
643 # source:some/file#L120 -> Link to line 120 of the file
641 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
644 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
642 # export:some/file -> Force the download of the file
645 # export:some/file -> Force the download of the file
643 # Forum messages:
646 # Forum messages:
644 # message#1218 -> Link to message with id 1218
647 # message#1218 -> Link to message with id 1218
645 #
648 #
646 # Links can refer other objects from other projects, using project identifier:
649 # Links can refer other objects from other projects, using project identifier:
647 # identifier:r52
650 # identifier:r52
648 # identifier:document:"Some document"
651 # identifier:document:"Some document"
649 # identifier:version:1.0.0
652 # identifier:version:1.0.0
650 # identifier:source:some/file
653 # identifier:source:some/file
651 def parse_redmine_links(text, project, obj, attr, only_path, options)
654 def parse_redmine_links(text, project, obj, attr, only_path, options)
652 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
655 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
653 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
656 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
654 link = nil
657 link = nil
655 if project_identifier
658 if project_identifier
656 project = Project.visible.find_by_identifier(project_identifier)
659 project = Project.visible.find_by_identifier(project_identifier)
657 end
660 end
658 if esc.nil?
661 if esc.nil?
659 if prefix.nil? && sep == 'r'
662 if prefix.nil? && sep == 'r'
660 # project.changesets.visible raises an SQL error because of a double join on repositories
663 # project.changesets.visible raises an SQL error because of a double join on repositories
661 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
664 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
662 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
665 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
663 :class => 'changeset',
666 :class => 'changeset',
664 :title => truncate_single_line(changeset.comments, :length => 100))
667 :title => truncate_single_line(changeset.comments, :length => 100))
665 end
668 end
666 elsif sep == '#'
669 elsif sep == '#'
667 oid = identifier.to_i
670 oid = identifier.to_i
668 case prefix
671 case prefix
669 when nil
672 when nil
670 if issue = Issue.visible.find_by_id(oid, :include => :status)
673 if issue = Issue.visible.find_by_id(oid, :include => :status)
671 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
674 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
672 :class => issue.css_classes,
675 :class => issue.css_classes,
673 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
676 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
674 end
677 end
675 when 'document'
678 when 'document'
676 if document = Document.visible.find_by_id(oid)
679 if document = Document.visible.find_by_id(oid)
677 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
680 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
678 :class => 'document'
681 :class => 'document'
679 end
682 end
680 when 'version'
683 when 'version'
681 if version = Version.visible.find_by_id(oid)
684 if version = Version.visible.find_by_id(oid)
682 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
685 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
683 :class => 'version'
686 :class => 'version'
684 end
687 end
685 when 'message'
688 when 'message'
686 if message = Message.visible.find_by_id(oid, :include => :parent)
689 if message = Message.visible.find_by_id(oid, :include => :parent)
687 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
690 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
688 end
691 end
689 when 'forum'
692 when 'forum'
690 if board = Board.visible.find_by_id(oid)
693 if board = Board.visible.find_by_id(oid)
691 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
694 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
692 :class => 'board'
695 :class => 'board'
693 end
696 end
694 when 'news'
697 when 'news'
695 if news = News.visible.find_by_id(oid)
698 if news = News.visible.find_by_id(oid)
696 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
699 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
697 :class => 'news'
700 :class => 'news'
698 end
701 end
699 when 'project'
702 when 'project'
700 if p = Project.visible.find_by_id(oid)
703 if p = Project.visible.find_by_id(oid)
701 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
704 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
702 end
705 end
703 end
706 end
704 elsif sep == ':'
707 elsif sep == ':'
705 # removes the double quotes if any
708 # removes the double quotes if any
706 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
709 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
707 case prefix
710 case prefix
708 when 'document'
711 when 'document'
709 if project && document = project.documents.visible.find_by_title(name)
712 if project && document = project.documents.visible.find_by_title(name)
710 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
713 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
711 :class => 'document'
714 :class => 'document'
712 end
715 end
713 when 'version'
716 when 'version'
714 if project && version = project.versions.visible.find_by_name(name)
717 if project && version = project.versions.visible.find_by_name(name)
715 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
718 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
716 :class => 'version'
719 :class => 'version'
717 end
720 end
718 when 'forum'
721 when 'forum'
719 if project && board = project.boards.visible.find_by_name(name)
722 if project && board = project.boards.visible.find_by_name(name)
720 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
723 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
721 :class => 'board'
724 :class => 'board'
722 end
725 end
723 when 'news'
726 when 'news'
724 if project && news = project.news.visible.find_by_title(name)
727 if project && news = project.news.visible.find_by_title(name)
725 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
728 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
726 :class => 'news'
729 :class => 'news'
727 end
730 end
728 when 'commit'
731 when 'commit'
729 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
732 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
730 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
733 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
731 :class => 'changeset',
734 :class => 'changeset',
732 :title => truncate_single_line(h(changeset.comments), :length => 100)
735 :title => truncate_single_line(h(changeset.comments), :length => 100)
733 end
736 end
734 when 'source', 'export'
737 when 'source', 'export'
735 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
738 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
736 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
739 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
737 path, rev, anchor = $1, $3, $5
740 path, rev, anchor = $1, $3, $5
738 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
741 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
739 :path => to_path_param(path),
742 :path => to_path_param(path),
740 :rev => rev,
743 :rev => rev,
741 :anchor => anchor,
744 :anchor => anchor,
742 :format => (prefix == 'export' ? 'raw' : nil)},
745 :format => (prefix == 'export' ? 'raw' : nil)},
743 :class => (prefix == 'export' ? 'source download' : 'source')
746 :class => (prefix == 'export' ? 'source download' : 'source')
744 end
747 end
745 when 'attachment'
748 when 'attachment'
746 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
749 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
747 if attachments && attachment = attachments.detect {|a| a.filename == name }
750 if attachments && attachment = attachments.detect {|a| a.filename == name }
748 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
751 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
749 :class => 'attachment'
752 :class => 'attachment'
750 end
753 end
751 when 'project'
754 when 'project'
752 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
755 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
753 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
756 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
754 end
757 end
755 end
758 end
756 end
759 end
757 end
760 end
758 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
761 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
759 end
762 end
760 end
763 end
761
764
762 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
765 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
763
766
764 def parse_sections(text, project, obj, attr, only_path, options)
767 def parse_sections(text, project, obj, attr, only_path, options)
765 return unless options[:edit_section_links]
768 return unless options[:edit_section_links]
766 text.gsub!(HEADING_RE) do
769 text.gsub!(HEADING_RE) do
767 @current_section += 1
770 @current_section += 1
768 if @current_section > 1
771 if @current_section > 1
769 content_tag('div',
772 content_tag('div',
770 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
773 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
771 :class => 'contextual',
774 :class => 'contextual',
772 :title => l(:button_edit_section)) + $1
775 :title => l(:button_edit_section)) + $1
773 else
776 else
774 $1
777 $1
775 end
778 end
776 end
779 end
777 end
780 end
778
781
779 # Headings and TOC
782 # Headings and TOC
780 # Adds ids and links to headings unless options[:headings] is set to false
783 # Adds ids and links to headings unless options[:headings] is set to false
781 def parse_headings(text, project, obj, attr, only_path, options)
784 def parse_headings(text, project, obj, attr, only_path, options)
782 return if options[:headings] == false
785 return if options[:headings] == false
783
786
784 text.gsub!(HEADING_RE) do
787 text.gsub!(HEADING_RE) do
785 level, attrs, content = $2.to_i, $3, $4
788 level, attrs, content = $2.to_i, $3, $4
786 item = strip_tags(content).strip
789 item = strip_tags(content).strip
787 anchor = sanitize_anchor_name(item)
790 anchor = sanitize_anchor_name(item)
788 # used for single-file wiki export
791 # used for single-file wiki export
789 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
792 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
790 @parsed_headings << [level, anchor, item]
793 @parsed_headings << [level, anchor, item]
791 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
794 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
792 end
795 end
793 end
796 end
794
797
795 MACROS_RE = /
798 MACROS_RE = /
796 (!)? # escaping
799 (!)? # escaping
797 (
800 (
798 \{\{ # opening tag
801 \{\{ # opening tag
799 ([\w]+) # macro name
802 ([\w]+) # macro name
800 (\(([^\}]*)\))? # optional arguments
803 (\(([^\}]*)\))? # optional arguments
801 \}\} # closing tag
804 \}\} # closing tag
802 )
805 )
803 /x unless const_defined?(:MACROS_RE)
806 /x unless const_defined?(:MACROS_RE)
804
807
805 # Macros substitution
808 # Macros substitution
806 def parse_macros(text, project, obj, attr, only_path, options)
809 def parse_macros(text, project, obj, attr, only_path, options)
807 text.gsub!(MACROS_RE) do
810 text.gsub!(MACROS_RE) do
808 esc, all, macro = $1, $2, $3.downcase
811 esc, all, macro = $1, $2, $3.downcase
809 args = ($5 || '').split(',').each(&:strip)
812 args = ($5 || '').split(',').each(&:strip)
810 if esc.nil?
813 if esc.nil?
811 begin
814 begin
812 exec_macro(macro, obj, args)
815 exec_macro(macro, obj, args)
813 rescue => e
816 rescue => e
814 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
817 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
815 end || all
818 end || all
816 else
819 else
817 all
820 all
818 end
821 end
819 end
822 end
820 end
823 end
821
824
822 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
825 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
823
826
824 # Renders the TOC with given headings
827 # Renders the TOC with given headings
825 def replace_toc(text, headings)
828 def replace_toc(text, headings)
826 text.gsub!(TOC_RE) do
829 text.gsub!(TOC_RE) do
827 if headings.empty?
830 if headings.empty?
828 ''
831 ''
829 else
832 else
830 div_class = 'toc'
833 div_class = 'toc'
831 div_class << ' right' if $1 == '>'
834 div_class << ' right' if $1 == '>'
832 div_class << ' left' if $1 == '<'
835 div_class << ' left' if $1 == '<'
833 out = "<ul class=\"#{div_class}\"><li>"
836 out = "<ul class=\"#{div_class}\"><li>"
834 root = headings.map(&:first).min
837 root = headings.map(&:first).min
835 current = root
838 current = root
836 started = false
839 started = false
837 headings.each do |level, anchor, item|
840 headings.each do |level, anchor, item|
838 if level > current
841 if level > current
839 out << '<ul><li>' * (level - current)
842 out << '<ul><li>' * (level - current)
840 elsif level < current
843 elsif level < current
841 out << "</li></ul>\n" * (current - level) + "</li><li>"
844 out << "</li></ul>\n" * (current - level) + "</li><li>"
842 elsif started
845 elsif started
843 out << '</li><li>'
846 out << '</li><li>'
844 end
847 end
845 out << "<a href=\"##{anchor}\">#{item}</a>"
848 out << "<a href=\"##{anchor}\">#{item}</a>"
846 current = level
849 current = level
847 started = true
850 started = true
848 end
851 end
849 out << '</li></ul>' * (current - root)
852 out << '</li></ul>' * (current - root)
850 out << '</li></ul>'
853 out << '</li></ul>'
851 end
854 end
852 end
855 end
853 end
856 end
854
857
855 # Same as Rails' simple_format helper without using paragraphs
858 # Same as Rails' simple_format helper without using paragraphs
856 def simple_format_without_paragraph(text)
859 def simple_format_without_paragraph(text)
857 text.to_s.
860 text.to_s.
858 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
861 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
859 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
862 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
860 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
863 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
861 html_safe
864 html_safe
862 end
865 end
863
866
864 def lang_options_for_select(blank=true)
867 def lang_options_for_select(blank=true)
865 (blank ? [["(auto)", ""]] : []) +
868 (blank ? [["(auto)", ""]] : []) +
866 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
869 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
867 end
870 end
868
871
869 def label_tag_for(name, option_tags = nil, options = {})
872 def label_tag_for(name, option_tags = nil, options = {})
870 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
873 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
871 content_tag("label", label_text)
874 content_tag("label", label_text)
872 end
875 end
873
876
874 def labelled_tabular_form_for(*args, &proc)
877 def labelled_tabular_form_for(*args, &proc)
875 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
878 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
876 args << {} unless args.last.is_a?(Hash)
879 args << {} unless args.last.is_a?(Hash)
877 options = args.last
880 options = args.last
878 options[:html] ||= {}
881 options[:html] ||= {}
879 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
882 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
880 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
883 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
881 form_for(*args, &proc)
884 form_for(*args, &proc)
882 end
885 end
883
886
884 def labelled_form_for(*args, &proc)
887 def labelled_form_for(*args, &proc)
885 args << {} unless args.last.is_a?(Hash)
888 args << {} unless args.last.is_a?(Hash)
886 options = args.last
889 options = args.last
887 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
890 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
888 form_for(*args, &proc)
891 form_for(*args, &proc)
889 end
892 end
890
893
891 def labelled_fields_for(*args, &proc)
894 def labelled_fields_for(*args, &proc)
892 args << {} unless args.last.is_a?(Hash)
895 args << {} unless args.last.is_a?(Hash)
893 options = args.last
896 options = args.last
894 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
897 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
895 fields_for(*args, &proc)
898 fields_for(*args, &proc)
896 end
899 end
897
900
898 def labelled_remote_form_for(*args, &proc)
901 def labelled_remote_form_for(*args, &proc)
899 args << {} unless args.last.is_a?(Hash)
902 args << {} unless args.last.is_a?(Hash)
900 options = args.last
903 options = args.last
901 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
904 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
902 remote_form_for(*args, &proc)
905 remote_form_for(*args, &proc)
903 end
906 end
904
907
905 def back_url_hidden_field_tag
908 def back_url_hidden_field_tag
906 back_url = params[:back_url] || request.env['HTTP_REFERER']
909 back_url = params[:back_url] || request.env['HTTP_REFERER']
907 back_url = CGI.unescape(back_url.to_s)
910 back_url = CGI.unescape(back_url.to_s)
908 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
911 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
909 end
912 end
910
913
911 def check_all_links(form_name)
914 def check_all_links(form_name)
912 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
915 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
913 " | ".html_safe +
916 " | ".html_safe +
914 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
917 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
915 end
918 end
916
919
917 def progress_bar(pcts, options={})
920 def progress_bar(pcts, options={})
918 pcts = [pcts, pcts] unless pcts.is_a?(Array)
921 pcts = [pcts, pcts] unless pcts.is_a?(Array)
919 pcts = pcts.collect(&:round)
922 pcts = pcts.collect(&:round)
920 pcts[1] = pcts[1] - pcts[0]
923 pcts[1] = pcts[1] - pcts[0]
921 pcts << (100 - pcts[1] - pcts[0])
924 pcts << (100 - pcts[1] - pcts[0])
922 width = options[:width] || '100px;'
925 width = options[:width] || '100px;'
923 legend = options[:legend] || ''
926 legend = options[:legend] || ''
924 content_tag('table',
927 content_tag('table',
925 content_tag('tr',
928 content_tag('tr',
926 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
929 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
927 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
930 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
928 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
931 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
929 ), :class => 'progress', :style => "width: #{width};").html_safe +
932 ), :class => 'progress', :style => "width: #{width};").html_safe +
930 content_tag('p', legend, :class => 'pourcent').html_safe
933 content_tag('p', legend, :class => 'pourcent').html_safe
931 end
934 end
932
935
933 def checked_image(checked=true)
936 def checked_image(checked=true)
934 if checked
937 if checked
935 image_tag 'toggle_check.png'
938 image_tag 'toggle_check.png'
936 end
939 end
937 end
940 end
938
941
939 def context_menu(url)
942 def context_menu(url)
940 unless @context_menu_included
943 unless @context_menu_included
941 content_for :header_tags do
944 content_for :header_tags do
942 javascript_include_tag('context_menu') +
945 javascript_include_tag('context_menu') +
943 stylesheet_link_tag('context_menu')
946 stylesheet_link_tag('context_menu')
944 end
947 end
945 if l(:direction) == 'rtl'
948 if l(:direction) == 'rtl'
946 content_for :header_tags do
949 content_for :header_tags do
947 stylesheet_link_tag('context_menu_rtl')
950 stylesheet_link_tag('context_menu_rtl')
948 end
951 end
949 end
952 end
950 @context_menu_included = true
953 @context_menu_included = true
951 end
954 end
952 javascript_tag "new ContextMenu('#{ url_for(url) }')"
955 javascript_tag "new ContextMenu('#{ url_for(url) }')"
953 end
956 end
954
957
955 def context_menu_link(name, url, options={})
958 def context_menu_link(name, url, options={})
956 options[:class] ||= ''
959 options[:class] ||= ''
957 if options.delete(:selected)
960 if options.delete(:selected)
958 options[:class] << ' icon-checked disabled'
961 options[:class] << ' icon-checked disabled'
959 options[:disabled] = true
962 options[:disabled] = true
960 end
963 end
961 if options.delete(:disabled)
964 if options.delete(:disabled)
962 options.delete(:method)
965 options.delete(:method)
963 options.delete(:confirm)
966 options.delete(:confirm)
964 options.delete(:onclick)
967 options.delete(:onclick)
965 options[:class] << ' disabled'
968 options[:class] << ' disabled'
966 url = '#'
969 url = '#'
967 end
970 end
968 link_to h(name), url, options
971 link_to h(name), url, options
969 end
972 end
970
973
971 def calendar_for(field_id)
974 def calendar_for(field_id)
972 include_calendar_headers_tags
975 include_calendar_headers_tags
973 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
976 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
974 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
977 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
975 end
978 end
976
979
977 def include_calendar_headers_tags
980 def include_calendar_headers_tags
978 unless @calendar_headers_tags_included
981 unless @calendar_headers_tags_included
979 @calendar_headers_tags_included = true
982 @calendar_headers_tags_included = true
980 content_for :header_tags do
983 content_for :header_tags do
981 start_of_week = case Setting.start_of_week.to_i
984 start_of_week = case Setting.start_of_week.to_i
982 when 1
985 when 1
983 'Calendar._FD = 1;' # Monday
986 'Calendar._FD = 1;' # Monday
984 when 7
987 when 7
985 'Calendar._FD = 0;' # Sunday
988 'Calendar._FD = 0;' # Sunday
986 when 6
989 when 6
987 'Calendar._FD = 6;' # Saturday
990 'Calendar._FD = 6;' # Saturday
988 else
991 else
989 '' # use language
992 '' # use language
990 end
993 end
991
994
992 javascript_include_tag('calendar/calendar') +
995 javascript_include_tag('calendar/calendar') +
993 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
996 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
994 javascript_tag(start_of_week) +
997 javascript_tag(start_of_week) +
995 javascript_include_tag('calendar/calendar-setup') +
998 javascript_include_tag('calendar/calendar-setup') +
996 stylesheet_link_tag('calendar')
999 stylesheet_link_tag('calendar')
997 end
1000 end
998 end
1001 end
999 end
1002 end
1000
1003
1001 def content_for(name, content = nil, &block)
1004 def content_for(name, content = nil, &block)
1002 @has_content ||= {}
1005 @has_content ||= {}
1003 @has_content[name] = true
1006 @has_content[name] = true
1004 super(name, content, &block)
1007 super(name, content, &block)
1005 end
1008 end
1006
1009
1007 def has_content?(name)
1010 def has_content?(name)
1008 (@has_content && @has_content[name]) || false
1011 (@has_content && @has_content[name]) || false
1009 end
1012 end
1010
1013
1011 def email_delivery_enabled?
1014 def email_delivery_enabled?
1012 !!ActionMailer::Base.perform_deliveries
1015 !!ActionMailer::Base.perform_deliveries
1013 end
1016 end
1014
1017
1015 # Returns the avatar image tag for the given +user+ if avatars are enabled
1018 # Returns the avatar image tag for the given +user+ if avatars are enabled
1016 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1019 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1017 def avatar(user, options = { })
1020 def avatar(user, options = { })
1018 if Setting.gravatar_enabled?
1021 if Setting.gravatar_enabled?
1019 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1022 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1020 email = nil
1023 email = nil
1021 if user.respond_to?(:mail)
1024 if user.respond_to?(:mail)
1022 email = user.mail
1025 email = user.mail
1023 elsif user.to_s =~ %r{<(.+?)>}
1026 elsif user.to_s =~ %r{<(.+?)>}
1024 email = $1
1027 email = $1
1025 end
1028 end
1026 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1029 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1027 else
1030 else
1028 ''
1031 ''
1029 end
1032 end
1030 end
1033 end
1031
1034
1032 def sanitize_anchor_name(anchor)
1035 def sanitize_anchor_name(anchor)
1033 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1036 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1034 end
1037 end
1035
1038
1036 # Returns the javascript tags that are included in the html layout head
1039 # Returns the javascript tags that are included in the html layout head
1037 def javascript_heads
1040 def javascript_heads
1038 tags = javascript_include_tag(:defaults)
1041 tags = javascript_include_tag(:defaults)
1039 unless User.current.pref.warn_on_leaving_unsaved == '0'
1042 unless User.current.pref.warn_on_leaving_unsaved == '0'
1040 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1043 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1041 end
1044 end
1042 tags
1045 tags
1043 end
1046 end
1044
1047
1045 def favicon
1048 def favicon
1046 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1049 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1047 end
1050 end
1048
1051
1049 def robot_exclusion_tag
1052 def robot_exclusion_tag
1050 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1053 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1051 end
1054 end
1052
1055
1053 # Returns true if arg is expected in the API response
1056 # Returns true if arg is expected in the API response
1054 def include_in_api_response?(arg)
1057 def include_in_api_response?(arg)
1055 unless @included_in_api_response
1058 unless @included_in_api_response
1056 param = params[:include]
1059 param = params[:include]
1057 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1060 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1058 @included_in_api_response.collect!(&:strip)
1061 @included_in_api_response.collect!(&:strip)
1059 end
1062 end
1060 @included_in_api_response.include?(arg.to_s)
1063 @included_in_api_response.include?(arg.to_s)
1061 end
1064 end
1062
1065
1063 # Returns options or nil if nometa param or X-Redmine-Nometa header
1066 # Returns options or nil if nometa param or X-Redmine-Nometa header
1064 # was set in the request
1067 # was set in the request
1065 def api_meta(options)
1068 def api_meta(options)
1066 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1069 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1067 # compatibility mode for activeresource clients that raise
1070 # compatibility mode for activeresource clients that raise
1068 # an error when unserializing an array with attributes
1071 # an error when unserializing an array with attributes
1069 nil
1072 nil
1070 else
1073 else
1071 options
1074 options
1072 end
1075 end
1073 end
1076 end
1074
1077
1075 private
1078 private
1076
1079
1077 def wiki_helper
1080 def wiki_helper
1078 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1081 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1079 extend helper
1082 extend helper
1080 return self
1083 return self
1081 end
1084 end
1082
1085
1083 def link_to_content_update(text, url_params = {}, html_options = {})
1086 def link_to_content_update(text, url_params = {}, html_options = {})
1084 link_to(text, url_params, html_options)
1087 link_to(text, url_params, html_options)
1085 end
1088 end
1086 end
1089 end
@@ -1,311 +1,314
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 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 'iconv'
20 require 'iconv'
21 require 'redmine/codeset_util'
21 require 'redmine/codeset_util'
22
22
23 module RepositoriesHelper
23 module RepositoriesHelper
24 def format_revision(revision)
24 def format_revision(revision)
25 if revision.respond_to? :format_identifier
25 if revision.respond_to? :format_identifier
26 revision.format_identifier
26 revision.format_identifier
27 else
27 else
28 revision.to_s
28 revision.to_s
29 end
29 end
30 end
30 end
31
31
32 def truncate_at_line_break(text, length = 255)
32 def truncate_at_line_break(text, length = 255)
33 if text
33 if text
34 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
34 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
35 end
35 end
36 end
36 end
37
37
38 def render_properties(properties)
38 def render_properties(properties)
39 unless properties.nil? || properties.empty?
39 unless properties.nil? || properties.empty?
40 content = ''
40 content = ''
41 properties.keys.sort.each do |property|
41 properties.keys.sort.each do |property|
42 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
42 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
43 end
43 end
44 content_tag('ul', content.html_safe, :class => 'properties')
44 content_tag('ul', content.html_safe, :class => 'properties')
45 end
45 end
46 end
46 end
47
47
48 def render_changeset_changes
48 def render_changeset_changes
49 changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
49 changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
50 case change.action
50 case change.action
51 when 'A'
51 when 'A'
52 # Detects moved/copied files
52 # Detects moved/copied files
53 if !change.from_path.blank?
53 if !change.from_path.blank?
54 change.action =
54 change.action =
55 @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
55 @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
56 end
56 end
57 change
57 change
58 when 'D'
58 when 'D'
59 @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
59 @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
60 else
60 else
61 change
61 change
62 end
62 end
63 end.compact
63 end.compact
64
64
65 tree = { }
65 tree = { }
66 changes.each do |change|
66 changes.each do |change|
67 p = tree
67 p = tree
68 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
68 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
69 path = ''
69 path = ''
70 dirs.each do |dir|
70 dirs.each do |dir|
71 path += '/' + dir
71 path += '/' + dir
72 p[:s] ||= {}
72 p[:s] ||= {}
73 p = p[:s]
73 p = p[:s]
74 p[path] ||= {}
74 p[path] ||= {}
75 p = p[path]
75 p = p[path]
76 end
76 end
77 p[:c] = change
77 p[:c] = change
78 end
78 end
79 render_changes_tree(tree[:s])
79 render_changes_tree(tree[:s])
80 end
80 end
81
81
82 def render_changes_tree(tree)
82 def render_changes_tree(tree)
83 return '' if tree.nil?
83 return '' if tree.nil?
84 output = ''
84 output = ''
85 output << '<ul>'
85 output << '<ul>'
86 tree.keys.sort.each do |file|
86 tree.keys.sort.each do |file|
87 style = 'change'
87 style = 'change'
88 text = File.basename(h(file))
88 text = File.basename(h(file))
89 if s = tree[file][:s]
89 if s = tree[file][:s]
90 style << ' folder'
90 style << ' folder'
91 path_param = to_path_param(@repository.relative_path(file))
91 path_param = to_path_param(@repository.relative_path(file))
92 text = link_to(h(text), :controller => 'repositories',
92 text = link_to(h(text), :controller => 'repositories',
93 :action => 'show',
93 :action => 'show',
94 :id => @project,
94 :id => @project,
95 :repository_id => @repository.identifier_param,
95 :path => path_param,
96 :path => path_param,
96 :rev => @changeset.identifier)
97 :rev => @changeset.identifier)
97 output << "<li class='#{style}'>#{text}"
98 output << "<li class='#{style}'>#{text}"
98 output << render_changes_tree(s)
99 output << render_changes_tree(s)
99 output << "</li>"
100 output << "</li>"
100 elsif c = tree[file][:c]
101 elsif c = tree[file][:c]
101 style << " change-#{c.action}"
102 style << " change-#{c.action}"
102 path_param = to_path_param(@repository.relative_path(c.path))
103 path_param = to_path_param(@repository.relative_path(c.path))
103 text = link_to(h(text), :controller => 'repositories',
104 text = link_to(h(text), :controller => 'repositories',
104 :action => 'entry',
105 :action => 'entry',
105 :id => @project,
106 :id => @project,
107 :repository_id => @repository.identifier_param,
106 :path => path_param,
108 :path => path_param,
107 :rev => @changeset.identifier) unless c.action == 'D'
109 :rev => @changeset.identifier) unless c.action == 'D'
108 text << " - #{h(c.revision)}" unless c.revision.blank?
110 text << " - #{h(c.revision)}" unless c.revision.blank?
109 text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
111 text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
110 :action => 'diff',
112 :action => 'diff',
111 :id => @project,
113 :id => @project,
114 :repository_id => @repository.identifier_param,
112 :path => path_param,
115 :path => path_param,
113 :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
116 :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
114 text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
117 text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
115 output << "<li class='#{style}'>#{text}</li>"
118 output << "<li class='#{style}'>#{text}</li>"
116 end
119 end
117 end
120 end
118 output << '</ul>'
121 output << '</ul>'
119 output.html_safe
122 output.html_safe
120 end
123 end
121
124
122 def repository_field_tags(form, repository)
125 def repository_field_tags(form, repository)
123 method = repository.class.name.demodulize.underscore + "_field_tags"
126 method = repository.class.name.demodulize.underscore + "_field_tags"
124 if repository.is_a?(Repository) &&
127 if repository.is_a?(Repository) &&
125 respond_to?(method) && method != 'repository_field_tags'
128 respond_to?(method) && method != 'repository_field_tags'
126 send(method, form, repository)
129 send(method, form, repository)
127 end
130 end
128 end
131 end
129
132
130 def scm_select_tag(repository)
133 def scm_select_tag(repository)
131 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
134 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
132 Redmine::Scm::Base.all.each do |scm|
135 Redmine::Scm::Base.all.each do |scm|
133 if Setting.enabled_scm.include?(scm) ||
136 if Setting.enabled_scm.include?(scm) ||
134 (repository && repository.class.name.demodulize == scm)
137 (repository && repository.class.name.demodulize == scm)
135 scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
138 scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
136 end
139 end
137 end
140 end
138 select_tag('repository_scm',
141 select_tag('repository_scm',
139 options_for_select(scm_options, repository.class.name.demodulize),
142 options_for_select(scm_options, repository.class.name.demodulize),
140 :disabled => (repository && !repository.new_record?),
143 :disabled => (repository && !repository.new_record?),
141 :onchange => remote_function(
144 :onchange => remote_function(
142 :url => new_project_repository_path(@project),
145 :url => new_project_repository_path(@project),
143 :method => :get,
146 :method => :get,
144 :update => 'content',
147 :update => 'content',
145 :with => "Form.serialize(this.form)")
148 :with => "Form.serialize(this.form)")
146 )
149 )
147 end
150 end
148
151
149 def with_leading_slash(path)
152 def with_leading_slash(path)
150 path.to_s.starts_with?('/') ? path : "/#{path}"
153 path.to_s.starts_with?('/') ? path : "/#{path}"
151 end
154 end
152
155
153 def without_leading_slash(path)
156 def without_leading_slash(path)
154 path.gsub(%r{^/+}, '')
157 path.gsub(%r{^/+}, '')
155 end
158 end
156
159
157 def subversion_field_tags(form, repository)
160 def subversion_field_tags(form, repository)
158 content_tag('p', form.text_field(:url, :size => 60, :required => true,
161 content_tag('p', form.text_field(:url, :size => 60, :required => true,
159 :disabled => (repository && !repository.root_url.blank?)) +
162 :disabled => (repository && !repository.root_url.blank?)) +
160 '<br />'.html_safe +
163 '<br />'.html_safe +
161 '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
164 '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
162 content_tag('p', form.text_field(:login, :size => 30)) +
165 content_tag('p', form.text_field(:login, :size => 30)) +
163 content_tag('p', form.password_field(
166 content_tag('p', form.password_field(
164 :password, :size => 30, :name => 'ignore',
167 :password, :size => 30, :name => 'ignore',
165 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
168 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
166 :onfocus => "this.value=''; this.name='repository[password]';",
169 :onfocus => "this.value=''; this.name='repository[password]';",
167 :onchange => "this.name='repository[password]';"))
170 :onchange => "this.name='repository[password]';"))
168 end
171 end
169
172
170 def darcs_field_tags(form, repository)
173 def darcs_field_tags(form, repository)
171 content_tag('p', form.text_field(
174 content_tag('p', form.text_field(
172 :url, :label => l(:field_path_to_repository),
175 :url, :label => l(:field_path_to_repository),
173 :size => 60, :required => true,
176 :size => 60, :required => true,
174 :disabled => (repository && !repository.new_record?))) +
177 :disabled => (repository && !repository.new_record?))) +
175 content_tag('p', form.select(
178 content_tag('p', form.select(
176 :log_encoding, [nil] + Setting::ENCODINGS,
179 :log_encoding, [nil] + Setting::ENCODINGS,
177 :label => l(:field_commit_logs_encoding), :required => true))
180 :label => l(:field_commit_logs_encoding), :required => true))
178 end
181 end
179
182
180 def mercurial_field_tags(form, repository)
183 def mercurial_field_tags(form, repository)
181 content_tag('p', form.text_field(
184 content_tag('p', form.text_field(
182 :url, :label => l(:field_path_to_repository),
185 :url, :label => l(:field_path_to_repository),
183 :size => 60, :required => true,
186 :size => 60, :required => true,
184 :disabled => (repository && !repository.root_url.blank?)
187 :disabled => (repository && !repository.root_url.blank?)
185 ) +
188 ) +
186 '<br />'.html_safe + l(:text_mercurial_repository_note)) +
189 '<br />'.html_safe + l(:text_mercurial_repository_note)) +
187 content_tag('p', form.select(
190 content_tag('p', form.select(
188 :path_encoding, [nil] + Setting::ENCODINGS,
191 :path_encoding, [nil] + Setting::ENCODINGS,
189 :label => l(:field_scm_path_encoding)
192 :label => l(:field_scm_path_encoding)
190 ) +
193 ) +
191 '<br />'.html_safe + l(:text_scm_path_encoding_note))
194 '<br />'.html_safe + l(:text_scm_path_encoding_note))
192 end
195 end
193
196
194 def git_field_tags(form, repository)
197 def git_field_tags(form, repository)
195 content_tag('p', form.text_field(
198 content_tag('p', form.text_field(
196 :url, :label => l(:field_path_to_repository),
199 :url, :label => l(:field_path_to_repository),
197 :size => 60, :required => true,
200 :size => 60, :required => true,
198 :disabled => (repository && !repository.root_url.blank?)
201 :disabled => (repository && !repository.root_url.blank?)
199 ) +
202 ) +
200 '<br />'.html_safe +
203 '<br />'.html_safe +
201 l(:text_git_repository_note)) +
204 l(:text_git_repository_note)) +
202 content_tag('p', form.select(
205 content_tag('p', form.select(
203 :path_encoding, [nil] + Setting::ENCODINGS,
206 :path_encoding, [nil] + Setting::ENCODINGS,
204 :label => l(:field_scm_path_encoding)
207 :label => l(:field_scm_path_encoding)
205 ) +
208 ) +
206 '<br />'.html_safe + l(:text_scm_path_encoding_note)) +
209 '<br />'.html_safe + l(:text_scm_path_encoding_note)) +
207 content_tag('p', form.check_box(
210 content_tag('p', form.check_box(
208 :extra_report_last_commit,
211 :extra_report_last_commit,
209 :label => l(:label_git_report_last_commit)
212 :label => l(:label_git_report_last_commit)
210 ))
213 ))
211 end
214 end
212
215
213 def cvs_field_tags(form, repository)
216 def cvs_field_tags(form, repository)
214 content_tag('p', form.text_field(
217 content_tag('p', form.text_field(
215 :root_url,
218 :root_url,
216 :label => l(:field_cvsroot),
219 :label => l(:field_cvsroot),
217 :size => 60, :required => true,
220 :size => 60, :required => true,
218 :disabled => !repository.new_record?)) +
221 :disabled => !repository.new_record?)) +
219 content_tag('p', form.text_field(
222 content_tag('p', form.text_field(
220 :url,
223 :url,
221 :label => l(:field_cvs_module),
224 :label => l(:field_cvs_module),
222 :size => 30, :required => true,
225 :size => 30, :required => true,
223 :disabled => !repository.new_record?)) +
226 :disabled => !repository.new_record?)) +
224 content_tag('p', form.select(
227 content_tag('p', form.select(
225 :log_encoding, [nil] + Setting::ENCODINGS,
228 :log_encoding, [nil] + Setting::ENCODINGS,
226 :label => l(:field_commit_logs_encoding), :required => true)) +
229 :label => l(:field_commit_logs_encoding), :required => true)) +
227 content_tag('p', form.select(
230 content_tag('p', form.select(
228 :path_encoding, [nil] + Setting::ENCODINGS,
231 :path_encoding, [nil] + Setting::ENCODINGS,
229 :label => l(:field_scm_path_encoding)
232 :label => l(:field_scm_path_encoding)
230 ) +
233 ) +
231 '<br />'.html_safe + l(:text_scm_path_encoding_note))
234 '<br />'.html_safe + l(:text_scm_path_encoding_note))
232 end
235 end
233
236
234 def bazaar_field_tags(form, repository)
237 def bazaar_field_tags(form, repository)
235 content_tag('p', form.text_field(
238 content_tag('p', form.text_field(
236 :url, :label => l(:field_path_to_repository),
239 :url, :label => l(:field_path_to_repository),
237 :size => 60, :required => true,
240 :size => 60, :required => true,
238 :disabled => (repository && !repository.new_record?))) +
241 :disabled => (repository && !repository.new_record?))) +
239 content_tag('p', form.select(
242 content_tag('p', form.select(
240 :log_encoding, [nil] + Setting::ENCODINGS,
243 :log_encoding, [nil] + Setting::ENCODINGS,
241 :label => l(:field_commit_logs_encoding), :required => true))
244 :label => l(:field_commit_logs_encoding), :required => true))
242 end
245 end
243
246
244 def filesystem_field_tags(form, repository)
247 def filesystem_field_tags(form, repository)
245 content_tag('p', form.text_field(
248 content_tag('p', form.text_field(
246 :url, :label => l(:field_root_directory),
249 :url, :label => l(:field_root_directory),
247 :size => 60, :required => true,
250 :size => 60, :required => true,
248 :disabled => (repository && !repository.root_url.blank?))) +
251 :disabled => (repository && !repository.root_url.blank?))) +
249 content_tag('p', form.select(
252 content_tag('p', form.select(
250 :path_encoding, [nil] + Setting::ENCODINGS,
253 :path_encoding, [nil] + Setting::ENCODINGS,
251 :label => l(:field_scm_path_encoding)
254 :label => l(:field_scm_path_encoding)
252 ) +
255 ) +
253 '<br />'.html_safe + l(:text_scm_path_encoding_note))
256 '<br />'.html_safe + l(:text_scm_path_encoding_note))
254 end
257 end
255
258
256 def index_commits(commits, heads, href_proc = nil)
259 def index_commits(commits, heads, href_proc = nil)
257 return nil if commits.nil? or commits.first.parents.nil?
260 return nil if commits.nil? or commits.first.parents.nil?
258 map = {}
261 map = {}
259 commit_hashes = []
262 commit_hashes = []
260 refs_map = {}
263 refs_map = {}
261 href_proc ||= Proc.new {|x|x}
264 href_proc ||= Proc.new {|x|x}
262 heads.each{|r| refs_map[r.scmid] ||= []; refs_map[r.scmid] << r}
265 heads.each{|r| refs_map[r.scmid] ||= []; refs_map[r.scmid] << r}
263 commits.reverse.each_with_index do |c, i|
266 commits.reverse.each_with_index do |c, i|
264 h = {}
267 h = {}
265 h[:parents] = c.parents.collect do |p|
268 h[:parents] = c.parents.collect do |p|
266 [p.scmid, 0, 0]
269 [p.scmid, 0, 0]
267 end
270 end
268 h[:rdmid] = i
271 h[:rdmid] = i
269 h[:space] = 0
272 h[:space] = 0
270 h[:refs] = refs_map[c.scmid].join(" ") if refs_map.include? c.scmid
273 h[:refs] = refs_map[c.scmid].join(" ") if refs_map.include? c.scmid
271 h[:scmid] = c.scmid
274 h[:scmid] = c.scmid
272 h[:href] = href_proc.call(c.scmid)
275 h[:href] = href_proc.call(c.scmid)
273 commit_hashes << h
276 commit_hashes << h
274 map[c.scmid] = h
277 map[c.scmid] = h
275 end
278 end
276 heads.sort! do |a,b|
279 heads.sort! do |a,b|
277 a.to_s <=> b.to_s
280 a.to_s <=> b.to_s
278 end
281 end
279 j = 0
282 j = 0
280 heads.each do |h|
283 heads.each do |h|
281 if map.include? h.scmid then
284 if map.include? h.scmid then
282 j = mark_chain(j += 1, map[h.scmid], map)
285 j = mark_chain(j += 1, map[h.scmid], map)
283 end
286 end
284 end
287 end
285 # when no head matched anything use first commit
288 # when no head matched anything use first commit
286 if j == 0 then
289 if j == 0 then
287 mark_chain(j += 1, map.values.first, map)
290 mark_chain(j += 1, map.values.first, map)
288 end
291 end
289 map
292 map
290 end
293 end
291
294
292 def mark_chain(mark, commit, map)
295 def mark_chain(mark, commit, map)
293 stack = [[mark, commit]]
296 stack = [[mark, commit]]
294 markmax = mark
297 markmax = mark
295 until stack.empty?
298 until stack.empty?
296 current = stack.pop
299 current = stack.pop
297 m, commit = current
300 m, commit = current
298 commit[:space] = m if commit[:space] == 0
301 commit[:space] = m if commit[:space] == 0
299 m1 = m - 1
302 m1 = m - 1
300 commit[:parents].each_with_index do |p, i|
303 commit[:parents].each_with_index do |p, i|
301 psha = p[0]
304 psha = p[0]
302 if map.include? psha and map[psha][:space] == 0 then
305 if map.include? psha and map[psha][:space] == 0 then
303 stack << [m1 += 1, map[psha]] if i == 0
306 stack << [m1 += 1, map[psha]] if i == 0
304 stack = [[m1 += 1, map[psha]]] + stack if i > 0
307 stack = [[m1 += 1, map[psha]]] + stack if i > 0
305 end
308 end
306 end
309 end
307 markmax = m1 if markmax < m1
310 markmax = m1 if markmax < m1
308 end
311 end
309 markmax
312 markmax
310 end
313 end
311 end
314 end
@@ -1,888 +1,889
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :destroy, :include => :author
46 has_many :news, :dependent => :destroy, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :dependent => :destroy
49 has_one :repository, :conditions => ["is_default = ?", true]
50 has_many :repositories, :dependent => :destroy
50 has_many :changesets, :through => :repository
51 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
52 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
53 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
54 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
55 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
56 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
58 :association_foreign_key => 'custom_field_id'
58
59
59 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_attachable :view_permission => :view_files,
61 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
62 :delete_permission => :manage_files
62
63
63 acts_as_customizable
64 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
68 :author => nil
68
69
69 attr_protected :status
70 attr_protected :status
70
71
71 validates_presence_of :name, :identifier
72 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
73 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
74 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
75 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
78 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
80 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
81 validates_exclusion_of :identifier, :in => %w( new )
81
82
82 before_destroy :delete_all_members
83 before_destroy :delete_all_members
83
84
84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
87 named_scope :all_public, { :conditions => { :is_public => true } }
88 named_scope :all_public, { :conditions => { :is_public => true } }
88 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
89 named_scope :like, lambda {|arg|
90 named_scope :like, lambda {|arg|
90 if arg.blank?
91 if arg.blank?
91 {}
92 {}
92 else
93 else
93 pattern = "%#{arg.to_s.strip.downcase}%"
94 pattern = "%#{arg.to_s.strip.downcase}%"
94 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
95 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
95 end
96 end
96 }
97 }
97
98
98 def initialize(attributes=nil, *args)
99 def initialize(attributes=nil, *args)
99 super
100 super
100
101
101 initialized = (attributes || {}).stringify_keys
102 initialized = (attributes || {}).stringify_keys
102 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
103 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
103 self.identifier = Project.next_identifier
104 self.identifier = Project.next_identifier
104 end
105 end
105 if !initialized.key?('is_public')
106 if !initialized.key?('is_public')
106 self.is_public = Setting.default_projects_public?
107 self.is_public = Setting.default_projects_public?
107 end
108 end
108 if !initialized.key?('enabled_module_names')
109 if !initialized.key?('enabled_module_names')
109 self.enabled_module_names = Setting.default_projects_modules
110 self.enabled_module_names = Setting.default_projects_modules
110 end
111 end
111 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
112 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
112 self.trackers = Tracker.all
113 self.trackers = Tracker.all
113 end
114 end
114 end
115 end
115
116
116 def identifier=(identifier)
117 def identifier=(identifier)
117 super unless identifier_frozen?
118 super unless identifier_frozen?
118 end
119 end
119
120
120 def identifier_frozen?
121 def identifier_frozen?
121 errors[:identifier].nil? && !(new_record? || identifier.blank?)
122 errors[:identifier].nil? && !(new_record? || identifier.blank?)
122 end
123 end
123
124
124 # returns latest created projects
125 # returns latest created projects
125 # non public projects will be returned only if user is a member of those
126 # non public projects will be returned only if user is a member of those
126 def self.latest(user=nil, count=5)
127 def self.latest(user=nil, count=5)
127 visible(user).find(:all, :limit => count, :order => "created_on DESC")
128 visible(user).find(:all, :limit => count, :order => "created_on DESC")
128 end
129 end
129
130
130 # Returns true if the project is visible to +user+ or to the current user.
131 # Returns true if the project is visible to +user+ or to the current user.
131 def visible?(user=User.current)
132 def visible?(user=User.current)
132 user.allowed_to?(:view_project, self)
133 user.allowed_to?(:view_project, self)
133 end
134 end
134
135
135 # Returns a SQL conditions string used to find all projects visible by the specified user.
136 # Returns a SQL conditions string used to find all projects visible by the specified user.
136 #
137 #
137 # Examples:
138 # Examples:
138 # Project.visible_condition(admin) => "projects.status = 1"
139 # Project.visible_condition(admin) => "projects.status = 1"
139 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
140 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
140 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
141 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
141 def self.visible_condition(user, options={})
142 def self.visible_condition(user, options={})
142 allowed_to_condition(user, :view_project, options)
143 allowed_to_condition(user, :view_project, options)
143 end
144 end
144
145
145 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
146 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
146 #
147 #
147 # Valid options:
148 # Valid options:
148 # * :project => limit the condition to project
149 # * :project => limit the condition to project
149 # * :with_subprojects => limit the condition to project and its subprojects
150 # * :with_subprojects => limit the condition to project and its subprojects
150 # * :member => limit the condition to the user projects
151 # * :member => limit the condition to the user projects
151 def self.allowed_to_condition(user, permission, options={})
152 def self.allowed_to_condition(user, permission, options={})
152 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
153 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
153 if perm = Redmine::AccessControl.permission(permission)
154 if perm = Redmine::AccessControl.permission(permission)
154 unless perm.project_module.nil?
155 unless perm.project_module.nil?
155 # If the permission belongs to a project module, make sure the module is enabled
156 # If the permission belongs to a project module, make sure the module is enabled
156 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
157 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
157 end
158 end
158 end
159 end
159 if options[:project]
160 if options[:project]
160 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
161 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
161 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
162 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
162 base_statement = "(#{project_statement}) AND (#{base_statement})"
163 base_statement = "(#{project_statement}) AND (#{base_statement})"
163 end
164 end
164
165
165 if user.admin?
166 if user.admin?
166 base_statement
167 base_statement
167 else
168 else
168 statement_by_role = {}
169 statement_by_role = {}
169 unless options[:member]
170 unless options[:member]
170 role = user.logged? ? Role.non_member : Role.anonymous
171 role = user.logged? ? Role.non_member : Role.anonymous
171 if role.allowed_to?(permission)
172 if role.allowed_to?(permission)
172 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
173 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
173 end
174 end
174 end
175 end
175 if user.logged?
176 if user.logged?
176 user.projects_by_role.each do |role, projects|
177 user.projects_by_role.each do |role, projects|
177 if role.allowed_to?(permission)
178 if role.allowed_to?(permission)
178 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
179 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
179 end
180 end
180 end
181 end
181 end
182 end
182 if statement_by_role.empty?
183 if statement_by_role.empty?
183 "1=0"
184 "1=0"
184 else
185 else
185 if block_given?
186 if block_given?
186 statement_by_role.each do |role, statement|
187 statement_by_role.each do |role, statement|
187 if s = yield(role, user)
188 if s = yield(role, user)
188 statement_by_role[role] = "(#{statement} AND (#{s}))"
189 statement_by_role[role] = "(#{statement} AND (#{s}))"
189 end
190 end
190 end
191 end
191 end
192 end
192 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
193 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
193 end
194 end
194 end
195 end
195 end
196 end
196
197
197 # Returns the Systemwide and project specific activities
198 # Returns the Systemwide and project specific activities
198 def activities(include_inactive=false)
199 def activities(include_inactive=false)
199 if include_inactive
200 if include_inactive
200 return all_activities
201 return all_activities
201 else
202 else
202 return active_activities
203 return active_activities
203 end
204 end
204 end
205 end
205
206
206 # Will create a new Project specific Activity or update an existing one
207 # Will create a new Project specific Activity or update an existing one
207 #
208 #
208 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
209 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
209 # does not successfully save.
210 # does not successfully save.
210 def update_or_create_time_entry_activity(id, activity_hash)
211 def update_or_create_time_entry_activity(id, activity_hash)
211 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
212 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
212 self.create_time_entry_activity_if_needed(activity_hash)
213 self.create_time_entry_activity_if_needed(activity_hash)
213 else
214 else
214 activity = project.time_entry_activities.find_by_id(id.to_i)
215 activity = project.time_entry_activities.find_by_id(id.to_i)
215 activity.update_attributes(activity_hash) if activity
216 activity.update_attributes(activity_hash) if activity
216 end
217 end
217 end
218 end
218
219
219 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
220 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
220 #
221 #
221 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
222 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
222 # does not successfully save.
223 # does not successfully save.
223 def create_time_entry_activity_if_needed(activity)
224 def create_time_entry_activity_if_needed(activity)
224 if activity['parent_id']
225 if activity['parent_id']
225
226
226 parent_activity = TimeEntryActivity.find(activity['parent_id'])
227 parent_activity = TimeEntryActivity.find(activity['parent_id'])
227 activity['name'] = parent_activity.name
228 activity['name'] = parent_activity.name
228 activity['position'] = parent_activity.position
229 activity['position'] = parent_activity.position
229
230
230 if Enumeration.overridding_change?(activity, parent_activity)
231 if Enumeration.overridding_change?(activity, parent_activity)
231 project_activity = self.time_entry_activities.create(activity)
232 project_activity = self.time_entry_activities.create(activity)
232
233
233 if project_activity.new_record?
234 if project_activity.new_record?
234 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
235 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
235 else
236 else
236 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
237 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
237 end
238 end
238 end
239 end
239 end
240 end
240 end
241 end
241
242
242 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
243 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
243 #
244 #
244 # Examples:
245 # Examples:
245 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
246 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
246 # project.project_condition(false) => "projects.id = 1"
247 # project.project_condition(false) => "projects.id = 1"
247 def project_condition(with_subprojects)
248 def project_condition(with_subprojects)
248 cond = "#{Project.table_name}.id = #{id}"
249 cond = "#{Project.table_name}.id = #{id}"
249 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
250 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
250 cond
251 cond
251 end
252 end
252
253
253 def self.find(*args)
254 def self.find(*args)
254 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
255 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
255 project = find_by_identifier(*args)
256 project = find_by_identifier(*args)
256 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
257 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
257 project
258 project
258 else
259 else
259 super
260 super
260 end
261 end
261 end
262 end
262
263
263 def to_param
264 def to_param
264 # id is used for projects with a numeric identifier (compatibility)
265 # id is used for projects with a numeric identifier (compatibility)
265 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
266 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
266 end
267 end
267
268
268 def active?
269 def active?
269 self.status == STATUS_ACTIVE
270 self.status == STATUS_ACTIVE
270 end
271 end
271
272
272 def archived?
273 def archived?
273 self.status == STATUS_ARCHIVED
274 self.status == STATUS_ARCHIVED
274 end
275 end
275
276
276 # Archives the project and its descendants
277 # Archives the project and its descendants
277 def archive
278 def archive
278 # Check that there is no issue of a non descendant project that is assigned
279 # Check that there is no issue of a non descendant project that is assigned
279 # to one of the project or descendant versions
280 # to one of the project or descendant versions
280 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
281 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
281 if v_ids.any? && Issue.find(:first, :include => :project,
282 if v_ids.any? && Issue.find(:first, :include => :project,
282 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
283 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
283 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
284 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
284 return false
285 return false
285 end
286 end
286 Project.transaction do
287 Project.transaction do
287 archive!
288 archive!
288 end
289 end
289 true
290 true
290 end
291 end
291
292
292 # Unarchives the project
293 # Unarchives the project
293 # All its ancestors must be active
294 # All its ancestors must be active
294 def unarchive
295 def unarchive
295 return false if ancestors.detect {|a| !a.active?}
296 return false if ancestors.detect {|a| !a.active?}
296 update_attribute :status, STATUS_ACTIVE
297 update_attribute :status, STATUS_ACTIVE
297 end
298 end
298
299
299 # Returns an array of projects the project can be moved to
300 # Returns an array of projects the project can be moved to
300 # by the current user
301 # by the current user
301 def allowed_parents
302 def allowed_parents
302 return @allowed_parents if @allowed_parents
303 return @allowed_parents if @allowed_parents
303 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
304 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
304 @allowed_parents = @allowed_parents - self_and_descendants
305 @allowed_parents = @allowed_parents - self_and_descendants
305 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
306 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
306 @allowed_parents << nil
307 @allowed_parents << nil
307 end
308 end
308 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
309 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
309 @allowed_parents << parent
310 @allowed_parents << parent
310 end
311 end
311 @allowed_parents
312 @allowed_parents
312 end
313 end
313
314
314 # Sets the parent of the project with authorization check
315 # Sets the parent of the project with authorization check
315 def set_allowed_parent!(p)
316 def set_allowed_parent!(p)
316 unless p.nil? || p.is_a?(Project)
317 unless p.nil? || p.is_a?(Project)
317 if p.to_s.blank?
318 if p.to_s.blank?
318 p = nil
319 p = nil
319 else
320 else
320 p = Project.find_by_id(p)
321 p = Project.find_by_id(p)
321 return false unless p
322 return false unless p
322 end
323 end
323 end
324 end
324 if p.nil?
325 if p.nil?
325 if !new_record? && allowed_parents.empty?
326 if !new_record? && allowed_parents.empty?
326 return false
327 return false
327 end
328 end
328 elsif !allowed_parents.include?(p)
329 elsif !allowed_parents.include?(p)
329 return false
330 return false
330 end
331 end
331 set_parent!(p)
332 set_parent!(p)
332 end
333 end
333
334
334 # Sets the parent of the project
335 # Sets the parent of the project
335 # Argument can be either a Project, a String, a Fixnum or nil
336 # Argument can be either a Project, a String, a Fixnum or nil
336 def set_parent!(p)
337 def set_parent!(p)
337 unless p.nil? || p.is_a?(Project)
338 unless p.nil? || p.is_a?(Project)
338 if p.to_s.blank?
339 if p.to_s.blank?
339 p = nil
340 p = nil
340 else
341 else
341 p = Project.find_by_id(p)
342 p = Project.find_by_id(p)
342 return false unless p
343 return false unless p
343 end
344 end
344 end
345 end
345 if p == parent && !p.nil?
346 if p == parent && !p.nil?
346 # Nothing to do
347 # Nothing to do
347 true
348 true
348 elsif p.nil? || (p.active? && move_possible?(p))
349 elsif p.nil? || (p.active? && move_possible?(p))
349 # Insert the project so that target's children or root projects stay alphabetically sorted
350 # Insert the project so that target's children or root projects stay alphabetically sorted
350 sibs = (p.nil? ? self.class.roots : p.children)
351 sibs = (p.nil? ? self.class.roots : p.children)
351 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
352 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
352 if to_be_inserted_before
353 if to_be_inserted_before
353 move_to_left_of(to_be_inserted_before)
354 move_to_left_of(to_be_inserted_before)
354 elsif p.nil?
355 elsif p.nil?
355 if sibs.empty?
356 if sibs.empty?
356 # move_to_root adds the project in first (ie. left) position
357 # move_to_root adds the project in first (ie. left) position
357 move_to_root
358 move_to_root
358 else
359 else
359 move_to_right_of(sibs.last) unless self == sibs.last
360 move_to_right_of(sibs.last) unless self == sibs.last
360 end
361 end
361 else
362 else
362 # move_to_child_of adds the project in last (ie.right) position
363 # move_to_child_of adds the project in last (ie.right) position
363 move_to_child_of(p)
364 move_to_child_of(p)
364 end
365 end
365 Issue.update_versions_from_hierarchy_change(self)
366 Issue.update_versions_from_hierarchy_change(self)
366 true
367 true
367 else
368 else
368 # Can not move to the given target
369 # Can not move to the given target
369 false
370 false
370 end
371 end
371 end
372 end
372
373
373 # Returns an array of the trackers used by the project and its active sub projects
374 # Returns an array of the trackers used by the project and its active sub projects
374 def rolled_up_trackers
375 def rolled_up_trackers
375 @rolled_up_trackers ||=
376 @rolled_up_trackers ||=
376 Tracker.find(:all, :joins => :projects,
377 Tracker.find(:all, :joins => :projects,
377 :select => "DISTINCT #{Tracker.table_name}.*",
378 :select => "DISTINCT #{Tracker.table_name}.*",
378 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
379 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
379 :order => "#{Tracker.table_name}.position")
380 :order => "#{Tracker.table_name}.position")
380 end
381 end
381
382
382 # Closes open and locked project versions that are completed
383 # Closes open and locked project versions that are completed
383 def close_completed_versions
384 def close_completed_versions
384 Version.transaction do
385 Version.transaction do
385 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
386 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
386 if version.completed?
387 if version.completed?
387 version.update_attribute(:status, 'closed')
388 version.update_attribute(:status, 'closed')
388 end
389 end
389 end
390 end
390 end
391 end
391 end
392 end
392
393
393 # Returns a scope of the Versions on subprojects
394 # Returns a scope of the Versions on subprojects
394 def rolled_up_versions
395 def rolled_up_versions
395 @rolled_up_versions ||=
396 @rolled_up_versions ||=
396 Version.scoped(:include => :project,
397 Version.scoped(:include => :project,
397 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
398 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
398 end
399 end
399
400
400 # Returns a scope of the Versions used by the project
401 # Returns a scope of the Versions used by the project
401 def shared_versions
402 def shared_versions
402 @shared_versions ||= begin
403 @shared_versions ||= begin
403 r = root? ? self : root
404 r = root? ? self : root
404 Version.scoped(:include => :project,
405 Version.scoped(:include => :project,
405 :conditions => "#{Project.table_name}.id = #{id}" +
406 :conditions => "#{Project.table_name}.id = #{id}" +
406 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
407 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
407 " #{Version.table_name}.sharing = 'system'" +
408 " #{Version.table_name}.sharing = 'system'" +
408 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
409 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
409 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
410 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
410 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
411 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
411 "))")
412 "))")
412 end
413 end
413 end
414 end
414
415
415 # Returns a hash of project users grouped by role
416 # Returns a hash of project users grouped by role
416 def users_by_role
417 def users_by_role
417 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
418 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
418 m.roles.each do |r|
419 m.roles.each do |r|
419 h[r] ||= []
420 h[r] ||= []
420 h[r] << m.user
421 h[r] << m.user
421 end
422 end
422 h
423 h
423 end
424 end
424 end
425 end
425
426
426 # Deletes all project's members
427 # Deletes all project's members
427 def delete_all_members
428 def delete_all_members
428 me, mr = Member.table_name, MemberRole.table_name
429 me, mr = Member.table_name, MemberRole.table_name
429 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
430 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
430 Member.delete_all(['project_id = ?', id])
431 Member.delete_all(['project_id = ?', id])
431 end
432 end
432
433
433 # Users/groups issues can be assigned to
434 # Users/groups issues can be assigned to
434 def assignable_users
435 def assignable_users
435 assignable = Setting.issue_group_assignment? ? member_principals : members
436 assignable = Setting.issue_group_assignment? ? member_principals : members
436 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
437 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
437 end
438 end
438
439
439 # Returns the mail adresses of users that should be always notified on project events
440 # Returns the mail adresses of users that should be always notified on project events
440 def recipients
441 def recipients
441 notified_users.collect {|user| user.mail}
442 notified_users.collect {|user| user.mail}
442 end
443 end
443
444
444 # Returns the users that should be notified on project events
445 # Returns the users that should be notified on project events
445 def notified_users
446 def notified_users
446 # TODO: User part should be extracted to User#notify_about?
447 # TODO: User part should be extracted to User#notify_about?
447 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
448 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
448 end
449 end
449
450
450 # Returns an array of all custom fields enabled for project issues
451 # Returns an array of all custom fields enabled for project issues
451 # (explictly associated custom fields and custom fields enabled for all projects)
452 # (explictly associated custom fields and custom fields enabled for all projects)
452 def all_issue_custom_fields
453 def all_issue_custom_fields
453 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
454 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
454 end
455 end
455
456
456 # Returns an array of all custom fields enabled for project time entries
457 # Returns an array of all custom fields enabled for project time entries
457 # (explictly associated custom fields and custom fields enabled for all projects)
458 # (explictly associated custom fields and custom fields enabled for all projects)
458 def all_time_entry_custom_fields
459 def all_time_entry_custom_fields
459 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
460 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
460 end
461 end
461
462
462 def project
463 def project
463 self
464 self
464 end
465 end
465
466
466 def <=>(project)
467 def <=>(project)
467 name.downcase <=> project.name.downcase
468 name.downcase <=> project.name.downcase
468 end
469 end
469
470
470 def to_s
471 def to_s
471 name
472 name
472 end
473 end
473
474
474 # Returns a short description of the projects (first lines)
475 # Returns a short description of the projects (first lines)
475 def short_description(length = 255)
476 def short_description(length = 255)
476 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
477 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
477 end
478 end
478
479
479 def css_classes
480 def css_classes
480 s = 'project'
481 s = 'project'
481 s << ' root' if root?
482 s << ' root' if root?
482 s << ' child' if child?
483 s << ' child' if child?
483 s << (leaf? ? ' leaf' : ' parent')
484 s << (leaf? ? ' leaf' : ' parent')
484 s
485 s
485 end
486 end
486
487
487 # The earliest start date of a project, based on it's issues and versions
488 # The earliest start date of a project, based on it's issues and versions
488 def start_date
489 def start_date
489 [
490 [
490 issues.minimum('start_date'),
491 issues.minimum('start_date'),
491 shared_versions.collect(&:effective_date),
492 shared_versions.collect(&:effective_date),
492 shared_versions.collect(&:start_date)
493 shared_versions.collect(&:start_date)
493 ].flatten.compact.min
494 ].flatten.compact.min
494 end
495 end
495
496
496 # The latest due date of an issue or version
497 # The latest due date of an issue or version
497 def due_date
498 def due_date
498 [
499 [
499 issues.maximum('due_date'),
500 issues.maximum('due_date'),
500 shared_versions.collect(&:effective_date),
501 shared_versions.collect(&:effective_date),
501 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
502 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
502 ].flatten.compact.max
503 ].flatten.compact.max
503 end
504 end
504
505
505 def overdue?
506 def overdue?
506 active? && !due_date.nil? && (due_date < Date.today)
507 active? && !due_date.nil? && (due_date < Date.today)
507 end
508 end
508
509
509 # Returns the percent completed for this project, based on the
510 # Returns the percent completed for this project, based on the
510 # progress on it's versions.
511 # progress on it's versions.
511 def completed_percent(options={:include_subprojects => false})
512 def completed_percent(options={:include_subprojects => false})
512 if options.delete(:include_subprojects)
513 if options.delete(:include_subprojects)
513 total = self_and_descendants.collect(&:completed_percent).sum
514 total = self_and_descendants.collect(&:completed_percent).sum
514
515
515 total / self_and_descendants.count
516 total / self_and_descendants.count
516 else
517 else
517 if versions.count > 0
518 if versions.count > 0
518 total = versions.collect(&:completed_pourcent).sum
519 total = versions.collect(&:completed_pourcent).sum
519
520
520 total / versions.count
521 total / versions.count
521 else
522 else
522 100
523 100
523 end
524 end
524 end
525 end
525 end
526 end
526
527
527 # Return true if this project is allowed to do the specified action.
528 # Return true if this project is allowed to do the specified action.
528 # action can be:
529 # action can be:
529 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
530 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
530 # * a permission Symbol (eg. :edit_project)
531 # * a permission Symbol (eg. :edit_project)
531 def allows_to?(action)
532 def allows_to?(action)
532 if action.is_a? Hash
533 if action.is_a? Hash
533 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
534 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
534 else
535 else
535 allowed_permissions.include? action
536 allowed_permissions.include? action
536 end
537 end
537 end
538 end
538
539
539 def module_enabled?(module_name)
540 def module_enabled?(module_name)
540 module_name = module_name.to_s
541 module_name = module_name.to_s
541 enabled_modules.detect {|m| m.name == module_name}
542 enabled_modules.detect {|m| m.name == module_name}
542 end
543 end
543
544
544 def enabled_module_names=(module_names)
545 def enabled_module_names=(module_names)
545 if module_names && module_names.is_a?(Array)
546 if module_names && module_names.is_a?(Array)
546 module_names = module_names.collect(&:to_s).reject(&:blank?)
547 module_names = module_names.collect(&:to_s).reject(&:blank?)
547 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
548 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
548 else
549 else
549 enabled_modules.clear
550 enabled_modules.clear
550 end
551 end
551 end
552 end
552
553
553 # Returns an array of the enabled modules names
554 # Returns an array of the enabled modules names
554 def enabled_module_names
555 def enabled_module_names
555 enabled_modules.collect(&:name)
556 enabled_modules.collect(&:name)
556 end
557 end
557
558
558 # Enable a specific module
559 # Enable a specific module
559 #
560 #
560 # Examples:
561 # Examples:
561 # project.enable_module!(:issue_tracking)
562 # project.enable_module!(:issue_tracking)
562 # project.enable_module!("issue_tracking")
563 # project.enable_module!("issue_tracking")
563 def enable_module!(name)
564 def enable_module!(name)
564 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
565 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
565 end
566 end
566
567
567 # Disable a module if it exists
568 # Disable a module if it exists
568 #
569 #
569 # Examples:
570 # Examples:
570 # project.disable_module!(:issue_tracking)
571 # project.disable_module!(:issue_tracking)
571 # project.disable_module!("issue_tracking")
572 # project.disable_module!("issue_tracking")
572 # project.disable_module!(project.enabled_modules.first)
573 # project.disable_module!(project.enabled_modules.first)
573 def disable_module!(target)
574 def disable_module!(target)
574 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
575 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
575 target.destroy unless target.blank?
576 target.destroy unless target.blank?
576 end
577 end
577
578
578 safe_attributes 'name',
579 safe_attributes 'name',
579 'description',
580 'description',
580 'homepage',
581 'homepage',
581 'is_public',
582 'is_public',
582 'identifier',
583 'identifier',
583 'custom_field_values',
584 'custom_field_values',
584 'custom_fields',
585 'custom_fields',
585 'tracker_ids',
586 'tracker_ids',
586 'issue_custom_field_ids'
587 'issue_custom_field_ids'
587
588
588 safe_attributes 'enabled_module_names',
589 safe_attributes 'enabled_module_names',
589 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
590 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
590
591
591 # Returns an array of projects that are in this project's hierarchy
592 # Returns an array of projects that are in this project's hierarchy
592 #
593 #
593 # Example: parents, children, siblings
594 # Example: parents, children, siblings
594 def hierarchy
595 def hierarchy
595 parents = project.self_and_ancestors || []
596 parents = project.self_and_ancestors || []
596 descendants = project.descendants || []
597 descendants = project.descendants || []
597 project_hierarchy = parents | descendants # Set union
598 project_hierarchy = parents | descendants # Set union
598 end
599 end
599
600
600 # Returns an auto-generated project identifier based on the last identifier used
601 # Returns an auto-generated project identifier based on the last identifier used
601 def self.next_identifier
602 def self.next_identifier
602 p = Project.find(:first, :order => 'created_on DESC')
603 p = Project.find(:first, :order => 'created_on DESC')
603 p.nil? ? nil : p.identifier.to_s.succ
604 p.nil? ? nil : p.identifier.to_s.succ
604 end
605 end
605
606
606 # Copies and saves the Project instance based on the +project+.
607 # Copies and saves the Project instance based on the +project+.
607 # Duplicates the source project's:
608 # Duplicates the source project's:
608 # * Wiki
609 # * Wiki
609 # * Versions
610 # * Versions
610 # * Categories
611 # * Categories
611 # * Issues
612 # * Issues
612 # * Members
613 # * Members
613 # * Queries
614 # * Queries
614 #
615 #
615 # Accepts an +options+ argument to specify what to copy
616 # Accepts an +options+ argument to specify what to copy
616 #
617 #
617 # Examples:
618 # Examples:
618 # project.copy(1) # => copies everything
619 # project.copy(1) # => copies everything
619 # project.copy(1, :only => 'members') # => copies members only
620 # project.copy(1, :only => 'members') # => copies members only
620 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
621 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
621 def copy(project, options={})
622 def copy(project, options={})
622 project = project.is_a?(Project) ? project : Project.find(project)
623 project = project.is_a?(Project) ? project : Project.find(project)
623
624
624 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
625 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
625 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
626 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
626
627
627 Project.transaction do
628 Project.transaction do
628 if save
629 if save
629 reload
630 reload
630 to_be_copied.each do |name|
631 to_be_copied.each do |name|
631 send "copy_#{name}", project
632 send "copy_#{name}", project
632 end
633 end
633 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
634 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
634 save
635 save
635 end
636 end
636 end
637 end
637 end
638 end
638
639
639
640
640 # Copies +project+ and returns the new instance. This will not save
641 # Copies +project+ and returns the new instance. This will not save
641 # the copy
642 # the copy
642 def self.copy_from(project)
643 def self.copy_from(project)
643 begin
644 begin
644 project = project.is_a?(Project) ? project : Project.find(project)
645 project = project.is_a?(Project) ? project : Project.find(project)
645 if project
646 if project
646 # clear unique attributes
647 # clear unique attributes
647 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
648 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
648 copy = Project.new(attributes)
649 copy = Project.new(attributes)
649 copy.enabled_modules = project.enabled_modules
650 copy.enabled_modules = project.enabled_modules
650 copy.trackers = project.trackers
651 copy.trackers = project.trackers
651 copy.custom_values = project.custom_values.collect {|v| v.clone}
652 copy.custom_values = project.custom_values.collect {|v| v.clone}
652 copy.issue_custom_fields = project.issue_custom_fields
653 copy.issue_custom_fields = project.issue_custom_fields
653 return copy
654 return copy
654 else
655 else
655 return nil
656 return nil
656 end
657 end
657 rescue ActiveRecord::RecordNotFound
658 rescue ActiveRecord::RecordNotFound
658 return nil
659 return nil
659 end
660 end
660 end
661 end
661
662
662 # Yields the given block for each project with its level in the tree
663 # Yields the given block for each project with its level in the tree
663 def self.project_tree(projects, &block)
664 def self.project_tree(projects, &block)
664 ancestors = []
665 ancestors = []
665 projects.sort_by(&:lft).each do |project|
666 projects.sort_by(&:lft).each do |project|
666 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
667 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
667 ancestors.pop
668 ancestors.pop
668 end
669 end
669 yield project, ancestors.size
670 yield project, ancestors.size
670 ancestors << project
671 ancestors << project
671 end
672 end
672 end
673 end
673
674
674 private
675 private
675
676
676 # Copies wiki from +project+
677 # Copies wiki from +project+
677 def copy_wiki(project)
678 def copy_wiki(project)
678 # Check that the source project has a wiki first
679 # Check that the source project has a wiki first
679 unless project.wiki.nil?
680 unless project.wiki.nil?
680 self.wiki ||= Wiki.new
681 self.wiki ||= Wiki.new
681 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
682 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
682 wiki_pages_map = {}
683 wiki_pages_map = {}
683 project.wiki.pages.each do |page|
684 project.wiki.pages.each do |page|
684 # Skip pages without content
685 # Skip pages without content
685 next if page.content.nil?
686 next if page.content.nil?
686 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
687 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
687 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
688 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
688 new_wiki_page.content = new_wiki_content
689 new_wiki_page.content = new_wiki_content
689 wiki.pages << new_wiki_page
690 wiki.pages << new_wiki_page
690 wiki_pages_map[page.id] = new_wiki_page
691 wiki_pages_map[page.id] = new_wiki_page
691 end
692 end
692 wiki.save
693 wiki.save
693 # Reproduce page hierarchy
694 # Reproduce page hierarchy
694 project.wiki.pages.each do |page|
695 project.wiki.pages.each do |page|
695 if page.parent_id && wiki_pages_map[page.id]
696 if page.parent_id && wiki_pages_map[page.id]
696 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
697 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
697 wiki_pages_map[page.id].save
698 wiki_pages_map[page.id].save
698 end
699 end
699 end
700 end
700 end
701 end
701 end
702 end
702
703
703 # Copies versions from +project+
704 # Copies versions from +project+
704 def copy_versions(project)
705 def copy_versions(project)
705 project.versions.each do |version|
706 project.versions.each do |version|
706 new_version = Version.new
707 new_version = Version.new
707 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
708 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
708 self.versions << new_version
709 self.versions << new_version
709 end
710 end
710 end
711 end
711
712
712 # Copies issue categories from +project+
713 # Copies issue categories from +project+
713 def copy_issue_categories(project)
714 def copy_issue_categories(project)
714 project.issue_categories.each do |issue_category|
715 project.issue_categories.each do |issue_category|
715 new_issue_category = IssueCategory.new
716 new_issue_category = IssueCategory.new
716 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
717 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
717 self.issue_categories << new_issue_category
718 self.issue_categories << new_issue_category
718 end
719 end
719 end
720 end
720
721
721 # Copies issues from +project+
722 # Copies issues from +project+
722 # Note: issues assigned to a closed version won't be copied due to validation rules
723 # Note: issues assigned to a closed version won't be copied due to validation rules
723 def copy_issues(project)
724 def copy_issues(project)
724 # Stores the source issue id as a key and the copied issues as the
725 # Stores the source issue id as a key and the copied issues as the
725 # value. Used to map the two togeather for issue relations.
726 # value. Used to map the two togeather for issue relations.
726 issues_map = {}
727 issues_map = {}
727
728
728 # Get issues sorted by root_id, lft so that parent issues
729 # Get issues sorted by root_id, lft so that parent issues
729 # get copied before their children
730 # get copied before their children
730 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
731 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
731 new_issue = Issue.new
732 new_issue = Issue.new
732 new_issue.copy_from(issue)
733 new_issue.copy_from(issue)
733 new_issue.project = self
734 new_issue.project = self
734 # Reassign fixed_versions by name, since names are unique per
735 # Reassign fixed_versions by name, since names are unique per
735 # project and the versions for self are not yet saved
736 # project and the versions for self are not yet saved
736 if issue.fixed_version
737 if issue.fixed_version
737 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
738 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
738 end
739 end
739 # Reassign the category by name, since names are unique per
740 # Reassign the category by name, since names are unique per
740 # project and the categories for self are not yet saved
741 # project and the categories for self are not yet saved
741 if issue.category
742 if issue.category
742 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
743 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
743 end
744 end
744 # Parent issue
745 # Parent issue
745 if issue.parent_id
746 if issue.parent_id
746 if copied_parent = issues_map[issue.parent_id]
747 if copied_parent = issues_map[issue.parent_id]
747 new_issue.parent_issue_id = copied_parent.id
748 new_issue.parent_issue_id = copied_parent.id
748 end
749 end
749 end
750 end
750
751
751 self.issues << new_issue
752 self.issues << new_issue
752 if new_issue.new_record?
753 if new_issue.new_record?
753 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
754 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
754 else
755 else
755 issues_map[issue.id] = new_issue unless new_issue.new_record?
756 issues_map[issue.id] = new_issue unless new_issue.new_record?
756 end
757 end
757 end
758 end
758
759
759 # Relations after in case issues related each other
760 # Relations after in case issues related each other
760 project.issues.each do |issue|
761 project.issues.each do |issue|
761 new_issue = issues_map[issue.id]
762 new_issue = issues_map[issue.id]
762 unless new_issue
763 unless new_issue
763 # Issue was not copied
764 # Issue was not copied
764 next
765 next
765 end
766 end
766
767
767 # Relations
768 # Relations
768 issue.relations_from.each do |source_relation|
769 issue.relations_from.each do |source_relation|
769 new_issue_relation = IssueRelation.new
770 new_issue_relation = IssueRelation.new
770 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
771 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
771 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
772 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
772 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
773 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
773 new_issue_relation.issue_to = source_relation.issue_to
774 new_issue_relation.issue_to = source_relation.issue_to
774 end
775 end
775 new_issue.relations_from << new_issue_relation
776 new_issue.relations_from << new_issue_relation
776 end
777 end
777
778
778 issue.relations_to.each do |source_relation|
779 issue.relations_to.each do |source_relation|
779 new_issue_relation = IssueRelation.new
780 new_issue_relation = IssueRelation.new
780 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
781 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
781 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
782 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
782 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
783 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
783 new_issue_relation.issue_from = source_relation.issue_from
784 new_issue_relation.issue_from = source_relation.issue_from
784 end
785 end
785 new_issue.relations_to << new_issue_relation
786 new_issue.relations_to << new_issue_relation
786 end
787 end
787 end
788 end
788 end
789 end
789
790
790 # Copies members from +project+
791 # Copies members from +project+
791 def copy_members(project)
792 def copy_members(project)
792 # Copy users first, then groups to handle members with inherited and given roles
793 # Copy users first, then groups to handle members with inherited and given roles
793 members_to_copy = []
794 members_to_copy = []
794 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
795 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
795 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
796 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
796
797
797 members_to_copy.each do |member|
798 members_to_copy.each do |member|
798 new_member = Member.new
799 new_member = Member.new
799 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
800 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
800 # only copy non inherited roles
801 # only copy non inherited roles
801 # inherited roles will be added when copying the group membership
802 # inherited roles will be added when copying the group membership
802 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
803 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
803 next if role_ids.empty?
804 next if role_ids.empty?
804 new_member.role_ids = role_ids
805 new_member.role_ids = role_ids
805 new_member.project = self
806 new_member.project = self
806 self.members << new_member
807 self.members << new_member
807 end
808 end
808 end
809 end
809
810
810 # Copies queries from +project+
811 # Copies queries from +project+
811 def copy_queries(project)
812 def copy_queries(project)
812 project.queries.each do |query|
813 project.queries.each do |query|
813 new_query = ::Query.new
814 new_query = ::Query.new
814 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
815 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
815 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
816 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
816 new_query.project = self
817 new_query.project = self
817 new_query.user_id = query.user_id
818 new_query.user_id = query.user_id
818 self.queries << new_query
819 self.queries << new_query
819 end
820 end
820 end
821 end
821
822
822 # Copies boards from +project+
823 # Copies boards from +project+
823 def copy_boards(project)
824 def copy_boards(project)
824 project.boards.each do |board|
825 project.boards.each do |board|
825 new_board = Board.new
826 new_board = Board.new
826 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
827 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
827 new_board.project = self
828 new_board.project = self
828 self.boards << new_board
829 self.boards << new_board
829 end
830 end
830 end
831 end
831
832
832 def allowed_permissions
833 def allowed_permissions
833 @allowed_permissions ||= begin
834 @allowed_permissions ||= begin
834 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
835 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
835 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
836 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
836 end
837 end
837 end
838 end
838
839
839 def allowed_actions
840 def allowed_actions
840 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
841 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
841 end
842 end
842
843
843 # Returns all the active Systemwide and project specific activities
844 # Returns all the active Systemwide and project specific activities
844 def active_activities
845 def active_activities
845 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
846 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
846
847
847 if overridden_activity_ids.empty?
848 if overridden_activity_ids.empty?
848 return TimeEntryActivity.shared.active
849 return TimeEntryActivity.shared.active
849 else
850 else
850 return system_activities_and_project_overrides
851 return system_activities_and_project_overrides
851 end
852 end
852 end
853 end
853
854
854 # Returns all the Systemwide and project specific activities
855 # Returns all the Systemwide and project specific activities
855 # (inactive and active)
856 # (inactive and active)
856 def all_activities
857 def all_activities
857 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
858 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
858
859
859 if overridden_activity_ids.empty?
860 if overridden_activity_ids.empty?
860 return TimeEntryActivity.shared
861 return TimeEntryActivity.shared
861 else
862 else
862 return system_activities_and_project_overrides(true)
863 return system_activities_and_project_overrides(true)
863 end
864 end
864 end
865 end
865
866
866 # Returns the systemwide active activities merged with the project specific overrides
867 # Returns the systemwide active activities merged with the project specific overrides
867 def system_activities_and_project_overrides(include_inactive=false)
868 def system_activities_and_project_overrides(include_inactive=false)
868 if include_inactive
869 if include_inactive
869 return TimeEntryActivity.shared.
870 return TimeEntryActivity.shared.
870 find(:all,
871 find(:all,
871 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
872 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
872 self.time_entry_activities
873 self.time_entry_activities
873 else
874 else
874 return TimeEntryActivity.shared.active.
875 return TimeEntryActivity.shared.active.
875 find(:all,
876 find(:all,
876 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
877 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
877 self.time_entry_activities.active
878 self.time_entry_activities.active
878 end
879 end
879 end
880 end
880
881
881 # Archives subprojects recursively
882 # Archives subprojects recursively
882 def archive!
883 def archive!
883 children.each do |subproject|
884 children.each do |subproject|
884 subproject.send :archive!
885 subproject.send :archive!
885 end
886 end
886 update_attribute :status, STATUS_ARCHIVED
887 update_attribute :status, STATUS_ARCHIVED
887 end
888 end
888 end
889 end
@@ -1,345 +1,408
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 ScmFetchError < Exception; end
18 class ScmFetchError < Exception; end
19
19
20 class Repository < ActiveRecord::Base
20 class Repository < ActiveRecord::Base
21 include Redmine::Ciphering
21 include Redmine::Ciphering
22
22
23 belongs_to :project
23 belongs_to :project
24 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
24 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
25 has_many :changes, :through => :changesets
25 has_many :changes, :through => :changesets
26
26
27 serialize :extra_info
27 serialize :extra_info
28
28
29 before_save :check_default
30
29 # Raw SQL to delete changesets and changes in the database
31 # Raw SQL to delete changesets and changes in the database
30 # has_many :changesets, :dependent => :destroy is too slow for big repositories
32 # has_many :changesets, :dependent => :destroy is too slow for big repositories
31 before_destroy :clear_changesets
33 before_destroy :clear_changesets
32
34
33 validates_length_of :password, :maximum => 255, :allow_nil => true
35 validates_length_of :password, :maximum => 255, :allow_nil => true
36 validates_length_of :identifier, :maximum => 255, :allow_blank => true
37 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
38 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
39 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
40 # donwcase letters, digits, dashes but not digits only
41 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :allow_blank => true
34 # Checks if the SCM is enabled when creating a repository
42 # Checks if the SCM is enabled when creating a repository
35 validate :repo_create_validation, :on => :create
43 validate :repo_create_validation, :on => :create
36
44
37 def repo_create_validation
45 def repo_create_validation
38 unless Setting.enabled_scm.include?(self.class.name.demodulize)
46 unless Setting.enabled_scm.include?(self.class.name.demodulize)
39 errors.add(:type, :invalid)
47 errors.add(:type, :invalid)
40 end
48 end
41 end
49 end
42
50
43 def self.human_attribute_name(attribute_key_name, *args)
51 def self.human_attribute_name(attribute_key_name, *args)
44 attr_name = attribute_key_name
52 attr_name = attribute_key_name
45 if attr_name == "log_encoding"
53 if attr_name == "log_encoding"
46 attr_name = "commit_logs_encoding"
54 attr_name = "commit_logs_encoding"
47 end
55 end
48 super(attr_name, *args)
56 super(attr_name, *args)
49 end
57 end
50
58
51 alias :attributes_without_extra_info= :attributes=
59 alias :attributes_without_extra_info= :attributes=
52 def attributes=(new_attributes, guard_protected_attributes = true)
60 def attributes=(new_attributes, guard_protected_attributes = true)
53 return if new_attributes.nil?
61 return if new_attributes.nil?
54 attributes = new_attributes.dup
62 attributes = new_attributes.dup
55 attributes.stringify_keys!
63 attributes.stringify_keys!
56
64
57 p = {}
65 p = {}
58 p_extra = {}
66 p_extra = {}
59 attributes.each do |k, v|
67 attributes.each do |k, v|
60 if k =~ /^extra_/
68 if k =~ /^extra_/
61 p_extra[k] = v
69 p_extra[k] = v
62 else
70 else
63 p[k] = v
71 p[k] = v
64 end
72 end
65 end
73 end
66
74
67 send :attributes_without_extra_info=, p, guard_protected_attributes
75 send :attributes_without_extra_info=, p, guard_protected_attributes
76 if p_extra.keys.any?
68 merge_extra_info(p_extra)
77 merge_extra_info(p_extra)
69 end
78 end
79 end
70
80
71 # Removes leading and trailing whitespace
81 # Removes leading and trailing whitespace
72 def url=(arg)
82 def url=(arg)
73 write_attribute(:url, arg ? arg.to_s.strip : nil)
83 write_attribute(:url, arg ? arg.to_s.strip : nil)
74 end
84 end
75
85
76 # Removes leading and trailing whitespace
86 # Removes leading and trailing whitespace
77 def root_url=(arg)
87 def root_url=(arg)
78 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
88 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
79 end
89 end
80
90
81 def password
91 def password
82 read_ciphered_attribute(:password)
92 read_ciphered_attribute(:password)
83 end
93 end
84
94
85 def password=(arg)
95 def password=(arg)
86 write_ciphered_attribute(:password, arg)
96 write_ciphered_attribute(:password, arg)
87 end
97 end
88
98
89 def scm_adapter
99 def scm_adapter
90 self.class.scm_adapter_class
100 self.class.scm_adapter_class
91 end
101 end
92
102
93 def scm
103 def scm
94 @scm ||= self.scm_adapter.new(url, root_url,
104 @scm ||= self.scm_adapter.new(url, root_url,
95 login, password, path_encoding)
105 login, password, path_encoding)
96 update_attribute(:root_url, @scm.root_url) if root_url.blank?
106 update_attribute(:root_url, @scm.root_url) if root_url.blank?
97 @scm
107 @scm
98 end
108 end
99
109
100 def scm_name
110 def scm_name
101 self.class.scm_name
111 self.class.scm_name
102 end
112 end
103
113
114 def name
115 if is_default?
116 l(:field_repository_is_default)
117 elsif identifier.present?
118 identifier
119 else
120 scm_name
121 end
122 end
123
124 def identifier_param
125 if is_default?
126 nil
127 elsif identifier.present?
128 identifier
129 else
130 id.to_s
131 end
132 end
133
134 def <=>(repository)
135 if is_default?
136 -1
137 elsif repository.is_default?
138 1
139 else
140 identifier <=> repository.identifier
141 end
142 end
143
144 def self.find_by_identifier_param(param)
145 if param.to_s =~ /^\d+$/
146 find_by_id(param)
147 else
148 find_by_identifier(param)
149 end
150 end
151
104 def merge_extra_info(arg)
152 def merge_extra_info(arg)
105 h = extra_info || {}
153 h = extra_info || {}
106 return h if arg.nil?
154 return h if arg.nil?
107 h.merge!(arg)
155 h.merge!(arg)
108 write_attribute(:extra_info, h)
156 write_attribute(:extra_info, h)
109 end
157 end
110
158
111 def report_last_commit
159 def report_last_commit
112 true
160 true
113 end
161 end
114
162
115 def supports_cat?
163 def supports_cat?
116 scm.supports_cat?
164 scm.supports_cat?
117 end
165 end
118
166
119 def supports_annotate?
167 def supports_annotate?
120 scm.supports_annotate?
168 scm.supports_annotate?
121 end
169 end
122
170
123 def supports_all_revisions?
171 def supports_all_revisions?
124 true
172 true
125 end
173 end
126
174
127 def supports_directory_revisions?
175 def supports_directory_revisions?
128 false
176 false
129 end
177 end
130
178
131 def supports_revision_graph?
179 def supports_revision_graph?
132 false
180 false
133 end
181 end
134
182
135 def entry(path=nil, identifier=nil)
183 def entry(path=nil, identifier=nil)
136 scm.entry(path, identifier)
184 scm.entry(path, identifier)
137 end
185 end
138
186
139 def entries(path=nil, identifier=nil)
187 def entries(path=nil, identifier=nil)
140 scm.entries(path, identifier)
188 scm.entries(path, identifier)
141 end
189 end
142
190
143 def branches
191 def branches
144 scm.branches
192 scm.branches
145 end
193 end
146
194
147 def tags
195 def tags
148 scm.tags
196 scm.tags
149 end
197 end
150
198
151 def default_branch
199 def default_branch
152 nil
200 nil
153 end
201 end
154
202
155 def properties(path, identifier=nil)
203 def properties(path, identifier=nil)
156 scm.properties(path, identifier)
204 scm.properties(path, identifier)
157 end
205 end
158
206
159 def cat(path, identifier=nil)
207 def cat(path, identifier=nil)
160 scm.cat(path, identifier)
208 scm.cat(path, identifier)
161 end
209 end
162
210
163 def diff(path, rev, rev_to)
211 def diff(path, rev, rev_to)
164 scm.diff(path, rev, rev_to)
212 scm.diff(path, rev, rev_to)
165 end
213 end
166
214
167 def diff_format_revisions(cs, cs_to, sep=':')
215 def diff_format_revisions(cs, cs_to, sep=':')
168 text = ""
216 text = ""
169 text << cs_to.format_identifier + sep if cs_to
217 text << cs_to.format_identifier + sep if cs_to
170 text << cs.format_identifier if cs
218 text << cs.format_identifier if cs
171 text
219 text
172 end
220 end
173
221
174 # Returns a path relative to the url of the repository
222 # Returns a path relative to the url of the repository
175 def relative_path(path)
223 def relative_path(path)
176 path
224 path
177 end
225 end
178
226
179 # Finds and returns a revision with a number or the beginning of a hash
227 # Finds and returns a revision with a number or the beginning of a hash
180 def find_changeset_by_name(name)
228 def find_changeset_by_name(name)
181 return nil if name.blank?
229 return nil if name.blank?
182 changesets.find(:first, :conditions => (name.match(/^\d*$/) ?
230 changesets.find(:first, :conditions => (name.match(/^\d*$/) ?
183 ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
231 ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
184 end
232 end
185
233
186 def latest_changeset
234 def latest_changeset
187 @latest_changeset ||= changesets.find(:first)
235 @latest_changeset ||= changesets.find(:first)
188 end
236 end
189
237
190 # Returns the latest changesets for +path+
238 # Returns the latest changesets for +path+
191 # Default behaviour is to search in cached changesets
239 # Default behaviour is to search in cached changesets
192 def latest_changesets(path, rev, limit=10)
240 def latest_changesets(path, rev, limit=10)
193 if path.blank?
241 if path.blank?
194 changesets.find(
242 changesets.find(
195 :all,
243 :all,
196 :include => :user,
244 :include => :user,
197 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
245 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
198 :limit => limit)
246 :limit => limit)
199 else
247 else
200 changes.find(
248 changes.find(
201 :all,
249 :all,
202 :include => {:changeset => :user},
250 :include => {:changeset => :user},
203 :conditions => ["path = ?", path.with_leading_slash],
251 :conditions => ["path = ?", path.with_leading_slash],
204 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
252 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
205 :limit => limit
253 :limit => limit
206 ).collect(&:changeset)
254 ).collect(&:changeset)
207 end
255 end
208 end
256 end
209
257
210 def scan_changesets_for_issue_ids
258 def scan_changesets_for_issue_ids
211 self.changesets.each(&:scan_comment_for_issue_ids)
259 self.changesets.each(&:scan_comment_for_issue_ids)
212 end
260 end
213
261
214 # Returns an array of committers usernames and associated user_id
262 # Returns an array of committers usernames and associated user_id
215 def committers
263 def committers
216 @committers ||= Changeset.connection.select_rows(
264 @committers ||= Changeset.connection.select_rows(
217 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
265 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
218 end
266 end
219
267
220 # Maps committers username to a user ids
268 # Maps committers username to a user ids
221 def committer_ids=(h)
269 def committer_ids=(h)
222 if h.is_a?(Hash)
270 if h.is_a?(Hash)
223 committers.each do |committer, user_id|
271 committers.each do |committer, user_id|
224 new_user_id = h[committer]
272 new_user_id = h[committer]
225 if new_user_id && (new_user_id.to_i != user_id.to_i)
273 if new_user_id && (new_user_id.to_i != user_id.to_i)
226 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
274 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
227 Changeset.update_all(
275 Changeset.update_all(
228 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
276 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
229 ["repository_id = ? AND committer = ?", id, committer])
277 ["repository_id = ? AND committer = ?", id, committer])
230 end
278 end
231 end
279 end
232 @committers = nil
280 @committers = nil
233 @found_committer_users = nil
281 @found_committer_users = nil
234 true
282 true
235 else
283 else
236 false
284 false
237 end
285 end
238 end
286 end
239
287
240 # Returns the Redmine User corresponding to the given +committer+
288 # Returns the Redmine User corresponding to the given +committer+
241 # It will return nil if the committer is not yet mapped and if no User
289 # It will return nil if the committer is not yet mapped and if no User
242 # with the same username or email was found
290 # with the same username or email was found
243 def find_committer_user(committer)
291 def find_committer_user(committer)
244 unless committer.blank?
292 unless committer.blank?
245 @found_committer_users ||= {}
293 @found_committer_users ||= {}
246 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
294 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
247
295
248 user = nil
296 user = nil
249 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
297 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
250 if c && c.user
298 if c && c.user
251 user = c.user
299 user = c.user
252 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
300 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
253 username, email = $1.strip, $3
301 username, email = $1.strip, $3
254 u = User.find_by_login(username)
302 u = User.find_by_login(username)
255 u ||= User.find_by_mail(email) unless email.blank?
303 u ||= User.find_by_mail(email) unless email.blank?
256 user = u
304 user = u
257 end
305 end
258 @found_committer_users[committer] = user
306 @found_committer_users[committer] = user
259 user
307 user
260 end
308 end
261 end
309 end
262
310
263 def repo_log_encoding
311 def repo_log_encoding
264 encoding = log_encoding.to_s.strip
312 encoding = log_encoding.to_s.strip
265 encoding.blank? ? 'UTF-8' : encoding
313 encoding.blank? ? 'UTF-8' : encoding
266 end
314 end
267
315
268 # Fetches new changesets for all repositories of active projects
316 # Fetches new changesets for all repositories of active projects
269 # Can be called periodically by an external script
317 # Can be called periodically by an external script
270 # eg. ruby script/runner "Repository.fetch_changesets"
318 # eg. ruby script/runner "Repository.fetch_changesets"
271 def self.fetch_changesets
319 def self.fetch_changesets
272 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
320 Project.active.has_module(:repository).all.each do |project|
273 if project.repository
321 project.repositories.each do |repository|
274 begin
322 begin
275 project.repository.fetch_changesets
323 repository.fetch_changesets
276 rescue Redmine::Scm::Adapters::CommandFailed => e
324 rescue Redmine::Scm::Adapters::CommandFailed => e
277 logger.error "scm: error during fetching changesets: #{e.message}"
325 logger.error "scm: error during fetching changesets: #{e.message}"
278 end
326 end
279 end
327 end
280 end
328 end
281 end
329 end
282
330
283 # scan changeset comments to find related and fixed issues for all repositories
331 # scan changeset comments to find related and fixed issues for all repositories
284 def self.scan_changesets_for_issue_ids
332 def self.scan_changesets_for_issue_ids
285 find(:all).each(&:scan_changesets_for_issue_ids)
333 find(:all).each(&:scan_changesets_for_issue_ids)
286 end
334 end
287
335
288 def self.scm_name
336 def self.scm_name
289 'Abstract'
337 'Abstract'
290 end
338 end
291
339
292 def self.available_scm
340 def self.available_scm
293 subclasses.collect {|klass| [klass.scm_name, klass.name]}
341 subclasses.collect {|klass| [klass.scm_name, klass.name]}
294 end
342 end
295
343
296 def self.factory(klass_name, *args)
344 def self.factory(klass_name, *args)
297 klass = "Repository::#{klass_name}".constantize
345 klass = "Repository::#{klass_name}".constantize
298 klass.new(*args)
346 klass.new(*args)
299 rescue
347 rescue
300 nil
348 nil
301 end
349 end
302
350
303 def self.scm_adapter_class
351 def self.scm_adapter_class
304 nil
352 nil
305 end
353 end
306
354
307 def self.scm_command
355 def self.scm_command
308 ret = ""
356 ret = ""
309 begin
357 begin
310 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
358 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
311 rescue Exception => e
359 rescue Exception => e
312 logger.error "scm: error during get command: #{e.message}"
360 logger.error "scm: error during get command: #{e.message}"
313 end
361 end
314 ret
362 ret
315 end
363 end
316
364
317 def self.scm_version_string
365 def self.scm_version_string
318 ret = ""
366 ret = ""
319 begin
367 begin
320 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
368 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
321 rescue Exception => e
369 rescue Exception => e
322 logger.error "scm: error during get version string: #{e.message}"
370 logger.error "scm: error during get version string: #{e.message}"
323 end
371 end
324 ret
372 ret
325 end
373 end
326
374
327 def self.scm_available
375 def self.scm_available
328 ret = false
376 ret = false
329 begin
377 begin
330 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
378 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
331 rescue Exception => e
379 rescue Exception => e
332 logger.error "scm: error during get scm available: #{e.message}"
380 logger.error "scm: error during get scm available: #{e.message}"
333 end
381 end
334 ret
382 ret
335 end
383 end
336
384
385 def set_as_default?
386 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
387 end
388
389 protected
390
391 def check_default
392 if !is_default? && set_as_default?
393 self.is_default = true
394 end
395 if is_default? && is_default_changed?
396 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
397 end
398 end
399
337 private
400 private
338
401
339 def clear_changesets
402 def clear_changesets
340 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
403 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
341 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
404 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
342 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
405 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
343 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
406 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
344 end
407 end
345 end
408 end
@@ -1,10 +1,10
1 <% changesets.each do |changeset| %>
1 <% changesets.each do |changeset| %>
2 <div class="changeset <%= cycle('odd', 'even') %>">
2 <div class="changeset <%= cycle('odd', 'even') %>">
3 <p><%= link_to_revision(changeset, changeset.project,
3 <p><%= link_to_revision(changeset, changeset.repository,
4 :text => "#{l(:label_revision)} #{changeset.format_identifier}") %><br />
4 :text => "#{l(:label_revision)} #{changeset.format_identifier}") %><br />
5 <span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p>
5 <span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p>
6 <div class="wiki">
6 <div class="wiki">
7 <%= textilizable(changeset, :comments) %>
7 <%= textilizable(changeset, :comments) %>
8 </div>
8 </div>
9 </div>
9 </div>
10 <% end %>
10 <% end %>
@@ -1,36 +1,41
1 <% if @project.repository %>
1 <% if @project.repositories.any? %>
2 <table class="list">
2 <table class="list">
3 <thead>
3 <thead>
4 <tr>
4 <tr>
5 <th><%= l(:label_scm) %></th>
5 <th><%= l(:label_scm) %></th>
6 <th><%= l(:field_identifier) %></th>
7 <th><%= l(:field_repository_is_default) %></th>
6 <th><%= l(:label_repository) %></th>
8 <th><%= l(:label_repository) %></th>
7 <th></th>
9 <th></th>
8 </tr>
10 </tr>
9 </thead>
11 </thead>
10 <tbody>
12 <tbody>
11 <% repository = @project.repository %>
13 <% @project.repositories.each do |repository| %>
12 <tr class="<%= cycle 'odd', 'even' %>">
14 <tr class="<%= cycle 'odd', 'even' %>">
13 <td><%=h repository.scm_name %></td>
15 <td><%=h repository.scm_name %></td>
16 <td><%=h repository.identifier %></td>
17 <td align="center"><%= checked_image repository.is_default? %></td>
14 <td><%=h repository.url %></td>
18 <td><%=h repository.url %></td>
15 <td class="buttons">
19 <td class="buttons">
16 <% if User.current.allowed_to?(:manage_repository, @project) %>
20 <% if User.current.allowed_to?(:manage_repository, @project) %>
17 <%= link_to(l(:label_user_plural), committers_repository_path(repository),
21 <%= link_to(l(:label_user_plural), committers_repository_path(repository),
18 :class => 'icon icon-user') %>
22 :class => 'icon icon-user') %>
19 <%= link_to(l(:button_edit), edit_repository_path(repository),
23 <%= link_to(l(:button_edit), edit_repository_path(repository),
20 :class => 'icon icon-edit') %>
24 :class => 'icon icon-edit') %>
21 <%= link_to(l(:button_delete), repository_path(repository),
25 <%= link_to(l(:button_delete), repository_path(repository),
22 :confirm => l(:text_are_you_sure),
26 :confirm => l(:text_are_you_sure),
23 :method => :delete,
27 :method => :delete,
24 :class => 'icon icon-del') %>
28 :class => 'icon icon-del') %>
25 <% end %>
29 <% end %>
26 </td>
30 </td>
27 </tr>
31 </tr>
32 <% end %>
28 </tbody>
33 </tbody>
29 </table>
34 </table>
30 <% else %>
35 <% else %>
31 <p class="nodata"><%= l(:label_no_data) %></p>
36 <p class="nodata"><%= l(:label_no_data) %></p>
32 <% end %>
37 <% end %>
33
38
34 <% if @project.repository.nil? && User.current.allowed_to?(:manage_repository, @project) %>
39 <% if User.current.allowed_to?(:manage_repository, @project) %>
35 <p><%= link_to l(:label_repository_new), new_project_repository_path(@project), :class => 'icon icon-add' %></p>
40 <p><%= link_to l(:label_repository_new), new_project_repository_path(@project), :class => 'icon icon-add' %></p>
36 <% end %>
41 <% end %>
@@ -1,28 +1,28
1 <%= link_to 'root', :action => 'show', :id => @project, :path => '', :rev => @rev %>
1 <%= link_to 'root', :action => 'show', :id => @project, :repository_id => @repository.identifier_param, :path => '', :rev => @rev %>
2 <%
2 <%
3 dirs = path.split('/')
3 dirs = path.split('/')
4 if 'file' == kind
4 if 'file' == kind
5 filename = dirs.pop
5 filename = dirs.pop
6 end
6 end
7 link_path = ''
7 link_path = ''
8 dirs.each do |dir|
8 dirs.each do |dir|
9 next if dir.blank?
9 next if dir.blank?
10 link_path << '/' unless link_path.empty?
10 link_path << '/' unless link_path.empty?
11 link_path << "#{dir}"
11 link_path << "#{dir}"
12 %>
12 %>
13 / <%= link_to h(dir), :action => 'show', :id => @project,
13 / <%= link_to h(dir), :action => 'show', :id => @project, :repository_id => @repository.identifier_param,
14 :path => to_path_param(link_path), :rev => @rev %>
14 :path => to_path_param(link_path), :rev => @rev %>
15 <% end %>
15 <% end %>
16 <% if filename %>
16 <% if filename %>
17 / <%= link_to h(filename),
17 / <%= link_to h(filename),
18 :action => 'changes', :id => @project,
18 :action => 'changes', :id => @project, :repository_id => @repository.identifier_param,
19 :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
19 :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
20 <% end %>
20 <% end %>
21 <%
21 <%
22 # @rev is revsion or Git and Mercurial branch or tag.
22 # @rev is revsion or Git and Mercurial branch or tag.
23 # For Mercurial *tip*, @rev and @changeset are nil.
23 # For Mercurial *tip*, @rev and @changeset are nil.
24 rev_text = @changeset.nil? ? @rev : format_revision(@changeset)
24 rev_text = @changeset.nil? ? @rev : format_revision(@changeset)
25 %>
25 %>
26 <%= "@ #{h rev_text}" unless rev_text.blank? %>
26 <%= "@ #{h rev_text}" unless rev_text.blank? %>
27
27
28 <% html_title(with_leading_slash(path)) -%>
28 <% html_title(with_leading_slash(path)) -%>
@@ -1,39 +1,40
1 <% @entries.each do |entry| %>
1 <% @entries.each do |entry| %>
2 <% tr_id = Digest::MD5.hexdigest(entry.path)
2 <% tr_id = Digest::MD5.hexdigest(entry.path)
3 depth = params[:depth].to_i %>
3 depth = params[:depth].to_i %>
4 <% ent_path = Redmine::CodesetUtil.replace_invalid_utf8(entry.path) %>
4 <% ent_path = Redmine::CodesetUtil.replace_invalid_utf8(entry.path) %>
5 <% ent_name = Redmine::CodesetUtil.replace_invalid_utf8(entry.name) %>
5 <% ent_name = Redmine::CodesetUtil.replace_invalid_utf8(entry.name) %>
6 <tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= entry.kind %>">
6 <tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= entry.kind %>">
7 <td style="padding-left: <%=18 * depth%>px;" class="<%=
7 <td style="padding-left: <%=18 * depth%>px;" class="<%=
8 @repository.report_last_commit ? "filename" : "filename_no_report" %>";>
8 @repository.report_last_commit ? "filename" : "filename_no_report" %>";>
9 <% if entry.is_dir? %>
9 <% if entry.is_dir? %>
10 <span class="expander" onclick="<%= remote_function(
10 <span class="expander" onclick="<%= remote_function(
11 :url => {
11 :url => {
12 :action => 'show',
12 :action => 'show',
13 :id => @project,
13 :id => @project,
14 :repository_id => @repository.identifier_param,
14 :path => to_path_param(ent_path),
15 :path => to_path_param(ent_path),
15 :rev => @rev,
16 :rev => @rev,
16 :depth => (depth + 1),
17 :depth => (depth + 1),
17 :parent_id => tr_id
18 :parent_id => tr_id
18 },
19 },
19 :method => :get,
20 :method => :get,
20 :update => { :success => tr_id },
21 :update => { :success => tr_id },
21 :position => :after,
22 :position => :after,
22 :success => "scmEntryLoaded('#{tr_id}')",
23 :success => "scmEntryLoaded('#{tr_id}')",
23 :condition => "scmEntryClick('#{tr_id}')"
24 :condition => "scmEntryClick('#{tr_id}')"
24 ) %>">&nbsp</span>
25 ) %>">&nbsp</span>
25 <% end %>
26 <% end %>
26 <%= link_to h(ent_name),
27 <%= link_to h(ent_name),
27 {:action => (entry.is_dir? ? 'show' : 'changes'), :id => @project, :path => to_path_param(ent_path), :rev => @rev},
28 {:action => (entry.is_dir? ? 'show' : 'changes'), :id => @project, :repository_id => @repository.identifier_param, :path => to_path_param(ent_path), :rev => @rev},
28 :class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
29 :class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
29 </td>
30 </td>
30 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
31 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
31 <% changeset = @project.repository.find_changeset_by_name(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
32 <% changeset = @project.repository.find_changeset_by_name(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
32 <% if @repository.report_last_commit %>
33 <% if @repository.report_last_commit %>
33 <td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
34 <td class="revision"><%= link_to_revision(changeset, @repository) if changeset %></td>
34 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
35 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
35 <td class="author"><%= changeset.nil? ? h(Redmine::CodesetUtil.replace_invalid_utf8(entry.lastrev.author.to_s.split('<').first)) : h(changeset.author) if entry.lastrev %></td>
36 <td class="author"><%= changeset.nil? ? h(Redmine::CodesetUtil.replace_invalid_utf8(entry.lastrev.author.to_s.split('<').first)) : h(changeset.author) if entry.lastrev %></td>
36 <td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
37 <td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
37 <% end %>
38 <% end %>
38 </tr>
39 </tr>
39 <% end %>
40 <% end %>
@@ -1,22 +1,25
1 <%= error_messages_for 'repository' %>
1 <%= error_messages_for 'repository' %>
2
2
3 <div class="box tabular">
3 <div class="box tabular">
4 <p>
4 <p>
5 <%= label_tag('repository_scm', l(:label_scm)) %><%= scm_select_tag(@repository) %>
5 <%= label_tag('repository_scm', l(:label_scm)) %><%= scm_select_tag(@repository) %>
6 <% if @repository && ! @repository.class.scm_available %>
6 <% if @repository && ! @repository.class.scm_available %>
7 <br />
7 <br />
8 <em><%= content_tag 'span', l(:text_scm_command_not_available), :class => 'error' %></em>
8 <em><%= content_tag 'span', l(:text_scm_command_not_available), :class => 'error' %></em>
9 <% end %>
9 <% end %>
10 </p>
10 </p>
11
11
12 <p><%= f.text_field :identifier %></p>
13 <p><%= f.check_box :is_default, :label => :field_repository_is_default %></p>
14
12 <% button_disabled = true %>
15 <% button_disabled = true %>
13 <% if @repository %>
16 <% if @repository %>
14 <% button_disabled = ! @repository.class.scm_available %>
17 <% button_disabled = ! @repository.class.scm_available %>
15 <%= repository_field_tags(f, @repository)%>
18 <%= repository_field_tags(f, @repository)%>
16 <% end %>
19 <% end %>
17 </div>
20 </div>
18
21
19 <p>
22 <p>
20 <%= submit_tag(@repository.new_record? ? l(:button_create) : l(:button_save), :disabled => button_disabled) %>
23 <%= submit_tag(@repository.new_record? ? l(:button_create) : l(:button_save), :disabled => button_disabled) %>
21 <%= link_to l(:button_cancel), settings_project_path(@project, :tab => 'repositories') %>
24 <%= link_to l(:button_cancel), settings_project_path(@project, :tab => 'repositories') %>
22 </p> No newline at end of file
25 </p>
@@ -1,31 +1,32
1 <% content_for :header_tags do %>
1 <% content_for :header_tags do %>
2 <%= javascript_include_tag 'repository_navigation' %>
2 <%= javascript_include_tag 'repository_navigation' %>
3 <% end %>
3 <% end %>
4
4
5 <%= link_to l(:label_statistics),
5 <%= link_to l(:label_statistics),
6 {:action => 'stats', :id => @project},
6 {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param},
7 :class => 'icon icon-stats' %>
7 :class => 'icon icon-stats' %>
8
8
9 <% form_tag({:action => controller.action_name,
9 <% form_tag({:action => controller.action_name,
10 :id => @project,
10 :id => @project,
11 :repository_id => @repository.identifier_param,
11 :path => to_path_param(@path),
12 :path => to_path_param(@path),
12 :rev => ''},
13 :rev => ''},
13 {:method => :get, :id => 'revision_selector'}) do -%>
14 {:method => :get, :id => 'revision_selector'}) do -%>
14 <!-- Branches Dropdown -->
15 <!-- Branches Dropdown -->
15 <% if !@repository.branches.nil? && @repository.branches.length > 0 -%>
16 <% if !@repository.branches.nil? && @repository.branches.length > 0 -%>
16 | <%= l(:label_branch) %>:
17 | <%= l(:label_branch) %>:
17 <%= select_tag :branch,
18 <%= select_tag :branch,
18 options_for_select([''] + @repository.branches, @rev),
19 options_for_select([''] + @repository.branches, @rev),
19 :id => 'branch' %>
20 :id => 'branch' %>
20 <% end -%>
21 <% end -%>
21
22
22 <% if !@repository.tags.nil? && @repository.tags.length > 0 -%>
23 <% if !@repository.tags.nil? && @repository.tags.length > 0 -%>
23 | <%= l(:label_tag) %>:
24 | <%= l(:label_tag) %>:
24 <%= select_tag :tag,
25 <%= select_tag :tag,
25 options_for_select([''] + @repository.tags, @rev),
26 options_for_select([''] + @repository.tags, @rev),
26 :id => 'tag' %>
27 :id => 'tag' %>
27 <% end -%>
28 <% end -%>
28
29
29 | <%= l(:label_revision) %>:
30 | <%= l(:label_revision) %>:
30 <%= text_field_tag 'rev', @rev, :size => 8 %>
31 <%= text_field_tag 'rev', @rev, :size => 8 %>
31 <% end -%>
32 <% end -%>
@@ -1,56 +1,57
1 <% show_revision_graph = ( @repository.supports_revision_graph? && path.blank? ) %>
1 <% show_revision_graph = ( @repository.supports_revision_graph? && path.blank? ) %>
2 <% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => to_path_param(path)}, :method => :get) do %>
2 <% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :repository_id => @repository.identifier_param, :path => to_path_param(path)}, :method => :get) do %>
3 <table class="list changesets">
3 <table class="list changesets">
4 <thead><tr>
4 <thead><tr>
5 <% if show_revision_graph %>
5 <% if show_revision_graph %>
6 <th></th>
6 <th></th>
7 <% end %>
7 <% end %>
8 <th>#</th>
8 <th>#</th>
9 <th></th>
9 <th></th>
10 <th></th>
10 <th></th>
11 <th><%= l(:label_date) %></th>
11 <th><%= l(:label_date) %></th>
12 <th><%= l(:field_author) %></th>
12 <th><%= l(:field_author) %></th>
13 <th><%= l(:field_comments) %></th>
13 <th><%= l(:field_comments) %></th>
14 </tr></thead>
14 </tr></thead>
15 <tbody>
15 <tbody>
16 <% show_diff = revisions.size > 1 %>
16 <% show_diff = revisions.size > 1 %>
17 <% line_num = 1 %>
17 <% line_num = 1 %>
18 <% revisions.each do |changeset| %>
18 <% revisions.each do |changeset| %>
19 <tr class="changeset <%= cycle 'odd', 'even' %>">
19 <tr class="changeset <%= cycle 'odd', 'even' %>">
20 <% if show_revision_graph %>
20 <% if show_revision_graph %>
21 <% if line_num == 1 %>
21 <% if line_num == 1 %>
22 <td class="revision_graph" rowspan="<%= revisions.size %>">
22 <td class="revision_graph" rowspan="<%= revisions.size %>">
23 <% href_base = Proc.new {|x| url_for(:controller => 'repositories',
23 <% href_base = Proc.new {|x| url_for(:controller => 'repositories',
24 :action => 'revision',
24 :action => 'revision',
25 :id => project,
25 :id => project,
26 :repository_id => @repository.identifier_param,
26 :rev => x) } %>
27 :rev => x) } %>
27 <%= render :partial => 'revision_graph',
28 <%= render :partial => 'revision_graph',
28 :locals => {
29 :locals => {
29 :commits => index_commits(
30 :commits => index_commits(
30 revisions,
31 revisions,
31 @repository.branches,
32 @repository.branches,
32 href_base
33 href_base
33 )
34 )
34 } %>
35 } %>
35 </td>
36 </td>
36 <% end %>
37 <% end %>
37 <% end %>
38 <% end %>
38 <td class="id"><%= link_to_revision(changeset, project) %></td>
39 <td class="id"><%= link_to_revision(changeset, @repository) %></td>
39 <td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
40 <td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
40 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
41 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
41 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
42 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
42 <td class="author"><%= h truncate(changeset.author.to_s, :length => 30) %></td>
43 <td class="author"><%= h truncate(changeset.author.to_s, :length => 30) %></td>
43 <% if show_revision_graph %>
44 <% if show_revision_graph %>
44 <td class="comments_nowrap">
45 <td class="comments_nowrap">
45 <%= textilizable(truncate(truncate_at_line_break(changeset.comments, 0), :length => 90)) %>
46 <%= textilizable(truncate(truncate_at_line_break(changeset.comments, 0), :length => 90)) %>
46 </td>
47 </td>
47 <% else %>
48 <% else %>
48 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
49 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
49 <% end %>
50 <% end %>
50 </tr>
51 </tr>
51 <% line_num += 1 %>
52 <% line_num += 1 %>
52 <% end %>
53 <% end %>
53 </tbody>
54 </tbody>
54 </table>
55 </table>
55 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
56 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
56 <% end %>
57 <% end %>
@@ -1,36 +1,36
1 <%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
1 <%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
2
2
3 <div class="contextual">
3 <div class="contextual">
4 <%= render :partial => 'navigation' %>
4 <%= render :partial => 'navigation' %>
5 </div>
5 </div>
6
6
7 <h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
7 <h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
8
8
9 <p><%= render :partial => 'link_to_functions' %></p>
9 <p><%= render :partial => 'link_to_functions' %></p>
10
10
11 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
11 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
12
12
13 <div class="autoscroll">
13 <div class="autoscroll">
14 <table class="filecontent annotate syntaxhl">
14 <table class="filecontent annotate syntaxhl">
15 <tbody>
15 <tbody>
16 <% line_num = 1 %>
16 <% line_num = 1 %>
17 <% syntax_highlight(@path, Redmine::CodesetUtil.to_utf8_by_setting(@annotate.content)).each_line do |line| %>
17 <% syntax_highlight(@path, Redmine::CodesetUtil.to_utf8_by_setting(@annotate.content)).each_line do |line| %>
18 <% revision = @annotate.revisions[line_num - 1] %>
18 <% revision = @annotate.revisions[line_num - 1] %>
19 <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
19 <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
20 <th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
20 <th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
21 <td class="revision">
21 <td class="revision">
22 <%= (revision.identifier ? link_to_revision(revision, @project) : format_revision(revision)) if revision %></td>
22 <%= (revision.identifier ? link_to_revision(revision, @repository) : format_revision(revision)) if revision %></td>
23 <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
23 <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
24 <td class="line-code"><pre><%= line %></pre></td>
24 <td class="line-code"><pre><%= line %></pre></td>
25 </tr>
25 </tr>
26 <% line_num += 1 %>
26 <% line_num += 1 %>
27 <% end %>
27 <% end %>
28 </tbody>
28 </tbody>
29 </table>
29 </table>
30 </div>
30 </div>
31
31
32 <% html_title(l(:button_annotate)) -%>
32 <% html_title(l(:button_annotate)) -%>
33
33
34 <% content_for :header_tags do %>
34 <% content_for :header_tags do %>
35 <%= stylesheet_link_tag 'scm' %>
35 <%= stylesheet_link_tag 'scm' %>
36 <% end %>
36 <% end %>
@@ -1,100 +1,102
1 <div class="contextual">
1 <div class="contextual">
2 &#171;
2 &#171;
3 <% unless @changeset.previous.nil? -%>
3 <% unless @changeset.previous.nil? -%>
4 <%= link_to_revision(@changeset.previous, @project, :text => l(:label_previous)) %>
4 <%= link_to_revision(@changeset.previous, @repository, :text => l(:label_previous)) %>
5 <% else -%>
5 <% else -%>
6 <%= l(:label_previous) %>
6 <%= l(:label_previous) %>
7 <% end -%>
7 <% end -%>
8 |
8 |
9 <% unless @changeset.next.nil? -%>
9 <% unless @changeset.next.nil? -%>
10 <%= link_to_revision(@changeset.next, @project, :text => l(:label_next)) %>
10 <%= link_to_revision(@changeset.next, @repository, :text => l(:label_next)) %>
11 <% else -%>
11 <% else -%>
12 <%= l(:label_next) %>
12 <%= l(:label_next) %>
13 <% end -%>
13 <% end -%>
14 &#187;&nbsp;
14 &#187;&nbsp;
15
15
16 <% form_tag({:controller => 'repositories',
16 <% form_tag({:controller => 'repositories',
17 :action => 'revision',
17 :action => 'revision',
18 :id => @project,
18 :id => @project,
19 :repository_id => @repository.identifier_param,
19 :rev => nil},
20 :rev => nil},
20 :method => :get) do %>
21 :method => :get) do %>
21 <%= text_field_tag 'rev', @rev, :size => 8 %>
22 <%= text_field_tag 'rev', @rev, :size => 8 %>
22 <%= submit_tag 'OK', :name => nil %>
23 <%= submit_tag 'OK', :name => nil %>
23 <% end %>
24 <% end %>
24 </div>
25 </div>
25
26
26 <h2><%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
27 <h2><%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
27
28
28 <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %>
29 <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %>
29 <table class="revision-info">
30 <table class="revision-info">
30 <% if @changeset.scmid.present? %>
31 <% if @changeset.scmid.present? %>
31 <tr>
32 <tr>
32 <td>ID</td><td><%= h(@changeset.scmid) %></td>
33 <td>ID</td><td><%= h(@changeset.scmid) %></td>
33 </tr>
34 </tr>
34 <% end %>
35 <% end %>
35 <% if @changeset.parents.present? %>
36 <% if @changeset.parents.present? %>
36 <tr>
37 <tr>
37 <td><%= l(:label_parent_revision) %></td>
38 <td><%= l(:label_parent_revision) %></td>
38 <td>
39 <td>
39 <%= @changeset.parents.collect{
40 <%= @changeset.parents.collect{
40 |p| link_to_revision(p, @project, :text => format_revision(p))
41 |p| link_to_revision(p, @repository, :text => format_revision(p))
41 }.join(", ").html_safe %>
42 }.join(", ").html_safe %>
42 </td>
43 </td>
43 </tr>
44 </tr>
44 <% end %>
45 <% end %>
45 <% if @changeset.children.present? %>
46 <% if @changeset.children.present? %>
46 <tr>
47 <tr>
47 <td><%= l(:label_child_revision) %></td>
48 <td><%= l(:label_child_revision) %></td>
48 <td>
49 <td>
49 <%= @changeset.children.collect{
50 <%= @changeset.children.collect{
50 |p| link_to_revision(p, @project, :text => format_revision(p))
51 |p| link_to_revision(p, @repository, :text => format_revision(p))
51 }.join(", ").html_safe %>
52 }.join(", ").html_safe %>
52 </td>
53 </td>
53 </tr>
54 </tr>
54 <% end %>
55 <% end %>
55 </table>
56 </table>
56 <% end %>
57 <% end %>
57
58
58 <p>
59 <p>
59 <span class="author">
60 <span class="author">
60 <%= authoring(@changeset.committed_on, @changeset.author) %>
61 <%= authoring(@changeset.committed_on, @changeset.author) %>
61 </span>
62 </span>
62 </p>
63 </p>
63
64
64 <%= textilizable @changeset.comments %>
65 <%= textilizable @changeset.comments %>
65
66
66 <% if @changeset.issues.visible.any? %>
67 <% if @changeset.issues.visible.any? %>
67 <h3><%= l(:label_related_issues) %></h3>
68 <h3><%= l(:label_related_issues) %></h3>
68 <ul>
69 <ul>
69 <% @changeset.issues.visible.each do |issue| %>
70 <% @changeset.issues.visible.each do |issue| %>
70 <li><%= link_to_issue issue %></li>
71 <li><%= link_to_issue issue %></li>
71 <% end %>
72 <% end %>
72 </ul>
73 </ul>
73 <% end %>
74 <% end %>
74
75
75 <% if User.current.allowed_to?(:browse_repository, @project) %>
76 <% if User.current.allowed_to?(:browse_repository, @project) %>
76 <h3><%= l(:label_attachment_plural) %></h3>
77 <h3><%= l(:label_attachment_plural) %></h3>
77 <ul id="changes-legend">
78 <ul id="changes-legend">
78 <li class="change change-A"><%= l(:label_added) %></li>
79 <li class="change change-A"><%= l(:label_added) %></li>
79 <li class="change change-M"><%= l(:label_modified) %></li>
80 <li class="change change-M"><%= l(:label_modified) %></li>
80 <li class="change change-C"><%= l(:label_copied) %></li>
81 <li class="change change-C"><%= l(:label_copied) %></li>
81 <li class="change change-R"><%= l(:label_renamed) %></li>
82 <li class="change change-R"><%= l(:label_renamed) %></li>
82 <li class="change change-D"><%= l(:label_deleted) %></li>
83 <li class="change change-D"><%= l(:label_deleted) %></li>
83 </ul>
84 </ul>
84
85
85 <p><%= link_to(l(:label_view_diff),
86 <p><%= link_to(l(:label_view_diff),
86 :action => 'diff',
87 :action => 'diff',
87 :id => @project,
88 :id => @project,
89 :repository_id => @repository.identifier_param,
88 :path => "",
90 :path => "",
89 :rev => @changeset.identifier) if @changeset.changes.any? %></p>
91 :rev => @changeset.identifier) if @changeset.changes.any? %></p>
90
92
91 <div class="changeset-changes">
93 <div class="changeset-changes">
92 <%= render_changeset_changes %>
94 <%= render_changeset_changes %>
93 </div>
95 </div>
94 <% end %>
96 <% end %>
95
97
96 <% content_for :header_tags do %>
98 <% content_for :header_tags do %>
97 <%= stylesheet_link_tag "scm" %>
99 <%= stylesheet_link_tag "scm" %>
98 <% end %>
100 <% end %>
99
101
100 <% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%>
102 <% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%>
@@ -1,30 +1,30
1 <div class="contextual">
1 <div class="contextual">
2 <% form_tag({:action => 'revision', :id => @project}) do %>
2 <% form_tag({:action => 'revision', :id => @project, :repository_id => @repository.identifier_param}) do %>
3 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
3 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
4 <%= submit_tag 'OK' %>
4 <%= submit_tag 'OK' %>
5 <% end %>
5 <% end %>
6 </div>
6 </div>
7
7
8 <h2><%= l(:label_revision_plural) %></h2>
8 <h2><%= l(:label_revision_plural) %></h2>
9
9
10 <%= render :partial => 'revisions',
10 <%= render :partial => 'revisions',
11 :locals => {:project => @project,
11 :locals => {:project => @project,
12 :path => '',
12 :path => '',
13 :revisions => @changesets,
13 :revisions => @changesets,
14 :entry => nil } %>
14 :entry => nil } %>
15
15
16 <p class="pagination"><%= pagination_links_full @changeset_pages,@changeset_count %></p>
16 <p class="pagination"><%= pagination_links_full @changeset_pages,@changeset_count %></p>
17
17
18 <% content_for :header_tags do %>
18 <% content_for :header_tags do %>
19 <%= stylesheet_link_tag "scm" %>
19 <%= stylesheet_link_tag "scm" %>
20 <%= auto_discovery_link_tag(
20 <%= auto_discovery_link_tag(
21 :atom,
21 :atom,
22 params.merge(
22 params.merge(
23 {:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
23 {:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
24 <% end %>
24 <% end %>
25
25
26 <% other_formats_links do |f| %>
26 <% other_formats_links do |f| %>
27 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
27 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
28 <% end %>
28 <% end %>
29
29
30 <% html_title(l(:label_revision_plural)) -%>
30 <% html_title(l(:label_revision_plural)) -%>
@@ -1,64 +1,76
1 <%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
1 <%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
2
2
3 <div class="contextual">
3 <div class="contextual">
4 <%= render :partial => 'navigation' %>
4 <%= render :partial => 'navigation' %>
5 </div>
5 </div>
6
6
7 <h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2>
7 <h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2>
8
8
9 <% if !@entries.nil? && authorize_for('repositories', 'browse') %>
9 <% if !@entries.nil? && authorize_for('repositories', 'browse') %>
10 <%= render :partial => 'dir_list' %>
10 <%= render :partial => 'dir_list' %>
11 <% end %>
11 <% end %>
12
12
13 <%= render_properties(@properties) %>
13 <%= render_properties(@properties) %>
14
14
15 <% if authorize_for('repositories', 'revisions') %>
15 <% if authorize_for('repositories', 'revisions') %>
16 <% if @changesets && !@changesets.empty? %>
16 <% if @changesets && !@changesets.empty? %>
17 <h3><%= l(:label_latest_revision_plural) %></h3>
17 <h3><%= l(:label_latest_revision_plural) %></h3>
18 <%= render :partial => 'revisions',
18 <%= render :partial => 'revisions',
19 :locals => {:project => @project, :path => @path,
19 :locals => {:project => @project, :path => @path,
20 :revisions => @changesets, :entry => nil }%>
20 :revisions => @changesets, :entry => nil }%>
21 <% end %>
21 <% end %>
22 <p>
22 <p>
23 <%
23 <%
24 has_branches = (!@repository.branches.nil? && @repository.branches.length > 0)
24 has_branches = (!@repository.branches.nil? && @repository.branches.length > 0)
25 sep = ''
25 sep = ''
26 %>
26 %>
27 <% if @repository.supports_all_revisions? && @path.blank? %>
27 <% if @repository.supports_all_revisions? && @path.blank? %>
28 <%= link_to l(:label_view_all_revisions), :action => 'revisions', :id => @project %>
28 <%= link_to l(:label_view_all_revisions), :action => 'revisions', :id => @project, :repository_id => @repository.identifier_param %>
29 <% sep = '|' %>
29 <% sep = '|' %>
30 <% end %>
30 <% end %>
31 <%
31 <%
32 if @repository.supports_directory_revisions? &&
32 if @repository.supports_directory_revisions? &&
33 ( has_branches || !@path.blank? || !@rev.blank? )
33 ( has_branches || !@path.blank? || !@rev.blank? )
34 %>
34 %>
35 <%= sep %>
35 <%= sep %>
36 <%=
36 <%=
37 link_to l(:label_view_revisions),
37 link_to l(:label_view_revisions),
38 :action => 'changes',
38 :action => 'changes',
39 :path => to_path_param(@path),
39 :path => to_path_param(@path),
40 :id => @project,
40 :id => @project,
41 :repository_id => @repository.identifier_param,
41 :rev => @rev
42 :rev => @rev
42 %>
43 %>
43 <% end %>
44 <% end %>
44 </p>
45 </p>
45
46
46 <% if true # @path.blank? %>
47 <% if true # @path.blank? %>
47 <% content_for :header_tags do %>
48 <% content_for :header_tags do %>
48 <%= auto_discovery_link_tag(
49 <%= auto_discovery_link_tag(
49 :atom, params.merge(
50 :atom, params.merge(
50 {:format => 'atom', :action => 'revisions',
51 {:format => 'atom', :action => 'revisions',
51 :id => @project, :page => nil, :key => User.current.rss_key})) %>
52 :id => @project, :page => nil, :key => User.current.rss_key})) %>
52 <% end %>
53 <% end %>
53
54
54 <% other_formats_links do |f| %>
55 <% other_formats_links do |f| %>
55 <%= f.link_to 'Atom', :url => {:action => 'revisions', :id => @project, :key => User.current.rss_key} %>
56 <%= f.link_to 'Atom', :url => {:action => 'revisions', :id => @project, :repository_id => @repository.identifier_param, :key => User.current.rss_key} %>
56 <% end %>
57 <% end %>
57 <% end %>
58 <% end %>
58 <% end %>
59 <% end %>
59
60
61 <% if @repositories.size > 1 %>
62 <% content_for :sidebar do %>
63 <h3><%= l(:label_repository_plural) %></h3>
64 <%= @repositories.sort.collect {|repo|
65 link_to h(repo.name),
66 {:controller => 'repositories', :action => 'show', :id => @project, :repository_id => repo.identifier_param, :rev => nil, :path => nil},
67 :class => 'repository' + (repo == @repository ? ' selected' : '')
68 }.join('<br />').html_safe %></p>
69 <% end %>
70 <% end %>
71
60 <% content_for :header_tags do %>
72 <% content_for :header_tags do %>
61 <%= stylesheet_link_tag "scm" %>
73 <%= stylesheet_link_tag "scm" %>
62 <% end %>
74 <% end %>
63
75
64 <% html_title(l(:label_repository)) -%>
76 <% html_title(l(:label_repository)) -%>
@@ -1,12 +1,12
1 <h2><%= l(:label_statistics) %></h2>
1 <h2><%= l(:label_statistics) %></h2>
2
2
3 <p>
3 <p>
4 <%= tag("embed", :width => 800, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
4 <%= tag("embed", :width => 800, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :repository_id => @repository.identifier_param, :graph => "commits_per_month")) %>
5 </p>
5 </p>
6 <p>
6 <p>
7 <%= tag("embed", :width => 800, :height => 400, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
7 <%= tag("embed", :width => 800, :height => 400, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :repository_id => @repository.identifier_param, :graph => "commits_per_author")) %>
8 </p>
8 </p>
9
9
10 <p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>
10 <p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>
11
11
12 <% html_title(l(:label_repository), l(:label_statistics)) -%>
12 <% html_title(l(:label_repository), l(:label_statistics)) -%>
@@ -1,1012 +1,1013
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_days:
52 x_days:
53 one: "1 day"
53 one: "1 day"
54 other: "%{count} days"
54 other: "%{count} days"
55 about_x_months:
55 about_x_months:
56 one: "about 1 month"
56 one: "about 1 month"
57 other: "about %{count} months"
57 other: "about %{count} months"
58 x_months:
58 x_months:
59 one: "1 month"
59 one: "1 month"
60 other: "%{count} months"
60 other: "%{count} months"
61 about_x_years:
61 about_x_years:
62 one: "about 1 year"
62 one: "about 1 year"
63 other: "about %{count} years"
63 other: "about %{count} years"
64 over_x_years:
64 over_x_years:
65 one: "over 1 year"
65 one: "over 1 year"
66 other: "over %{count} years"
66 other: "over %{count} years"
67 almost_x_years:
67 almost_x_years:
68 one: "almost 1 year"
68 one: "almost 1 year"
69 other: "almost %{count} years"
69 other: "almost %{count} years"
70
70
71 number:
71 number:
72 format:
72 format:
73 separator: "."
73 separator: "."
74 delimiter: ""
74 delimiter: ""
75 precision: 3
75 precision: 3
76
76
77 human:
77 human:
78 format:
78 format:
79 delimiter: ""
79 delimiter: ""
80 precision: 1
80 precision: 1
81 storage_units:
81 storage_units:
82 format: "%n %u"
82 format: "%n %u"
83 units:
83 units:
84 byte:
84 byte:
85 one: "Byte"
85 one: "Byte"
86 other: "Bytes"
86 other: "Bytes"
87 kb: "kB"
87 kb: "kB"
88 mb: "MB"
88 mb: "MB"
89 gb: "GB"
89 gb: "GB"
90 tb: "TB"
90 tb: "TB"
91
91
92 # Used in array.to_sentence.
92 # Used in array.to_sentence.
93 support:
93 support:
94 array:
94 array:
95 sentence_connector: "and"
95 sentence_connector: "and"
96 skip_last_comma: false
96 skip_last_comma: false
97
97
98 activerecord:
98 activerecord:
99 errors:
99 errors:
100 template:
100 template:
101 header:
101 header:
102 one: "1 error prohibited this %{model} from being saved"
102 one: "1 error prohibited this %{model} from being saved"
103 other: "%{count} errors prohibited this %{model} from being saved"
103 other: "%{count} errors prohibited this %{model} from being saved"
104 messages:
104 messages:
105 inclusion: "is not included in the list"
105 inclusion: "is not included in the list"
106 exclusion: "is reserved"
106 exclusion: "is reserved"
107 invalid: "is invalid"
107 invalid: "is invalid"
108 confirmation: "doesn't match confirmation"
108 confirmation: "doesn't match confirmation"
109 accepted: "must be accepted"
109 accepted: "must be accepted"
110 empty: "can't be empty"
110 empty: "can't be empty"
111 blank: "can't be blank"
111 blank: "can't be blank"
112 too_long: "is too long (maximum is %{count} characters)"
112 too_long: "is too long (maximum is %{count} characters)"
113 too_short: "is too short (minimum is %{count} characters)"
113 too_short: "is too short (minimum is %{count} characters)"
114 wrong_length: "is the wrong length (should be %{count} characters)"
114 wrong_length: "is the wrong length (should be %{count} characters)"
115 taken: "has already been taken"
115 taken: "has already been taken"
116 not_a_number: "is not a number"
116 not_a_number: "is not a number"
117 not_a_date: "is not a valid date"
117 not_a_date: "is not a valid date"
118 greater_than: "must be greater than %{count}"
118 greater_than: "must be greater than %{count}"
119 greater_than_or_equal_to: "must be greater than or equal to %{count}"
119 greater_than_or_equal_to: "must be greater than or equal to %{count}"
120 equal_to: "must be equal to %{count}"
120 equal_to: "must be equal to %{count}"
121 less_than: "must be less than %{count}"
121 less_than: "must be less than %{count}"
122 less_than_or_equal_to: "must be less than or equal to %{count}"
122 less_than_or_equal_to: "must be less than or equal to %{count}"
123 odd: "must be odd"
123 odd: "must be odd"
124 even: "must be even"
124 even: "must be even"
125 greater_than_start_date: "must be greater than start date"
125 greater_than_start_date: "must be greater than start date"
126 not_same_project: "doesn't belong to the same project"
126 not_same_project: "doesn't belong to the same project"
127 circular_dependency: "This relation would create a circular dependency"
127 circular_dependency: "This relation would create a circular dependency"
128 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
128 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
129
129
130 actionview_instancetag_blank_option: Please select
130 actionview_instancetag_blank_option: Please select
131
131
132 general_text_No: 'No'
132 general_text_No: 'No'
133 general_text_Yes: 'Yes'
133 general_text_Yes: 'Yes'
134 general_text_no: 'no'
134 general_text_no: 'no'
135 general_text_yes: 'yes'
135 general_text_yes: 'yes'
136 general_lang_name: 'English'
136 general_lang_name: 'English'
137 general_csv_separator: ','
137 general_csv_separator: ','
138 general_csv_decimal_separator: '.'
138 general_csv_decimal_separator: '.'
139 general_csv_encoding: ISO-8859-1
139 general_csv_encoding: ISO-8859-1
140 general_pdf_encoding: UTF-8
140 general_pdf_encoding: UTF-8
141 general_first_day_of_week: '7'
141 general_first_day_of_week: '7'
142
142
143 notice_account_updated: Account was successfully updated.
143 notice_account_updated: Account was successfully updated.
144 notice_account_invalid_creditentials: Invalid user or password
144 notice_account_invalid_creditentials: Invalid user or password
145 notice_account_password_updated: Password was successfully updated.
145 notice_account_password_updated: Password was successfully updated.
146 notice_account_wrong_password: Wrong password
146 notice_account_wrong_password: Wrong password
147 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
147 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
148 notice_account_unknown_email: Unknown user.
148 notice_account_unknown_email: Unknown user.
149 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
149 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
150 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
150 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
151 notice_account_activated: Your account has been activated. You can now log in.
151 notice_account_activated: Your account has been activated. You can now log in.
152 notice_successful_create: Successful creation.
152 notice_successful_create: Successful creation.
153 notice_successful_update: Successful update.
153 notice_successful_update: Successful update.
154 notice_successful_delete: Successful deletion.
154 notice_successful_delete: Successful deletion.
155 notice_successful_connection: Successful connection.
155 notice_successful_connection: Successful connection.
156 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
156 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
157 notice_locking_conflict: Data has been updated by another user.
157 notice_locking_conflict: Data has been updated by another user.
158 notice_not_authorized: You are not authorized to access this page.
158 notice_not_authorized: You are not authorized to access this page.
159 notice_not_authorized_archived_project: The project you're trying to access has been archived.
159 notice_not_authorized_archived_project: The project you're trying to access has been archived.
160 notice_email_sent: "An email was sent to %{value}"
160 notice_email_sent: "An email was sent to %{value}"
161 notice_email_error: "An error occurred while sending mail (%{value})"
161 notice_email_error: "An error occurred while sending mail (%{value})"
162 notice_feeds_access_key_reseted: Your RSS access key was reset.
162 notice_feeds_access_key_reseted: Your RSS access key was reset.
163 notice_api_access_key_reseted: Your API access key was reset.
163 notice_api_access_key_reseted: Your API access key was reset.
164 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
164 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
165 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
165 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
166 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
166 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
167 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
167 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
168 notice_account_pending: "Your account was created and is now pending administrator approval."
168 notice_account_pending: "Your account was created and is now pending administrator approval."
169 notice_default_data_loaded: Default configuration successfully loaded.
169 notice_default_data_loaded: Default configuration successfully loaded.
170 notice_unable_delete_version: Unable to delete version.
170 notice_unable_delete_version: Unable to delete version.
171 notice_unable_delete_time_entry: Unable to delete time log entry.
171 notice_unable_delete_time_entry: Unable to delete time log entry.
172 notice_issue_done_ratios_updated: Issue done ratios updated.
172 notice_issue_done_ratios_updated: Issue done ratios updated.
173 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
173 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
174 notice_issue_successful_create: "Issue %{id} created."
174 notice_issue_successful_create: "Issue %{id} created."
175
175
176 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
176 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
177 error_scm_not_found: "The entry or revision was not found in the repository."
177 error_scm_not_found: "The entry or revision was not found in the repository."
178 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
178 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
179 error_scm_annotate: "The entry does not exist or cannot be annotated."
179 error_scm_annotate: "The entry does not exist or cannot be annotated."
180 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
180 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
181 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
181 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
182 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
182 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
183 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
183 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
184 error_can_not_delete_custom_field: Unable to delete custom field
184 error_can_not_delete_custom_field: Unable to delete custom field
185 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
185 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
186 error_can_not_remove_role: "This role is in use and cannot be deleted."
186 error_can_not_remove_role: "This role is in use and cannot be deleted."
187 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
187 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
188 error_can_not_archive_project: This project cannot be archived
188 error_can_not_archive_project: This project cannot be archived
189 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
189 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
190 error_workflow_copy_source: 'Please select a source tracker or role'
190 error_workflow_copy_source: 'Please select a source tracker or role'
191 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
191 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
192 error_unable_delete_issue_status: 'Unable to delete issue status'
192 error_unable_delete_issue_status: 'Unable to delete issue status'
193 error_unable_to_connect: "Unable to connect (%{value})"
193 error_unable_to_connect: "Unable to connect (%{value})"
194 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
194 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
195 warning_attachments_not_saved: "%{count} file(s) could not be saved."
195 warning_attachments_not_saved: "%{count} file(s) could not be saved."
196
196
197 mail_subject_lost_password: "Your %{value} password"
197 mail_subject_lost_password: "Your %{value} password"
198 mail_body_lost_password: 'To change your password, click on the following link:'
198 mail_body_lost_password: 'To change your password, click on the following link:'
199 mail_subject_register: "Your %{value} account activation"
199 mail_subject_register: "Your %{value} account activation"
200 mail_body_register: 'To activate your account, click on the following link:'
200 mail_body_register: 'To activate your account, click on the following link:'
201 mail_body_account_information_external: "You can use your %{value} account to log in."
201 mail_body_account_information_external: "You can use your %{value} account to log in."
202 mail_body_account_information: Your account information
202 mail_body_account_information: Your account information
203 mail_subject_account_activation_request: "%{value} account activation request"
203 mail_subject_account_activation_request: "%{value} account activation request"
204 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
204 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
205 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
205 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
206 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
206 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
207 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
207 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
208 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
208 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
209 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
209 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
210 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
210 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
211
211
212 gui_validation_error: 1 error
212 gui_validation_error: 1 error
213 gui_validation_error_plural: "%{count} errors"
213 gui_validation_error_plural: "%{count} errors"
214
214
215 field_name: Name
215 field_name: Name
216 field_description: Description
216 field_description: Description
217 field_summary: Summary
217 field_summary: Summary
218 field_is_required: Required
218 field_is_required: Required
219 field_firstname: First name
219 field_firstname: First name
220 field_lastname: Last name
220 field_lastname: Last name
221 field_mail: Email
221 field_mail: Email
222 field_filename: File
222 field_filename: File
223 field_filesize: Size
223 field_filesize: Size
224 field_downloads: Downloads
224 field_downloads: Downloads
225 field_author: Author
225 field_author: Author
226 field_created_on: Created
226 field_created_on: Created
227 field_updated_on: Updated
227 field_updated_on: Updated
228 field_field_format: Format
228 field_field_format: Format
229 field_is_for_all: For all projects
229 field_is_for_all: For all projects
230 field_possible_values: Possible values
230 field_possible_values: Possible values
231 field_regexp: Regular expression
231 field_regexp: Regular expression
232 field_min_length: Minimum length
232 field_min_length: Minimum length
233 field_max_length: Maximum length
233 field_max_length: Maximum length
234 field_value: Value
234 field_value: Value
235 field_category: Category
235 field_category: Category
236 field_title: Title
236 field_title: Title
237 field_project: Project
237 field_project: Project
238 field_issue: Issue
238 field_issue: Issue
239 field_status: Status
239 field_status: Status
240 field_notes: Notes
240 field_notes: Notes
241 field_is_closed: Issue closed
241 field_is_closed: Issue closed
242 field_is_default: Default value
242 field_is_default: Default value
243 field_tracker: Tracker
243 field_tracker: Tracker
244 field_subject: Subject
244 field_subject: Subject
245 field_due_date: Due date
245 field_due_date: Due date
246 field_assigned_to: Assignee
246 field_assigned_to: Assignee
247 field_priority: Priority
247 field_priority: Priority
248 field_fixed_version: Target version
248 field_fixed_version: Target version
249 field_user: User
249 field_user: User
250 field_principal: Principal
250 field_principal: Principal
251 field_role: Role
251 field_role: Role
252 field_homepage: Homepage
252 field_homepage: Homepage
253 field_is_public: Public
253 field_is_public: Public
254 field_parent: Subproject of
254 field_parent: Subproject of
255 field_is_in_roadmap: Issues displayed in roadmap
255 field_is_in_roadmap: Issues displayed in roadmap
256 field_login: Login
256 field_login: Login
257 field_mail_notification: Email notifications
257 field_mail_notification: Email notifications
258 field_admin: Administrator
258 field_admin: Administrator
259 field_last_login_on: Last connection
259 field_last_login_on: Last connection
260 field_language: Language
260 field_language: Language
261 field_effective_date: Date
261 field_effective_date: Date
262 field_password: Password
262 field_password: Password
263 field_new_password: New password
263 field_new_password: New password
264 field_password_confirmation: Confirmation
264 field_password_confirmation: Confirmation
265 field_version: Version
265 field_version: Version
266 field_type: Type
266 field_type: Type
267 field_host: Host
267 field_host: Host
268 field_port: Port
268 field_port: Port
269 field_account: Account
269 field_account: Account
270 field_base_dn: Base DN
270 field_base_dn: Base DN
271 field_attr_login: Login attribute
271 field_attr_login: Login attribute
272 field_attr_firstname: Firstname attribute
272 field_attr_firstname: Firstname attribute
273 field_attr_lastname: Lastname attribute
273 field_attr_lastname: Lastname attribute
274 field_attr_mail: Email attribute
274 field_attr_mail: Email attribute
275 field_onthefly: On-the-fly user creation
275 field_onthefly: On-the-fly user creation
276 field_start_date: Start date
276 field_start_date: Start date
277 field_done_ratio: "% Done"
277 field_done_ratio: "% Done"
278 field_auth_source: Authentication mode
278 field_auth_source: Authentication mode
279 field_hide_mail: Hide my email address
279 field_hide_mail: Hide my email address
280 field_comments: Comment
280 field_comments: Comment
281 field_url: URL
281 field_url: URL
282 field_start_page: Start page
282 field_start_page: Start page
283 field_subproject: Subproject
283 field_subproject: Subproject
284 field_hours: Hours
284 field_hours: Hours
285 field_activity: Activity
285 field_activity: Activity
286 field_spent_on: Date
286 field_spent_on: Date
287 field_identifier: Identifier
287 field_identifier: Identifier
288 field_is_filter: Used as a filter
288 field_is_filter: Used as a filter
289 field_issue_to: Related issue
289 field_issue_to: Related issue
290 field_delay: Delay
290 field_delay: Delay
291 field_assignable: Issues can be assigned to this role
291 field_assignable: Issues can be assigned to this role
292 field_redirect_existing_links: Redirect existing links
292 field_redirect_existing_links: Redirect existing links
293 field_estimated_hours: Estimated time
293 field_estimated_hours: Estimated time
294 field_column_names: Columns
294 field_column_names: Columns
295 field_time_entries: Log time
295 field_time_entries: Log time
296 field_time_zone: Time zone
296 field_time_zone: Time zone
297 field_searchable: Searchable
297 field_searchable: Searchable
298 field_default_value: Default value
298 field_default_value: Default value
299 field_comments_sorting: Display comments
299 field_comments_sorting: Display comments
300 field_parent_title: Parent page
300 field_parent_title: Parent page
301 field_editable: Editable
301 field_editable: Editable
302 field_watcher: Watcher
302 field_watcher: Watcher
303 field_identity_url: OpenID URL
303 field_identity_url: OpenID URL
304 field_content: Content
304 field_content: Content
305 field_group_by: Group results by
305 field_group_by: Group results by
306 field_sharing: Sharing
306 field_sharing: Sharing
307 field_parent_issue: Parent task
307 field_parent_issue: Parent task
308 field_member_of_group: "Assignee's group"
308 field_member_of_group: "Assignee's group"
309 field_assigned_to_role: "Assignee's role"
309 field_assigned_to_role: "Assignee's role"
310 field_text: Text field
310 field_text: Text field
311 field_visible: Visible
311 field_visible: Visible
312 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
312 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
313 field_issues_visibility: Issues visibility
313 field_issues_visibility: Issues visibility
314 field_is_private: Private
314 field_is_private: Private
315 field_commit_logs_encoding: Commit messages encoding
315 field_commit_logs_encoding: Commit messages encoding
316 field_scm_path_encoding: Path encoding
316 field_scm_path_encoding: Path encoding
317 field_path_to_repository: Path to repository
317 field_path_to_repository: Path to repository
318 field_root_directory: Root directory
318 field_root_directory: Root directory
319 field_cvsroot: CVSROOT
319 field_cvsroot: CVSROOT
320 field_cvs_module: Module
320 field_cvs_module: Module
321 field_repository_is_default: Main repository
321
322
322 setting_app_title: Application title
323 setting_app_title: Application title
323 setting_app_subtitle: Application subtitle
324 setting_app_subtitle: Application subtitle
324 setting_welcome_text: Welcome text
325 setting_welcome_text: Welcome text
325 setting_default_language: Default language
326 setting_default_language: Default language
326 setting_login_required: Authentication required
327 setting_login_required: Authentication required
327 setting_self_registration: Self-registration
328 setting_self_registration: Self-registration
328 setting_attachment_max_size: Maximum attachment size
329 setting_attachment_max_size: Maximum attachment size
329 setting_issues_export_limit: Issues export limit
330 setting_issues_export_limit: Issues export limit
330 setting_mail_from: Emission email address
331 setting_mail_from: Emission email address
331 setting_bcc_recipients: Blind carbon copy recipients (bcc)
332 setting_bcc_recipients: Blind carbon copy recipients (bcc)
332 setting_plain_text_mail: Plain text mail (no HTML)
333 setting_plain_text_mail: Plain text mail (no HTML)
333 setting_host_name: Host name and path
334 setting_host_name: Host name and path
334 setting_text_formatting: Text formatting
335 setting_text_formatting: Text formatting
335 setting_wiki_compression: Wiki history compression
336 setting_wiki_compression: Wiki history compression
336 setting_feeds_limit: Maximum number of items in Atom feeds
337 setting_feeds_limit: Maximum number of items in Atom feeds
337 setting_default_projects_public: New projects are public by default
338 setting_default_projects_public: New projects are public by default
338 setting_autofetch_changesets: Fetch commits automatically
339 setting_autofetch_changesets: Fetch commits automatically
339 setting_sys_api_enabled: Enable WS for repository management
340 setting_sys_api_enabled: Enable WS for repository management
340 setting_commit_ref_keywords: Referencing keywords
341 setting_commit_ref_keywords: Referencing keywords
341 setting_commit_fix_keywords: Fixing keywords
342 setting_commit_fix_keywords: Fixing keywords
342 setting_autologin: Autologin
343 setting_autologin: Autologin
343 setting_date_format: Date format
344 setting_date_format: Date format
344 setting_time_format: Time format
345 setting_time_format: Time format
345 setting_cross_project_issue_relations: Allow cross-project issue relations
346 setting_cross_project_issue_relations: Allow cross-project issue relations
346 setting_issue_list_default_columns: Default columns displayed on the issue list
347 setting_issue_list_default_columns: Default columns displayed on the issue list
347 setting_repositories_encodings: Attachments and repositories encodings
348 setting_repositories_encodings: Attachments and repositories encodings
348 setting_emails_header: Emails header
349 setting_emails_header: Emails header
349 setting_emails_footer: Emails footer
350 setting_emails_footer: Emails footer
350 setting_protocol: Protocol
351 setting_protocol: Protocol
351 setting_per_page_options: Objects per page options
352 setting_per_page_options: Objects per page options
352 setting_user_format: Users display format
353 setting_user_format: Users display format
353 setting_activity_days_default: Days displayed on project activity
354 setting_activity_days_default: Days displayed on project activity
354 setting_display_subprojects_issues: Display subprojects issues on main projects by default
355 setting_display_subprojects_issues: Display subprojects issues on main projects by default
355 setting_enabled_scm: Enabled SCM
356 setting_enabled_scm: Enabled SCM
356 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
357 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
357 setting_mail_handler_api_enabled: Enable WS for incoming emails
358 setting_mail_handler_api_enabled: Enable WS for incoming emails
358 setting_mail_handler_api_key: API key
359 setting_mail_handler_api_key: API key
359 setting_sequential_project_identifiers: Generate sequential project identifiers
360 setting_sequential_project_identifiers: Generate sequential project identifiers
360 setting_gravatar_enabled: Use Gravatar user icons
361 setting_gravatar_enabled: Use Gravatar user icons
361 setting_gravatar_default: Default Gravatar image
362 setting_gravatar_default: Default Gravatar image
362 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
363 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
363 setting_file_max_size_displayed: Maximum size of text files displayed inline
364 setting_file_max_size_displayed: Maximum size of text files displayed inline
364 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
365 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
365 setting_openid: Allow OpenID login and registration
366 setting_openid: Allow OpenID login and registration
366 setting_password_min_length: Minimum password length
367 setting_password_min_length: Minimum password length
367 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
368 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
368 setting_default_projects_modules: Default enabled modules for new projects
369 setting_default_projects_modules: Default enabled modules for new projects
369 setting_issue_done_ratio: Calculate the issue done ratio with
370 setting_issue_done_ratio: Calculate the issue done ratio with
370 setting_issue_done_ratio_issue_field: Use the issue field
371 setting_issue_done_ratio_issue_field: Use the issue field
371 setting_issue_done_ratio_issue_status: Use the issue status
372 setting_issue_done_ratio_issue_status: Use the issue status
372 setting_start_of_week: Start calendars on
373 setting_start_of_week: Start calendars on
373 setting_rest_api_enabled: Enable REST web service
374 setting_rest_api_enabled: Enable REST web service
374 setting_cache_formatted_text: Cache formatted text
375 setting_cache_formatted_text: Cache formatted text
375 setting_default_notification_option: Default notification option
376 setting_default_notification_option: Default notification option
376 setting_commit_logtime_enabled: Enable time logging
377 setting_commit_logtime_enabled: Enable time logging
377 setting_commit_logtime_activity_id: Activity for logged time
378 setting_commit_logtime_activity_id: Activity for logged time
378 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
379 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
379 setting_issue_group_assignment: Allow issue assignment to groups
380 setting_issue_group_assignment: Allow issue assignment to groups
380 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
381 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
381
382
382 permission_add_project: Create project
383 permission_add_project: Create project
383 permission_add_subprojects: Create subprojects
384 permission_add_subprojects: Create subprojects
384 permission_edit_project: Edit project
385 permission_edit_project: Edit project
385 permission_select_project_modules: Select project modules
386 permission_select_project_modules: Select project modules
386 permission_manage_members: Manage members
387 permission_manage_members: Manage members
387 permission_manage_project_activities: Manage project activities
388 permission_manage_project_activities: Manage project activities
388 permission_manage_versions: Manage versions
389 permission_manage_versions: Manage versions
389 permission_manage_categories: Manage issue categories
390 permission_manage_categories: Manage issue categories
390 permission_view_issues: View Issues
391 permission_view_issues: View Issues
391 permission_add_issues: Add issues
392 permission_add_issues: Add issues
392 permission_edit_issues: Edit issues
393 permission_edit_issues: Edit issues
393 permission_manage_issue_relations: Manage issue relations
394 permission_manage_issue_relations: Manage issue relations
394 permission_set_issues_private: Set issues public or private
395 permission_set_issues_private: Set issues public or private
395 permission_set_own_issues_private: Set own issues public or private
396 permission_set_own_issues_private: Set own issues public or private
396 permission_add_issue_notes: Add notes
397 permission_add_issue_notes: Add notes
397 permission_edit_issue_notes: Edit notes
398 permission_edit_issue_notes: Edit notes
398 permission_edit_own_issue_notes: Edit own notes
399 permission_edit_own_issue_notes: Edit own notes
399 permission_move_issues: Move issues
400 permission_move_issues: Move issues
400 permission_delete_issues: Delete issues
401 permission_delete_issues: Delete issues
401 permission_manage_public_queries: Manage public queries
402 permission_manage_public_queries: Manage public queries
402 permission_save_queries: Save queries
403 permission_save_queries: Save queries
403 permission_view_gantt: View gantt chart
404 permission_view_gantt: View gantt chart
404 permission_view_calendar: View calendar
405 permission_view_calendar: View calendar
405 permission_view_issue_watchers: View watchers list
406 permission_view_issue_watchers: View watchers list
406 permission_add_issue_watchers: Add watchers
407 permission_add_issue_watchers: Add watchers
407 permission_delete_issue_watchers: Delete watchers
408 permission_delete_issue_watchers: Delete watchers
408 permission_log_time: Log spent time
409 permission_log_time: Log spent time
409 permission_view_time_entries: View spent time
410 permission_view_time_entries: View spent time
410 permission_edit_time_entries: Edit time logs
411 permission_edit_time_entries: Edit time logs
411 permission_edit_own_time_entries: Edit own time logs
412 permission_edit_own_time_entries: Edit own time logs
412 permission_manage_news: Manage news
413 permission_manage_news: Manage news
413 permission_comment_news: Comment news
414 permission_comment_news: Comment news
414 permission_manage_documents: Manage documents
415 permission_manage_documents: Manage documents
415 permission_view_documents: View documents
416 permission_view_documents: View documents
416 permission_manage_files: Manage files
417 permission_manage_files: Manage files
417 permission_view_files: View files
418 permission_view_files: View files
418 permission_manage_wiki: Manage wiki
419 permission_manage_wiki: Manage wiki
419 permission_rename_wiki_pages: Rename wiki pages
420 permission_rename_wiki_pages: Rename wiki pages
420 permission_delete_wiki_pages: Delete wiki pages
421 permission_delete_wiki_pages: Delete wiki pages
421 permission_view_wiki_pages: View wiki
422 permission_view_wiki_pages: View wiki
422 permission_view_wiki_edits: View wiki history
423 permission_view_wiki_edits: View wiki history
423 permission_edit_wiki_pages: Edit wiki pages
424 permission_edit_wiki_pages: Edit wiki pages
424 permission_delete_wiki_pages_attachments: Delete attachments
425 permission_delete_wiki_pages_attachments: Delete attachments
425 permission_protect_wiki_pages: Protect wiki pages
426 permission_protect_wiki_pages: Protect wiki pages
426 permission_manage_repository: Manage repository
427 permission_manage_repository: Manage repository
427 permission_browse_repository: Browse repository
428 permission_browse_repository: Browse repository
428 permission_view_changesets: View changesets
429 permission_view_changesets: View changesets
429 permission_commit_access: Commit access
430 permission_commit_access: Commit access
430 permission_manage_boards: Manage forums
431 permission_manage_boards: Manage forums
431 permission_view_messages: View messages
432 permission_view_messages: View messages
432 permission_add_messages: Post messages
433 permission_add_messages: Post messages
433 permission_edit_messages: Edit messages
434 permission_edit_messages: Edit messages
434 permission_edit_own_messages: Edit own messages
435 permission_edit_own_messages: Edit own messages
435 permission_delete_messages: Delete messages
436 permission_delete_messages: Delete messages
436 permission_delete_own_messages: Delete own messages
437 permission_delete_own_messages: Delete own messages
437 permission_export_wiki_pages: Export wiki pages
438 permission_export_wiki_pages: Export wiki pages
438 permission_manage_subtasks: Manage subtasks
439 permission_manage_subtasks: Manage subtasks
439
440
440 project_module_issue_tracking: Issue tracking
441 project_module_issue_tracking: Issue tracking
441 project_module_time_tracking: Time tracking
442 project_module_time_tracking: Time tracking
442 project_module_news: News
443 project_module_news: News
443 project_module_documents: Documents
444 project_module_documents: Documents
444 project_module_files: Files
445 project_module_files: Files
445 project_module_wiki: Wiki
446 project_module_wiki: Wiki
446 project_module_repository: Repository
447 project_module_repository: Repository
447 project_module_boards: Forums
448 project_module_boards: Forums
448 project_module_calendar: Calendar
449 project_module_calendar: Calendar
449 project_module_gantt: Gantt
450 project_module_gantt: Gantt
450
451
451 label_user: User
452 label_user: User
452 label_user_plural: Users
453 label_user_plural: Users
453 label_user_new: New user
454 label_user_new: New user
454 label_user_anonymous: Anonymous
455 label_user_anonymous: Anonymous
455 label_project: Project
456 label_project: Project
456 label_project_new: New project
457 label_project_new: New project
457 label_project_plural: Projects
458 label_project_plural: Projects
458 label_x_projects:
459 label_x_projects:
459 zero: no projects
460 zero: no projects
460 one: 1 project
461 one: 1 project
461 other: "%{count} projects"
462 other: "%{count} projects"
462 label_project_all: All Projects
463 label_project_all: All Projects
463 label_project_latest: Latest projects
464 label_project_latest: Latest projects
464 label_issue: Issue
465 label_issue: Issue
465 label_issue_new: New issue
466 label_issue_new: New issue
466 label_issue_plural: Issues
467 label_issue_plural: Issues
467 label_issue_view_all: View all issues
468 label_issue_view_all: View all issues
468 label_issues_by: "Issues by %{value}"
469 label_issues_by: "Issues by %{value}"
469 label_issue_added: Issue added
470 label_issue_added: Issue added
470 label_issue_updated: Issue updated
471 label_issue_updated: Issue updated
471 label_issue_note_added: Note added
472 label_issue_note_added: Note added
472 label_issue_status_updated: Status updated
473 label_issue_status_updated: Status updated
473 label_issue_priority_updated: Priority updated
474 label_issue_priority_updated: Priority updated
474 label_document: Document
475 label_document: Document
475 label_document_new: New document
476 label_document_new: New document
476 label_document_plural: Documents
477 label_document_plural: Documents
477 label_document_added: Document added
478 label_document_added: Document added
478 label_role: Role
479 label_role: Role
479 label_role_plural: Roles
480 label_role_plural: Roles
480 label_role_new: New role
481 label_role_new: New role
481 label_role_and_permissions: Roles and permissions
482 label_role_and_permissions: Roles and permissions
482 label_role_anonymous: Anonymous
483 label_role_anonymous: Anonymous
483 label_role_non_member: Non member
484 label_role_non_member: Non member
484 label_member: Member
485 label_member: Member
485 label_member_new: New member
486 label_member_new: New member
486 label_member_plural: Members
487 label_member_plural: Members
487 label_tracker: Tracker
488 label_tracker: Tracker
488 label_tracker_plural: Trackers
489 label_tracker_plural: Trackers
489 label_tracker_new: New tracker
490 label_tracker_new: New tracker
490 label_workflow: Workflow
491 label_workflow: Workflow
491 label_issue_status: Issue status
492 label_issue_status: Issue status
492 label_issue_status_plural: Issue statuses
493 label_issue_status_plural: Issue statuses
493 label_issue_status_new: New status
494 label_issue_status_new: New status
494 label_issue_category: Issue category
495 label_issue_category: Issue category
495 label_issue_category_plural: Issue categories
496 label_issue_category_plural: Issue categories
496 label_issue_category_new: New category
497 label_issue_category_new: New category
497 label_custom_field: Custom field
498 label_custom_field: Custom field
498 label_custom_field_plural: Custom fields
499 label_custom_field_plural: Custom fields
499 label_custom_field_new: New custom field
500 label_custom_field_new: New custom field
500 label_enumerations: Enumerations
501 label_enumerations: Enumerations
501 label_enumeration_new: New value
502 label_enumeration_new: New value
502 label_information: Information
503 label_information: Information
503 label_information_plural: Information
504 label_information_plural: Information
504 label_please_login: Please log in
505 label_please_login: Please log in
505 label_register: Register
506 label_register: Register
506 label_login_with_open_id_option: or login with OpenID
507 label_login_with_open_id_option: or login with OpenID
507 label_password_lost: Lost password
508 label_password_lost: Lost password
508 label_home: Home
509 label_home: Home
509 label_my_page: My page
510 label_my_page: My page
510 label_my_account: My account
511 label_my_account: My account
511 label_my_projects: My projects
512 label_my_projects: My projects
512 label_my_page_block: My page block
513 label_my_page_block: My page block
513 label_administration: Administration
514 label_administration: Administration
514 label_login: Sign in
515 label_login: Sign in
515 label_logout: Sign out
516 label_logout: Sign out
516 label_help: Help
517 label_help: Help
517 label_reported_issues: Reported issues
518 label_reported_issues: Reported issues
518 label_assigned_to_me_issues: Issues assigned to me
519 label_assigned_to_me_issues: Issues assigned to me
519 label_last_login: Last connection
520 label_last_login: Last connection
520 label_registered_on: Registered on
521 label_registered_on: Registered on
521 label_activity: Activity
522 label_activity: Activity
522 label_overall_activity: Overall activity
523 label_overall_activity: Overall activity
523 label_user_activity: "%{value}'s activity"
524 label_user_activity: "%{value}'s activity"
524 label_new: New
525 label_new: New
525 label_logged_as: Logged in as
526 label_logged_as: Logged in as
526 label_environment: Environment
527 label_environment: Environment
527 label_authentication: Authentication
528 label_authentication: Authentication
528 label_auth_source: Authentication mode
529 label_auth_source: Authentication mode
529 label_auth_source_new: New authentication mode
530 label_auth_source_new: New authentication mode
530 label_auth_source_plural: Authentication modes
531 label_auth_source_plural: Authentication modes
531 label_subproject_plural: Subprojects
532 label_subproject_plural: Subprojects
532 label_subproject_new: New subproject
533 label_subproject_new: New subproject
533 label_and_its_subprojects: "%{value} and its subprojects"
534 label_and_its_subprojects: "%{value} and its subprojects"
534 label_min_max_length: Min - Max length
535 label_min_max_length: Min - Max length
535 label_list: List
536 label_list: List
536 label_date: Date
537 label_date: Date
537 label_integer: Integer
538 label_integer: Integer
538 label_float: Float
539 label_float: Float
539 label_boolean: Boolean
540 label_boolean: Boolean
540 label_string: Text
541 label_string: Text
541 label_text: Long text
542 label_text: Long text
542 label_attribute: Attribute
543 label_attribute: Attribute
543 label_attribute_plural: Attributes
544 label_attribute_plural: Attributes
544 label_download: "%{count} Download"
545 label_download: "%{count} Download"
545 label_download_plural: "%{count} Downloads"
546 label_download_plural: "%{count} Downloads"
546 label_no_data: No data to display
547 label_no_data: No data to display
547 label_change_status: Change status
548 label_change_status: Change status
548 label_history: History
549 label_history: History
549 label_attachment: File
550 label_attachment: File
550 label_attachment_new: New file
551 label_attachment_new: New file
551 label_attachment_delete: Delete file
552 label_attachment_delete: Delete file
552 label_attachment_plural: Files
553 label_attachment_plural: Files
553 label_file_added: File added
554 label_file_added: File added
554 label_report: Report
555 label_report: Report
555 label_report_plural: Reports
556 label_report_plural: Reports
556 label_news: News
557 label_news: News
557 label_news_new: Add news
558 label_news_new: Add news
558 label_news_plural: News
559 label_news_plural: News
559 label_news_latest: Latest news
560 label_news_latest: Latest news
560 label_news_view_all: View all news
561 label_news_view_all: View all news
561 label_news_added: News added
562 label_news_added: News added
562 label_news_comment_added: Comment added to a news
563 label_news_comment_added: Comment added to a news
563 label_settings: Settings
564 label_settings: Settings
564 label_overview: Overview
565 label_overview: Overview
565 label_version: Version
566 label_version: Version
566 label_version_new: New version
567 label_version_new: New version
567 label_version_plural: Versions
568 label_version_plural: Versions
568 label_close_versions: Close completed versions
569 label_close_versions: Close completed versions
569 label_confirmation: Confirmation
570 label_confirmation: Confirmation
570 label_export_to: 'Also available in:'
571 label_export_to: 'Also available in:'
571 label_read: Read...
572 label_read: Read...
572 label_public_projects: Public projects
573 label_public_projects: Public projects
573 label_open_issues: open
574 label_open_issues: open
574 label_open_issues_plural: open
575 label_open_issues_plural: open
575 label_closed_issues: closed
576 label_closed_issues: closed
576 label_closed_issues_plural: closed
577 label_closed_issues_plural: closed
577 label_x_open_issues_abbr_on_total:
578 label_x_open_issues_abbr_on_total:
578 zero: 0 open / %{total}
579 zero: 0 open / %{total}
579 one: 1 open / %{total}
580 one: 1 open / %{total}
580 other: "%{count} open / %{total}"
581 other: "%{count} open / %{total}"
581 label_x_open_issues_abbr:
582 label_x_open_issues_abbr:
582 zero: 0 open
583 zero: 0 open
583 one: 1 open
584 one: 1 open
584 other: "%{count} open"
585 other: "%{count} open"
585 label_x_closed_issues_abbr:
586 label_x_closed_issues_abbr:
586 zero: 0 closed
587 zero: 0 closed
587 one: 1 closed
588 one: 1 closed
588 other: "%{count} closed"
589 other: "%{count} closed"
589 label_x_issues:
590 label_x_issues:
590 zero: 0 issues
591 zero: 0 issues
591 one: 1 issue
592 one: 1 issue
592 other: "%{count} issues"
593 other: "%{count} issues"
593 label_total: Total
594 label_total: Total
594 label_permissions: Permissions
595 label_permissions: Permissions
595 label_current_status: Current status
596 label_current_status: Current status
596 label_new_statuses_allowed: New statuses allowed
597 label_new_statuses_allowed: New statuses allowed
597 label_all: all
598 label_all: all
598 label_none: none
599 label_none: none
599 label_nobody: nobody
600 label_nobody: nobody
600 label_next: Next
601 label_next: Next
601 label_previous: Previous
602 label_previous: Previous
602 label_used_by: Used by
603 label_used_by: Used by
603 label_details: Details
604 label_details: Details
604 label_add_note: Add a note
605 label_add_note: Add a note
605 label_per_page: Per page
606 label_per_page: Per page
606 label_calendar: Calendar
607 label_calendar: Calendar
607 label_months_from: months from
608 label_months_from: months from
608 label_gantt: Gantt
609 label_gantt: Gantt
609 label_internal: Internal
610 label_internal: Internal
610 label_last_changes: "last %{count} changes"
611 label_last_changes: "last %{count} changes"
611 label_change_view_all: View all changes
612 label_change_view_all: View all changes
612 label_personalize_page: Personalize this page
613 label_personalize_page: Personalize this page
613 label_comment: Comment
614 label_comment: Comment
614 label_comment_plural: Comments
615 label_comment_plural: Comments
615 label_x_comments:
616 label_x_comments:
616 zero: no comments
617 zero: no comments
617 one: 1 comment
618 one: 1 comment
618 other: "%{count} comments"
619 other: "%{count} comments"
619 label_comment_add: Add a comment
620 label_comment_add: Add a comment
620 label_comment_added: Comment added
621 label_comment_added: Comment added
621 label_comment_delete: Delete comments
622 label_comment_delete: Delete comments
622 label_query: Custom query
623 label_query: Custom query
623 label_query_plural: Custom queries
624 label_query_plural: Custom queries
624 label_query_new: New query
625 label_query_new: New query
625 label_my_queries: My custom queries
626 label_my_queries: My custom queries
626 label_filter_add: Add filter
627 label_filter_add: Add filter
627 label_filter_plural: Filters
628 label_filter_plural: Filters
628 label_equals: is
629 label_equals: is
629 label_not_equals: is not
630 label_not_equals: is not
630 label_in_less_than: in less than
631 label_in_less_than: in less than
631 label_in_more_than: in more than
632 label_in_more_than: in more than
632 label_greater_or_equal: '>='
633 label_greater_or_equal: '>='
633 label_less_or_equal: '<='
634 label_less_or_equal: '<='
634 label_between: between
635 label_between: between
635 label_in: in
636 label_in: in
636 label_today: today
637 label_today: today
637 label_all_time: all time
638 label_all_time: all time
638 label_yesterday: yesterday
639 label_yesterday: yesterday
639 label_this_week: this week
640 label_this_week: this week
640 label_last_week: last week
641 label_last_week: last week
641 label_last_n_days: "last %{count} days"
642 label_last_n_days: "last %{count} days"
642 label_this_month: this month
643 label_this_month: this month
643 label_last_month: last month
644 label_last_month: last month
644 label_this_year: this year
645 label_this_year: this year
645 label_date_range: Date range
646 label_date_range: Date range
646 label_less_than_ago: less than days ago
647 label_less_than_ago: less than days ago
647 label_more_than_ago: more than days ago
648 label_more_than_ago: more than days ago
648 label_ago: days ago
649 label_ago: days ago
649 label_contains: contains
650 label_contains: contains
650 label_not_contains: doesn't contain
651 label_not_contains: doesn't contain
651 label_day_plural: days
652 label_day_plural: days
652 label_repository: Repository
653 label_repository: Repository
653 label_repository_new: New repository
654 label_repository_new: New repository
654 label_repository_plural: Repositories
655 label_repository_plural: Repositories
655 label_browse: Browse
656 label_browse: Browse
656 label_modification: "%{count} change"
657 label_modification: "%{count} change"
657 label_modification_plural: "%{count} changes"
658 label_modification_plural: "%{count} changes"
658 label_branch: Branch
659 label_branch: Branch
659 label_tag: Tag
660 label_tag: Tag
660 label_revision: Revision
661 label_revision: Revision
661 label_revision_plural: Revisions
662 label_revision_plural: Revisions
662 label_revision_id: "Revision %{value}"
663 label_revision_id: "Revision %{value}"
663 label_associated_revisions: Associated revisions
664 label_associated_revisions: Associated revisions
664 label_added: added
665 label_added: added
665 label_modified: modified
666 label_modified: modified
666 label_copied: copied
667 label_copied: copied
667 label_renamed: renamed
668 label_renamed: renamed
668 label_deleted: deleted
669 label_deleted: deleted
669 label_latest_revision: Latest revision
670 label_latest_revision: Latest revision
670 label_latest_revision_plural: Latest revisions
671 label_latest_revision_plural: Latest revisions
671 label_view_revisions: View revisions
672 label_view_revisions: View revisions
672 label_view_all_revisions: View all revisions
673 label_view_all_revisions: View all revisions
673 label_max_size: Maximum size
674 label_max_size: Maximum size
674 label_sort_highest: Move to top
675 label_sort_highest: Move to top
675 label_sort_higher: Move up
676 label_sort_higher: Move up
676 label_sort_lower: Move down
677 label_sort_lower: Move down
677 label_sort_lowest: Move to bottom
678 label_sort_lowest: Move to bottom
678 label_roadmap: Roadmap
679 label_roadmap: Roadmap
679 label_roadmap_due_in: "Due in %{value}"
680 label_roadmap_due_in: "Due in %{value}"
680 label_roadmap_overdue: "%{value} late"
681 label_roadmap_overdue: "%{value} late"
681 label_roadmap_no_issues: No issues for this version
682 label_roadmap_no_issues: No issues for this version
682 label_search: Search
683 label_search: Search
683 label_result_plural: Results
684 label_result_plural: Results
684 label_all_words: All words
685 label_all_words: All words
685 label_wiki: Wiki
686 label_wiki: Wiki
686 label_wiki_edit: Wiki edit
687 label_wiki_edit: Wiki edit
687 label_wiki_edit_plural: Wiki edits
688 label_wiki_edit_plural: Wiki edits
688 label_wiki_page: Wiki page
689 label_wiki_page: Wiki page
689 label_wiki_page_plural: Wiki pages
690 label_wiki_page_plural: Wiki pages
690 label_index_by_title: Index by title
691 label_index_by_title: Index by title
691 label_index_by_date: Index by date
692 label_index_by_date: Index by date
692 label_current_version: Current version
693 label_current_version: Current version
693 label_preview: Preview
694 label_preview: Preview
694 label_feed_plural: Feeds
695 label_feed_plural: Feeds
695 label_changes_details: Details of all changes
696 label_changes_details: Details of all changes
696 label_issue_tracking: Issue tracking
697 label_issue_tracking: Issue tracking
697 label_spent_time: Spent time
698 label_spent_time: Spent time
698 label_overall_spent_time: Overall spent time
699 label_overall_spent_time: Overall spent time
699 label_f_hour: "%{value} hour"
700 label_f_hour: "%{value} hour"
700 label_f_hour_plural: "%{value} hours"
701 label_f_hour_plural: "%{value} hours"
701 label_time_tracking: Time tracking
702 label_time_tracking: Time tracking
702 label_change_plural: Changes
703 label_change_plural: Changes
703 label_statistics: Statistics
704 label_statistics: Statistics
704 label_commits_per_month: Commits per month
705 label_commits_per_month: Commits per month
705 label_commits_per_author: Commits per author
706 label_commits_per_author: Commits per author
706 label_diff: diff
707 label_diff: diff
707 label_view_diff: View differences
708 label_view_diff: View differences
708 label_diff_inline: inline
709 label_diff_inline: inline
709 label_diff_side_by_side: side by side
710 label_diff_side_by_side: side by side
710 label_options: Options
711 label_options: Options
711 label_copy_workflow_from: Copy workflow from
712 label_copy_workflow_from: Copy workflow from
712 label_permissions_report: Permissions report
713 label_permissions_report: Permissions report
713 label_watched_issues: Watched issues
714 label_watched_issues: Watched issues
714 label_related_issues: Related issues
715 label_related_issues: Related issues
715 label_applied_status: Applied status
716 label_applied_status: Applied status
716 label_loading: Loading...
717 label_loading: Loading...
717 label_relation_new: New relation
718 label_relation_new: New relation
718 label_relation_delete: Delete relation
719 label_relation_delete: Delete relation
719 label_relates_to: related to
720 label_relates_to: related to
720 label_duplicates: duplicates
721 label_duplicates: duplicates
721 label_duplicated_by: duplicated by
722 label_duplicated_by: duplicated by
722 label_blocks: blocks
723 label_blocks: blocks
723 label_blocked_by: blocked by
724 label_blocked_by: blocked by
724 label_precedes: precedes
725 label_precedes: precedes
725 label_follows: follows
726 label_follows: follows
726 label_end_to_start: end to start
727 label_end_to_start: end to start
727 label_end_to_end: end to end
728 label_end_to_end: end to end
728 label_start_to_start: start to start
729 label_start_to_start: start to start
729 label_start_to_end: start to end
730 label_start_to_end: start to end
730 label_stay_logged_in: Stay logged in
731 label_stay_logged_in: Stay logged in
731 label_disabled: disabled
732 label_disabled: disabled
732 label_show_completed_versions: Show completed versions
733 label_show_completed_versions: Show completed versions
733 label_me: me
734 label_me: me
734 label_board: Forum
735 label_board: Forum
735 label_board_new: New forum
736 label_board_new: New forum
736 label_board_plural: Forums
737 label_board_plural: Forums
737 label_board_locked: Locked
738 label_board_locked: Locked
738 label_board_sticky: Sticky
739 label_board_sticky: Sticky
739 label_topic_plural: Topics
740 label_topic_plural: Topics
740 label_message_plural: Messages
741 label_message_plural: Messages
741 label_message_last: Last message
742 label_message_last: Last message
742 label_message_new: New message
743 label_message_new: New message
743 label_message_posted: Message added
744 label_message_posted: Message added
744 label_reply_plural: Replies
745 label_reply_plural: Replies
745 label_send_information: Send account information to the user
746 label_send_information: Send account information to the user
746 label_year: Year
747 label_year: Year
747 label_month: Month
748 label_month: Month
748 label_week: Week
749 label_week: Week
749 label_date_from: From
750 label_date_from: From
750 label_date_to: To
751 label_date_to: To
751 label_language_based: Based on user's language
752 label_language_based: Based on user's language
752 label_sort_by: "Sort by %{value}"
753 label_sort_by: "Sort by %{value}"
753 label_send_test_email: Send a test email
754 label_send_test_email: Send a test email
754 label_feeds_access_key: RSS access key
755 label_feeds_access_key: RSS access key
755 label_missing_feeds_access_key: Missing a RSS access key
756 label_missing_feeds_access_key: Missing a RSS access key
756 label_feeds_access_key_created_on: "RSS access key created %{value} ago"
757 label_feeds_access_key_created_on: "RSS access key created %{value} ago"
757 label_module_plural: Modules
758 label_module_plural: Modules
758 label_added_time_by: "Added by %{author} %{age} ago"
759 label_added_time_by: "Added by %{author} %{age} ago"
759 label_updated_time_by: "Updated by %{author} %{age} ago"
760 label_updated_time_by: "Updated by %{author} %{age} ago"
760 label_updated_time: "Updated %{value} ago"
761 label_updated_time: "Updated %{value} ago"
761 label_jump_to_a_project: Jump to a project...
762 label_jump_to_a_project: Jump to a project...
762 label_file_plural: Files
763 label_file_plural: Files
763 label_changeset_plural: Changesets
764 label_changeset_plural: Changesets
764 label_default_columns: Default columns
765 label_default_columns: Default columns
765 label_no_change_option: (No change)
766 label_no_change_option: (No change)
766 label_bulk_edit_selected_issues: Bulk edit selected issues
767 label_bulk_edit_selected_issues: Bulk edit selected issues
767 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
768 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
768 label_theme: Theme
769 label_theme: Theme
769 label_default: Default
770 label_default: Default
770 label_search_titles_only: Search titles only
771 label_search_titles_only: Search titles only
771 label_user_mail_option_all: "For any event on all my projects"
772 label_user_mail_option_all: "For any event on all my projects"
772 label_user_mail_option_selected: "For any event on the selected projects only..."
773 label_user_mail_option_selected: "For any event on the selected projects only..."
773 label_user_mail_option_none: "No events"
774 label_user_mail_option_none: "No events"
774 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
775 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
775 label_user_mail_option_only_assigned: "Only for things I am assigned to"
776 label_user_mail_option_only_assigned: "Only for things I am assigned to"
776 label_user_mail_option_only_owner: "Only for things I am the owner of"
777 label_user_mail_option_only_owner: "Only for things I am the owner of"
777 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
778 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
778 label_registration_activation_by_email: account activation by email
779 label_registration_activation_by_email: account activation by email
779 label_registration_manual_activation: manual account activation
780 label_registration_manual_activation: manual account activation
780 label_registration_automatic_activation: automatic account activation
781 label_registration_automatic_activation: automatic account activation
781 label_display_per_page: "Per page: %{value}"
782 label_display_per_page: "Per page: %{value}"
782 label_age: Age
783 label_age: Age
783 label_change_properties: Change properties
784 label_change_properties: Change properties
784 label_general: General
785 label_general: General
785 label_more: More
786 label_more: More
786 label_scm: SCM
787 label_scm: SCM
787 label_plugins: Plugins
788 label_plugins: Plugins
788 label_ldap_authentication: LDAP authentication
789 label_ldap_authentication: LDAP authentication
789 label_downloads_abbr: D/L
790 label_downloads_abbr: D/L
790 label_optional_description: Optional description
791 label_optional_description: Optional description
791 label_add_another_file: Add another file
792 label_add_another_file: Add another file
792 label_preferences: Preferences
793 label_preferences: Preferences
793 label_chronological_order: In chronological order
794 label_chronological_order: In chronological order
794 label_reverse_chronological_order: In reverse chronological order
795 label_reverse_chronological_order: In reverse chronological order
795 label_planning: Planning
796 label_planning: Planning
796 label_incoming_emails: Incoming emails
797 label_incoming_emails: Incoming emails
797 label_generate_key: Generate a key
798 label_generate_key: Generate a key
798 label_issue_watchers: Watchers
799 label_issue_watchers: Watchers
799 label_example: Example
800 label_example: Example
800 label_display: Display
801 label_display: Display
801 label_sort: Sort
802 label_sort: Sort
802 label_ascending: Ascending
803 label_ascending: Ascending
803 label_descending: Descending
804 label_descending: Descending
804 label_date_from_to: From %{start} to %{end}
805 label_date_from_to: From %{start} to %{end}
805 label_wiki_content_added: Wiki page added
806 label_wiki_content_added: Wiki page added
806 label_wiki_content_updated: Wiki page updated
807 label_wiki_content_updated: Wiki page updated
807 label_group: Group
808 label_group: Group
808 label_group_plural: Groups
809 label_group_plural: Groups
809 label_group_new: New group
810 label_group_new: New group
810 label_time_entry_plural: Spent time
811 label_time_entry_plural: Spent time
811 label_version_sharing_none: Not shared
812 label_version_sharing_none: Not shared
812 label_version_sharing_descendants: With subprojects
813 label_version_sharing_descendants: With subprojects
813 label_version_sharing_hierarchy: With project hierarchy
814 label_version_sharing_hierarchy: With project hierarchy
814 label_version_sharing_tree: With project tree
815 label_version_sharing_tree: With project tree
815 label_version_sharing_system: With all projects
816 label_version_sharing_system: With all projects
816 label_update_issue_done_ratios: Update issue done ratios
817 label_update_issue_done_ratios: Update issue done ratios
817 label_copy_source: Source
818 label_copy_source: Source
818 label_copy_target: Target
819 label_copy_target: Target
819 label_copy_same_as_target: Same as target
820 label_copy_same_as_target: Same as target
820 label_display_used_statuses_only: Only display statuses that are used by this tracker
821 label_display_used_statuses_only: Only display statuses that are used by this tracker
821 label_api_access_key: API access key
822 label_api_access_key: API access key
822 label_missing_api_access_key: Missing an API access key
823 label_missing_api_access_key: Missing an API access key
823 label_api_access_key_created_on: "API access key created %{value} ago"
824 label_api_access_key_created_on: "API access key created %{value} ago"
824 label_profile: Profile
825 label_profile: Profile
825 label_subtask_plural: Subtasks
826 label_subtask_plural: Subtasks
826 label_project_copy_notifications: Send email notifications during the project copy
827 label_project_copy_notifications: Send email notifications during the project copy
827 label_principal_search: "Search for user or group:"
828 label_principal_search: "Search for user or group:"
828 label_user_search: "Search for user:"
829 label_user_search: "Search for user:"
829 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
830 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
830 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
831 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
831 label_issues_visibility_all: All issues
832 label_issues_visibility_all: All issues
832 label_issues_visibility_public: All non private issues
833 label_issues_visibility_public: All non private issues
833 label_issues_visibility_own: Issues created by or assigned to the user
834 label_issues_visibility_own: Issues created by or assigned to the user
834 label_git_report_last_commit: Report last commit for files and directories
835 label_git_report_last_commit: Report last commit for files and directories
835 label_parent_revision: Parent
836 label_parent_revision: Parent
836 label_child_revision: Child
837 label_child_revision: Child
837 label_export_options: "%{export_format} export options"
838 label_export_options: "%{export_format} export options"
838
839
839 button_login: Login
840 button_login: Login
840 button_submit: Submit
841 button_submit: Submit
841 button_save: Save
842 button_save: Save
842 button_check_all: Check all
843 button_check_all: Check all
843 button_uncheck_all: Uncheck all
844 button_uncheck_all: Uncheck all
844 button_collapse_all: Collapse all
845 button_collapse_all: Collapse all
845 button_expand_all: Expand all
846 button_expand_all: Expand all
846 button_delete: Delete
847 button_delete: Delete
847 button_create: Create
848 button_create: Create
848 button_create_and_continue: Create and continue
849 button_create_and_continue: Create and continue
849 button_test: Test
850 button_test: Test
850 button_edit: Edit
851 button_edit: Edit
851 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
852 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
852 button_add: Add
853 button_add: Add
853 button_change: Change
854 button_change: Change
854 button_apply: Apply
855 button_apply: Apply
855 button_clear: Clear
856 button_clear: Clear
856 button_lock: Lock
857 button_lock: Lock
857 button_unlock: Unlock
858 button_unlock: Unlock
858 button_download: Download
859 button_download: Download
859 button_list: List
860 button_list: List
860 button_view: View
861 button_view: View
861 button_move: Move
862 button_move: Move
862 button_move_and_follow: Move and follow
863 button_move_and_follow: Move and follow
863 button_back: Back
864 button_back: Back
864 button_cancel: Cancel
865 button_cancel: Cancel
865 button_activate: Activate
866 button_activate: Activate
866 button_sort: Sort
867 button_sort: Sort
867 button_log_time: Log time
868 button_log_time: Log time
868 button_rollback: Rollback to this version
869 button_rollback: Rollback to this version
869 button_watch: Watch
870 button_watch: Watch
870 button_unwatch: Unwatch
871 button_unwatch: Unwatch
871 button_reply: Reply
872 button_reply: Reply
872 button_archive: Archive
873 button_archive: Archive
873 button_unarchive: Unarchive
874 button_unarchive: Unarchive
874 button_reset: Reset
875 button_reset: Reset
875 button_rename: Rename
876 button_rename: Rename
876 button_change_password: Change password
877 button_change_password: Change password
877 button_copy: Copy
878 button_copy: Copy
878 button_copy_and_follow: Copy and follow
879 button_copy_and_follow: Copy and follow
879 button_annotate: Annotate
880 button_annotate: Annotate
880 button_update: Update
881 button_update: Update
881 button_configure: Configure
882 button_configure: Configure
882 button_quote: Quote
883 button_quote: Quote
883 button_duplicate: Duplicate
884 button_duplicate: Duplicate
884 button_show: Show
885 button_show: Show
885 button_edit_section: Edit this section
886 button_edit_section: Edit this section
886 button_export: Export
887 button_export: Export
887
888
888 status_active: active
889 status_active: active
889 status_registered: registered
890 status_registered: registered
890 status_locked: locked
891 status_locked: locked
891
892
892 version_status_open: open
893 version_status_open: open
893 version_status_locked: locked
894 version_status_locked: locked
894 version_status_closed: closed
895 version_status_closed: closed
895
896
896 field_active: Active
897 field_active: Active
897
898
898 text_select_mail_notifications: Select actions for which email notifications should be sent.
899 text_select_mail_notifications: Select actions for which email notifications should be sent.
899 text_regexp_info: eg. ^[A-Z0-9]+$
900 text_regexp_info: eg. ^[A-Z0-9]+$
900 text_min_max_length_info: 0 means no restriction
901 text_min_max_length_info: 0 means no restriction
901 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
902 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
902 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
903 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
903 text_workflow_edit: Select a role and a tracker to edit the workflow
904 text_workflow_edit: Select a role and a tracker to edit the workflow
904 text_are_you_sure: Are you sure?
905 text_are_you_sure: Are you sure?
905 text_are_you_sure_with_children: "Delete issue and all child issues?"
906 text_are_you_sure_with_children: "Delete issue and all child issues?"
906 text_journal_changed: "%{label} changed from %{old} to %{new}"
907 text_journal_changed: "%{label} changed from %{old} to %{new}"
907 text_journal_changed_no_detail: "%{label} updated"
908 text_journal_changed_no_detail: "%{label} updated"
908 text_journal_set_to: "%{label} set to %{value}"
909 text_journal_set_to: "%{label} set to %{value}"
909 text_journal_deleted: "%{label} deleted (%{old})"
910 text_journal_deleted: "%{label} deleted (%{old})"
910 text_journal_added: "%{label} %{value} added"
911 text_journal_added: "%{label} %{value} added"
911 text_tip_issue_begin_day: issue beginning this day
912 text_tip_issue_begin_day: issue beginning this day
912 text_tip_issue_end_day: issue ending this day
913 text_tip_issue_end_day: issue ending this day
913 text_tip_issue_begin_end_day: issue beginning and ending this day
914 text_tip_issue_begin_end_day: issue beginning and ending this day
914 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier cannot be changed.'
915 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier cannot be changed.'
915 text_caracters_maximum: "%{count} characters maximum."
916 text_caracters_maximum: "%{count} characters maximum."
916 text_caracters_minimum: "Must be at least %{count} characters long."
917 text_caracters_minimum: "Must be at least %{count} characters long."
917 text_length_between: "Length between %{min} and %{max} characters."
918 text_length_between: "Length between %{min} and %{max} characters."
918 text_tracker_no_workflow: No workflow defined for this tracker
919 text_tracker_no_workflow: No workflow defined for this tracker
919 text_unallowed_characters: Unallowed characters
920 text_unallowed_characters: Unallowed characters
920 text_comma_separated: Multiple values allowed (comma separated).
921 text_comma_separated: Multiple values allowed (comma separated).
921 text_line_separated: Multiple values allowed (one line for each value).
922 text_line_separated: Multiple values allowed (one line for each value).
922 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
923 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
923 text_issue_added: "Issue %{id} has been reported by %{author}."
924 text_issue_added: "Issue %{id} has been reported by %{author}."
924 text_issue_updated: "Issue %{id} has been updated by %{author}."
925 text_issue_updated: "Issue %{id} has been updated by %{author}."
925 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
926 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
926 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
927 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
927 text_issue_category_destroy_assignments: Remove category assignments
928 text_issue_category_destroy_assignments: Remove category assignments
928 text_issue_category_reassign_to: Reassign issues to this category
929 text_issue_category_reassign_to: Reassign issues to this category
929 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)."
930 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)."
930 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."
931 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."
931 text_load_default_configuration: Load the default configuration
932 text_load_default_configuration: Load the default configuration
932 text_status_changed_by_changeset: "Applied in changeset %{value}."
933 text_status_changed_by_changeset: "Applied in changeset %{value}."
933 text_time_logged_by_changeset: "Applied in changeset %{value}."
934 text_time_logged_by_changeset: "Applied in changeset %{value}."
934 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
935 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
935 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
936 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
936 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
937 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
937 text_select_project_modules: 'Select modules to enable for this project:'
938 text_select_project_modules: 'Select modules to enable for this project:'
938 text_default_administrator_account_changed: Default administrator account changed
939 text_default_administrator_account_changed: Default administrator account changed
939 text_file_repository_writable: Attachments directory writable
940 text_file_repository_writable: Attachments directory writable
940 text_plugin_assets_writable: Plugin assets directory writable
941 text_plugin_assets_writable: Plugin assets directory writable
941 text_rmagick_available: RMagick available (optional)
942 text_rmagick_available: RMagick available (optional)
942 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
943 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
943 text_destroy_time_entries: Delete reported hours
944 text_destroy_time_entries: Delete reported hours
944 text_assign_time_entries_to_project: Assign reported hours to the project
945 text_assign_time_entries_to_project: Assign reported hours to the project
945 text_reassign_time_entries: 'Reassign reported hours to this issue:'
946 text_reassign_time_entries: 'Reassign reported hours to this issue:'
946 text_user_wrote: "%{value} wrote:"
947 text_user_wrote: "%{value} wrote:"
947 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
948 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
948 text_enumeration_category_reassign_to: 'Reassign them to this value:'
949 text_enumeration_category_reassign_to: 'Reassign them to this value:'
949 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."
950 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."
950 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."
951 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."
951 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
952 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
952 text_custom_field_possible_values_info: 'One line for each value'
953 text_custom_field_possible_values_info: 'One line for each value'
953 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
954 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
954 text_wiki_page_nullify_children: "Keep child pages as root pages"
955 text_wiki_page_nullify_children: "Keep child pages as root pages"
955 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
956 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
956 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
957 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
957 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?"
958 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?"
958 text_zoom_in: Zoom in
959 text_zoom_in: Zoom in
959 text_zoom_out: Zoom out
960 text_zoom_out: Zoom out
960 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
961 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
961 text_scm_path_encoding_note: "Default: UTF-8"
962 text_scm_path_encoding_note: "Default: UTF-8"
962 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
963 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
963 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
964 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
964 text_scm_command: Command
965 text_scm_command: Command
965 text_scm_command_version: Version
966 text_scm_command_version: Version
966 text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it.
967 text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it.
967 text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel.
968 text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel.
968
969
969 default_role_manager: Manager
970 default_role_manager: Manager
970 default_role_developer: Developer
971 default_role_developer: Developer
971 default_role_reporter: Reporter
972 default_role_reporter: Reporter
972 default_tracker_bug: Bug
973 default_tracker_bug: Bug
973 default_tracker_feature: Feature
974 default_tracker_feature: Feature
974 default_tracker_support: Support
975 default_tracker_support: Support
975 default_issue_status_new: New
976 default_issue_status_new: New
976 default_issue_status_in_progress: In Progress
977 default_issue_status_in_progress: In Progress
977 default_issue_status_resolved: Resolved
978 default_issue_status_resolved: Resolved
978 default_issue_status_feedback: Feedback
979 default_issue_status_feedback: Feedback
979 default_issue_status_closed: Closed
980 default_issue_status_closed: Closed
980 default_issue_status_rejected: Rejected
981 default_issue_status_rejected: Rejected
981 default_doc_category_user: User documentation
982 default_doc_category_user: User documentation
982 default_doc_category_tech: Technical documentation
983 default_doc_category_tech: Technical documentation
983 default_priority_low: Low
984 default_priority_low: Low
984 default_priority_normal: Normal
985 default_priority_normal: Normal
985 default_priority_high: High
986 default_priority_high: High
986 default_priority_urgent: Urgent
987 default_priority_urgent: Urgent
987 default_priority_immediate: Immediate
988 default_priority_immediate: Immediate
988 default_activity_design: Design
989 default_activity_design: Design
989 default_activity_development: Development
990 default_activity_development: Development
990
991
991 enumeration_issue_priorities: Issue priorities
992 enumeration_issue_priorities: Issue priorities
992 enumeration_doc_categories: Document categories
993 enumeration_doc_categories: Document categories
993 enumeration_activities: Activities (time tracking)
994 enumeration_activities: Activities (time tracking)
994 enumeration_system_activity: System Activity
995 enumeration_system_activity: System Activity
995 description_filter: Filter
996 description_filter: Filter
996 description_search: Searchfield
997 description_search: Searchfield
997 description_choose_project: Projects
998 description_choose_project: Projects
998 description_project_scope: Search scope
999 description_project_scope: Search scope
999 description_notes: Notes
1000 description_notes: Notes
1000 description_message_content: Message content
1001 description_message_content: Message content
1001 description_query_sort_criteria_attribute: Sort attribute
1002 description_query_sort_criteria_attribute: Sort attribute
1002 description_query_sort_criteria_direction: Sort direction
1003 description_query_sort_criteria_direction: Sort direction
1003 description_user_mail_notification: Mail notification settings
1004 description_user_mail_notification: Mail notification settings
1004 description_available_columns: Available Columns
1005 description_available_columns: Available Columns
1005 description_selected_columns: Selected Columns
1006 description_selected_columns: Selected Columns
1006 description_all_columns: All Columns
1007 description_all_columns: All Columns
1007 description_issue_category_reassign: Choose issue category
1008 description_issue_category_reassign: Choose issue category
1008 description_wiki_subpages_reassign: Choose new parent page
1009 description_wiki_subpages_reassign: Choose new parent page
1009 description_date_range_list: Choose range from list
1010 description_date_range_list: Choose range from list
1010 description_date_range_interval: Choose range by selecting start and end date
1011 description_date_range_interval: Choose range by selecting start and end date
1011 description_date_from: Enter start date
1012 description_date_from: Enter start date
1012 description_date_to: Enter end date
1013 description_date_to: Enter end date
@@ -1,1029 +1,1030
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 month_names: [~, janvier, fΓ©vrier, mars, avril, mai, juin, juillet, aoΓ»t, septembre, octobre, novembre, dΓ©cembre]
18 month_names: [~, janvier, fΓ©vrier, mars, avril, mai, juin, juillet, aoΓ»t, septembre, octobre, novembre, dΓ©cembre]
19 abbr_month_names: [~, jan., fΓ©v., mar., avr., mai, juin, juil., aoΓ»t, sept., oct., nov., dΓ©c.]
19 abbr_month_names: [~, jan., fΓ©v., mar., avr., mai, juin, juil., aoΓ»t, sept., oct., nov., dΓ©c.]
20 order:
20 order:
21 - :day
21 - :day
22 - :month
22 - :month
23 - :year
23 - :year
24
24
25 time:
25 time:
26 formats:
26 formats:
27 default: "%d/%m/%Y %H:%M"
27 default: "%d/%m/%Y %H:%M"
28 time: "%H:%M"
28 time: "%H:%M"
29 short: "%d %b %H:%M"
29 short: "%d %b %H:%M"
30 long: "%A %d %B %Y %H:%M:%S %Z"
30 long: "%A %d %B %Y %H:%M:%S %Z"
31 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
31 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
32 only_second: "%S"
32 only_second: "%S"
33 am: 'am'
33 am: 'am'
34 pm: 'pm'
34 pm: 'pm'
35
35
36 datetime:
36 datetime:
37 distance_in_words:
37 distance_in_words:
38 half_a_minute: "30 secondes"
38 half_a_minute: "30 secondes"
39 less_than_x_seconds:
39 less_than_x_seconds:
40 zero: "moins d'une seconde"
40 zero: "moins d'une seconde"
41 one: "moins d'uneΒ seconde"
41 one: "moins d'uneΒ seconde"
42 other: "moins de %{count}Β secondes"
42 other: "moins de %{count}Β secondes"
43 x_seconds:
43 x_seconds:
44 one: "1Β seconde"
44 one: "1Β seconde"
45 other: "%{count}Β secondes"
45 other: "%{count}Β secondes"
46 less_than_x_minutes:
46 less_than_x_minutes:
47 zero: "moins d'une minute"
47 zero: "moins d'une minute"
48 one: "moins d'uneΒ minute"
48 one: "moins d'uneΒ minute"
49 other: "moins de %{count}Β minutes"
49 other: "moins de %{count}Β minutes"
50 x_minutes:
50 x_minutes:
51 one: "1Β minute"
51 one: "1Β minute"
52 other: "%{count}Β minutes"
52 other: "%{count}Β minutes"
53 about_x_hours:
53 about_x_hours:
54 one: "environ une heure"
54 one: "environ une heure"
55 other: "environ %{count}Β heures"
55 other: "environ %{count}Β heures"
56 x_days:
56 x_days:
57 one: "unΒ jour"
57 one: "unΒ jour"
58 other: "%{count}Β jours"
58 other: "%{count}Β jours"
59 about_x_months:
59 about_x_months:
60 one: "environ un mois"
60 one: "environ un mois"
61 other: "environ %{count}Β mois"
61 other: "environ %{count}Β mois"
62 x_months:
62 x_months:
63 one: "unΒ mois"
63 one: "unΒ mois"
64 other: "%{count}Β mois"
64 other: "%{count}Β mois"
65 about_x_years:
65 about_x_years:
66 one: "environ un an"
66 one: "environ un an"
67 other: "environ %{count}Β ans"
67 other: "environ %{count}Β ans"
68 over_x_years:
68 over_x_years:
69 one: "plus d'un an"
69 one: "plus d'un an"
70 other: "plus de %{count}Β ans"
70 other: "plus de %{count}Β ans"
71 almost_x_years:
71 almost_x_years:
72 one: "presqu'un an"
72 one: "presqu'un an"
73 other: "presque %{count} ans"
73 other: "presque %{count} ans"
74 prompts:
74 prompts:
75 year: "AnnΓ©e"
75 year: "AnnΓ©e"
76 month: "Mois"
76 month: "Mois"
77 day: "Jour"
77 day: "Jour"
78 hour: "Heure"
78 hour: "Heure"
79 minute: "Minute"
79 minute: "Minute"
80 second: "Seconde"
80 second: "Seconde"
81
81
82 number:
82 number:
83 format:
83 format:
84 precision: 3
84 precision: 3
85 separator: ','
85 separator: ','
86 delimiter: 'Β '
86 delimiter: 'Β '
87 currency:
87 currency:
88 format:
88 format:
89 unit: '€'
89 unit: '€'
90 precision: 2
90 precision: 2
91 format: '%nΒ %u'
91 format: '%nΒ %u'
92 human:
92 human:
93 format:
93 format:
94 precision: 2
94 precision: 2
95 storage_units:
95 storage_units:
96 format: "%n %u"
96 format: "%n %u"
97 units:
97 units:
98 byte:
98 byte:
99 one: "octet"
99 one: "octet"
100 other: "octet"
100 other: "octet"
101 kb: "ko"
101 kb: "ko"
102 mb: "Mo"
102 mb: "Mo"
103 gb: "Go"
103 gb: "Go"
104 tb: "To"
104 tb: "To"
105
105
106 support:
106 support:
107 array:
107 array:
108 sentence_connector: 'et'
108 sentence_connector: 'et'
109 skip_last_comma: true
109 skip_last_comma: true
110 word_connector: ", "
110 word_connector: ", "
111 two_words_connector: " et "
111 two_words_connector: " et "
112 last_word_connector: " et "
112 last_word_connector: " et "
113
113
114 activerecord:
114 activerecord:
115 errors:
115 errors:
116 template:
116 template:
117 header:
117 header:
118 one: "Impossible d'enregistrer %{model} : une erreur"
118 one: "Impossible d'enregistrer %{model} : une erreur"
119 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
119 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
120 body: "Veuillez vΓ©rifier les champs suivantsΒ :"
120 body: "Veuillez vΓ©rifier les champs suivantsΒ :"
121 messages:
121 messages:
122 inclusion: "n'est pas inclus(e) dans la liste"
122 inclusion: "n'est pas inclus(e) dans la liste"
123 exclusion: "n'est pas disponible"
123 exclusion: "n'est pas disponible"
124 invalid: "n'est pas valide"
124 invalid: "n'est pas valide"
125 confirmation: "ne concorde pas avec la confirmation"
125 confirmation: "ne concorde pas avec la confirmation"
126 accepted: "doit Γͺtre acceptΓ©(e)"
126 accepted: "doit Γͺtre acceptΓ©(e)"
127 empty: "doit Γͺtre renseignΓ©(e)"
127 empty: "doit Γͺtre renseignΓ©(e)"
128 blank: "doit Γͺtre renseignΓ©(e)"
128 blank: "doit Γͺtre renseignΓ©(e)"
129 too_long: "est trop long (pas plus de %{count} caractères)"
129 too_long: "est trop long (pas plus de %{count} caractères)"
130 too_short: "est trop court (au moins %{count} caractères)"
130 too_short: "est trop court (au moins %{count} caractères)"
131 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
131 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
132 taken: "est dΓ©jΓ  utilisΓ©"
132 taken: "est dΓ©jΓ  utilisΓ©"
133 not_a_number: "n'est pas un nombre"
133 not_a_number: "n'est pas un nombre"
134 not_a_date: "n'est pas une date valide"
134 not_a_date: "n'est pas une date valide"
135 greater_than: "doit Γͺtre supΓ©rieur Γ  %{count}"
135 greater_than: "doit Γͺtre supΓ©rieur Γ  %{count}"
136 greater_than_or_equal_to: "doit Γͺtre supΓ©rieur ou Γ©gal Γ  %{count}"
136 greater_than_or_equal_to: "doit Γͺtre supΓ©rieur ou Γ©gal Γ  %{count}"
137 equal_to: "doit Γͺtre Γ©gal Γ  %{count}"
137 equal_to: "doit Γͺtre Γ©gal Γ  %{count}"
138 less_than: "doit Γͺtre infΓ©rieur Γ  %{count}"
138 less_than: "doit Γͺtre infΓ©rieur Γ  %{count}"
139 less_than_or_equal_to: "doit Γͺtre infΓ©rieur ou Γ©gal Γ  %{count}"
139 less_than_or_equal_to: "doit Γͺtre infΓ©rieur ou Γ©gal Γ  %{count}"
140 odd: "doit Γͺtre impair"
140 odd: "doit Γͺtre impair"
141 even: "doit Γͺtre pair"
141 even: "doit Γͺtre pair"
142 greater_than_start_date: "doit Γͺtre postΓ©rieure Γ  la date de dΓ©but"
142 greater_than_start_date: "doit Γͺtre postΓ©rieure Γ  la date de dΓ©but"
143 not_same_project: "n'appartient pas au mΓͺme projet"
143 not_same_project: "n'appartient pas au mΓͺme projet"
144 circular_dependency: "Cette relation crΓ©erait une dΓ©pendance circulaire"
144 circular_dependency: "Cette relation crΓ©erait une dΓ©pendance circulaire"
145 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas Γͺtre liΓ©e Γ  l'une de ses sous-tΓ’ches"
145 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas Γͺtre liΓ©e Γ  l'une de ses sous-tΓ’ches"
146
146
147 actionview_instancetag_blank_option: Choisir
147 actionview_instancetag_blank_option: Choisir
148
148
149 general_text_No: 'Non'
149 general_text_No: 'Non'
150 general_text_Yes: 'Oui'
150 general_text_Yes: 'Oui'
151 general_text_no: 'non'
151 general_text_no: 'non'
152 general_text_yes: 'oui'
152 general_text_yes: 'oui'
153 general_lang_name: 'FranΓ§ais'
153 general_lang_name: 'FranΓ§ais'
154 general_csv_separator: ';'
154 general_csv_separator: ';'
155 general_csv_decimal_separator: ','
155 general_csv_decimal_separator: ','
156 general_csv_encoding: ISO-8859-1
156 general_csv_encoding: ISO-8859-1
157 general_pdf_encoding: UTF-8
157 general_pdf_encoding: UTF-8
158 general_first_day_of_week: '1'
158 general_first_day_of_week: '1'
159
159
160 notice_account_updated: Le compte a été mis à jour avec succès.
160 notice_account_updated: Le compte a été mis à jour avec succès.
161 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
161 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
162 notice_account_password_updated: Mot de passe mis à jour avec succès.
162 notice_account_password_updated: Mot de passe mis à jour avec succès.
163 notice_account_wrong_password: Mot de passe incorrect
163 notice_account_wrong_password: Mot de passe incorrect
164 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a Γ©tΓ© envoyΓ©.
164 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a Γ©tΓ© envoyΓ©.
165 notice_account_unknown_email: Aucun compte ne correspond Γ  cette adresse.
165 notice_account_unknown_email: Aucun compte ne correspond Γ  cette adresse.
166 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
166 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
167 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a Γ©tΓ© envoyΓ©.
167 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a Γ©tΓ© envoyΓ©.
168 notice_account_activated: Votre compte a Γ©tΓ© activΓ©. Vous pouvez Γ  prΓ©sent vous connecter.
168 notice_account_activated: Votre compte a Γ©tΓ© activΓ©. Vous pouvez Γ  prΓ©sent vous connecter.
169 notice_successful_create: Création effectuée avec succès.
169 notice_successful_create: Création effectuée avec succès.
170 notice_successful_update: Mise à jour effectuée avec succès.
170 notice_successful_update: Mise à jour effectuée avec succès.
171 notice_successful_delete: Suppression effectuée avec succès.
171 notice_successful_delete: Suppression effectuée avec succès.
172 notice_successful_connection: Connexion rΓ©ussie.
172 notice_successful_connection: Connexion rΓ©ussie.
173 notice_file_not_found: "La page Γ  laquelle vous souhaitez accΓ©der n'existe pas ou a Γ©tΓ© supprimΓ©e."
173 notice_file_not_found: "La page Γ  laquelle vous souhaitez accΓ©der n'existe pas ou a Γ©tΓ© supprimΓ©e."
174 notice_locking_conflict: Les donnΓ©es ont Γ©tΓ© mises Γ  jour par un autre utilisateur. Mise Γ  jour impossible.
174 notice_locking_conflict: Les donnΓ©es ont Γ©tΓ© mises Γ  jour par un autre utilisateur. Mise Γ  jour impossible.
175 notice_not_authorized: "Vous n'Γͺtes pas autorisΓ© Γ  accΓ©der Γ  cette page."
175 notice_not_authorized: "Vous n'Γͺtes pas autorisΓ© Γ  accΓ©der Γ  cette page."
176 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accΓ©der a Γ©tΓ© archivΓ©.
176 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accΓ©der a Γ©tΓ© archivΓ©.
177 notice_email_sent: "Un email a Γ©tΓ© envoyΓ© Γ  %{value}"
177 notice_email_sent: "Un email a Γ©tΓ© envoyΓ© Γ  %{value}"
178 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
178 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
179 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée."
179 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée."
180 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sΓ©lectionnΓ©es n'ont pas pu Γͺtre mise(s) Γ  jour : %{ids}."
180 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sΓ©lectionnΓ©es n'ont pas pu Γͺtre mise(s) Γ  jour : %{ids}."
181 notice_failed_to_save_time_entries: "%{count} temps passΓ©(s) sur les %{total} sΓ©lectionnΓ©s n'ont pas pu Γͺtre mis Γ  jour: %{ids}."
181 notice_failed_to_save_time_entries: "%{count} temps passΓ©(s) sur les %{total} sΓ©lectionnΓ©s n'ont pas pu Γͺtre mis Γ  jour: %{ids}."
182 notice_no_issue_selected: "Aucune demande sΓ©lectionnΓ©e ! Cochez les demandes que vous voulez mettre Γ  jour."
182 notice_no_issue_selected: "Aucune demande sΓ©lectionnΓ©e ! Cochez les demandes que vous voulez mettre Γ  jour."
183 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
183 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
184 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
184 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
185 notice_unable_delete_version: Impossible de supprimer cette version.
185 notice_unable_delete_version: Impossible de supprimer cette version.
186 notice_issue_done_ratios_updated: L'avancement des demandes a Γ©tΓ© mis Γ  jour.
186 notice_issue_done_ratios_updated: L'avancement des demandes a Γ©tΓ© mis Γ  jour.
187 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
187 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
188 notice_gantt_chart_truncated: "Le diagramme a Γ©tΓ© tronquΓ© car il excΓ¨de le nombre maximal d'Γ©lΓ©ments pouvant Γͺtre affichΓ©s (%{max})"
188 notice_gantt_chart_truncated: "Le diagramme a Γ©tΓ© tronquΓ© car il excΓ¨de le nombre maximal d'Γ©lΓ©ments pouvant Γͺtre affichΓ©s (%{max})"
189 notice_issue_successful_create: "La demande %{id} a été créée."
189 notice_issue_successful_create: "La demande %{id} a été créée."
190
190
191 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramΓ©trage : %{value}"
191 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramΓ©trage : %{value}"
192 error_scm_not_found: "L'entrΓ©e et/ou la rΓ©vision demandΓ©e n'existe pas dans le dΓ©pΓ΄t."
192 error_scm_not_found: "L'entrΓ©e et/ou la rΓ©vision demandΓ©e n'existe pas dans le dΓ©pΓ΄t."
193 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
193 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
194 error_scm_annotate: "L'entrΓ©e n'existe pas ou ne peut pas Γͺtre annotΓ©e."
194 error_scm_annotate: "L'entrΓ©e n'existe pas ou ne peut pas Γͺtre annotΓ©e."
195 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas Γ  ce projet"
195 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas Γ  ce projet"
196 error_can_not_reopen_issue_on_closed_version: 'Une demande assignΓ©e Γ  une version fermΓ©e ne peut pas Γͺtre rΓ©ouverte'
196 error_can_not_reopen_issue_on_closed_version: 'Une demande assignΓ©e Γ  une version fermΓ©e ne peut pas Γͺtre rΓ©ouverte'
197 error_can_not_archive_project: "Ce projet ne peut pas Γͺtre archivΓ©"
197 error_can_not_archive_project: "Ce projet ne peut pas Γͺtre archivΓ©"
198 error_workflow_copy_source: 'Veuillez sΓ©lectionner un tracker et/ou un rΓ΄le source'
198 error_workflow_copy_source: 'Veuillez sΓ©lectionner un tracker et/ou un rΓ΄le source'
199 error_workflow_copy_target: 'Veuillez sΓ©lectionner les trackers et rΓ΄les cibles'
199 error_workflow_copy_target: 'Veuillez sΓ©lectionner les trackers et rΓ΄les cibles'
200 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu Γͺtre mis Γ  jour.
200 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu Γͺtre mis Γ  jour.
201 error_attachment_too_big: Ce fichier ne peut pas Γͺtre attachΓ© car il excΓ¨de la taille maximale autorisΓ©e (%{max_size})
201 error_attachment_too_big: Ce fichier ne peut pas Γͺtre attachΓ© car il excΓ¨de la taille maximale autorisΓ©e (%{max_size})
202
202
203 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu Γͺtre sauvegardΓ©s."
203 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu Γͺtre sauvegardΓ©s."
204
204
205 mail_subject_lost_password: "Votre mot de passe %{value}"
205 mail_subject_lost_password: "Votre mot de passe %{value}"
206 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
206 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
207 mail_subject_register: "Activation de votre compte %{value}"
207 mail_subject_register: "Activation de votre compte %{value}"
208 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
208 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
209 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
209 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
210 mail_body_account_information: Paramètres de connexion de votre compte
210 mail_body_account_information: Paramètres de connexion de votre compte
211 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
211 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
212 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nΓ©cessite votre approbation :"
212 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nΓ©cessite votre approbation :"
213 mail_subject_reminder: "%{count} demande(s) arrivent Γ  Γ©chΓ©ance (%{days})"
213 mail_subject_reminder: "%{count} demande(s) arrivent Γ  Γ©chΓ©ance (%{days})"
214 mail_body_reminder: "%{count} demande(s) qui vous sont assignΓ©es arrivent Γ  Γ©chΓ©ance dans les %{days} prochains jours :"
214 mail_body_reminder: "%{count} demande(s) qui vous sont assignΓ©es arrivent Γ  Γ©chΓ©ance dans les %{days} prochains jours :"
215 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutΓ©e"
215 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutΓ©e"
216 mail_body_wiki_content_added: "La page wiki '%{id}' a Γ©tΓ© ajoutΓ©e par %{author}."
216 mail_body_wiki_content_added: "La page wiki '%{id}' a Γ©tΓ© ajoutΓ©e par %{author}."
217 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise Γ  jour"
217 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise Γ  jour"
218 mail_body_wiki_content_updated: "La page wiki '%{id}' a Γ©tΓ© mise Γ  jour par %{author}."
218 mail_body_wiki_content_updated: "La page wiki '%{id}' a Γ©tΓ© mise Γ  jour par %{author}."
219
219
220 gui_validation_error: 1 erreur
220 gui_validation_error: 1 erreur
221 gui_validation_error_plural: "%{count} erreurs"
221 gui_validation_error_plural: "%{count} erreurs"
222
222
223 field_name: Nom
223 field_name: Nom
224 field_description: Description
224 field_description: Description
225 field_summary: RΓ©sumΓ©
225 field_summary: RΓ©sumΓ©
226 field_is_required: Obligatoire
226 field_is_required: Obligatoire
227 field_firstname: PrΓ©nom
227 field_firstname: PrΓ©nom
228 field_lastname: Nom
228 field_lastname: Nom
229 field_mail: "Email "
229 field_mail: "Email "
230 field_filename: Fichier
230 field_filename: Fichier
231 field_filesize: Taille
231 field_filesize: Taille
232 field_downloads: TΓ©lΓ©chargements
232 field_downloads: TΓ©lΓ©chargements
233 field_author: Auteur
233 field_author: Auteur
234 field_created_on: "Créé "
234 field_created_on: "Créé "
235 field_updated_on: "Mis-Γ -jour "
235 field_updated_on: "Mis-Γ -jour "
236 field_field_format: Format
236 field_field_format: Format
237 field_is_for_all: Pour tous les projets
237 field_is_for_all: Pour tous les projets
238 field_possible_values: Valeurs possibles
238 field_possible_values: Valeurs possibles
239 field_regexp: Expression régulière
239 field_regexp: Expression régulière
240 field_min_length: Longueur minimum
240 field_min_length: Longueur minimum
241 field_max_length: Longueur maximum
241 field_max_length: Longueur maximum
242 field_value: Valeur
242 field_value: Valeur
243 field_category: CatΓ©gorie
243 field_category: CatΓ©gorie
244 field_title: Titre
244 field_title: Titre
245 field_project: Projet
245 field_project: Projet
246 field_issue: Demande
246 field_issue: Demande
247 field_status: Statut
247 field_status: Statut
248 field_notes: Notes
248 field_notes: Notes
249 field_is_closed: Demande fermΓ©e
249 field_is_closed: Demande fermΓ©e
250 field_is_default: Valeur par dΓ©faut
250 field_is_default: Valeur par dΓ©faut
251 field_tracker: Tracker
251 field_tracker: Tracker
252 field_subject: Sujet
252 field_subject: Sujet
253 field_due_date: EchΓ©ance
253 field_due_date: EchΓ©ance
254 field_assigned_to: AssignΓ© Γ 
254 field_assigned_to: AssignΓ© Γ 
255 field_priority: PrioritΓ©
255 field_priority: PrioritΓ©
256 field_fixed_version: Version cible
256 field_fixed_version: Version cible
257 field_user: Utilisateur
257 field_user: Utilisateur
258 field_role: RΓ΄le
258 field_role: RΓ΄le
259 field_homepage: "Site web "
259 field_homepage: "Site web "
260 field_is_public: Public
260 field_is_public: Public
261 field_parent: Sous-projet de
261 field_parent: Sous-projet de
262 field_is_in_roadmap: Demandes affichΓ©es dans la roadmap
262 field_is_in_roadmap: Demandes affichΓ©es dans la roadmap
263 field_login: "Identifiant "
263 field_login: "Identifiant "
264 field_mail_notification: Notifications par mail
264 field_mail_notification: Notifications par mail
265 field_admin: Administrateur
265 field_admin: Administrateur
266 field_last_login_on: "Dernière connexion "
266 field_last_login_on: "Dernière connexion "
267 field_language: Langue
267 field_language: Langue
268 field_effective_date: Date
268 field_effective_date: Date
269 field_password: Mot de passe
269 field_password: Mot de passe
270 field_new_password: Nouveau mot de passe
270 field_new_password: Nouveau mot de passe
271 field_password_confirmation: Confirmation
271 field_password_confirmation: Confirmation
272 field_version: Version
272 field_version: Version
273 field_type: Type
273 field_type: Type
274 field_host: HΓ΄te
274 field_host: HΓ΄te
275 field_port: Port
275 field_port: Port
276 field_account: Compte
276 field_account: Compte
277 field_base_dn: Base DN
277 field_base_dn: Base DN
278 field_attr_login: Attribut Identifiant
278 field_attr_login: Attribut Identifiant
279 field_attr_firstname: Attribut PrΓ©nom
279 field_attr_firstname: Attribut PrΓ©nom
280 field_attr_lastname: Attribut Nom
280 field_attr_lastname: Attribut Nom
281 field_attr_mail: Attribut Email
281 field_attr_mail: Attribut Email
282 field_onthefly: CrΓ©ation des utilisateurs Γ  la volΓ©e
282 field_onthefly: CrΓ©ation des utilisateurs Γ  la volΓ©e
283 field_start_date: DΓ©but
283 field_start_date: DΓ©but
284 field_done_ratio: "% rΓ©alisΓ©"
284 field_done_ratio: "% rΓ©alisΓ©"
285 field_auth_source: Mode d'authentification
285 field_auth_source: Mode d'authentification
286 field_hide_mail: Cacher mon adresse mail
286 field_hide_mail: Cacher mon adresse mail
287 field_comments: Commentaire
287 field_comments: Commentaire
288 field_url: URL
288 field_url: URL
289 field_start_page: Page de dΓ©marrage
289 field_start_page: Page de dΓ©marrage
290 field_subproject: Sous-projet
290 field_subproject: Sous-projet
291 field_hours: Heures
291 field_hours: Heures
292 field_activity: ActivitΓ©
292 field_activity: ActivitΓ©
293 field_spent_on: Date
293 field_spent_on: Date
294 field_identifier: Identifiant
294 field_identifier: Identifiant
295 field_is_filter: UtilisΓ© comme filtre
295 field_is_filter: UtilisΓ© comme filtre
296 field_issue_to: Demande liΓ©e
296 field_issue_to: Demande liΓ©e
297 field_delay: Retard
297 field_delay: Retard
298 field_assignable: Demandes assignables Γ  ce rΓ΄le
298 field_assignable: Demandes assignables Γ  ce rΓ΄le
299 field_redirect_existing_links: Rediriger les liens existants
299 field_redirect_existing_links: Rediriger les liens existants
300 field_estimated_hours: Temps estimΓ©
300 field_estimated_hours: Temps estimΓ©
301 field_column_names: Colonnes
301 field_column_names: Colonnes
302 field_time_zone: Fuseau horaire
302 field_time_zone: Fuseau horaire
303 field_searchable: UtilisΓ© pour les recherches
303 field_searchable: UtilisΓ© pour les recherches
304 field_default_value: Valeur par dΓ©faut
304 field_default_value: Valeur par dΓ©faut
305 field_comments_sorting: Afficher les commentaires
305 field_comments_sorting: Afficher les commentaires
306 field_parent_title: Page parent
306 field_parent_title: Page parent
307 field_editable: Modifiable
307 field_editable: Modifiable
308 field_watcher: Observateur
308 field_watcher: Observateur
309 field_identity_url: URL OpenID
309 field_identity_url: URL OpenID
310 field_content: Contenu
310 field_content: Contenu
311 field_group_by: Grouper par
311 field_group_by: Grouper par
312 field_sharing: Partage
312 field_sharing: Partage
313 field_active: Actif
313 field_active: Actif
314 field_parent_issue: TΓ’che parente
314 field_parent_issue: TΓ’che parente
315 field_visible: Visible
315 field_visible: Visible
316 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardΓ©"
316 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardΓ©"
317 field_issues_visibility: VisibilitΓ© des demandes
317 field_issues_visibility: VisibilitΓ© des demandes
318 field_is_private: PrivΓ©e
318 field_is_private: PrivΓ©e
319 field_commit_logs_encoding: Encodage des messages de commit
319 field_commit_logs_encoding: Encodage des messages de commit
320 field_repository_is_default: DΓ©pΓ΄t principal
320
321
321 setting_app_title: Titre de l'application
322 setting_app_title: Titre de l'application
322 setting_app_subtitle: Sous-titre de l'application
323 setting_app_subtitle: Sous-titre de l'application
323 setting_welcome_text: Texte d'accueil
324 setting_welcome_text: Texte d'accueil
324 setting_default_language: Langue par dΓ©faut
325 setting_default_language: Langue par dΓ©faut
325 setting_login_required: Authentification obligatoire
326 setting_login_required: Authentification obligatoire
326 setting_self_registration: Inscription des nouveaux utilisateurs
327 setting_self_registration: Inscription des nouveaux utilisateurs
327 setting_attachment_max_size: Taille maximale des fichiers
328 setting_attachment_max_size: Taille maximale des fichiers
328 setting_issues_export_limit: Limite d'exportation des demandes
329 setting_issues_export_limit: Limite d'exportation des demandes
329 setting_mail_from: Adresse d'Γ©mission
330 setting_mail_from: Adresse d'Γ©mission
330 setting_bcc_recipients: Destinataires en copie cachΓ©e (cci)
331 setting_bcc_recipients: Destinataires en copie cachΓ©e (cci)
331 setting_plain_text_mail: Mail en texte brut (non HTML)
332 setting_plain_text_mail: Mail en texte brut (non HTML)
332 setting_host_name: Nom d'hΓ΄te et chemin
333 setting_host_name: Nom d'hΓ΄te et chemin
333 setting_text_formatting: Formatage du texte
334 setting_text_formatting: Formatage du texte
334 setting_wiki_compression: Compression de l'historique des pages wiki
335 setting_wiki_compression: Compression de l'historique des pages wiki
335 setting_feeds_limit: Nombre maximal d'Γ©lΓ©ments dans les flux Atom
336 setting_feeds_limit: Nombre maximal d'Γ©lΓ©ments dans les flux Atom
336 setting_default_projects_public: DΓ©finir les nouveaux projets comme publics par dΓ©faut
337 setting_default_projects_public: DΓ©finir les nouveaux projets comme publics par dΓ©faut
337 setting_autofetch_changesets: RΓ©cupΓ©ration automatique des commits
338 setting_autofetch_changesets: RΓ©cupΓ©ration automatique des commits
338 setting_sys_api_enabled: Activer les WS pour la gestion des dΓ©pΓ΄ts
339 setting_sys_api_enabled: Activer les WS pour la gestion des dΓ©pΓ΄ts
339 setting_commit_ref_keywords: Mots-clΓ©s de rΓ©fΓ©rencement
340 setting_commit_ref_keywords: Mots-clΓ©s de rΓ©fΓ©rencement
340 setting_commit_fix_keywords: Mots-clΓ©s de rΓ©solution
341 setting_commit_fix_keywords: Mots-clΓ©s de rΓ©solution
341 setting_autologin: DurΓ©e maximale de connexion automatique
342 setting_autologin: DurΓ©e maximale de connexion automatique
342 setting_date_format: Format de date
343 setting_date_format: Format de date
343 setting_time_format: Format d'heure
344 setting_time_format: Format d'heure
344 setting_cross_project_issue_relations: Autoriser les relations entre demandes de diffΓ©rents projets
345 setting_cross_project_issue_relations: Autoriser les relations entre demandes de diffΓ©rents projets
345 setting_issue_list_default_columns: Colonnes affichΓ©es par dΓ©faut sur la liste des demandes
346 setting_issue_list_default_columns: Colonnes affichΓ©es par dΓ©faut sur la liste des demandes
346 setting_emails_footer: Pied-de-page des emails
347 setting_emails_footer: Pied-de-page des emails
347 setting_protocol: Protocole
348 setting_protocol: Protocole
348 setting_per_page_options: Options d'objets affichΓ©s par page
349 setting_per_page_options: Options d'objets affichΓ©s par page
349 setting_user_format: Format d'affichage des utilisateurs
350 setting_user_format: Format d'affichage des utilisateurs
350 setting_activity_days_default: Nombre de jours affichΓ©s sur l'activitΓ© des projets
351 setting_activity_days_default: Nombre de jours affichΓ©s sur l'activitΓ© des projets
351 setting_display_subprojects_issues: Afficher par dΓ©faut les demandes des sous-projets sur les projets principaux
352 setting_display_subprojects_issues: Afficher par dΓ©faut les demandes des sous-projets sur les projets principaux
352 setting_enabled_scm: SCM activΓ©s
353 setting_enabled_scm: SCM activΓ©s
353 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
354 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
354 setting_mail_handler_api_enabled: "Activer le WS pour la rΓ©ception d'emails"
355 setting_mail_handler_api_enabled: "Activer le WS pour la rΓ©ception d'emails"
355 setting_mail_handler_api_key: ClΓ© de protection de l'API
356 setting_mail_handler_api_key: ClΓ© de protection de l'API
356 setting_sequential_project_identifiers: GΓ©nΓ©rer des identifiants de projet sΓ©quentiels
357 setting_sequential_project_identifiers: GΓ©nΓ©rer des identifiants de projet sΓ©quentiels
357 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
358 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
358 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichΓ©es
359 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichΓ©es
359 setting_file_max_size_displayed: Taille maximum des fichiers texte affichΓ©s en ligne
360 setting_file_max_size_displayed: Taille maximum des fichiers texte affichΓ©s en ligne
360 setting_repository_log_display_limit: "Nombre maximum de rΓ©visions affichΓ©es sur l'historique d'un fichier"
361 setting_repository_log_display_limit: "Nombre maximum de rΓ©visions affichΓ©es sur l'historique d'un fichier"
361 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
362 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
362 setting_password_min_length: Longueur minimum des mots de passe
363 setting_password_min_length: Longueur minimum des mots de passe
363 setting_new_project_user_role_id: RΓ΄le donnΓ© Γ  un utilisateur non-administrateur qui crΓ©e un projet
364 setting_new_project_user_role_id: RΓ΄le donnΓ© Γ  un utilisateur non-administrateur qui crΓ©e un projet
364 setting_default_projects_modules: Modules activΓ©s par dΓ©faut pour les nouveaux projets
365 setting_default_projects_modules: Modules activΓ©s par dΓ©faut pour les nouveaux projets
365 setting_issue_done_ratio: Calcul de l'avancement des demandes
366 setting_issue_done_ratio: Calcul de l'avancement des demandes
366 setting_issue_done_ratio_issue_status: Utiliser le statut
367 setting_issue_done_ratio_issue_status: Utiliser le statut
367 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectuΓ©'
368 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectuΓ©'
368 setting_rest_api_enabled: Activer l'API REST
369 setting_rest_api_enabled: Activer l'API REST
369 setting_gravatar_default: Image Gravatar par dΓ©faut
370 setting_gravatar_default: Image Gravatar par dΓ©faut
370 setting_start_of_week: Jour de dΓ©but des calendriers
371 setting_start_of_week: Jour de dΓ©but des calendriers
371 setting_cache_formatted_text: Mettre en cache le texte formatΓ©
372 setting_cache_formatted_text: Mettre en cache le texte formatΓ©
372 setting_commit_logtime_enabled: Permettre la saisie de temps
373 setting_commit_logtime_enabled: Permettre la saisie de temps
373 setting_commit_logtime_activity_id: ActivitΓ© pour le temps saisi
374 setting_commit_logtime_activity_id: ActivitΓ© pour le temps saisi
374 setting_gantt_items_limit: Nombre maximum d'Γ©lΓ©ments affichΓ©s sur le gantt
375 setting_gantt_items_limit: Nombre maximum d'Γ©lΓ©ments affichΓ©s sur le gantt
375 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
376 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
376 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
377 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
377
378
378 permission_add_project: CrΓ©er un projet
379 permission_add_project: CrΓ©er un projet
379 permission_add_subprojects: CrΓ©er des sous-projets
380 permission_add_subprojects: CrΓ©er des sous-projets
380 permission_edit_project: Modifier le projet
381 permission_edit_project: Modifier le projet
381 permission_select_project_modules: Choisir les modules
382 permission_select_project_modules: Choisir les modules
382 permission_manage_members: GΓ©rer les membres
383 permission_manage_members: GΓ©rer les membres
383 permission_manage_versions: GΓ©rer les versions
384 permission_manage_versions: GΓ©rer les versions
384 permission_manage_categories: GΓ©rer les catΓ©gories de demandes
385 permission_manage_categories: GΓ©rer les catΓ©gories de demandes
385 permission_view_issues: Voir les demandes
386 permission_view_issues: Voir les demandes
386 permission_add_issues: CrΓ©er des demandes
387 permission_add_issues: CrΓ©er des demandes
387 permission_edit_issues: Modifier les demandes
388 permission_edit_issues: Modifier les demandes
388 permission_manage_issue_relations: GΓ©rer les relations
389 permission_manage_issue_relations: GΓ©rer les relations
389 permission_set_issues_private: Rendre les demandes publiques ou privΓ©es
390 permission_set_issues_private: Rendre les demandes publiques ou privΓ©es
390 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privΓ©es
391 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privΓ©es
391 permission_add_issue_notes: Ajouter des notes
392 permission_add_issue_notes: Ajouter des notes
392 permission_edit_issue_notes: Modifier les notes
393 permission_edit_issue_notes: Modifier les notes
393 permission_edit_own_issue_notes: Modifier ses propres notes
394 permission_edit_own_issue_notes: Modifier ses propres notes
394 permission_move_issues: DΓ©placer les demandes
395 permission_move_issues: DΓ©placer les demandes
395 permission_delete_issues: Supprimer les demandes
396 permission_delete_issues: Supprimer les demandes
396 permission_manage_public_queries: GΓ©rer les requΓͺtes publiques
397 permission_manage_public_queries: GΓ©rer les requΓͺtes publiques
397 permission_save_queries: Sauvegarder les requΓͺtes
398 permission_save_queries: Sauvegarder les requΓͺtes
398 permission_view_gantt: Voir le gantt
399 permission_view_gantt: Voir le gantt
399 permission_view_calendar: Voir le calendrier
400 permission_view_calendar: Voir le calendrier
400 permission_view_issue_watchers: Voir la liste des observateurs
401 permission_view_issue_watchers: Voir la liste des observateurs
401 permission_add_issue_watchers: Ajouter des observateurs
402 permission_add_issue_watchers: Ajouter des observateurs
402 permission_delete_issue_watchers: Supprimer des observateurs
403 permission_delete_issue_watchers: Supprimer des observateurs
403 permission_log_time: Saisir le temps passΓ©
404 permission_log_time: Saisir le temps passΓ©
404 permission_view_time_entries: Voir le temps passΓ©
405 permission_view_time_entries: Voir le temps passΓ©
405 permission_edit_time_entries: Modifier les temps passΓ©s
406 permission_edit_time_entries: Modifier les temps passΓ©s
406 permission_edit_own_time_entries: Modifier son propre temps passΓ©
407 permission_edit_own_time_entries: Modifier son propre temps passΓ©
407 permission_manage_news: GΓ©rer les annonces
408 permission_manage_news: GΓ©rer les annonces
408 permission_comment_news: Commenter les annonces
409 permission_comment_news: Commenter les annonces
409 permission_manage_documents: GΓ©rer les documents
410 permission_manage_documents: GΓ©rer les documents
410 permission_view_documents: Voir les documents
411 permission_view_documents: Voir les documents
411 permission_manage_files: GΓ©rer les fichiers
412 permission_manage_files: GΓ©rer les fichiers
412 permission_view_files: Voir les fichiers
413 permission_view_files: Voir les fichiers
413 permission_manage_wiki: GΓ©rer le wiki
414 permission_manage_wiki: GΓ©rer le wiki
414 permission_rename_wiki_pages: Renommer les pages
415 permission_rename_wiki_pages: Renommer les pages
415 permission_delete_wiki_pages: Supprimer les pages
416 permission_delete_wiki_pages: Supprimer les pages
416 permission_view_wiki_pages: Voir le wiki
417 permission_view_wiki_pages: Voir le wiki
417 permission_view_wiki_edits: "Voir l'historique des modifications"
418 permission_view_wiki_edits: "Voir l'historique des modifications"
418 permission_edit_wiki_pages: Modifier les pages
419 permission_edit_wiki_pages: Modifier les pages
419 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
420 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
420 permission_protect_wiki_pages: ProtΓ©ger les pages
421 permission_protect_wiki_pages: ProtΓ©ger les pages
421 permission_manage_repository: GΓ©rer le dΓ©pΓ΄t de sources
422 permission_manage_repository: GΓ©rer le dΓ©pΓ΄t de sources
422 permission_browse_repository: Parcourir les sources
423 permission_browse_repository: Parcourir les sources
423 permission_view_changesets: Voir les rΓ©visions
424 permission_view_changesets: Voir les rΓ©visions
424 permission_commit_access: Droit de commit
425 permission_commit_access: Droit de commit
425 permission_manage_boards: GΓ©rer les forums
426 permission_manage_boards: GΓ©rer les forums
426 permission_view_messages: Voir les messages
427 permission_view_messages: Voir les messages
427 permission_add_messages: Poster un message
428 permission_add_messages: Poster un message
428 permission_edit_messages: Modifier les messages
429 permission_edit_messages: Modifier les messages
429 permission_edit_own_messages: Modifier ses propres messages
430 permission_edit_own_messages: Modifier ses propres messages
430 permission_delete_messages: Supprimer les messages
431 permission_delete_messages: Supprimer les messages
431 permission_delete_own_messages: Supprimer ses propres messages
432 permission_delete_own_messages: Supprimer ses propres messages
432 permission_export_wiki_pages: Exporter les pages
433 permission_export_wiki_pages: Exporter les pages
433 permission_manage_project_activities: GΓ©rer les activitΓ©s
434 permission_manage_project_activities: GΓ©rer les activitΓ©s
434 permission_manage_subtasks: GΓ©rer les sous-tΓ’ches
435 permission_manage_subtasks: GΓ©rer les sous-tΓ’ches
435
436
436 project_module_issue_tracking: Suivi des demandes
437 project_module_issue_tracking: Suivi des demandes
437 project_module_time_tracking: Suivi du temps passΓ©
438 project_module_time_tracking: Suivi du temps passΓ©
438 project_module_news: Publication d'annonces
439 project_module_news: Publication d'annonces
439 project_module_documents: Publication de documents
440 project_module_documents: Publication de documents
440 project_module_files: Publication de fichiers
441 project_module_files: Publication de fichiers
441 project_module_wiki: Wiki
442 project_module_wiki: Wiki
442 project_module_repository: DΓ©pΓ΄t de sources
443 project_module_repository: DΓ©pΓ΄t de sources
443 project_module_boards: Forums de discussion
444 project_module_boards: Forums de discussion
444
445
445 label_user: Utilisateur
446 label_user: Utilisateur
446 label_user_plural: Utilisateurs
447 label_user_plural: Utilisateurs
447 label_user_new: Nouvel utilisateur
448 label_user_new: Nouvel utilisateur
448 label_user_anonymous: Anonyme
449 label_user_anonymous: Anonyme
449 label_project: Projet
450 label_project: Projet
450 label_project_new: Nouveau projet
451 label_project_new: Nouveau projet
451 label_project_plural: Projets
452 label_project_plural: Projets
452 label_x_projects:
453 label_x_projects:
453 zero: aucun projet
454 zero: aucun projet
454 one: un projet
455 one: un projet
455 other: "%{count} projets"
456 other: "%{count} projets"
456 label_project_all: Tous les projets
457 label_project_all: Tous les projets
457 label_project_latest: Derniers projets
458 label_project_latest: Derniers projets
458 label_issue: Demande
459 label_issue: Demande
459 label_issue_new: Nouvelle demande
460 label_issue_new: Nouvelle demande
460 label_issue_plural: Demandes
461 label_issue_plural: Demandes
461 label_issue_view_all: Voir toutes les demandes
462 label_issue_view_all: Voir toutes les demandes
462 label_issue_added: Demande ajoutΓ©e
463 label_issue_added: Demande ajoutΓ©e
463 label_issue_updated: Demande mise Γ  jour
464 label_issue_updated: Demande mise Γ  jour
464 label_issue_note_added: Note ajoutΓ©e
465 label_issue_note_added: Note ajoutΓ©e
465 label_issue_status_updated: Statut changΓ©
466 label_issue_status_updated: Statut changΓ©
466 label_issue_priority_updated: PrioritΓ© changΓ©e
467 label_issue_priority_updated: PrioritΓ© changΓ©e
467 label_issues_by: "Demandes par %{value}"
468 label_issues_by: "Demandes par %{value}"
468 label_document: Document
469 label_document: Document
469 label_document_new: Nouveau document
470 label_document_new: Nouveau document
470 label_document_plural: Documents
471 label_document_plural: Documents
471 label_document_added: Document ajoutΓ©
472 label_document_added: Document ajoutΓ©
472 label_role: RΓ΄le
473 label_role: RΓ΄le
473 label_role_plural: RΓ΄les
474 label_role_plural: RΓ΄les
474 label_role_new: Nouveau rΓ΄le
475 label_role_new: Nouveau rΓ΄le
475 label_role_and_permissions: RΓ΄les et permissions
476 label_role_and_permissions: RΓ΄les et permissions
476 label_role_anonymous: Anonyme
477 label_role_anonymous: Anonyme
477 label_role_non_member: Non membre
478 label_role_non_member: Non membre
478 label_member: Membre
479 label_member: Membre
479 label_member_new: Nouveau membre
480 label_member_new: Nouveau membre
480 label_member_plural: Membres
481 label_member_plural: Membres
481 label_tracker: Tracker
482 label_tracker: Tracker
482 label_tracker_plural: Trackers
483 label_tracker_plural: Trackers
483 label_tracker_new: Nouveau tracker
484 label_tracker_new: Nouveau tracker
484 label_workflow: Workflow
485 label_workflow: Workflow
485 label_issue_status: Statut de demandes
486 label_issue_status: Statut de demandes
486 label_issue_status_plural: Statuts de demandes
487 label_issue_status_plural: Statuts de demandes
487 label_issue_status_new: Nouveau statut
488 label_issue_status_new: Nouveau statut
488 label_issue_category: CatΓ©gorie de demandes
489 label_issue_category: CatΓ©gorie de demandes
489 label_issue_category_plural: CatΓ©gories de demandes
490 label_issue_category_plural: CatΓ©gories de demandes
490 label_issue_category_new: Nouvelle catΓ©gorie
491 label_issue_category_new: Nouvelle catΓ©gorie
491 label_custom_field: Champ personnalisΓ©
492 label_custom_field: Champ personnalisΓ©
492 label_custom_field_plural: Champs personnalisΓ©s
493 label_custom_field_plural: Champs personnalisΓ©s
493 label_custom_field_new: Nouveau champ personnalisΓ©
494 label_custom_field_new: Nouveau champ personnalisΓ©
494 label_enumerations: Listes de valeurs
495 label_enumerations: Listes de valeurs
495 label_enumeration_new: Nouvelle valeur
496 label_enumeration_new: Nouvelle valeur
496 label_information: Information
497 label_information: Information
497 label_information_plural: Informations
498 label_information_plural: Informations
498 label_please_login: Identification
499 label_please_login: Identification
499 label_register: S'enregistrer
500 label_register: S'enregistrer
500 label_login_with_open_id_option: S'authentifier avec OpenID
501 label_login_with_open_id_option: S'authentifier avec OpenID
501 label_password_lost: Mot de passe perdu
502 label_password_lost: Mot de passe perdu
502 label_home: Accueil
503 label_home: Accueil
503 label_my_page: Ma page
504 label_my_page: Ma page
504 label_my_account: Mon compte
505 label_my_account: Mon compte
505 label_my_projects: Mes projets
506 label_my_projects: Mes projets
506 label_my_page_block: Blocs disponibles
507 label_my_page_block: Blocs disponibles
507 label_administration: Administration
508 label_administration: Administration
508 label_login: Connexion
509 label_login: Connexion
509 label_logout: DΓ©connexion
510 label_logout: DΓ©connexion
510 label_help: Aide
511 label_help: Aide
511 label_reported_issues: "Demandes soumises "
512 label_reported_issues: "Demandes soumises "
512 label_assigned_to_me_issues: Demandes qui me sont assignΓ©es
513 label_assigned_to_me_issues: Demandes qui me sont assignΓ©es
513 label_last_login: "Dernière connexion "
514 label_last_login: "Dernière connexion "
514 label_registered_on: "Inscrit le "
515 label_registered_on: "Inscrit le "
515 label_activity: ActivitΓ©
516 label_activity: ActivitΓ©
516 label_overall_activity: ActivitΓ© globale
517 label_overall_activity: ActivitΓ© globale
517 label_user_activity: "ActivitΓ© de %{value}"
518 label_user_activity: "ActivitΓ© de %{value}"
518 label_new: Nouveau
519 label_new: Nouveau
519 label_logged_as: ConnectΓ© en tant que
520 label_logged_as: ConnectΓ© en tant que
520 label_environment: Environnement
521 label_environment: Environnement
521 label_authentication: Authentification
522 label_authentication: Authentification
522 label_auth_source: Mode d'authentification
523 label_auth_source: Mode d'authentification
523 label_auth_source_new: Nouveau mode d'authentification
524 label_auth_source_new: Nouveau mode d'authentification
524 label_auth_source_plural: Modes d'authentification
525 label_auth_source_plural: Modes d'authentification
525 label_subproject_plural: Sous-projets
526 label_subproject_plural: Sous-projets
526 label_subproject_new: Nouveau sous-projet
527 label_subproject_new: Nouveau sous-projet
527 label_and_its_subprojects: "%{value} et ses sous-projets"
528 label_and_its_subprojects: "%{value} et ses sous-projets"
528 label_min_max_length: Longueurs mini - maxi
529 label_min_max_length: Longueurs mini - maxi
529 label_list: Liste
530 label_list: Liste
530 label_date: Date
531 label_date: Date
531 label_integer: Entier
532 label_integer: Entier
532 label_float: Nombre dΓ©cimal
533 label_float: Nombre dΓ©cimal
533 label_boolean: BoolΓ©en
534 label_boolean: BoolΓ©en
534 label_string: Texte
535 label_string: Texte
535 label_text: Texte long
536 label_text: Texte long
536 label_attribute: Attribut
537 label_attribute: Attribut
537 label_attribute_plural: Attributs
538 label_attribute_plural: Attributs
538 label_download: "%{count} tΓ©lΓ©chargement"
539 label_download: "%{count} tΓ©lΓ©chargement"
539 label_download_plural: "%{count} tΓ©lΓ©chargements"
540 label_download_plural: "%{count} tΓ©lΓ©chargements"
540 label_no_data: Aucune donnΓ©e Γ  afficher
541 label_no_data: Aucune donnΓ©e Γ  afficher
541 label_change_status: Changer le statut
542 label_change_status: Changer le statut
542 label_history: Historique
543 label_history: Historique
543 label_attachment: Fichier
544 label_attachment: Fichier
544 label_attachment_new: Nouveau fichier
545 label_attachment_new: Nouveau fichier
545 label_attachment_delete: Supprimer le fichier
546 label_attachment_delete: Supprimer le fichier
546 label_attachment_plural: Fichiers
547 label_attachment_plural: Fichiers
547 label_file_added: Fichier ajoutΓ©
548 label_file_added: Fichier ajoutΓ©
548 label_report: Rapport
549 label_report: Rapport
549 label_report_plural: Rapports
550 label_report_plural: Rapports
550 label_news: Annonce
551 label_news: Annonce
551 label_news_new: Nouvelle annonce
552 label_news_new: Nouvelle annonce
552 label_news_plural: Annonces
553 label_news_plural: Annonces
553 label_news_latest: Dernières annonces
554 label_news_latest: Dernières annonces
554 label_news_view_all: Voir toutes les annonces
555 label_news_view_all: Voir toutes les annonces
555 label_news_added: Annonce ajoutΓ©e
556 label_news_added: Annonce ajoutΓ©e
556 label_news_comment_added: Commentaire ajoutΓ© Γ  une annonce
557 label_news_comment_added: Commentaire ajoutΓ© Γ  une annonce
557 label_settings: Configuration
558 label_settings: Configuration
558 label_overview: AperΓ§u
559 label_overview: AperΓ§u
559 label_version: Version
560 label_version: Version
560 label_version_new: Nouvelle version
561 label_version_new: Nouvelle version
561 label_version_plural: Versions
562 label_version_plural: Versions
562 label_confirmation: Confirmation
563 label_confirmation: Confirmation
563 label_export_to: 'Formats disponibles :'
564 label_export_to: 'Formats disponibles :'
564 label_read: Lire...
565 label_read: Lire...
565 label_public_projects: Projets publics
566 label_public_projects: Projets publics
566 label_open_issues: ouvert
567 label_open_issues: ouvert
567 label_open_issues_plural: ouverts
568 label_open_issues_plural: ouverts
568 label_closed_issues: fermΓ©
569 label_closed_issues: fermΓ©
569 label_closed_issues_plural: fermΓ©s
570 label_closed_issues_plural: fermΓ©s
570 label_x_open_issues_abbr_on_total:
571 label_x_open_issues_abbr_on_total:
571 zero: 0 ouverte sur %{total}
572 zero: 0 ouverte sur %{total}
572 one: 1 ouverte sur %{total}
573 one: 1 ouverte sur %{total}
573 other: "%{count} ouvertes sur %{total}"
574 other: "%{count} ouvertes sur %{total}"
574 label_x_open_issues_abbr:
575 label_x_open_issues_abbr:
575 zero: 0 ouverte
576 zero: 0 ouverte
576 one: 1 ouverte
577 one: 1 ouverte
577 other: "%{count} ouvertes"
578 other: "%{count} ouvertes"
578 label_x_closed_issues_abbr:
579 label_x_closed_issues_abbr:
579 zero: 0 fermΓ©e
580 zero: 0 fermΓ©e
580 one: 1 fermΓ©e
581 one: 1 fermΓ©e
581 other: "%{count} fermΓ©es"
582 other: "%{count} fermΓ©es"
582 label_x_issues:
583 label_x_issues:
583 zero: 0 demande
584 zero: 0 demande
584 one: 1 demande
585 one: 1 demande
585 other: "%{count} demandes"
586 other: "%{count} demandes"
586 label_total: Total
587 label_total: Total
587 label_permissions: Permissions
588 label_permissions: Permissions
588 label_current_status: Statut actuel
589 label_current_status: Statut actuel
589 label_new_statuses_allowed: Nouveaux statuts autorisΓ©s
590 label_new_statuses_allowed: Nouveaux statuts autorisΓ©s
590 label_all: tous
591 label_all: tous
591 label_none: aucun
592 label_none: aucun
592 label_nobody: personne
593 label_nobody: personne
593 label_next: Suivant
594 label_next: Suivant
594 label_previous: PrΓ©cΓ©dent
595 label_previous: PrΓ©cΓ©dent
595 label_used_by: UtilisΓ© par
596 label_used_by: UtilisΓ© par
596 label_details: DΓ©tails
597 label_details: DΓ©tails
597 label_add_note: Ajouter une note
598 label_add_note: Ajouter une note
598 label_per_page: Par page
599 label_per_page: Par page
599 label_calendar: Calendrier
600 label_calendar: Calendrier
600 label_months_from: mois depuis
601 label_months_from: mois depuis
601 label_gantt: Gantt
602 label_gantt: Gantt
602 label_internal: Interne
603 label_internal: Interne
603 label_last_changes: "%{count} derniers changements"
604 label_last_changes: "%{count} derniers changements"
604 label_change_view_all: Voir tous les changements
605 label_change_view_all: Voir tous les changements
605 label_personalize_page: Personnaliser cette page
606 label_personalize_page: Personnaliser cette page
606 label_comment: Commentaire
607 label_comment: Commentaire
607 label_comment_plural: Commentaires
608 label_comment_plural: Commentaires
608 label_x_comments:
609 label_x_comments:
609 zero: aucun commentaire
610 zero: aucun commentaire
610 one: un commentaire
611 one: un commentaire
611 other: "%{count} commentaires"
612 other: "%{count} commentaires"
612 label_comment_add: Ajouter un commentaire
613 label_comment_add: Ajouter un commentaire
613 label_comment_added: Commentaire ajoutΓ©
614 label_comment_added: Commentaire ajoutΓ©
614 label_comment_delete: Supprimer les commentaires
615 label_comment_delete: Supprimer les commentaires
615 label_query: Rapport personnalisΓ©
616 label_query: Rapport personnalisΓ©
616 label_query_plural: Rapports personnalisΓ©s
617 label_query_plural: Rapports personnalisΓ©s
617 label_query_new: Nouveau rapport
618 label_query_new: Nouveau rapport
618 label_my_queries: Mes rapports personnalisΓ©s
619 label_my_queries: Mes rapports personnalisΓ©s
619 label_filter_add: "Ajouter le filtre "
620 label_filter_add: "Ajouter le filtre "
620 label_filter_plural: Filtres
621 label_filter_plural: Filtres
621 label_equals: Γ©gal
622 label_equals: Γ©gal
622 label_not_equals: diffΓ©rent
623 label_not_equals: diffΓ©rent
623 label_in_less_than: dans moins de
624 label_in_less_than: dans moins de
624 label_in_more_than: dans plus de
625 label_in_more_than: dans plus de
625 label_in: dans
626 label_in: dans
626 label_today: aujourd'hui
627 label_today: aujourd'hui
627 label_all_time: toute la pΓ©riode
628 label_all_time: toute la pΓ©riode
628 label_yesterday: hier
629 label_yesterday: hier
629 label_this_week: cette semaine
630 label_this_week: cette semaine
630 label_last_week: la semaine dernière
631 label_last_week: la semaine dernière
631 label_last_n_days: "les %{count} derniers jours"
632 label_last_n_days: "les %{count} derniers jours"
632 label_this_month: ce mois-ci
633 label_this_month: ce mois-ci
633 label_last_month: le mois dernier
634 label_last_month: le mois dernier
634 label_this_year: cette annΓ©e
635 label_this_year: cette annΓ©e
635 label_date_range: PΓ©riode
636 label_date_range: PΓ©riode
636 label_less_than_ago: il y a moins de
637 label_less_than_ago: il y a moins de
637 label_more_than_ago: il y a plus de
638 label_more_than_ago: il y a plus de
638 label_ago: il y a
639 label_ago: il y a
639 label_contains: contient
640 label_contains: contient
640 label_not_contains: ne contient pas
641 label_not_contains: ne contient pas
641 label_day_plural: jours
642 label_day_plural: jours
642 label_repository: DΓ©pΓ΄t
643 label_repository: DΓ©pΓ΄t
643 label_repository_new: Nouveau dΓ©pΓ΄t
644 label_repository_new: Nouveau dΓ©pΓ΄t
644 label_repository_plural: DΓ©pΓ΄ts
645 label_repository_plural: DΓ©pΓ΄ts
645 label_browse: Parcourir
646 label_browse: Parcourir
646 label_modification: "%{count} modification"
647 label_modification: "%{count} modification"
647 label_modification_plural: "%{count} modifications"
648 label_modification_plural: "%{count} modifications"
648 label_revision: "RΓ©vision "
649 label_revision: "RΓ©vision "
649 label_revision_plural: RΓ©visions
650 label_revision_plural: RΓ©visions
650 label_associated_revisions: RΓ©visions associΓ©es
651 label_associated_revisions: RΓ©visions associΓ©es
651 label_added: ajoutΓ©
652 label_added: ajoutΓ©
652 label_modified: modifiΓ©
653 label_modified: modifiΓ©
653 label_copied: copiΓ©
654 label_copied: copiΓ©
654 label_renamed: renommΓ©
655 label_renamed: renommΓ©
655 label_deleted: supprimΓ©
656 label_deleted: supprimΓ©
656 label_latest_revision: Dernière révision
657 label_latest_revision: Dernière révision
657 label_latest_revision_plural: Dernières révisions
658 label_latest_revision_plural: Dernières révisions
658 label_view_revisions: Voir les rΓ©visions
659 label_view_revisions: Voir les rΓ©visions
659 label_max_size: Taille maximale
660 label_max_size: Taille maximale
660 label_sort_highest: Remonter en premier
661 label_sort_highest: Remonter en premier
661 label_sort_higher: Remonter
662 label_sort_higher: Remonter
662 label_sort_lower: Descendre
663 label_sort_lower: Descendre
663 label_sort_lowest: Descendre en dernier
664 label_sort_lowest: Descendre en dernier
664 label_roadmap: Roadmap
665 label_roadmap: Roadmap
665 label_roadmap_due_in: "Γ‰chΓ©ance dans %{value}"
666 label_roadmap_due_in: "Γ‰chΓ©ance dans %{value}"
666 label_roadmap_overdue: "En retard de %{value}"
667 label_roadmap_overdue: "En retard de %{value}"
667 label_roadmap_no_issues: Aucune demande pour cette version
668 label_roadmap_no_issues: Aucune demande pour cette version
668 label_search: "Recherche "
669 label_search: "Recherche "
669 label_result_plural: RΓ©sultats
670 label_result_plural: RΓ©sultats
670 label_all_words: Tous les mots
671 label_all_words: Tous les mots
671 label_wiki: Wiki
672 label_wiki: Wiki
672 label_wiki_edit: RΓ©vision wiki
673 label_wiki_edit: RΓ©vision wiki
673 label_wiki_edit_plural: RΓ©visions wiki
674 label_wiki_edit_plural: RΓ©visions wiki
674 label_wiki_page: Page wiki
675 label_wiki_page: Page wiki
675 label_wiki_page_plural: Pages wiki
676 label_wiki_page_plural: Pages wiki
676 label_index_by_title: Index par titre
677 label_index_by_title: Index par titre
677 label_index_by_date: Index par date
678 label_index_by_date: Index par date
678 label_current_version: Version actuelle
679 label_current_version: Version actuelle
679 label_preview: PrΓ©visualisation
680 label_preview: PrΓ©visualisation
680 label_feed_plural: Flux RSS
681 label_feed_plural: Flux RSS
681 label_changes_details: DΓ©tails de tous les changements
682 label_changes_details: DΓ©tails de tous les changements
682 label_issue_tracking: Suivi des demandes
683 label_issue_tracking: Suivi des demandes
683 label_spent_time: Temps passΓ©
684 label_spent_time: Temps passΓ©
684 label_f_hour: "%{value} heure"
685 label_f_hour: "%{value} heure"
685 label_f_hour_plural: "%{value} heures"
686 label_f_hour_plural: "%{value} heures"
686 label_time_tracking: Suivi du temps
687 label_time_tracking: Suivi du temps
687 label_change_plural: Changements
688 label_change_plural: Changements
688 label_statistics: Statistiques
689 label_statistics: Statistiques
689 label_commits_per_month: Commits par mois
690 label_commits_per_month: Commits par mois
690 label_commits_per_author: Commits par auteur
691 label_commits_per_author: Commits par auteur
691 label_view_diff: Voir les diffΓ©rences
692 label_view_diff: Voir les diffΓ©rences
692 label_diff_inline: en ligne
693 label_diff_inline: en ligne
693 label_diff_side_by_side: cΓ΄te Γ  cΓ΄te
694 label_diff_side_by_side: cΓ΄te Γ  cΓ΄te
694 label_options: Options
695 label_options: Options
695 label_copy_workflow_from: Copier le workflow de
696 label_copy_workflow_from: Copier le workflow de
696 label_permissions_report: Synthèse des permissions
697 label_permissions_report: Synthèse des permissions
697 label_watched_issues: Demandes surveillΓ©es
698 label_watched_issues: Demandes surveillΓ©es
698 label_related_issues: Demandes liΓ©es
699 label_related_issues: Demandes liΓ©es
699 label_applied_status: Statut appliquΓ©
700 label_applied_status: Statut appliquΓ©
700 label_loading: Chargement...
701 label_loading: Chargement...
701 label_relation_new: Nouvelle relation
702 label_relation_new: Nouvelle relation
702 label_relation_delete: Supprimer la relation
703 label_relation_delete: Supprimer la relation
703 label_relates_to: liΓ© Γ 
704 label_relates_to: liΓ© Γ 
704 label_duplicates: duplique
705 label_duplicates: duplique
705 label_duplicated_by: dupliquΓ© par
706 label_duplicated_by: dupliquΓ© par
706 label_blocks: bloque
707 label_blocks: bloque
707 label_blocked_by: bloquΓ© par
708 label_blocked_by: bloquΓ© par
708 label_precedes: précède
709 label_precedes: précède
709 label_follows: suit
710 label_follows: suit
710 label_end_to_start: fin Γ  dΓ©but
711 label_end_to_start: fin Γ  dΓ©but
711 label_end_to_end: fin Γ  fin
712 label_end_to_end: fin Γ  fin
712 label_start_to_start: dΓ©but Γ  dΓ©but
713 label_start_to_start: dΓ©but Γ  dΓ©but
713 label_start_to_end: dΓ©but Γ  fin
714 label_start_to_end: dΓ©but Γ  fin
714 label_stay_logged_in: Rester connectΓ©
715 label_stay_logged_in: Rester connectΓ©
715 label_disabled: dΓ©sactivΓ©
716 label_disabled: dΓ©sactivΓ©
716 label_show_completed_versions: Voir les versions passΓ©es
717 label_show_completed_versions: Voir les versions passΓ©es
717 label_me: moi
718 label_me: moi
718 label_board: Forum
719 label_board: Forum
719 label_board_new: Nouveau forum
720 label_board_new: Nouveau forum
720 label_board_plural: Forums
721 label_board_plural: Forums
721 label_topic_plural: Discussions
722 label_topic_plural: Discussions
722 label_message_plural: Messages
723 label_message_plural: Messages
723 label_message_last: Dernier message
724 label_message_last: Dernier message
724 label_message_new: Nouveau message
725 label_message_new: Nouveau message
725 label_message_posted: Message ajoutΓ©
726 label_message_posted: Message ajoutΓ©
726 label_reply_plural: RΓ©ponses
727 label_reply_plural: RΓ©ponses
727 label_send_information: Envoyer les informations Γ  l'utilisateur
728 label_send_information: Envoyer les informations Γ  l'utilisateur
728 label_year: AnnΓ©e
729 label_year: AnnΓ©e
729 label_month: Mois
730 label_month: Mois
730 label_week: Semaine
731 label_week: Semaine
731 label_date_from: Du
732 label_date_from: Du
732 label_date_to: Au
733 label_date_to: Au
733 label_language_based: BasΓ© sur la langue de l'utilisateur
734 label_language_based: BasΓ© sur la langue de l'utilisateur
734 label_sort_by: "Trier par %{value}"
735 label_sort_by: "Trier par %{value}"
735 label_send_test_email: Envoyer un email de test
736 label_send_test_email: Envoyer un email de test
736 label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a %{value}"
737 label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a %{value}"
737 label_module_plural: Modules
738 label_module_plural: Modules
738 label_added_time_by: "AjoutΓ© par %{author} il y a %{age}"
739 label_added_time_by: "AjoutΓ© par %{author} il y a %{age}"
739 label_updated_time_by: "Mis Γ  jour par %{author} il y a %{age}"
740 label_updated_time_by: "Mis Γ  jour par %{author} il y a %{age}"
740 label_updated_time: "Mis Γ  jour il y a %{value}"
741 label_updated_time: "Mis Γ  jour il y a %{value}"
741 label_jump_to_a_project: Aller Γ  un projet...
742 label_jump_to_a_project: Aller Γ  un projet...
742 label_file_plural: Fichiers
743 label_file_plural: Fichiers
743 label_changeset_plural: RΓ©visions
744 label_changeset_plural: RΓ©visions
744 label_default_columns: Colonnes par dΓ©faut
745 label_default_columns: Colonnes par dΓ©faut
745 label_no_change_option: (Pas de changement)
746 label_no_change_option: (Pas de changement)
746 label_bulk_edit_selected_issues: Modifier les demandes sΓ©lectionnΓ©es
747 label_bulk_edit_selected_issues: Modifier les demandes sΓ©lectionnΓ©es
747 label_theme: Thème
748 label_theme: Thème
748 label_default: DΓ©faut
749 label_default: DΓ©faut
749 label_search_titles_only: Uniquement dans les titres
750 label_search_titles_only: Uniquement dans les titres
750 label_user_mail_option_all: "Pour tous les Γ©vΓ©nements de tous mes projets"
751 label_user_mail_option_all: "Pour tous les Γ©vΓ©nements de tous mes projets"
751 label_user_mail_option_selected: "Pour tous les Γ©vΓ©nements des projets sΓ©lectionnΓ©s..."
752 label_user_mail_option_selected: "Pour tous les Γ©vΓ©nements des projets sΓ©lectionnΓ©s..."
752 label_user_mail_no_self_notified: "Je ne veux pas Γͺtre notifiΓ© des changements que j'effectue"
753 label_user_mail_no_self_notified: "Je ne veux pas Γͺtre notifiΓ© des changements que j'effectue"
753 label_registration_activation_by_email: activation du compte par email
754 label_registration_activation_by_email: activation du compte par email
754 label_registration_manual_activation: activation manuelle du compte
755 label_registration_manual_activation: activation manuelle du compte
755 label_registration_automatic_activation: activation automatique du compte
756 label_registration_automatic_activation: activation automatique du compte
756 label_display_per_page: "Par page : %{value}"
757 label_display_per_page: "Par page : %{value}"
757 label_age: Γ‚ge
758 label_age: Γ‚ge
758 label_change_properties: Changer les propriΓ©tΓ©s
759 label_change_properties: Changer les propriΓ©tΓ©s
759 label_general: GΓ©nΓ©ral
760 label_general: GΓ©nΓ©ral
760 label_more: Plus
761 label_more: Plus
761 label_scm: SCM
762 label_scm: SCM
762 label_plugins: Plugins
763 label_plugins: Plugins
763 label_ldap_authentication: Authentification LDAP
764 label_ldap_authentication: Authentification LDAP
764 label_downloads_abbr: D/L
765 label_downloads_abbr: D/L
765 label_optional_description: Description facultative
766 label_optional_description: Description facultative
766 label_add_another_file: Ajouter un autre fichier
767 label_add_another_file: Ajouter un autre fichier
767 label_preferences: PrΓ©fΓ©rences
768 label_preferences: PrΓ©fΓ©rences
768 label_chronological_order: Dans l'ordre chronologique
769 label_chronological_order: Dans l'ordre chronologique
769 label_reverse_chronological_order: Dans l'ordre chronologique inverse
770 label_reverse_chronological_order: Dans l'ordre chronologique inverse
770 label_planning: Planning
771 label_planning: Planning
771 label_incoming_emails: Emails entrants
772 label_incoming_emails: Emails entrants
772 label_generate_key: GΓ©nΓ©rer une clΓ©
773 label_generate_key: GΓ©nΓ©rer une clΓ©
773 label_issue_watchers: Observateurs
774 label_issue_watchers: Observateurs
774 label_example: Exemple
775 label_example: Exemple
775 label_display: Affichage
776 label_display: Affichage
776 label_sort: Tri
777 label_sort: Tri
777 label_ascending: Croissant
778 label_ascending: Croissant
778 label_descending: DΓ©croissant
779 label_descending: DΓ©croissant
779 label_date_from_to: Du %{start} au %{end}
780 label_date_from_to: Du %{start} au %{end}
780 label_wiki_content_added: Page wiki ajoutΓ©e
781 label_wiki_content_added: Page wiki ajoutΓ©e
781 label_wiki_content_updated: Page wiki mise Γ  jour
782 label_wiki_content_updated: Page wiki mise Γ  jour
782 label_group_plural: Groupes
783 label_group_plural: Groupes
783 label_group: Groupe
784 label_group: Groupe
784 label_group_new: Nouveau groupe
785 label_group_new: Nouveau groupe
785 label_time_entry_plural: Temps passΓ©
786 label_time_entry_plural: Temps passΓ©
786 label_version_sharing_none: Non partagΓ©
787 label_version_sharing_none: Non partagΓ©
787 label_version_sharing_descendants: Avec les sous-projets
788 label_version_sharing_descendants: Avec les sous-projets
788 label_version_sharing_hierarchy: Avec toute la hiΓ©rarchie
789 label_version_sharing_hierarchy: Avec toute la hiΓ©rarchie
789 label_version_sharing_tree: Avec tout l'arbre
790 label_version_sharing_tree: Avec tout l'arbre
790 label_version_sharing_system: Avec tous les projets
791 label_version_sharing_system: Avec tous les projets
791 label_copy_source: Source
792 label_copy_source: Source
792 label_copy_target: Cible
793 label_copy_target: Cible
793 label_copy_same_as_target: Comme la cible
794 label_copy_same_as_target: Comme la cible
794 label_update_issue_done_ratios: Mettre Γ  jour l'avancement des demandes
795 label_update_issue_done_ratios: Mettre Γ  jour l'avancement des demandes
795 label_display_used_statuses_only: N'afficher que les statuts utilisΓ©s dans ce tracker
796 label_display_used_statuses_only: N'afficher que les statuts utilisΓ©s dans ce tracker
796 label_api_access_key: Clé d'accès API
797 label_api_access_key: Clé d'accès API
797 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
798 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
798 label_feeds_access_key: Clé d'accès RSS
799 label_feeds_access_key: Clé d'accès RSS
799 label_missing_api_access_key: Clé d'accès API manquante
800 label_missing_api_access_key: Clé d'accès API manquante
800 label_missing_feeds_access_key: Clé d'accès RSS manquante
801 label_missing_feeds_access_key: Clé d'accès RSS manquante
801 label_close_versions: Fermer les versions terminΓ©es
802 label_close_versions: Fermer les versions terminΓ©es
802 label_revision_id: Revision %{value}
803 label_revision_id: Revision %{value}
803 label_profile: Profil
804 label_profile: Profil
804 label_subtask_plural: Sous-tΓ’ches
805 label_subtask_plural: Sous-tΓ’ches
805 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
806 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
806 label_principal_search: "Rechercher un utilisateur ou un groupe :"
807 label_principal_search: "Rechercher un utilisateur ou un groupe :"
807 label_user_search: "Rechercher un utilisateur :"
808 label_user_search: "Rechercher un utilisateur :"
808 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
809 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
809 label_additional_workflow_transitions_for_assignee: Autorisations supplΓ©mentaires lorsque la demande est assignΓ©e Γ  l'utilisateur
810 label_additional_workflow_transitions_for_assignee: Autorisations supplΓ©mentaires lorsque la demande est assignΓ©e Γ  l'utilisateur
810 label_issues_visibility_all: Toutes les demandes
811 label_issues_visibility_all: Toutes les demandes
811 label_issues_visibility_public: Toutes les demandes non privΓ©es
812 label_issues_visibility_public: Toutes les demandes non privΓ©es
812 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
813 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
813 label_export_options: Options d'exportation %{export_format}
814 label_export_options: Options d'exportation %{export_format}
814
815
815 button_login: Connexion
816 button_login: Connexion
816 button_submit: Soumettre
817 button_submit: Soumettre
817 button_save: Sauvegarder
818 button_save: Sauvegarder
818 button_check_all: Tout cocher
819 button_check_all: Tout cocher
819 button_uncheck_all: Tout dΓ©cocher
820 button_uncheck_all: Tout dΓ©cocher
820 button_collapse_all: Plier tout
821 button_collapse_all: Plier tout
821 button_expand_all: DΓ©plier tout
822 button_expand_all: DΓ©plier tout
822 button_delete: Supprimer
823 button_delete: Supprimer
823 button_create: CrΓ©er
824 button_create: CrΓ©er
824 button_create_and_continue: CrΓ©er et continuer
825 button_create_and_continue: CrΓ©er et continuer
825 button_test: Tester
826 button_test: Tester
826 button_edit: Modifier
827 button_edit: Modifier
827 button_add: Ajouter
828 button_add: Ajouter
828 button_change: Changer
829 button_change: Changer
829 button_apply: Appliquer
830 button_apply: Appliquer
830 button_clear: Effacer
831 button_clear: Effacer
831 button_lock: Verrouiller
832 button_lock: Verrouiller
832 button_unlock: DΓ©verrouiller
833 button_unlock: DΓ©verrouiller
833 button_download: TΓ©lΓ©charger
834 button_download: TΓ©lΓ©charger
834 button_list: Lister
835 button_list: Lister
835 button_view: Voir
836 button_view: Voir
836 button_move: DΓ©placer
837 button_move: DΓ©placer
837 button_move_and_follow: DΓ©placer et suivre
838 button_move_and_follow: DΓ©placer et suivre
838 button_back: Retour
839 button_back: Retour
839 button_cancel: Annuler
840 button_cancel: Annuler
840 button_activate: Activer
841 button_activate: Activer
841 button_sort: Trier
842 button_sort: Trier
842 button_log_time: Saisir temps
843 button_log_time: Saisir temps
843 button_rollback: Revenir Γ  cette version
844 button_rollback: Revenir Γ  cette version
844 button_watch: Surveiller
845 button_watch: Surveiller
845 button_unwatch: Ne plus surveiller
846 button_unwatch: Ne plus surveiller
846 button_reply: RΓ©pondre
847 button_reply: RΓ©pondre
847 button_archive: Archiver
848 button_archive: Archiver
848 button_unarchive: DΓ©sarchiver
849 button_unarchive: DΓ©sarchiver
849 button_reset: RΓ©initialiser
850 button_reset: RΓ©initialiser
850 button_rename: Renommer
851 button_rename: Renommer
851 button_change_password: Changer de mot de passe
852 button_change_password: Changer de mot de passe
852 button_copy: Copier
853 button_copy: Copier
853 button_copy_and_follow: Copier et suivre
854 button_copy_and_follow: Copier et suivre
854 button_annotate: Annoter
855 button_annotate: Annoter
855 button_update: Mettre Γ  jour
856 button_update: Mettre Γ  jour
856 button_configure: Configurer
857 button_configure: Configurer
857 button_quote: Citer
858 button_quote: Citer
858 button_duplicate: Dupliquer
859 button_duplicate: Dupliquer
859 button_show: Afficher
860 button_show: Afficher
860 button_edit_section: Modifier cette section
861 button_edit_section: Modifier cette section
861 button_export: Exporter
862 button_export: Exporter
862
863
863 status_active: actif
864 status_active: actif
864 status_registered: enregistrΓ©
865 status_registered: enregistrΓ©
865 status_locked: verrouillΓ©
866 status_locked: verrouillΓ©
866
867
867 version_status_open: ouvert
868 version_status_open: ouvert
868 version_status_locked: verrouillΓ©
869 version_status_locked: verrouillΓ©
869 version_status_closed: fermΓ©
870 version_status_closed: fermΓ©
870
871
871 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyΓ©e
872 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyΓ©e
872 text_regexp_info: ex. ^[A-Z0-9]+$
873 text_regexp_info: ex. ^[A-Z0-9]+$
873 text_min_max_length_info: 0 pour aucune restriction
874 text_min_max_length_info: 0 pour aucune restriction
874 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
875 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
875 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront Γ©galement supprimΓ©s."
876 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront Γ©galement supprimΓ©s."
876 text_workflow_edit: SΓ©lectionner un tracker et un rΓ΄le pour Γ©diter le workflow
877 text_workflow_edit: SΓ©lectionner un tracker et un rΓ΄le pour Γ©diter le workflow
877 text_are_you_sure: Êtes-vous sûr ?
878 text_are_you_sure: Êtes-vous sûr ?
878 text_tip_issue_begin_day: tΓ’che commenΓ§ant ce jour
879 text_tip_issue_begin_day: tΓ’che commenΓ§ant ce jour
879 text_tip_issue_end_day: tΓ’che finissant ce jour
880 text_tip_issue_end_day: tΓ’che finissant ce jour
880 text_tip_issue_begin_end_day: tΓ’che commenΓ§ant et finissant ce jour
881 text_tip_issue_begin_end_day: tΓ’che commenΓ§ant et finissant ce jour
881 text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres et tirets sont autorisΓ©s.<br />Un fois sauvegardΓ©, l''identifiant ne pourra plus Γͺtre modifiΓ©.'
882 text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres et tirets sont autorisΓ©s.<br />Un fois sauvegardΓ©, l''identifiant ne pourra plus Γͺtre modifiΓ©.'
882 text_caracters_maximum: "%{count} caractères maximum."
883 text_caracters_maximum: "%{count} caractères maximum."
883 text_caracters_minimum: "%{count} caractères minimum."
884 text_caracters_minimum: "%{count} caractères minimum."
884 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
885 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
885 text_tracker_no_workflow: Aucun worflow n'est dΓ©fini pour ce tracker
886 text_tracker_no_workflow: Aucun worflow n'est dΓ©fini pour ce tracker
886 text_unallowed_characters: Caractères non autorisés
887 text_unallowed_characters: Caractères non autorisés
887 text_comma_separated: Plusieurs valeurs possibles (sΓ©parΓ©es par des virgules).
888 text_comma_separated: Plusieurs valeurs possibles (sΓ©parΓ©es par des virgules).
888 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
889 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
889 text_issues_ref_in_commit_messages: RΓ©fΓ©rencement et rΓ©solution des demandes dans les commentaires de commits
890 text_issues_ref_in_commit_messages: RΓ©fΓ©rencement et rΓ©solution des demandes dans les commentaires de commits
890 text_issue_added: "La demande %{id} a Γ©tΓ© soumise par %{author}."
891 text_issue_added: "La demande %{id} a Γ©tΓ© soumise par %{author}."
891 text_issue_updated: "La demande %{id} a Γ©tΓ© mise Γ  jour par %{author}."
892 text_issue_updated: "La demande %{id} a Γ©tΓ© mise Γ  jour par %{author}."
892 text_wiki_destroy_confirmation: Etes-vous sΓ»r de vouloir supprimer ce wiki et tout son contenu ?
893 text_wiki_destroy_confirmation: Etes-vous sΓ»r de vouloir supprimer ce wiki et tout son contenu ?
893 text_issue_category_destroy_question: "%{count} demandes sont affectΓ©es Γ  cette catΓ©gorie. Que voulez-vous faire ?"
894 text_issue_category_destroy_question: "%{count} demandes sont affectΓ©es Γ  cette catΓ©gorie. Que voulez-vous faire ?"
894 text_issue_category_destroy_assignments: N'affecter les demandes Γ  aucune autre catΓ©gorie
895 text_issue_category_destroy_assignments: N'affecter les demandes Γ  aucune autre catΓ©gorie
895 text_issue_category_reassign_to: RΓ©affecter les demandes Γ  cette catΓ©gorie
896 text_issue_category_reassign_to: RΓ©affecter les demandes Γ  cette catΓ©gorie
896 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)."
897 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)."
897 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Γ©."
898 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Γ©."
898 text_load_default_configuration: Charger le paramΓ©trage par dΓ©faut
899 text_load_default_configuration: Charger le paramΓ©trage par dΓ©faut
899 text_status_changed_by_changeset: "AppliquΓ© par commit %{value}."
900 text_status_changed_by_changeset: "AppliquΓ© par commit %{value}."
900 text_time_logged_by_changeset: "AppliquΓ© par commit %{value}"
901 text_time_logged_by_changeset: "AppliquΓ© par commit %{value}"
901 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
902 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
902 text_issues_destroy_descendants_confirmation: "Cela entrainera Γ©galement la suppression de %{count} sous-tΓ’che(s)."
903 text_issues_destroy_descendants_confirmation: "Cela entrainera Γ©galement la suppression de %{count} sous-tΓ’che(s)."
903 text_select_project_modules: 'SΓ©lectionner les modules Γ  activer pour ce projet :'
904 text_select_project_modules: 'SΓ©lectionner les modules Γ  activer pour ce projet :'
904 text_default_administrator_account_changed: Compte administrateur par dΓ©faut changΓ©
905 text_default_administrator_account_changed: Compte administrateur par dΓ©faut changΓ©
905 text_file_repository_writable: RΓ©pertoire de stockage des fichiers accessible en Γ©criture
906 text_file_repository_writable: RΓ©pertoire de stockage des fichiers accessible en Γ©criture
906 text_plugin_assets_writable: RΓ©pertoire public des plugins accessible en Γ©criture
907 text_plugin_assets_writable: RΓ©pertoire public des plugins accessible en Γ©criture
907 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
908 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
908 text_destroy_time_entries_question: "%{hours} heures ont Γ©tΓ© enregistrΓ©es sur les demandes Γ  supprimer. Que voulez-vous faire ?"
909 text_destroy_time_entries_question: "%{hours} heures ont Γ©tΓ© enregistrΓ©es sur les demandes Γ  supprimer. Que voulez-vous faire ?"
909 text_destroy_time_entries: Supprimer les heures
910 text_destroy_time_entries: Supprimer les heures
910 text_assign_time_entries_to_project: Reporter les heures sur le projet
911 text_assign_time_entries_to_project: Reporter les heures sur le projet
911 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
912 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
912 text_user_wrote: "%{value} a Γ©crit :"
913 text_user_wrote: "%{value} a Γ©crit :"
913 text_enumeration_destroy_question: "Cette valeur est affectΓ©e Γ  %{count} objets."
914 text_enumeration_destroy_question: "Cette valeur est affectΓ©e Γ  %{count} objets."
914 text_enumeration_category_reassign_to: 'RΓ©affecter les objets Γ  cette valeur:'
915 text_enumeration_category_reassign_to: 'RΓ©affecter les objets Γ  cette valeur:'
915 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."
916 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."
916 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."
917 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."
917 text_diff_truncated: '... Ce diffΓ©rentiel a Γ©tΓ© tronquΓ© car il excΓ¨de la taille maximale pouvant Γͺtre affichΓ©e.'
918 text_diff_truncated: '... Ce diffΓ©rentiel a Γ©tΓ© tronquΓ© car il excΓ¨de la taille maximale pouvant Γͺtre affichΓ©e.'
918 text_custom_field_possible_values_info: 'Une ligne par valeur'
919 text_custom_field_possible_values_info: 'Une ligne par valeur'
919 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
920 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
920 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
921 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
921 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
922 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
922 text_wiki_page_reassign_children: "RΓ©affecter les sous-pages Γ  cette page"
923 text_wiki_page_reassign_children: "RΓ©affecter les sous-pages Γ  cette page"
923 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 ?"
924 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 ?"
924 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardΓ© qui sera perdu si vous quittez la page."
925 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardΓ© qui sera perdu si vous quittez la page."
925
926
926 default_role_manager: "Manager "
927 default_role_manager: "Manager "
927 default_role_developer: "DΓ©veloppeur "
928 default_role_developer: "DΓ©veloppeur "
928 default_role_reporter: "Rapporteur "
929 default_role_reporter: "Rapporteur "
929 default_tracker_bug: Anomalie
930 default_tracker_bug: Anomalie
930 default_tracker_feature: Evolution
931 default_tracker_feature: Evolution
931 default_tracker_support: Assistance
932 default_tracker_support: Assistance
932 default_issue_status_new: Nouveau
933 default_issue_status_new: Nouveau
933 default_issue_status_in_progress: En cours
934 default_issue_status_in_progress: En cours
934 default_issue_status_resolved: RΓ©solu
935 default_issue_status_resolved: RΓ©solu
935 default_issue_status_feedback: Commentaire
936 default_issue_status_feedback: Commentaire
936 default_issue_status_closed: FermΓ©
937 default_issue_status_closed: FermΓ©
937 default_issue_status_rejected: RejetΓ©
938 default_issue_status_rejected: RejetΓ©
938 default_doc_category_user: Documentation utilisateur
939 default_doc_category_user: Documentation utilisateur
939 default_doc_category_tech: Documentation technique
940 default_doc_category_tech: Documentation technique
940 default_priority_low: Bas
941 default_priority_low: Bas
941 default_priority_normal: Normal
942 default_priority_normal: Normal
942 default_priority_high: Haut
943 default_priority_high: Haut
943 default_priority_urgent: Urgent
944 default_priority_urgent: Urgent
944 default_priority_immediate: ImmΓ©diat
945 default_priority_immediate: ImmΓ©diat
945 default_activity_design: Conception
946 default_activity_design: Conception
946 default_activity_development: DΓ©veloppement
947 default_activity_development: DΓ©veloppement
947
948
948 enumeration_issue_priorities: PrioritΓ©s des demandes
949 enumeration_issue_priorities: PrioritΓ©s des demandes
949 enumeration_doc_categories: CatΓ©gories des documents
950 enumeration_doc_categories: CatΓ©gories des documents
950 enumeration_activities: ActivitΓ©s (suivi du temps)
951 enumeration_activities: ActivitΓ©s (suivi du temps)
951 label_greater_or_equal: ">="
952 label_greater_or_equal: ">="
952 label_less_or_equal: "<="
953 label_less_or_equal: "<="
953 label_between: entre
954 label_between: entre
954 label_view_all_revisions: Voir toutes les rΓ©visions
955 label_view_all_revisions: Voir toutes les rΓ©visions
955 label_tag: Tag
956 label_tag: Tag
956 label_branch: Branche
957 label_branch: Branche
957 error_no_tracker_in_project: "Aucun tracker n'est associΓ© Γ  ce projet. VΓ©rifier la configuration du projet."
958 error_no_tracker_in_project: "Aucun tracker n'est associΓ© Γ  ce projet. VΓ©rifier la configuration du projet."
958 error_no_default_issue_status: "Aucun statut de demande n'est dΓ©fini par dΓ©faut. VΓ©rifier votre configuration (Administration -> Statuts de demandes)."
959 error_no_default_issue_status: "Aucun statut de demande n'est dΓ©fini par dΓ©faut. VΓ©rifier votre configuration (Administration -> Statuts de demandes)."
959 text_journal_changed: "%{label} changΓ© de %{old} Γ  %{new}"
960 text_journal_changed: "%{label} changΓ© de %{old} Γ  %{new}"
960 text_journal_changed_no_detail: "%{label} mis Γ  jour"
961 text_journal_changed_no_detail: "%{label} mis Γ  jour"
961 text_journal_set_to: "%{label} mis Γ  %{value}"
962 text_journal_set_to: "%{label} mis Γ  %{value}"
962 text_journal_deleted: "%{label} %{old} supprimΓ©"
963 text_journal_deleted: "%{label} %{old} supprimΓ©"
963 text_journal_added: "%{label} %{value} ajoutΓ©"
964 text_journal_added: "%{label} %{value} ajoutΓ©"
964 enumeration_system_activity: Activité système
965 enumeration_system_activity: Activité système
965 label_board_sticky: Sticky
966 label_board_sticky: Sticky
966 label_board_locked: VerrouillΓ©
967 label_board_locked: VerrouillΓ©
967 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
968 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
968 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisΓ©
969 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisΓ©
969 error_unable_to_connect: Connexion impossible (%{value})
970 error_unable_to_connect: Connexion impossible (%{value})
970 error_can_not_remove_role: Ce rΓ΄le est utilisΓ© et ne peut pas Γͺtre supprimΓ©.
971 error_can_not_remove_role: Ce rΓ΄le est utilisΓ© et ne peut pas Γͺtre supprimΓ©.
971 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas Γͺtre supprimΓ©.
972 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas Γͺtre supprimΓ©.
972 field_principal: Principal
973 field_principal: Principal
973 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
974 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
974 text_zoom_out: Zoom arrière
975 text_zoom_out: Zoom arrière
975 text_zoom_in: Zoom avant
976 text_zoom_in: Zoom avant
976 notice_unable_delete_time_entry: Impossible de supprimer le temps passΓ©.
977 notice_unable_delete_time_entry: Impossible de supprimer le temps passΓ©.
977 label_overall_spent_time: Temps passΓ© global
978 label_overall_spent_time: Temps passΓ© global
978 field_time_entries: Temps passΓ©
979 field_time_entries: Temps passΓ©
979 project_module_gantt: Gantt
980 project_module_gantt: Gantt
980 project_module_calendar: Calendrier
981 project_module_calendar: Calendrier
981 button_edit_associated_wikipage: "Modifier la page wiki associΓ©e: %{page_title}"
982 button_edit_associated_wikipage: "Modifier la page wiki associΓ©e: %{page_title}"
982 text_are_you_sure_with_children: Supprimer la demande et toutes ses sous-demandes ?
983 text_are_you_sure_with_children: Supprimer la demande et toutes ses sous-demandes ?
983 field_text: Champ texte
984 field_text: Champ texte
984 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
985 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
985 setting_default_notification_option: Option de notification par dΓ©faut
986 setting_default_notification_option: Option de notification par dΓ©faut
986 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
987 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
987 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assignΓ©
988 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assignΓ©
988 label_user_mail_option_none: Aucune notification
989 label_user_mail_option_none: Aucune notification
989 field_member_of_group: Groupe de l'assignΓ©
990 field_member_of_group: Groupe de l'assignΓ©
990 field_assigned_to_role: RΓ΄le de l'assignΓ©
991 field_assigned_to_role: RΓ΄le de l'assignΓ©
991 setting_emails_header: En-tΓͺte des emails
992 setting_emails_header: En-tΓͺte des emails
992 label_bulk_edit_selected_time_entries: Modifier les temps passΓ©s sΓ©lectionnΓ©s
993 label_bulk_edit_selected_time_entries: Modifier les temps passΓ©s sΓ©lectionnΓ©s
993 text_time_entries_destroy_confirmation: "Etes-vous sΓ»r de vouloir supprimer les temps passΓ©s sΓ©lectionnΓ©s ?"
994 text_time_entries_destroy_confirmation: "Etes-vous sΓ»r de vouloir supprimer les temps passΓ©s sΓ©lectionnΓ©s ?"
994 field_scm_path_encoding: Encodage des chemins
995 field_scm_path_encoding: Encodage des chemins
995 text_scm_path_encoding_note: "DΓ©faut : UTF-8"
996 text_scm_path_encoding_note: "DΓ©faut : UTF-8"
996 field_path_to_repository: Chemin du dΓ©pΓ΄t
997 field_path_to_repository: Chemin du dΓ©pΓ΄t
997 field_root_directory: RΓ©pertoire racine
998 field_root_directory: RΓ©pertoire racine
998 field_cvs_module: Module
999 field_cvs_module: Module
999 field_cvsroot: CVSROOT
1000 field_cvsroot: CVSROOT
1000 text_mercurial_repository_note: "DΓ©pΓ΄t local (exemples : /hgrepo, c:\\hgrepo)"
1001 text_mercurial_repository_note: "DΓ©pΓ΄t local (exemples : /hgrepo, c:\\hgrepo)"
1001 text_scm_command: Commande
1002 text_scm_command: Commande
1002 text_scm_command_version: Version
1003 text_scm_command_version: Version
1003 label_git_report_last_commit: Afficher le dernier commit des fichiers et rΓ©pertoires
1004 label_git_report_last_commit: Afficher le dernier commit des fichiers et rΓ©pertoires
1004 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1005 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1005 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1006 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1006 label_diff: diff
1007 label_diff: diff
1007 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1008 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1008 description_query_sort_criteria_direction: Ordre de tri
1009 description_query_sort_criteria_direction: Ordre de tri
1009 description_project_scope: Périmètre de recherche
1010 description_project_scope: Périmètre de recherche
1010 description_filter: Filtre
1011 description_filter: Filtre
1011 description_user_mail_notification: Option de notification
1012 description_user_mail_notification: Option de notification
1012 description_date_from: Date de dΓ©but
1013 description_date_from: Date de dΓ©but
1013 description_message_content: Contenu du message
1014 description_message_content: Contenu du message
1014 description_available_columns: Colonnes disponibles
1015 description_available_columns: Colonnes disponibles
1015 description_all_columns: Toutes les colonnes
1016 description_all_columns: Toutes les colonnes
1016 description_date_range_interval: Choisir une pΓ©riode
1017 description_date_range_interval: Choisir une pΓ©riode
1017 description_issue_category_reassign: Choisir une catΓ©gorie
1018 description_issue_category_reassign: Choisir une catΓ©gorie
1018 description_search: Champ de recherche
1019 description_search: Champ de recherche
1019 description_notes: Notes
1020 description_notes: Notes
1020 description_date_range_list: Choisir une pΓ©riode prΓ©dΓ©finie
1021 description_date_range_list: Choisir une pΓ©riode prΓ©dΓ©finie
1021 description_choose_project: Projets
1022 description_choose_project: Projets
1022 description_date_to: Date de fin
1023 description_date_to: Date de fin
1023 description_query_sort_criteria_attribute: Critère de tri
1024 description_query_sort_criteria_attribute: Critère de tri
1024 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1025 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1025 description_selected_columns: Colonnes sΓ©lectionnΓ©es
1026 description_selected_columns: Colonnes sΓ©lectionnΓ©es
1026 label_parent_revision: Parent
1027 label_parent_revision: Parent
1027 label_child_revision: Enfant
1028 label_child_revision: Enfant
1028 error_scm_annotate_big_text_file: Cette entrΓ©e ne peut pas Γͺtre annotΓ©e car elle excΓ¨de la taille maximale.
1029 error_scm_annotate_big_text_file: Cette entrΓ©e ne peut pas Γͺtre annotΓ©e car elle excΓ¨de la taille maximale.
1029 setting_repositories_encodings: Encodages des fichiers et des dΓ©pΓ΄ts
1030 setting_repositories_encodings: Encodages des fichiers et des dΓ©pΓ΄ts
@@ -1,392 +1,404
1 ActionController::Routing::Routes.draw do |map|
1 ActionController::Routing::Routes.draw do |map|
2 # Add your own custom routes here.
2 # Add your own custom routes here.
3 # The priority is based upon order of creation: first created -> highest priority.
3 # The priority is based upon order of creation: first created -> highest priority.
4
4
5 # Here's a sample route:
5 # Here's a sample route:
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 # Keep in mind you can assign values other than :controller and :action
7 # Keep in mind you can assign values other than :controller and :action
8
8
9 map.home '', :controller => 'welcome', :conditions => {:method => :get}
9 map.home '', :controller => 'welcome', :conditions => {:method => :get}
10
10
11 map.signin 'login', :controller => 'account', :action => 'login',
11 map.signin 'login', :controller => 'account', :action => 'login',
12 :conditions => {:method => [:get, :post]}
12 :conditions => {:method => [:get, :post]}
13 map.signout 'logout', :controller => 'account', :action => 'logout',
13 map.signout 'logout', :controller => 'account', :action => 'logout',
14 :conditions => {:method => :get}
14 :conditions => {:method => :get}
15 map.connect 'account/register', :controller => 'account', :action => 'register',
15 map.connect 'account/register', :controller => 'account', :action => 'register',
16 :conditions => {:method => [:get, :post]}
16 :conditions => {:method => [:get, :post]}
17 map.connect 'account/lost_password', :controller => 'account', :action => 'lost_password',
17 map.connect 'account/lost_password', :controller => 'account', :action => 'lost_password',
18 :conditions => {:method => [:get, :post]}
18 :conditions => {:method => [:get, :post]}
19 map.connect 'account/activate', :controller => 'account', :action => 'activate',
19 map.connect 'account/activate', :controller => 'account', :action => 'activate',
20 :conditions => {:method => :get}
20 :conditions => {:method => :get}
21
21
22 map.connect 'projects/:id/wiki', :controller => 'wikis',
22 map.connect 'projects/:id/wiki', :controller => 'wikis',
23 :action => 'edit', :conditions => {:method => :post}
23 :action => 'edit', :conditions => {:method => :post}
24 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis',
24 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis',
25 :action => 'destroy', :conditions => {:method => [:get, :post]}
25 :action => 'destroy', :conditions => {:method => [:get, :post]}
26
26
27 map.with_options :controller => 'messages' do |messages_routes|
27 map.with_options :controller => 'messages' do |messages_routes|
28 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
28 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
29 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
29 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
30 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
30 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
31 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
31 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
32 end
32 end
33 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
33 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
34 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
34 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
35 messages_actions.connect 'boards/:board_id/topics/preview', :action => 'preview'
35 messages_actions.connect 'boards/:board_id/topics/preview', :action => 'preview'
36 messages_actions.connect 'boards/:board_id/topics/quote/:id', :action => 'quote'
36 messages_actions.connect 'boards/:board_id/topics/quote/:id', :action => 'quote'
37 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
37 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
38 messages_actions.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
38 messages_actions.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
39 messages_actions.connect 'boards/:board_id/topics/:id/destroy', :action => 'destroy'
39 messages_actions.connect 'boards/:board_id/topics/:id/destroy', :action => 'destroy'
40 end
40 end
41 end
41 end
42
42
43 # Misc issue routes. TODO: move into resources
43 # Misc issue routes. TODO: move into resources
44 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes',
44 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes',
45 :action => 'issues', :conditions => { :method => :get }
45 :action => 'issues', :conditions => { :method => :get }
46 # TODO: would look nicer as /issues/:id/preview
46 # TODO: would look nicer as /issues/:id/preview
47 map.preview_issue '/issues/preview/:id', :controller => 'previews',
47 map.preview_issue '/issues/preview/:id', :controller => 'previews',
48 :action => 'issue'
48 :action => 'issue'
49 map.issues_context_menu '/issues/context_menu',
49 map.issues_context_menu '/issues/context_menu',
50 :controller => 'context_menus', :action => 'issues'
50 :controller => 'context_menus', :action => 'issues'
51
51
52 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
52 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
53 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new',
53 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new',
54 :id => /\d+/, :conditions => { :method => :post }
54 :id => /\d+/, :conditions => { :method => :post }
55
55
56 map.connect '/journals/diff/:id', :controller => 'journals', :action => 'diff',
56 map.connect '/journals/diff/:id', :controller => 'journals', :action => 'diff',
57 :id => /\d+/, :conditions => { :method => :get }
57 :id => /\d+/, :conditions => { :method => :get }
58 map.connect '/journals/edit/:id', :controller => 'journals', :action => 'edit',
58 map.connect '/journals/edit/:id', :controller => 'journals', :action => 'edit',
59 :id => /\d+/, :conditions => { :method => [:get, :post] }
59 :id => /\d+/, :conditions => { :method => [:get, :post] }
60
60
61 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
61 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
62 gantts_routes.connect '/projects/:project_id/issues/gantt'
62 gantts_routes.connect '/projects/:project_id/issues/gantt'
63 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
63 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
64 gantts_routes.connect '/issues/gantt.:format'
64 gantts_routes.connect '/issues/gantt.:format'
65 end
65 end
66
66
67 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
67 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
68 calendars_routes.connect '/projects/:project_id/issues/calendar'
68 calendars_routes.connect '/projects/:project_id/issues/calendar'
69 calendars_routes.connect '/issues/calendar'
69 calendars_routes.connect '/issues/calendar'
70 end
70 end
71
71
72 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
72 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
73 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
73 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
74 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
74 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
75 end
75 end
76
76
77 map.connect 'my/account', :controller => 'my', :action => 'account',
77 map.connect 'my/account', :controller => 'my', :action => 'account',
78 :conditions => {:method => [:get, :post]}
78 :conditions => {:method => [:get, :post]}
79 map.connect 'my/page', :controller => 'my', :action => 'page',
79 map.connect 'my/page', :controller => 'my', :action => 'page',
80 :conditions => {:method => :get}
80 :conditions => {:method => :get}
81 # Redirects to my/page
81 # Redirects to my/page
82 map.connect 'my', :controller => 'my', :action => 'index',
82 map.connect 'my', :controller => 'my', :action => 'index',
83 :conditions => {:method => :get}
83 :conditions => {:method => :get}
84 map.connect 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key',
84 map.connect 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key',
85 :conditions => {:method => :post}
85 :conditions => {:method => :post}
86 map.connect 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key',
86 map.connect 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key',
87 :conditions => {:method => :post}
87 :conditions => {:method => :post}
88 map.connect 'my/password', :controller => 'my', :action => 'password',
88 map.connect 'my/password', :controller => 'my', :action => 'password',
89 :conditions => {:method => [:get, :post]}
89 :conditions => {:method => [:get, :post]}
90 map.connect 'my/page_layout', :controller => 'my', :action => 'page_layout',
90 map.connect 'my/page_layout', :controller => 'my', :action => 'page_layout',
91 :conditions => {:method => :get}
91 :conditions => {:method => :get}
92 map.connect 'my/add_block', :controller => 'my', :action => 'add_block',
92 map.connect 'my/add_block', :controller => 'my', :action => 'add_block',
93 :conditions => {:method => :post}
93 :conditions => {:method => :post}
94 map.connect 'my/remove_block', :controller => 'my', :action => 'remove_block',
94 map.connect 'my/remove_block', :controller => 'my', :action => 'remove_block',
95 :conditions => {:method => :post}
95 :conditions => {:method => :post}
96 map.connect 'my/order_blocks', :controller => 'my', :action => 'order_blocks',
96 map.connect 'my/order_blocks', :controller => 'my', :action => 'order_blocks',
97 :conditions => {:method => :post}
97 :conditions => {:method => :post}
98
98
99 map.connect 'projects/:id/members/new', :controller => 'members',
99 map.connect 'projects/:id/members/new', :controller => 'members',
100 :action => 'new', :conditions => { :method => :post }
100 :action => 'new', :conditions => { :method => :post }
101 map.connect 'members/edit/:id', :controller => 'members',
101 map.connect 'members/edit/:id', :controller => 'members',
102 :action => 'edit', :id => /\d+/, :conditions => { :method => :post }
102 :action => 'edit', :id => /\d+/, :conditions => { :method => :post }
103 map.connect 'members/destroy/:id', :controller => 'members',
103 map.connect 'members/destroy/:id', :controller => 'members',
104 :action => 'destroy', :id => /\d+/, :conditions => { :method => :post }
104 :action => 'destroy', :id => /\d+/, :conditions => { :method => :post }
105 map.connect 'members/autocomplete_for_member/:id', :controller => 'members',
105 map.connect 'members/autocomplete_for_member/:id', :controller => 'members',
106 :action => 'autocomplete_for_member', :conditions => { :method => :post }
106 :action => 'autocomplete_for_member', :conditions => { :method => :post }
107
107
108 map.with_options :controller => 'users' do |users|
108 map.with_options :controller => 'users' do |users|
109 users.user_membership 'users/:id/memberships/:membership_id',
109 users.user_membership 'users/:id/memberships/:membership_id',
110 :action => 'edit_membership',
110 :action => 'edit_membership',
111 :conditions => {:method => :put}
111 :conditions => {:method => :put}
112 users.connect 'users/:id/memberships/:membership_id',
112 users.connect 'users/:id/memberships/:membership_id',
113 :action => 'destroy_membership',
113 :action => 'destroy_membership',
114 :conditions => {:method => :delete}
114 :conditions => {:method => :delete}
115 users.user_memberships 'users/:id/memberships',
115 users.user_memberships 'users/:id/memberships',
116 :action => 'edit_membership',
116 :action => 'edit_membership',
117 :conditions => {:method => :post}
117 :conditions => {:method => :post}
118 end
118 end
119 map.resources :users
119 map.resources :users
120
120
121 # For nice "roadmap" in the url for the index action
121 # For nice "roadmap" in the url for the index action
122 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
122 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
123
123
124 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
124 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
125 map.connect 'news/:id/comments', :controller => 'comments',
125 map.connect 'news/:id/comments', :controller => 'comments',
126 :action => 'create', :conditions => {:method => :post}
126 :action => 'create', :conditions => {:method => :post}
127 map.connect 'news/:id/comments/:comment_id', :controller => 'comments',
127 map.connect 'news/:id/comments/:comment_id', :controller => 'comments',
128 :action => 'destroy', :conditions => {:method => :delete}
128 :action => 'destroy', :conditions => {:method => :delete}
129
129
130 map.connect 'watchers/new', :controller=> 'watchers', :action => 'new',
130 map.connect 'watchers/new', :controller=> 'watchers', :action => 'new',
131 :conditions => {:method => :get}
131 :conditions => {:method => :get}
132 map.connect 'watchers', :controller=> 'watchers', :action => 'create',
132 map.connect 'watchers', :controller=> 'watchers', :action => 'create',
133 :conditions => {:method => :post}
133 :conditions => {:method => :post}
134 map.connect 'watchers/destroy', :controller=> 'watchers', :action => 'destroy',
134 map.connect 'watchers/destroy', :controller=> 'watchers', :action => 'destroy',
135 :conditions => {:method => :post}
135 :conditions => {:method => :post}
136 map.connect 'watchers/watch', :controller=> 'watchers', :action => 'watch',
136 map.connect 'watchers/watch', :controller=> 'watchers', :action => 'watch',
137 :conditions => {:method => :post}
137 :conditions => {:method => :post}
138 map.connect 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch',
138 map.connect 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch',
139 :conditions => {:method => :post}
139 :conditions => {:method => :post}
140 map.connect 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user',
140 map.connect 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user',
141 :conditions => {:method => :get}
141 :conditions => {:method => :get}
142
142
143 # TODO: port to be part of the resources route(s)
143 # TODO: port to be part of the resources route(s)
144 map.with_options :conditions => {:method => :get} do |project_views|
144 map.with_options :conditions => {:method => :get} do |project_views|
145 project_views.connect 'projects/:id/settings/:tab',
145 project_views.connect 'projects/:id/settings/:tab',
146 :controller => 'projects', :action => 'settings'
146 :controller => 'projects', :action => 'settings'
147 project_views.connect 'projects/:project_id/issues/:copy_from/copy',
147 project_views.connect 'projects/:project_id/issues/:copy_from/copy',
148 :controller => 'issues', :action => 'new'
148 :controller => 'issues', :action => 'new'
149 end
149 end
150
150
151 map.resources :projects, :member => {
151 map.resources :projects, :member => {
152 :copy => [:get, :post],
152 :copy => [:get, :post],
153 :settings => :get,
153 :settings => :get,
154 :modules => :post,
154 :modules => :post,
155 :archive => :post,
155 :archive => :post,
156 :unarchive => :post
156 :unarchive => :post
157 } do |project|
157 } do |project|
158 project.resource :enumerations, :controller => 'project_enumerations',
158 project.resource :enumerations, :controller => 'project_enumerations',
159 :only => [:update, :destroy]
159 :only => [:update, :destroy]
160 # issue form update
160 # issue form update
161 project.issue_form 'issues/new', :controller => 'issues',
161 project.issue_form 'issues/new', :controller => 'issues',
162 :action => 'new', :conditions => {:method => [:post, :put]}
162 :action => 'new', :conditions => {:method => [:post, :put]}
163 project.resources :issues, :only => [:index, :new, :create] do |issues|
163 project.resources :issues, :only => [:index, :new, :create] do |issues|
164 issues.resources :time_entries, :controller => 'timelog',
164 issues.resources :time_entries, :controller => 'timelog',
165 :collection => {:report => :get}
165 :collection => {:report => :get}
166 end
166 end
167
167
168 project.resources :files, :only => [:index, :new, :create]
168 project.resources :files, :only => [:index, :new, :create]
169 project.resources :versions, :shallow => true,
169 project.resources :versions, :shallow => true,
170 :collection => {:close_completed => :put},
170 :collection => {:close_completed => :put},
171 :member => {:status_by => :post}
171 :member => {:status_by => :post}
172 project.resources :news, :shallow => true
172 project.resources :news, :shallow => true
173 project.resources :time_entries, :controller => 'timelog',
173 project.resources :time_entries, :controller => 'timelog',
174 :collection => {:report => :get}
174 :collection => {:report => :get}
175 project.resources :queries, :only => [:new, :create]
175 project.resources :queries, :only => [:new, :create]
176 project.resources :issue_categories, :shallow => true
176 project.resources :issue_categories, :shallow => true
177 project.resources :documents, :shallow => true, :member => {:add_attachment => :post}
177 project.resources :documents, :shallow => true, :member => {:add_attachment => :post}
178 project.resources :boards
178 project.resources :boards
179 project.resources :repositories, :shallow => true, :except => [:index, :show],
179 project.resources :repositories, :shallow => true, :except => [:index, :show],
180 :member => {:committers => [:get, :post]}
180 :member => {:committers => [:get, :post]}
181
181
182 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
182 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
183 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
183 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
184 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
184 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
185 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
185 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
186 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
186 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
187 project.resources :wiki, :except => [:new, :create], :member => {
187 project.resources :wiki, :except => [:new, :create], :member => {
188 :rename => [:get, :post],
188 :rename => [:get, :post],
189 :history => :get,
189 :history => :get,
190 :preview => :any,
190 :preview => :any,
191 :protect => :post,
191 :protect => :post,
192 :add_attachment => :post
192 :add_attachment => :post
193 }, :collection => {
193 }, :collection => {
194 :export => :get,
194 :export => :get,
195 :date_index => :get
195 :date_index => :get
196 }
196 }
197 end
197 end
198
198
199 map.connect 'news', :controller => 'news', :action => 'index'
199 map.connect 'news', :controller => 'news', :action => 'index'
200 map.connect 'news.:format', :controller => 'news', :action => 'index'
200 map.connect 'news.:format', :controller => 'news', :action => 'index'
201
201
202 map.resources :queries, :except => [:show]
202 map.resources :queries, :except => [:show]
203 map.resources :issues,
203 map.resources :issues,
204 :collection => {:bulk_edit => [:get, :post], :bulk_update => :post} do |issues|
204 :collection => {:bulk_edit => [:get, :post], :bulk_update => :post} do |issues|
205 issues.resources :time_entries, :controller => 'timelog',
205 issues.resources :time_entries, :controller => 'timelog',
206 :collection => {:report => :get}
206 :collection => {:report => :get}
207 issues.resources :relations, :shallow => true,
207 issues.resources :relations, :shallow => true,
208 :controller => 'issue_relations',
208 :controller => 'issue_relations',
209 :only => [:index, :show, :create, :destroy]
209 :only => [:index, :show, :create, :destroy]
210 end
210 end
211 # Bulk deletion
211 # Bulk deletion
212 map.connect '/issues', :controller => 'issues', :action => 'destroy',
212 map.connect '/issues', :controller => 'issues', :action => 'destroy',
213 :conditions => {:method => :delete}
213 :conditions => {:method => :delete}
214
214
215 map.connect '/time_entries/destroy',
215 map.connect '/time_entries/destroy',
216 :controller => 'timelog', :action => 'destroy',
216 :controller => 'timelog', :action => 'destroy',
217 :conditions => { :method => :delete }
217 :conditions => { :method => :delete }
218 map.time_entries_context_menu '/time_entries/context_menu',
218 map.time_entries_context_menu '/time_entries/context_menu',
219 :controller => 'context_menus', :action => 'time_entries'
219 :controller => 'context_menus', :action => 'time_entries'
220
220
221 map.resources :time_entries, :controller => 'timelog',
221 map.resources :time_entries, :controller => 'timelog',
222 :collection => {:report => :get, :bulk_edit => :get, :bulk_update => :post}
222 :collection => {:report => :get, :bulk_edit => :get, :bulk_update => :post}
223
223
224 map.with_options :controller => 'activities', :action => 'index',
224 map.with_options :controller => 'activities', :action => 'index',
225 :conditions => {:method => :get} do |activity|
225 :conditions => {:method => :get} do |activity|
226 activity.connect 'projects/:id/activity'
226 activity.connect 'projects/:id/activity'
227 activity.connect 'projects/:id/activity.:format'
227 activity.connect 'projects/:id/activity.:format'
228 activity.connect 'activity', :id => nil
228 activity.connect 'activity', :id => nil
229 activity.connect 'activity.:format', :id => nil
229 activity.connect 'activity.:format', :id => nil
230 end
230 end
231
231
232 map.with_options :controller => 'repositories' do |repositories|
232 map.with_options :controller => 'repositories' do |repositories|
233 repositories.with_options :conditions => {:method => :get} do |repository_views|
233 repositories.with_options :conditions => {:method => :get} do |repository_views|
234 repository_views.connect 'projects/:id/repository',
234 repository_views.connect 'projects/:id/repository',
235 :action => 'show'
235 :action => 'show'
236 repository_views.connect 'projects/:id/repository/statistics',
236 repository_views.connect 'projects/:id/repository/statistics',
237 :action => 'stats'
237 :action => 'stats'
238
238 repository_views.connect 'projects/:id/repository/graph',
239 :action => 'graph'
239 repository_views.connect 'projects/:id/repository/revisions',
240 repository_views.connect 'projects/:id/repository/revisions',
240 :action => 'revisions'
241 :action => 'revisions'
241 repository_views.connect 'projects/:id/repository/revisions.:format',
242 repository_views.connect 'projects/:id/repository/revisions.:format',
242 :action => 'revisions'
243 :action => 'revisions'
243 repository_views.connect 'projects/:id/repository/revisions/:rev',
244 repository_views.connect 'projects/:id/repository/revisions/:rev',
244 :action => 'revision'
245 :action => 'revision'
245 repository_views.connect 'projects/:id/repository/revisions/:rev/diff',
246 repository_views.connect 'projects/:id/repository/revisions/:rev/diff',
246 :action => 'diff'
247 :action => 'diff'
247 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format',
248 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format',
248 :action => 'diff'
249 :action => 'diff'
249 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path',
250 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path',
250 :action => 'entry',
251 :action => 'entry', :format => 'raw'
251 :format => 'raw',
252 :requirements => { :rev => /[a-z0-9\.\-_]+/ }
253 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path',
252 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path',
254 :requirements => { :rev => /[a-z0-9\.\-_]+/ }
253 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
255
256 repository_views.connect 'projects/:id/repository/raw/*path',
254 repository_views.connect 'projects/:id/repository/raw/*path',
257 :action => 'entry', :format => 'raw'
255 :action => 'entry', :format => 'raw'
258 repository_views.connect 'projects/:id/repository/browse/*path',
256 repository_views.connect 'projects/:id/repository/:action/*path',
259 :action => 'browse'
257 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
260 repository_views.connect 'projects/:id/repository/entry/*path',
258
261 :action => 'entry'
259 # Same routes with a repository_id
262 repository_views.connect 'projects/:id/repository/changes/*path',
260 repository_views.connect 'projects/:id/repository/:repository_id/statistics',
263 :action => 'changes'
261 :action => 'stats'
264 repository_views.connect 'projects/:id/repository/annotate/*path',
262 repository_views.connect 'projects/:id/repository/:repository_id/graph',
265 :action => 'annotate'
263 :action => 'graph'
266 repository_views.connect 'projects/:id/repository/diff/*path',
264 repository_views.connect 'projects/:id/repository/:repository_id/revisions',
265 :action => 'revisions'
266 repository_views.connect 'projects/:id/repository/:repository_id/revisions.:format',
267 :action => 'revisions'
268 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev',
269 :action => 'revision'
270 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff',
271 :action => 'diff'
272 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff.:format',
267 :action => 'diff'
273 :action => 'diff'
268 repository_views.connect 'projects/:id/repository/show/*path',
274 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/raw/*path',
275 :action => 'entry', :format => 'raw'
276 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/:action/*path',
277 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
278 repository_views.connect 'projects/:id/repository/:repository_id/raw/*path',
279 :action => 'entry', :format => 'raw'
280 repository_views.connect 'projects/:id/repository/:repository_id/:action/*path',
281 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
282 repository_views.connect 'projects/:id/repository/:repository_id',
269 :action => 'show'
283 :action => 'show'
270 repository_views.connect 'projects/:id/repository/graph',
271 :action => 'graph'
272 end
284 end
273
285
274 repositories.connect 'projects/:id/repository/revision',
286 repositories.connect 'projects/:id/repository/revision',
275 :action => 'revision',
287 :action => 'revision',
276 :conditions => {:method => [:get, :post]}
288 :conditions => {:method => [:get, :post]}
277 end
289 end
278
290
279 # additional routes for having the file name at the end of url
291 # additional routes for having the file name at the end of url
280 map.connect 'attachments/:id/:filename', :controller => 'attachments',
292 map.connect 'attachments/:id/:filename', :controller => 'attachments',
281 :action => 'show', :id => /\d+/, :filename => /.*/,
293 :action => 'show', :id => /\d+/, :filename => /.*/,
282 :conditions => {:method => :get}
294 :conditions => {:method => :get}
283 map.connect 'attachments/download/:id/:filename', :controller => 'attachments',
295 map.connect 'attachments/download/:id/:filename', :controller => 'attachments',
284 :action => 'download', :id => /\d+/, :filename => /.*/,
296 :action => 'download', :id => /\d+/, :filename => /.*/,
285 :conditions => {:method => :get}
297 :conditions => {:method => :get}
286 map.connect 'attachments/download/:id', :controller => 'attachments',
298 map.connect 'attachments/download/:id', :controller => 'attachments',
287 :action => 'download', :id => /\d+/,
299 :action => 'download', :id => /\d+/,
288 :conditions => {:method => :get}
300 :conditions => {:method => :get}
289 map.resources :attachments, :only => [:show, :destroy]
301 map.resources :attachments, :only => [:show, :destroy]
290
302
291 map.resources :groups, :member => {:autocomplete_for_user => :get}
303 map.resources :groups, :member => {:autocomplete_for_user => :get}
292 map.group_users 'groups/:id/users', :controller => 'groups',
304 map.group_users 'groups/:id/users', :controller => 'groups',
293 :action => 'add_users', :id => /\d+/,
305 :action => 'add_users', :id => /\d+/,
294 :conditions => {:method => :post}
306 :conditions => {:method => :post}
295 map.group_user 'groups/:id/users/:user_id', :controller => 'groups',
307 map.group_user 'groups/:id/users/:user_id', :controller => 'groups',
296 :action => 'remove_user', :id => /\d+/,
308 :action => 'remove_user', :id => /\d+/,
297 :conditions => {:method => :delete}
309 :conditions => {:method => :delete}
298 map.connect 'groups/destroy_membership/:id', :controller => 'groups',
310 map.connect 'groups/destroy_membership/:id', :controller => 'groups',
299 :action => 'destroy_membership', :id => /\d+/,
311 :action => 'destroy_membership', :id => /\d+/,
300 :conditions => {:method => :post}
312 :conditions => {:method => :post}
301 map.connect 'groups/edit_membership/:id', :controller => 'groups',
313 map.connect 'groups/edit_membership/:id', :controller => 'groups',
302 :action => 'edit_membership', :id => /\d+/,
314 :action => 'edit_membership', :id => /\d+/,
303 :conditions => {:method => :post}
315 :conditions => {:method => :post}
304
316
305 map.resources :trackers, :except => :show
317 map.resources :trackers, :except => :show
306 map.resources :issue_statuses, :except => :show, :collection => {:update_issue_done_ratio => :post}
318 map.resources :issue_statuses, :except => :show, :collection => {:update_issue_done_ratio => :post}
307 map.resources :custom_fields, :except => :show
319 map.resources :custom_fields, :except => :show
308 map.resources :roles, :except => :show, :collection => {:permissions => [:get, :post]}
320 map.resources :roles, :except => :show, :collection => {:permissions => [:get, :post]}
309 map.resources :enumerations, :except => :show
321 map.resources :enumerations, :except => :show
310
322
311 map.connect 'search', :controller => 'search', :action => 'index', :conditions => {:method => :get}
323 map.connect 'search', :controller => 'search', :action => 'index', :conditions => {:method => :get}
312
324
313 map.connect 'mail_handler', :controller => 'mail_handler',
325 map.connect 'mail_handler', :controller => 'mail_handler',
314 :action => 'index', :conditions => {:method => :post}
326 :action => 'index', :conditions => {:method => :post}
315
327
316 map.connect 'admin', :controller => 'admin', :action => 'index',
328 map.connect 'admin', :controller => 'admin', :action => 'index',
317 :conditions => {:method => :get}
329 :conditions => {:method => :get}
318 map.connect 'admin/projects', :controller => 'admin', :action => 'projects',
330 map.connect 'admin/projects', :controller => 'admin', :action => 'projects',
319 :conditions => {:method => :get}
331 :conditions => {:method => :get}
320 map.connect 'admin/plugins', :controller => 'admin', :action => 'plugins',
332 map.connect 'admin/plugins', :controller => 'admin', :action => 'plugins',
321 :conditions => {:method => :get}
333 :conditions => {:method => :get}
322 map.connect 'admin/info', :controller => 'admin', :action => 'info',
334 map.connect 'admin/info', :controller => 'admin', :action => 'info',
323 :conditions => {:method => :get}
335 :conditions => {:method => :get}
324 map.connect 'admin/test_email', :controller => 'admin', :action => 'test_email',
336 map.connect 'admin/test_email', :controller => 'admin', :action => 'test_email',
325 :conditions => {:method => :get}
337 :conditions => {:method => :get}
326 map.connect 'admin/default_configuration', :controller => 'admin',
338 map.connect 'admin/default_configuration', :controller => 'admin',
327 :action => 'default_configuration', :conditions => {:method => :post}
339 :action => 'default_configuration', :conditions => {:method => :post}
328
340
329 # Used by AuthSourcesControllerTest
341 # Used by AuthSourcesControllerTest
330 # TODO : refactor *AuthSourcesController to remove these routes
342 # TODO : refactor *AuthSourcesController to remove these routes
331 map.connect 'auth_sources', :controller => 'auth_sources',
343 map.connect 'auth_sources', :controller => 'auth_sources',
332 :action => 'index', :conditions => {:method => :get}
344 :action => 'index', :conditions => {:method => :get}
333 map.connect 'auth_sources/new', :controller => 'auth_sources',
345 map.connect 'auth_sources/new', :controller => 'auth_sources',
334 :action => 'new', :conditions => {:method => :get}
346 :action => 'new', :conditions => {:method => :get}
335 map.connect 'auth_sources/create', :controller => 'auth_sources',
347 map.connect 'auth_sources/create', :controller => 'auth_sources',
336 :action => 'create', :conditions => {:method => :post}
348 :action => 'create', :conditions => {:method => :post}
337 map.connect 'auth_sources/destroy/:id', :controller => 'auth_sources',
349 map.connect 'auth_sources/destroy/:id', :controller => 'auth_sources',
338 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
350 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
339 map.connect 'auth_sources/test_connection/:id', :controller => 'auth_sources',
351 map.connect 'auth_sources/test_connection/:id', :controller => 'auth_sources',
340 :action => 'test_connection', :conditions => {:method => :get}
352 :action => 'test_connection', :conditions => {:method => :get}
341 map.connect 'auth_sources/edit/:id', :controller => 'auth_sources',
353 map.connect 'auth_sources/edit/:id', :controller => 'auth_sources',
342 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
354 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
343 map.connect 'auth_sources/update/:id', :controller => 'auth_sources',
355 map.connect 'auth_sources/update/:id', :controller => 'auth_sources',
344 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
356 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
345
357
346 map.connect 'ldap_auth_sources', :controller => 'ldap_auth_sources',
358 map.connect 'ldap_auth_sources', :controller => 'ldap_auth_sources',
347 :action => 'index', :conditions => {:method => :get}
359 :action => 'index', :conditions => {:method => :get}
348 map.connect 'ldap_auth_sources/new', :controller => 'ldap_auth_sources',
360 map.connect 'ldap_auth_sources/new', :controller => 'ldap_auth_sources',
349 :action => 'new', :conditions => {:method => :get}
361 :action => 'new', :conditions => {:method => :get}
350 map.connect 'ldap_auth_sources/create', :controller => 'ldap_auth_sources',
362 map.connect 'ldap_auth_sources/create', :controller => 'ldap_auth_sources',
351 :action => 'create', :conditions => {:method => :post}
363 :action => 'create', :conditions => {:method => :post}
352 map.connect 'ldap_auth_sources/destroy/:id', :controller => 'ldap_auth_sources',
364 map.connect 'ldap_auth_sources/destroy/:id', :controller => 'ldap_auth_sources',
353 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
365 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
354 map.connect 'ldap_auth_sources/test_connection/:id', :controller => 'ldap_auth_sources',
366 map.connect 'ldap_auth_sources/test_connection/:id', :controller => 'ldap_auth_sources',
355 :action => 'test_connection', :conditions => {:method => :get}
367 :action => 'test_connection', :conditions => {:method => :get}
356 map.connect 'ldap_auth_sources/edit/:id', :controller => 'ldap_auth_sources',
368 map.connect 'ldap_auth_sources/edit/:id', :controller => 'ldap_auth_sources',
357 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
369 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
358 map.connect 'ldap_auth_sources/update/:id', :controller => 'ldap_auth_sources',
370 map.connect 'ldap_auth_sources/update/:id', :controller => 'ldap_auth_sources',
359 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
371 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
360
372
361 map.connect 'workflows', :controller => 'workflows',
373 map.connect 'workflows', :controller => 'workflows',
362 :action => 'index', :conditions => {:method => :get}
374 :action => 'index', :conditions => {:method => :get}
363 map.connect 'workflows/edit', :controller => 'workflows',
375 map.connect 'workflows/edit', :controller => 'workflows',
364 :action => 'edit', :conditions => {:method => [:get, :post]}
376 :action => 'edit', :conditions => {:method => [:get, :post]}
365 map.connect 'workflows/copy', :controller => 'workflows',
377 map.connect 'workflows/copy', :controller => 'workflows',
366 :action => 'copy', :conditions => {:method => [:get, :post]}
378 :action => 'copy', :conditions => {:method => [:get, :post]}
367
379
368 map.connect 'settings', :controller => 'settings',
380 map.connect 'settings', :controller => 'settings',
369 :action => 'index', :conditions => {:method => :get}
381 :action => 'index', :conditions => {:method => :get}
370 map.connect 'settings/edit', :controller => 'settings',
382 map.connect 'settings/edit', :controller => 'settings',
371 :action => 'edit', :conditions => {:method => [:get, :post]}
383 :action => 'edit', :conditions => {:method => [:get, :post]}
372 map.connect 'settings/plugin/:id', :controller => 'settings',
384 map.connect 'settings/plugin/:id', :controller => 'settings',
373 :action => 'plugin', :conditions => {:method => [:get, :post]}
385 :action => 'plugin', :conditions => {:method => [:get, :post]}
374
386
375 map.with_options :controller => 'sys' do |sys|
387 map.with_options :controller => 'sys' do |sys|
376 sys.connect 'sys/projects.:format',
388 sys.connect 'sys/projects.:format',
377 :action => 'projects',
389 :action => 'projects',
378 :conditions => {:method => :get}
390 :conditions => {:method => :get}
379 sys.connect 'sys/projects/:id/repository.:format',
391 sys.connect 'sys/projects/:id/repository.:format',
380 :action => 'create_project_repository',
392 :action => 'create_project_repository',
381 :conditions => {:method => :post}
393 :conditions => {:method => :post}
382 sys.connect 'sys/fetch_changesets',
394 sys.connect 'sys/fetch_changesets',
383 :action => 'fetch_changesets',
395 :action => 'fetch_changesets',
384 :conditions => {:method => :get}
396 :conditions => {:method => :get}
385 end
397 end
386
398
387 map.connect 'robots.txt', :controller => 'welcome',
399 map.connect 'robots.txt', :controller => 'welcome',
388 :action => 'robots', :conditions => {:method => :get}
400 :action => 'robots', :conditions => {:method => :get}
389
401
390 # Used for OpenID
402 # Used for OpenID
391 map.root :controller => 'account', :action => 'login'
403 map.root :controller => 'account', :action => 'login'
392 end
404 end
@@ -1,1040 +1,1041
1 html {overflow-y:scroll;}
1 html {overflow-y:scroll;}
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3
3
4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
5 h1 {margin:0; padding:0; font-size: 24px;}
5 h1 {margin:0; padding:0; font-size: 24px;}
6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
9
9
10 /***** Layout *****/
10 /***** Layout *****/
11 #wrapper {background: white;}
11 #wrapper {background: white;}
12
12
13 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 #top-menu ul {margin: 0; padding: 0;}
14 #top-menu ul {margin: 0; padding: 0;}
15 #top-menu li {
15 #top-menu li {
16 float:left;
16 float:left;
17 list-style-type:none;
17 list-style-type:none;
18 margin: 0px 0px 0px 0px;
18 margin: 0px 0px 0px 0px;
19 padding: 0px 0px 0px 0px;
19 padding: 0px 0px 0px 0px;
20 white-space:nowrap;
20 white-space:nowrap;
21 }
21 }
22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24
24
25 #account {float:right;}
25 #account {float:right;}
26
26
27 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 #header a {color:#f8f8f8;}
28 #header a {color:#f8f8f8;}
29 #header h1 a.ancestor { font-size: 80%; }
29 #header h1 a.ancestor { font-size: 80%; }
30 #quick-search {float:right;}
30 #quick-search {float:right;}
31
31
32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 #main-menu ul {margin: 0; padding: 0;}
33 #main-menu ul {margin: 0; padding: 0;}
34 #main-menu li {
34 #main-menu li {
35 float:left;
35 float:left;
36 list-style-type:none;
36 list-style-type:none;
37 margin: 0px 2px 0px 0px;
37 margin: 0px 2px 0px 0px;
38 padding: 0px 0px 0px 0px;
38 padding: 0px 0px 0px 0px;
39 white-space:nowrap;
39 white-space:nowrap;
40 }
40 }
41 #main-menu li a {
41 #main-menu li a {
42 display: block;
42 display: block;
43 color: #fff;
43 color: #fff;
44 text-decoration: none;
44 text-decoration: none;
45 font-weight: bold;
45 font-weight: bold;
46 margin: 0;
46 margin: 0;
47 padding: 4px 10px 4px 10px;
47 padding: 4px 10px 4px 10px;
48 }
48 }
49 #main-menu li a:hover {background:#759FCF; color:#fff;}
49 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51
51
52 #admin-menu ul {margin: 0; padding: 0;}
52 #admin-menu ul {margin: 0; padding: 0;}
53 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
53 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
54
54
55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 #admin-menu a.projects { background-image: url(../images/projects.png); }
56 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 #admin-menu a.users { background-image: url(../images/user.png); }
57 #admin-menu a.users { background-image: url(../images/user.png); }
58 #admin-menu a.groups { background-image: url(../images/group.png); }
58 #admin-menu a.groups { background-image: url(../images/group.png); }
59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 #admin-menu a.info { background-image: url(../images/help.png); }
67 #admin-menu a.info { background-image: url(../images/help.png); }
68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69
69
70 #main {background-color:#EEEEEE;}
70 #main {background-color:#EEEEEE;}
71
71
72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 * html #sidebar{ width: 22%; }
73 * html #sidebar{ width: 22%; }
74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 #sidebar .contextual { margin-right: 1em; }
77 #sidebar .contextual { margin-right: 1em; }
78
78
79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 html>body #content { min-height: 600px; }
81 html>body #content { min-height: 600px; }
82 * html body #content { height: 600px; } /* IE */
82 * html body #content { height: 600px; } /* IE */
83
83
84 #main.nosidebar #sidebar{ display: none; }
84 #main.nosidebar #sidebar{ display: none; }
85 #main.nosidebar #content{ width: auto; border-right: 0; }
85 #main.nosidebar #content{ width: auto; border-right: 0; }
86
86
87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88
88
89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 #login-form table td {padding: 6px;}
90 #login-form table td {padding: 6px;}
91 #login-form label {font-weight: bold;}
91 #login-form label {font-weight: bold;}
92 #login-form input#username, #login-form input#password { width: 300px; }
92 #login-form input#username, #login-form input#password { width: 300px; }
93
93
94 #modalbg {position:absolute; top:0; left:0; width:100%; height:100%; background:#ccc; z-index:49; opacity:0.5;}
94 #modalbg {position:absolute; top:0; left:0; width:100%; height:100%; background:#ccc; z-index:49; opacity:0.5;}
95 html>body #modalbg {position:fixed;}
95 html>body #modalbg {position:fixed;}
96 div.modal { border-radius:5px; position:absolute; top:25%; background:#fff; border:2px solid #759FCF; z-index:50; padding:0px; padding:8px;}
96 div.modal { border-radius:5px; position:absolute; top:25%; background:#fff; border:2px solid #759FCF; z-index:50; padding:0px; padding:8px;}
97 div.modal h3.title {background:#759FCF; color:#fff; border:0; padding-left:8px; margin:-8px; margin-bottom: 1em; border-top-left-radius:2px;border-top-right-radius:2px;}
97 div.modal h3.title {background:#759FCF; color:#fff; border:0; padding-left:8px; margin:-8px; margin-bottom: 1em; border-top-left-radius:2px;border-top-right-radius:2px;}
98 div.modal p.buttons {text-align:right; margin-bottom:0;}
98 div.modal p.buttons {text-align:right; margin-bottom:0;}
99 html>body div.modal {position:fixed;}
99 html>body div.modal {position:fixed;}
100
100
101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
102
102
103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
104
104
105 /***** Links *****/
105 /***** Links *****/
106 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
106 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
108 a img{ border: 0; }
108 a img{ border: 0; }
109
109
110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
111 a.repository.selected {font-weight:bold;}
111
112
112 /***** Tables *****/
113 /***** Tables *****/
113 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
114 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
114 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
115 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
115 table.list td { vertical-align: top; }
116 table.list td { vertical-align: top; }
116 table.list td.id { width: 2%; text-align: center;}
117 table.list td.id { width: 2%; text-align: center;}
117 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
118 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
118 table.list td.checkbox input {padding:0px;}
119 table.list td.checkbox input {padding:0px;}
119 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
120 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
120 table.list td.buttons a { padding-right: 0.6em; }
121 table.list td.buttons a { padding-right: 0.6em; }
121 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
122 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
122
123
123 tr.project td.name a { white-space:nowrap; }
124 tr.project td.name a { white-space:nowrap; }
124
125
125 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
126 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
126 tr.project.idnt-1 td.name {padding-left: 0.5em;}
127 tr.project.idnt-1 td.name {padding-left: 0.5em;}
127 tr.project.idnt-2 td.name {padding-left: 2em;}
128 tr.project.idnt-2 td.name {padding-left: 2em;}
128 tr.project.idnt-3 td.name {padding-left: 3.5em;}
129 tr.project.idnt-3 td.name {padding-left: 3.5em;}
129 tr.project.idnt-4 td.name {padding-left: 5em;}
130 tr.project.idnt-4 td.name {padding-left: 5em;}
130 tr.project.idnt-5 td.name {padding-left: 6.5em;}
131 tr.project.idnt-5 td.name {padding-left: 6.5em;}
131 tr.project.idnt-6 td.name {padding-left: 8em;}
132 tr.project.idnt-6 td.name {padding-left: 8em;}
132 tr.project.idnt-7 td.name {padding-left: 9.5em;}
133 tr.project.idnt-7 td.name {padding-left: 9.5em;}
133 tr.project.idnt-8 td.name {padding-left: 11em;}
134 tr.project.idnt-8 td.name {padding-left: 11em;}
134 tr.project.idnt-9 td.name {padding-left: 12.5em;}
135 tr.project.idnt-9 td.name {padding-left: 12.5em;}
135
136
136 tr.issue { text-align: center; white-space: nowrap; }
137 tr.issue { text-align: center; white-space: nowrap; }
137 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
138 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
138 tr.issue td.subject { text-align: left; }
139 tr.issue td.subject { text-align: left; }
139 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
140 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
140
141
141 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
142 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
142 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
143 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
143 tr.issue.idnt-2 td.subject {padding-left: 2em;}
144 tr.issue.idnt-2 td.subject {padding-left: 2em;}
144 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
145 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
145 tr.issue.idnt-4 td.subject {padding-left: 5em;}
146 tr.issue.idnt-4 td.subject {padding-left: 5em;}
146 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
147 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
147 tr.issue.idnt-6 td.subject {padding-left: 8em;}
148 tr.issue.idnt-6 td.subject {padding-left: 8em;}
148 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
149 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
149 tr.issue.idnt-8 td.subject {padding-left: 11em;}
150 tr.issue.idnt-8 td.subject {padding-left: 11em;}
150 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
151 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
151
152
152 tr.entry { border: 1px solid #f8f8f8; }
153 tr.entry { border: 1px solid #f8f8f8; }
153 tr.entry td { white-space: nowrap; }
154 tr.entry td { white-space: nowrap; }
154 tr.entry td.filename { width: 30%; }
155 tr.entry td.filename { width: 30%; }
155 tr.entry td.filename_no_report { width: 70%; }
156 tr.entry td.filename_no_report { width: 70%; }
156 tr.entry td.size { text-align: right; font-size: 90%; }
157 tr.entry td.size { text-align: right; font-size: 90%; }
157 tr.entry td.revision, tr.entry td.author { text-align: center; }
158 tr.entry td.revision, tr.entry td.author { text-align: center; }
158 tr.entry td.age { text-align: right; }
159 tr.entry td.age { text-align: right; }
159 tr.entry.file td.filename a { margin-left: 16px; }
160 tr.entry.file td.filename a { margin-left: 16px; }
160 tr.entry.file td.filename_no_report a { margin-left: 16px; }
161 tr.entry.file td.filename_no_report a { margin-left: 16px; }
161
162
162 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
163 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
163 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
164 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
164
165
165 tr.changeset { height: 20px }
166 tr.changeset { height: 20px }
166 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
167 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
167 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
168 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
168 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
169 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
169 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
170 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
170 tr.changeset td.comments_nowrap { width: 45%; white-space:nowrap;}
171 tr.changeset td.comments_nowrap { width: 45%; white-space:nowrap;}
171
172
172 table.files tr.file td { text-align: center; }
173 table.files tr.file td { text-align: center; }
173 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
174 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
174 table.files tr.file td.digest { font-size: 80%; }
175 table.files tr.file td.digest { font-size: 80%; }
175
176
176 table.members td.roles, table.memberships td.roles { width: 45%; }
177 table.members td.roles, table.memberships td.roles { width: 45%; }
177
178
178 tr.message { height: 2.6em; }
179 tr.message { height: 2.6em; }
179 tr.message td.subject { padding-left: 20px; }
180 tr.message td.subject { padding-left: 20px; }
180 tr.message td.created_on { white-space: nowrap; }
181 tr.message td.created_on { white-space: nowrap; }
181 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
182 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
182 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
183 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
183 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
184 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
184
185
185 tr.version.closed, tr.version.closed a { color: #999; }
186 tr.version.closed, tr.version.closed a { color: #999; }
186 tr.version td.name { padding-left: 20px; }
187 tr.version td.name { padding-left: 20px; }
187 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
188 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
188 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
189 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
189
190
190 tr.user td { width:13%; }
191 tr.user td { width:13%; }
191 tr.user td.email { width:18%; }
192 tr.user td.email { width:18%; }
192 tr.user td { white-space: nowrap; }
193 tr.user td { white-space: nowrap; }
193 tr.user.locked, tr.user.registered { color: #aaa; }
194 tr.user.locked, tr.user.registered { color: #aaa; }
194 tr.user.locked a, tr.user.registered a { color: #aaa; }
195 tr.user.locked a, tr.user.registered a { color: #aaa; }
195
196
196 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
197 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
197
198
198 tr.time-entry { text-align: center; white-space: nowrap; }
199 tr.time-entry { text-align: center; white-space: nowrap; }
199 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
200 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
200 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
201 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
201 td.hours .hours-dec { font-size: 0.9em; }
202 td.hours .hours-dec { font-size: 0.9em; }
202
203
203 table.plugins td { vertical-align: middle; }
204 table.plugins td { vertical-align: middle; }
204 table.plugins td.configure { text-align: right; padding-right: 1em; }
205 table.plugins td.configure { text-align: right; padding-right: 1em; }
205 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
206 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
206 table.plugins span.description { display: block; font-size: 0.9em; }
207 table.plugins span.description { display: block; font-size: 0.9em; }
207 table.plugins span.url { display: block; font-size: 0.9em; }
208 table.plugins span.url { display: block; font-size: 0.9em; }
208
209
209 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
210 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
210 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
211 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
211 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
212 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
212 tr.group:hover a.toggle-all { display:inline;}
213 tr.group:hover a.toggle-all { display:inline;}
213 a.toggle-all:hover {text-decoration:none;}
214 a.toggle-all:hover {text-decoration:none;}
214
215
215 table.list tbody tr:hover { background-color:#ffffdd; }
216 table.list tbody tr:hover { background-color:#ffffdd; }
216 table.list tbody tr.group:hover { background-color:inherit; }
217 table.list tbody tr.group:hover { background-color:inherit; }
217 table td {padding:2px;}
218 table td {padding:2px;}
218 table p {margin:0;}
219 table p {margin:0;}
219 .odd {background-color:#f6f7f8;}
220 .odd {background-color:#f6f7f8;}
220 .even {background-color: #fff;}
221 .even {background-color: #fff;}
221
222
222 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
223 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
223 a.sort.asc { background-image: url(../images/sort_asc.png); }
224 a.sort.asc { background-image: url(../images/sort_asc.png); }
224 a.sort.desc { background-image: url(../images/sort_desc.png); }
225 a.sort.desc { background-image: url(../images/sort_desc.png); }
225
226
226 table.attributes { width: 100% }
227 table.attributes { width: 100% }
227 table.attributes th { vertical-align: top; text-align: left; }
228 table.attributes th { vertical-align: top; text-align: left; }
228 table.attributes td { vertical-align: top; }
229 table.attributes td { vertical-align: top; }
229
230
230 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
231 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
231
232
232 td.center {text-align:center;}
233 td.center {text-align:center;}
233
234
234 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
235 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
235
236
236 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
237 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
237 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
238 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
238 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
239 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
239 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
240 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
240
241
241 #watchers ul {margin: 0; padding: 0;}
242 #watchers ul {margin: 0; padding: 0;}
242 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
243 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
243 #watchers select {width: 95%; display: block;}
244 #watchers select {width: 95%; display: block;}
244 #watchers a.delete {opacity: 0.4;}
245 #watchers a.delete {opacity: 0.4;}
245 #watchers a.delete:hover {opacity: 1;}
246 #watchers a.delete:hover {opacity: 1;}
246 #watchers img.gravatar {margin: 0 4px 2px 0;}
247 #watchers img.gravatar {margin: 0 4px 2px 0;}
247
248
248 .highlight { background-color: #FCFD8D;}
249 .highlight { background-color: #FCFD8D;}
249 .highlight.token-1 { background-color: #faa;}
250 .highlight.token-1 { background-color: #faa;}
250 .highlight.token-2 { background-color: #afa;}
251 .highlight.token-2 { background-color: #afa;}
251 .highlight.token-3 { background-color: #aaf;}
252 .highlight.token-3 { background-color: #aaf;}
252
253
253 .box{
254 .box{
254 padding:6px;
255 padding:6px;
255 margin-bottom: 10px;
256 margin-bottom: 10px;
256 background-color:#f6f6f6;
257 background-color:#f6f6f6;
257 color:#505050;
258 color:#505050;
258 line-height:1.5em;
259 line-height:1.5em;
259 border: 1px solid #e4e4e4;
260 border: 1px solid #e4e4e4;
260 }
261 }
261
262
262 div.square {
263 div.square {
263 border: 1px solid #999;
264 border: 1px solid #999;
264 float: left;
265 float: left;
265 margin: .3em .4em 0 .4em;
266 margin: .3em .4em 0 .4em;
266 overflow: hidden;
267 overflow: hidden;
267 width: .6em; height: .6em;
268 width: .6em; height: .6em;
268 }
269 }
269 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
270 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
270 .contextual input, .contextual select {font-size:0.9em;}
271 .contextual input, .contextual select {font-size:0.9em;}
271 .message .contextual { margin-top: 0; }
272 .message .contextual { margin-top: 0; }
272
273
273 .splitcontentleft{float:left; width:49%;}
274 .splitcontentleft{float:left; width:49%;}
274 .splitcontentright{float:right; width:49%;}
275 .splitcontentright{float:right; width:49%;}
275 form {display: inline;}
276 form {display: inline;}
276 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
277 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
277 fieldset {border: 1px solid #e4e4e4; margin:0;}
278 fieldset {border: 1px solid #e4e4e4; margin:0;}
278 legend {color: #484848;}
279 legend {color: #484848;}
279 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
280 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
280 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
281 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
281 blockquote blockquote { margin-left: 0;}
282 blockquote blockquote { margin-left: 0;}
282 acronym { border-bottom: 1px dotted; cursor: help; }
283 acronym { border-bottom: 1px dotted; cursor: help; }
283 textarea.wiki-edit { width: 99%; }
284 textarea.wiki-edit { width: 99%; }
284 li p {margin-top: 0;}
285 li p {margin-top: 0;}
285 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
286 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
286 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
287 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
287 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
288 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
288 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
289 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
289
290
290 div.issue div.subject div div { padding-left: 16px; }
291 div.issue div.subject div div { padding-left: 16px; }
291 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
292 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
292 div.issue div.subject>div>p { margin-top: 0.5em; }
293 div.issue div.subject>div>p { margin-top: 0.5em; }
293 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
294 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
294 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px; -moz-border-radius: 2px;}
295 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px; -moz-border-radius: 2px;}
295 div.issue .next-prev-links {color:#999;}
296 div.issue .next-prev-links {color:#999;}
296 div.issue table.attributes th {width:22%;}
297 div.issue table.attributes th {width:22%;}
297 div.issue table.attributes td {width:28%;}
298 div.issue table.attributes td {width:28%;}
298
299
299 #issue_tree table.issues, #relations table.issues { border: 0; }
300 #issue_tree table.issues, #relations table.issues { border: 0; }
300 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
301 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
301 #relations td.buttons {padding:0;}
302 #relations td.buttons {padding:0;}
302
303
303 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
304 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
304 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
305 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
305 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
306 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
306
307
307 fieldset#date-range p { margin: 2px 0 2px 0; }
308 fieldset#date-range p { margin: 2px 0 2px 0; }
308 fieldset#filters table { border-collapse: collapse; }
309 fieldset#filters table { border-collapse: collapse; }
309 fieldset#filters table td { padding: 0; vertical-align: middle; }
310 fieldset#filters table td { padding: 0; vertical-align: middle; }
310 fieldset#filters tr.filter { height: 2em; }
311 fieldset#filters tr.filter { height: 2em; }
311 fieldset#filters td.field { width:200px; }
312 fieldset#filters td.field { width:200px; }
312 fieldset#filters td.operator { width:170px; }
313 fieldset#filters td.operator { width:170px; }
313 fieldset#filters td.values { white-space:nowrap; }
314 fieldset#filters td.values { white-space:nowrap; }
314 fieldset#filters td.values img { vertical-align: middle; margin-left:1px; }
315 fieldset#filters td.values img { vertical-align: middle; margin-left:1px; }
315 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
316 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
316 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
317 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
317
318
318 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
319 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
319 div#issue-changesets div.changeset { padding: 4px;}
320 div#issue-changesets div.changeset { padding: 4px;}
320 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
321 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
321 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
322 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
322
323
323 div#activity dl, #search-results { margin-left: 2em; }
324 div#activity dl, #search-results { margin-left: 2em; }
324 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
325 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
325 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
326 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
326 div#activity dt.me .time { border-bottom: 1px solid #999; }
327 div#activity dt.me .time { border-bottom: 1px solid #999; }
327 div#activity dt .time { color: #777; font-size: 80%; }
328 div#activity dt .time { color: #777; font-size: 80%; }
328 div#activity dd .description, #search-results dd .description { font-style: italic; }
329 div#activity dd .description, #search-results dd .description { font-style: italic; }
329 div#activity span.project:after, #search-results span.project:after { content: " -"; }
330 div#activity span.project:after, #search-results span.project:after { content: " -"; }
330 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
331 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
331
332
332 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
333 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
333
334
334 div#search-results-counts {float:right;}
335 div#search-results-counts {float:right;}
335 div#search-results-counts ul { margin-top: 0.5em; }
336 div#search-results-counts ul { margin-top: 0.5em; }
336 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
337 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
337
338
338 dt.issue { background-image: url(../images/ticket.png); }
339 dt.issue { background-image: url(../images/ticket.png); }
339 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
340 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
340 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
341 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
341 dt.issue-note { background-image: url(../images/ticket_note.png); }
342 dt.issue-note { background-image: url(../images/ticket_note.png); }
342 dt.changeset { background-image: url(../images/changeset.png); }
343 dt.changeset { background-image: url(../images/changeset.png); }
343 dt.news { background-image: url(../images/news.png); }
344 dt.news { background-image: url(../images/news.png); }
344 dt.message { background-image: url(../images/message.png); }
345 dt.message { background-image: url(../images/message.png); }
345 dt.reply { background-image: url(../images/comments.png); }
346 dt.reply { background-image: url(../images/comments.png); }
346 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
347 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
347 dt.attachment { background-image: url(../images/attachment.png); }
348 dt.attachment { background-image: url(../images/attachment.png); }
348 dt.document { background-image: url(../images/document.png); }
349 dt.document { background-image: url(../images/document.png); }
349 dt.project { background-image: url(../images/projects.png); }
350 dt.project { background-image: url(../images/projects.png); }
350 dt.time-entry { background-image: url(../images/time.png); }
351 dt.time-entry { background-image: url(../images/time.png); }
351
352
352 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
353 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
353
354
354 div#roadmap .related-issues { margin-bottom: 1em; }
355 div#roadmap .related-issues { margin-bottom: 1em; }
355 div#roadmap .related-issues td.checkbox { display: none; }
356 div#roadmap .related-issues td.checkbox { display: none; }
356 div#roadmap .wiki h1:first-child { display: none; }
357 div#roadmap .wiki h1:first-child { display: none; }
357 div#roadmap .wiki h1 { font-size: 120%; }
358 div#roadmap .wiki h1 { font-size: 120%; }
358 div#roadmap .wiki h2 { font-size: 110%; }
359 div#roadmap .wiki h2 { font-size: 110%; }
359 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
360 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
360
361
361 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
362 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
362 div#version-summary fieldset { margin-bottom: 1em; }
363 div#version-summary fieldset { margin-bottom: 1em; }
363 div#version-summary fieldset.time-tracking table { width:100%; }
364 div#version-summary fieldset.time-tracking table { width:100%; }
364 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
365 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
365
366
366 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
367 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
367 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
368 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
368 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
369 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
369 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
370 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
370 table#time-report .hours-dec { font-size: 0.9em; }
371 table#time-report .hours-dec { font-size: 0.9em; }
371
372
372 div.wiki-page .contextual a {opacity: 0.4}
373 div.wiki-page .contextual a {opacity: 0.4}
373 div.wiki-page .contextual a:hover {opacity: 1}
374 div.wiki-page .contextual a:hover {opacity: 1}
374
375
375 form .attributes select { width: 60%; }
376 form .attributes select { width: 60%; }
376 input#issue_subject { width: 99%; }
377 input#issue_subject { width: 99%; }
377 select#issue_done_ratio { width: 95px; }
378 select#issue_done_ratio { width: 95px; }
378
379
379 ul.projects { margin: 0; padding-left: 1em; }
380 ul.projects { margin: 0; padding-left: 1em; }
380 ul.projects.root { margin: 0; padding: 0; }
381 ul.projects.root { margin: 0; padding: 0; }
381 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
382 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
382 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
383 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
383 ul.projects li.child { list-style-type:none; margin-top: 1em;}
384 ul.projects li.child { list-style-type:none; margin-top: 1em;}
384 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
385 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
385 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
386 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
386
387
387 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
388 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
388 #tracker_project_ids li { list-style-type:none; }
389 #tracker_project_ids li { list-style-type:none; }
389
390
390 ul.properties {padding:0; font-size: 0.9em; color: #777;}
391 ul.properties {padding:0; font-size: 0.9em; color: #777;}
391 ul.properties li {list-style-type:none;}
392 ul.properties li {list-style-type:none;}
392 ul.properties li span {font-style:italic;}
393 ul.properties li span {font-style:italic;}
393
394
394 .total-hours { font-size: 110%; font-weight: bold; }
395 .total-hours { font-size: 110%; font-weight: bold; }
395 .total-hours span.hours-int { font-size: 120%; }
396 .total-hours span.hours-int { font-size: 120%; }
396
397
397 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
398 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
398 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
399 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
399
400
400 #workflow_copy_form select { width: 200px; }
401 #workflow_copy_form select { width: 200px; }
401
402
402 textarea#custom_field_possible_values {width: 99%}
403 textarea#custom_field_possible_values {width: 99%}
403 input#content_comments {width: 99%}
404 input#content_comments {width: 99%}
404
405
405 .pagination {font-size: 90%}
406 .pagination {font-size: 90%}
406 p.pagination {margin-top:8px;}
407 p.pagination {margin-top:8px;}
407
408
408 /***** Tabular forms ******/
409 /***** Tabular forms ******/
409 .tabular p{
410 .tabular p{
410 margin: 0;
411 margin: 0;
411 padding: 3px 0 3px 0;
412 padding: 3px 0 3px 0;
412 padding-left: 180px; /* width of left column containing the label elements */
413 padding-left: 180px; /* width of left column containing the label elements */
413 min-height: 1.8em;
414 min-height: 1.8em;
414 clear:left;
415 clear:left;
415 }
416 }
416
417
417 html>body .tabular p {overflow:hidden;}
418 html>body .tabular p {overflow:hidden;}
418
419
419 .tabular label{
420 .tabular label{
420 font-weight: bold;
421 font-weight: bold;
421 float: left;
422 float: left;
422 text-align: right;
423 text-align: right;
423 /* width of left column */
424 /* width of left column */
424 margin-left: -180px;
425 margin-left: -180px;
425 /* width of labels. Should be smaller than left column to create some right margin */
426 /* width of labels. Should be smaller than left column to create some right margin */
426 width: 175px;
427 width: 175px;
427 }
428 }
428
429
429 .tabular label.floating{
430 .tabular label.floating{
430 font-weight: normal;
431 font-weight: normal;
431 margin-left: 0px;
432 margin-left: 0px;
432 text-align: left;
433 text-align: left;
433 width: 270px;
434 width: 270px;
434 }
435 }
435
436
436 .tabular label.block{
437 .tabular label.block{
437 font-weight: normal;
438 font-weight: normal;
438 margin-left: 0px !important;
439 margin-left: 0px !important;
439 text-align: left;
440 text-align: left;
440 float: none;
441 float: none;
441 display: block;
442 display: block;
442 width: auto;
443 width: auto;
443 }
444 }
444
445
445 .tabular label.inline{
446 .tabular label.inline{
446 float:none;
447 float:none;
447 margin-left: 5px !important;
448 margin-left: 5px !important;
448 width: auto;
449 width: auto;
449 }
450 }
450
451
451 form em {font-style:normal;font-size:90%;color:#888;}
452 form em {font-style:normal;font-size:90%;color:#888;}
452
453
453 label.no-css {
454 label.no-css {
454 font-weight: inherit;
455 font-weight: inherit;
455 float:none;
456 float:none;
456 text-align:left;
457 text-align:left;
457 margin-left:0px;
458 margin-left:0px;
458 width:auto;
459 width:auto;
459 }
460 }
460 input#time_entry_comments { width: 90%;}
461 input#time_entry_comments { width: 90%;}
461
462
462 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
463 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
463
464
464 .tabular.settings p{ padding-left: 300px; }
465 .tabular.settings p{ padding-left: 300px; }
465 .tabular.settings label{ margin-left: -300px; width: 295px; }
466 .tabular.settings label{ margin-left: -300px; width: 295px; }
466 .tabular.settings textarea { width: 99%; }
467 .tabular.settings textarea { width: 99%; }
467
468
468 .settings.enabled_scm table {width:100%}
469 .settings.enabled_scm table {width:100%}
469 .settings.enabled_scm td.scm_name{ font-weight: bold; }
470 .settings.enabled_scm td.scm_name{ font-weight: bold; }
470
471
471 fieldset.settings label { display: block; }
472 fieldset.settings label { display: block; }
472 fieldset#notified_events .parent { padding-left: 20px; }
473 fieldset#notified_events .parent { padding-left: 20px; }
473
474
474 .required {color: #bb0000;}
475 .required {color: #bb0000;}
475 .summary {font-style: italic;}
476 .summary {font-style: italic;}
476
477
477 #attachments_fields input[type=text] {margin-left: 8px; }
478 #attachments_fields input[type=text] {margin-left: 8px; }
478 #attachments_fields span {display:block; white-space:nowrap;}
479 #attachments_fields span {display:block; white-space:nowrap;}
479 #attachments_fields img {vertical-align: middle;}
480 #attachments_fields img {vertical-align: middle;}
480
481
481 div.attachments { margin-top: 12px; }
482 div.attachments { margin-top: 12px; }
482 div.attachments p { margin:4px 0 2px 0; }
483 div.attachments p { margin:4px 0 2px 0; }
483 div.attachments img { vertical-align: middle; }
484 div.attachments img { vertical-align: middle; }
484 div.attachments span.author { font-size: 0.9em; color: #888; }
485 div.attachments span.author { font-size: 0.9em; color: #888; }
485
486
486 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
487 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
487 .other-formats span + span:before { content: "| "; }
488 .other-formats span + span:before { content: "| "; }
488
489
489 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
490 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
490
491
491 /* Project members tab */
492 /* Project members tab */
492 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
493 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
493 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
494 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
494 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
495 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
495 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
496 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
496 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
497 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
497 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
498 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
498
499
499 #users_for_watcher {height: 200px; overflow:auto;}
500 #users_for_watcher {height: 200px; overflow:auto;}
500 #users_for_watcher label {display: block;}
501 #users_for_watcher label {display: block;}
501
502
502 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
503 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
503
504
504 input#principal_search, input#user_search {width:100%}
505 input#principal_search, input#user_search {width:100%}
505
506
506 * html div#tab-content-members fieldset div { height: 450px; }
507 * html div#tab-content-members fieldset div { height: 450px; }
507
508
508 /***** Flash & error messages ****/
509 /***** Flash & error messages ****/
509 #errorExplanation, div.flash, .nodata, .warning {
510 #errorExplanation, div.flash, .nodata, .warning {
510 padding: 4px 4px 4px 30px;
511 padding: 4px 4px 4px 30px;
511 margin-bottom: 12px;
512 margin-bottom: 12px;
512 font-size: 1.1em;
513 font-size: 1.1em;
513 border: 2px solid;
514 border: 2px solid;
514 }
515 }
515
516
516 div.flash {margin-top: 8px;}
517 div.flash {margin-top: 8px;}
517
518
518 div.flash.error, #errorExplanation {
519 div.flash.error, #errorExplanation {
519 background: url(../images/exclamation.png) 8px 50% no-repeat;
520 background: url(../images/exclamation.png) 8px 50% no-repeat;
520 background-color: #ffe3e3;
521 background-color: #ffe3e3;
521 border-color: #dd0000;
522 border-color: #dd0000;
522 color: #880000;
523 color: #880000;
523 }
524 }
524
525
525 div.flash.notice {
526 div.flash.notice {
526 background: url(../images/true.png) 8px 5px no-repeat;
527 background: url(../images/true.png) 8px 5px no-repeat;
527 background-color: #dfffdf;
528 background-color: #dfffdf;
528 border-color: #9fcf9f;
529 border-color: #9fcf9f;
529 color: #005f00;
530 color: #005f00;
530 }
531 }
531
532
532 div.flash.warning {
533 div.flash.warning {
533 background: url(../images/warning.png) 8px 5px no-repeat;
534 background: url(../images/warning.png) 8px 5px no-repeat;
534 background-color: #FFEBC1;
535 background-color: #FFEBC1;
535 border-color: #FDBF3B;
536 border-color: #FDBF3B;
536 color: #A6750C;
537 color: #A6750C;
537 text-align: left;
538 text-align: left;
538 }
539 }
539
540
540 .nodata, .warning {
541 .nodata, .warning {
541 text-align: center;
542 text-align: center;
542 background-color: #FFEBC1;
543 background-color: #FFEBC1;
543 border-color: #FDBF3B;
544 border-color: #FDBF3B;
544 color: #A6750C;
545 color: #A6750C;
545 }
546 }
546
547
547 span.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
548 span.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
548
549
549 #errorExplanation ul { font-size: 0.9em;}
550 #errorExplanation ul { font-size: 0.9em;}
550 #errorExplanation h2, #errorExplanation p { display: none; }
551 #errorExplanation h2, #errorExplanation p { display: none; }
551
552
552 /***** Ajax indicator ******/
553 /***** Ajax indicator ******/
553 #ajax-indicator {
554 #ajax-indicator {
554 position: absolute; /* fixed not supported by IE */
555 position: absolute; /* fixed not supported by IE */
555 background-color:#eee;
556 background-color:#eee;
556 border: 1px solid #bbb;
557 border: 1px solid #bbb;
557 top:35%;
558 top:35%;
558 left:40%;
559 left:40%;
559 width:20%;
560 width:20%;
560 font-weight:bold;
561 font-weight:bold;
561 text-align:center;
562 text-align:center;
562 padding:0.6em;
563 padding:0.6em;
563 z-index:100;
564 z-index:100;
564 opacity: 0.5;
565 opacity: 0.5;
565 }
566 }
566
567
567 html>body #ajax-indicator { position: fixed; }
568 html>body #ajax-indicator { position: fixed; }
568
569
569 #ajax-indicator span {
570 #ajax-indicator span {
570 background-position: 0% 40%;
571 background-position: 0% 40%;
571 background-repeat: no-repeat;
572 background-repeat: no-repeat;
572 background-image: url(../images/loading.gif);
573 background-image: url(../images/loading.gif);
573 padding-left: 26px;
574 padding-left: 26px;
574 vertical-align: bottom;
575 vertical-align: bottom;
575 }
576 }
576
577
577 /***** Calendar *****/
578 /***** Calendar *****/
578 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
579 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
579 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
580 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
580 table.cal thead th.week-number {width: auto;}
581 table.cal thead th.week-number {width: auto;}
581 table.cal tbody tr {height: 100px;}
582 table.cal tbody tr {height: 100px;}
582 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
583 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
583 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
584 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
584 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
585 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
585 table.cal td.odd p.day-num {color: #bbb;}
586 table.cal td.odd p.day-num {color: #bbb;}
586 table.cal td.today {background:#ffffdd;}
587 table.cal td.today {background:#ffffdd;}
587 table.cal td.today p.day-num {font-weight: bold;}
588 table.cal td.today p.day-num {font-weight: bold;}
588 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
589 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
589 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
590 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
590 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
591 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
591 p.cal.legend span {display:block;}
592 p.cal.legend span {display:block;}
592
593
593 /***** Tooltips ******/
594 /***** Tooltips ******/
594 .tooltip{position:relative;z-index:24;}
595 .tooltip{position:relative;z-index:24;}
595 .tooltip:hover{z-index:25;color:#000;}
596 .tooltip:hover{z-index:25;color:#000;}
596 .tooltip span.tip{display: none; text-align:left;}
597 .tooltip span.tip{display: none; text-align:left;}
597
598
598 div.tooltip:hover span.tip{
599 div.tooltip:hover span.tip{
599 display:block;
600 display:block;
600 position:absolute;
601 position:absolute;
601 top:12px; left:24px; width:270px;
602 top:12px; left:24px; width:270px;
602 border:1px solid #555;
603 border:1px solid #555;
603 background-color:#fff;
604 background-color:#fff;
604 padding: 4px;
605 padding: 4px;
605 font-size: 0.8em;
606 font-size: 0.8em;
606 color:#505050;
607 color:#505050;
607 }
608 }
608
609
609 /***** Progress bar *****/
610 /***** Progress bar *****/
610 table.progress {
611 table.progress {
611 border-collapse: collapse;
612 border-collapse: collapse;
612 border-spacing: 0pt;
613 border-spacing: 0pt;
613 empty-cells: show;
614 empty-cells: show;
614 text-align: center;
615 text-align: center;
615 float:left;
616 float:left;
616 margin: 1px 6px 1px 0px;
617 margin: 1px 6px 1px 0px;
617 }
618 }
618
619
619 table.progress td { height: 1em; }
620 table.progress td { height: 1em; }
620 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
621 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
621 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
622 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
622 table.progress td.todo { background: #eee none repeat scroll 0%; }
623 table.progress td.todo { background: #eee none repeat scroll 0%; }
623 p.pourcent {font-size: 80%;}
624 p.pourcent {font-size: 80%;}
624 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
625 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
625
626
626 #roadmap table.progress td { height: 1.2em; }
627 #roadmap table.progress td { height: 1.2em; }
627 /***** Tabs *****/
628 /***** Tabs *****/
628 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
629 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
629 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
630 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
630 #content .tabs ul li {
631 #content .tabs ul li {
631 float:left;
632 float:left;
632 list-style-type:none;
633 list-style-type:none;
633 white-space:nowrap;
634 white-space:nowrap;
634 margin-right:4px;
635 margin-right:4px;
635 background:#fff;
636 background:#fff;
636 position:relative;
637 position:relative;
637 margin-bottom:-1px;
638 margin-bottom:-1px;
638 }
639 }
639 #content .tabs ul li a{
640 #content .tabs ul li a{
640 display:block;
641 display:block;
641 font-size: 0.9em;
642 font-size: 0.9em;
642 text-decoration:none;
643 text-decoration:none;
643 line-height:1.3em;
644 line-height:1.3em;
644 padding:4px 6px 4px 6px;
645 padding:4px 6px 4px 6px;
645 border: 1px solid #ccc;
646 border: 1px solid #ccc;
646 border-bottom: 1px solid #bbbbbb;
647 border-bottom: 1px solid #bbbbbb;
647 background-color: #f6f6f6;
648 background-color: #f6f6f6;
648 color:#999;
649 color:#999;
649 font-weight:bold;
650 font-weight:bold;
650 border-top-left-radius:3px;
651 border-top-left-radius:3px;
651 border-top-right-radius:3px;
652 border-top-right-radius:3px;
652 }
653 }
653
654
654 #content .tabs ul li a:hover {
655 #content .tabs ul li a:hover {
655 background-color: #ffffdd;
656 background-color: #ffffdd;
656 text-decoration:none;
657 text-decoration:none;
657 }
658 }
658
659
659 #content .tabs ul li a.selected {
660 #content .tabs ul li a.selected {
660 background-color: #fff;
661 background-color: #fff;
661 border: 1px solid #bbbbbb;
662 border: 1px solid #bbbbbb;
662 border-bottom: 1px solid #fff;
663 border-bottom: 1px solid #fff;
663 color:#444;
664 color:#444;
664 }
665 }
665
666
666 #content .tabs ul li a.selected:hover {
667 #content .tabs ul li a.selected:hover {
667 background-color: #fff;
668 background-color: #fff;
668 }
669 }
669
670
670 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
671 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
671
672
672 button.tab-left, button.tab-right {
673 button.tab-left, button.tab-right {
673 font-size: 0.9em;
674 font-size: 0.9em;
674 cursor: pointer;
675 cursor: pointer;
675 height:24px;
676 height:24px;
676 border: 1px solid #ccc;
677 border: 1px solid #ccc;
677 border-bottom: 1px solid #bbbbbb;
678 border-bottom: 1px solid #bbbbbb;
678 position:absolute;
679 position:absolute;
679 padding:4px;
680 padding:4px;
680 width: 20px;
681 width: 20px;
681 bottom: -1px;
682 bottom: -1px;
682 }
683 }
683
684
684 button.tab-left {
685 button.tab-left {
685 right: 20px;
686 right: 20px;
686 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
687 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
687 border-top-left-radius:3px;
688 border-top-left-radius:3px;
688 }
689 }
689
690
690 button.tab-right {
691 button.tab-right {
691 right: 0;
692 right: 0;
692 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
693 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
693 border-top-right-radius:3px;
694 border-top-right-radius:3px;
694 }
695 }
695
696
696 /***** Auto-complete *****/
697 /***** Auto-complete *****/
697 div.autocomplete {
698 div.autocomplete {
698 position:absolute;
699 position:absolute;
699 width:400px;
700 width:400px;
700 margin:0;
701 margin:0;
701 padding:0;
702 padding:0;
702 }
703 }
703 div.autocomplete ul {
704 div.autocomplete ul {
704 list-style-type:none;
705 list-style-type:none;
705 margin:0;
706 margin:0;
706 padding:0;
707 padding:0;
707 }
708 }
708 div.autocomplete ul li {
709 div.autocomplete ul li {
709 list-style-type:none;
710 list-style-type:none;
710 display:block;
711 display:block;
711 margin:-1px 0 0 0;
712 margin:-1px 0 0 0;
712 padding:2px;
713 padding:2px;
713 cursor:pointer;
714 cursor:pointer;
714 font-size: 90%;
715 font-size: 90%;
715 border: 1px solid #ccc;
716 border: 1px solid #ccc;
716 border-left: 1px solid #ccc;
717 border-left: 1px solid #ccc;
717 border-right: 1px solid #ccc;
718 border-right: 1px solid #ccc;
718 background-color:white;
719 background-color:white;
719 }
720 }
720 div.autocomplete ul li.selected { background-color: #ffb;}
721 div.autocomplete ul li.selected { background-color: #ffb;}
721 div.autocomplete ul li span.informal {
722 div.autocomplete ul li span.informal {
722 font-size: 80%;
723 font-size: 80%;
723 color: #aaa;
724 color: #aaa;
724 }
725 }
725
726
726 #parent_issue_candidates ul li {width: 500px;}
727 #parent_issue_candidates ul li {width: 500px;}
727 #related_issue_candidates ul li {width: 500px;}
728 #related_issue_candidates ul li {width: 500px;}
728
729
729 /***** Diff *****/
730 /***** Diff *****/
730 .diff_out { background: #fcc; }
731 .diff_out { background: #fcc; }
731 .diff_out span { background: #faa; }
732 .diff_out span { background: #faa; }
732 .diff_in { background: #cfc; }
733 .diff_in { background: #cfc; }
733 .diff_in span { background: #afa; }
734 .diff_in span { background: #afa; }
734
735
735 .text-diff {
736 .text-diff {
736 padding: 1em;
737 padding: 1em;
737 background-color:#f6f6f6;
738 background-color:#f6f6f6;
738 color:#505050;
739 color:#505050;
739 border: 1px solid #e4e4e4;
740 border: 1px solid #e4e4e4;
740 }
741 }
741
742
742 /***** Wiki *****/
743 /***** Wiki *****/
743 div.wiki table {
744 div.wiki table {
744 border: 1px solid #505050;
745 border: 1px solid #505050;
745 border-collapse: collapse;
746 border-collapse: collapse;
746 margin-bottom: 1em;
747 margin-bottom: 1em;
747 }
748 }
748
749
749 div.wiki table, div.wiki td, div.wiki th {
750 div.wiki table, div.wiki td, div.wiki th {
750 border: 1px solid #bbb;
751 border: 1px solid #bbb;
751 padding: 4px;
752 padding: 4px;
752 }
753 }
753
754
754 div.wiki .external {
755 div.wiki .external {
755 background-position: 0% 60%;
756 background-position: 0% 60%;
756 background-repeat: no-repeat;
757 background-repeat: no-repeat;
757 padding-left: 12px;
758 padding-left: 12px;
758 background-image: url(../images/external.png);
759 background-image: url(../images/external.png);
759 }
760 }
760
761
761 div.wiki a.new {
762 div.wiki a.new {
762 color: #b73535;
763 color: #b73535;
763 }
764 }
764
765
765 div.wiki ul, div.wiki ol {margin-bottom:1em;}
766 div.wiki ul, div.wiki ol {margin-bottom:1em;}
766
767
767 div.wiki pre {
768 div.wiki pre {
768 margin: 1em 1em 1em 1.6em;
769 margin: 1em 1em 1em 1.6em;
769 padding: 2px 2px 2px 0;
770 padding: 2px 2px 2px 0;
770 background-color: #fafafa;
771 background-color: #fafafa;
771 border: 1px solid #dadada;
772 border: 1px solid #dadada;
772 width:auto;
773 width:auto;
773 overflow-x: auto;
774 overflow-x: auto;
774 overflow-y: hidden;
775 overflow-y: hidden;
775 }
776 }
776
777
777 div.wiki ul.toc {
778 div.wiki ul.toc {
778 background-color: #ffffdd;
779 background-color: #ffffdd;
779 border: 1px solid #e4e4e4;
780 border: 1px solid #e4e4e4;
780 padding: 4px;
781 padding: 4px;
781 line-height: 1.2em;
782 line-height: 1.2em;
782 margin-bottom: 12px;
783 margin-bottom: 12px;
783 margin-right: 12px;
784 margin-right: 12px;
784 margin-left: 0;
785 margin-left: 0;
785 display: table
786 display: table
786 }
787 }
787 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
788 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
788
789
789 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
790 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
790 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
791 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
791 div.wiki ul.toc ul { margin: 0; padding: 0; }
792 div.wiki ul.toc ul { margin: 0; padding: 0; }
792 div.wiki ul.toc li { list-style-type:none; margin: 0;}
793 div.wiki ul.toc li { list-style-type:none; margin: 0;}
793 div.wiki ul.toc li li { margin-left: 1.5em; }
794 div.wiki ul.toc li li { margin-left: 1.5em; }
794 div.wiki ul.toc li li li { font-size: 0.8em; }
795 div.wiki ul.toc li li li { font-size: 0.8em; }
795
796
796 div.wiki ul.toc a {
797 div.wiki ul.toc a {
797 font-size: 0.9em;
798 font-size: 0.9em;
798 font-weight: normal;
799 font-weight: normal;
799 text-decoration: none;
800 text-decoration: none;
800 color: #606060;
801 color: #606060;
801 }
802 }
802 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
803 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
803
804
804 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
805 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
805 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
806 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
806 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
807 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
807
808
808 div.wiki img { vertical-align: middle; }
809 div.wiki img { vertical-align: middle; }
809
810
810 /***** My page layout *****/
811 /***** My page layout *****/
811 .block-receiver {
812 .block-receiver {
812 border:1px dashed #c0c0c0;
813 border:1px dashed #c0c0c0;
813 margin-bottom: 20px;
814 margin-bottom: 20px;
814 padding: 15px 0 15px 0;
815 padding: 15px 0 15px 0;
815 }
816 }
816
817
817 .mypage-box {
818 .mypage-box {
818 margin:0 0 20px 0;
819 margin:0 0 20px 0;
819 color:#505050;
820 color:#505050;
820 line-height:1.5em;
821 line-height:1.5em;
821 }
822 }
822
823
823 .handle {
824 .handle {
824 cursor: move;
825 cursor: move;
825 }
826 }
826
827
827 a.close-icon {
828 a.close-icon {
828 display:block;
829 display:block;
829 margin-top:3px;
830 margin-top:3px;
830 overflow:hidden;
831 overflow:hidden;
831 width:12px;
832 width:12px;
832 height:12px;
833 height:12px;
833 background-repeat: no-repeat;
834 background-repeat: no-repeat;
834 cursor:pointer;
835 cursor:pointer;
835 background-image:url('../images/close.png');
836 background-image:url('../images/close.png');
836 }
837 }
837
838
838 a.close-icon:hover {
839 a.close-icon:hover {
839 background-image:url('../images/close_hl.png');
840 background-image:url('../images/close_hl.png');
840 }
841 }
841
842
842 /***** Gantt chart *****/
843 /***** Gantt chart *****/
843 .gantt_hdr {
844 .gantt_hdr {
844 position:absolute;
845 position:absolute;
845 top:0;
846 top:0;
846 height:16px;
847 height:16px;
847 border-top: 1px solid #c0c0c0;
848 border-top: 1px solid #c0c0c0;
848 border-bottom: 1px solid #c0c0c0;
849 border-bottom: 1px solid #c0c0c0;
849 border-right: 1px solid #c0c0c0;
850 border-right: 1px solid #c0c0c0;
850 text-align: center;
851 text-align: center;
851 overflow: hidden;
852 overflow: hidden;
852 }
853 }
853
854
854 .gantt_subjects { font-size: 0.8em; }
855 .gantt_subjects { font-size: 0.8em; }
855 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
856 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
856
857
857 .task {
858 .task {
858 position: absolute;
859 position: absolute;
859 height:8px;
860 height:8px;
860 font-size:0.8em;
861 font-size:0.8em;
861 color:#888;
862 color:#888;
862 padding:0;
863 padding:0;
863 margin:0;
864 margin:0;
864 line-height:16px;
865 line-height:16px;
865 white-space:nowrap;
866 white-space:nowrap;
866 }
867 }
867
868
868 .task.label {width:100%;}
869 .task.label {width:100%;}
869 .task.label.project, .task.label.version { font-weight: bold; }
870 .task.label.project, .task.label.version { font-weight: bold; }
870
871
871 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
872 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
872 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
873 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
873 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
874 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
874
875
875 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
876 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
876 .task_late.parent, .task_done.parent { height: 3px;}
877 .task_late.parent, .task_done.parent { height: 3px;}
877 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
878 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
878 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
879 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
879
880
880 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
881 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
881 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
882 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
882 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
883 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
883 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
884 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
884
885
885 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
886 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
886 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
887 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
887 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
888 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
888 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
889 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
889
890
890 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
891 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
891 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
892 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
892
893
893 /***** Icons *****/
894 /***** Icons *****/
894 .icon {
895 .icon {
895 background-position: 0% 50%;
896 background-position: 0% 50%;
896 background-repeat: no-repeat;
897 background-repeat: no-repeat;
897 padding-left: 20px;
898 padding-left: 20px;
898 padding-top: 2px;
899 padding-top: 2px;
899 padding-bottom: 3px;
900 padding-bottom: 3px;
900 }
901 }
901
902
902 .icon-add { background-image: url(../images/add.png); }
903 .icon-add { background-image: url(../images/add.png); }
903 .icon-edit { background-image: url(../images/edit.png); }
904 .icon-edit { background-image: url(../images/edit.png); }
904 .icon-copy { background-image: url(../images/copy.png); }
905 .icon-copy { background-image: url(../images/copy.png); }
905 .icon-duplicate { background-image: url(../images/duplicate.png); }
906 .icon-duplicate { background-image: url(../images/duplicate.png); }
906 .icon-del { background-image: url(../images/delete.png); }
907 .icon-del { background-image: url(../images/delete.png); }
907 .icon-move { background-image: url(../images/move.png); }
908 .icon-move { background-image: url(../images/move.png); }
908 .icon-save { background-image: url(../images/save.png); }
909 .icon-save { background-image: url(../images/save.png); }
909 .icon-cancel { background-image: url(../images/cancel.png); }
910 .icon-cancel { background-image: url(../images/cancel.png); }
910 .icon-multiple { background-image: url(../images/table_multiple.png); }
911 .icon-multiple { background-image: url(../images/table_multiple.png); }
911 .icon-folder { background-image: url(../images/folder.png); }
912 .icon-folder { background-image: url(../images/folder.png); }
912 .open .icon-folder { background-image: url(../images/folder_open.png); }
913 .open .icon-folder { background-image: url(../images/folder_open.png); }
913 .icon-package { background-image: url(../images/package.png); }
914 .icon-package { background-image: url(../images/package.png); }
914 .icon-user { background-image: url(../images/user.png); }
915 .icon-user { background-image: url(../images/user.png); }
915 .icon-projects { background-image: url(../images/projects.png); }
916 .icon-projects { background-image: url(../images/projects.png); }
916 .icon-help { background-image: url(../images/help.png); }
917 .icon-help { background-image: url(../images/help.png); }
917 .icon-attachment { background-image: url(../images/attachment.png); }
918 .icon-attachment { background-image: url(../images/attachment.png); }
918 .icon-history { background-image: url(../images/history.png); }
919 .icon-history { background-image: url(../images/history.png); }
919 .icon-time { background-image: url(../images/time.png); }
920 .icon-time { background-image: url(../images/time.png); }
920 .icon-time-add { background-image: url(../images/time_add.png); }
921 .icon-time-add { background-image: url(../images/time_add.png); }
921 .icon-stats { background-image: url(../images/stats.png); }
922 .icon-stats { background-image: url(../images/stats.png); }
922 .icon-warning { background-image: url(../images/warning.png); }
923 .icon-warning { background-image: url(../images/warning.png); }
923 .icon-fav { background-image: url(../images/fav.png); }
924 .icon-fav { background-image: url(../images/fav.png); }
924 .icon-fav-off { background-image: url(../images/fav_off.png); }
925 .icon-fav-off { background-image: url(../images/fav_off.png); }
925 .icon-reload { background-image: url(../images/reload.png); }
926 .icon-reload { background-image: url(../images/reload.png); }
926 .icon-lock { background-image: url(../images/locked.png); }
927 .icon-lock { background-image: url(../images/locked.png); }
927 .icon-unlock { background-image: url(../images/unlock.png); }
928 .icon-unlock { background-image: url(../images/unlock.png); }
928 .icon-checked { background-image: url(../images/true.png); }
929 .icon-checked { background-image: url(../images/true.png); }
929 .icon-details { background-image: url(../images/zoom_in.png); }
930 .icon-details { background-image: url(../images/zoom_in.png); }
930 .icon-report { background-image: url(../images/report.png); }
931 .icon-report { background-image: url(../images/report.png); }
931 .icon-comment { background-image: url(../images/comment.png); }
932 .icon-comment { background-image: url(../images/comment.png); }
932 .icon-summary { background-image: url(../images/lightning.png); }
933 .icon-summary { background-image: url(../images/lightning.png); }
933 .icon-server-authentication { background-image: url(../images/server_key.png); }
934 .icon-server-authentication { background-image: url(../images/server_key.png); }
934 .icon-issue { background-image: url(../images/ticket.png); }
935 .icon-issue { background-image: url(../images/ticket.png); }
935 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
936 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
936 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
937 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
937 .icon-passwd { background-image: url(../images/textfield_key.png); }
938 .icon-passwd { background-image: url(../images/textfield_key.png); }
938
939
939 .icon-file { background-image: url(../images/files/default.png); }
940 .icon-file { background-image: url(../images/files/default.png); }
940 .icon-file.text-plain { background-image: url(../images/files/text.png); }
941 .icon-file.text-plain { background-image: url(../images/files/text.png); }
941 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
942 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
942 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
943 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
943 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
944 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
944 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
945 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
945 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
946 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
946 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
947 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
947 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
948 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
948 .icon-file.text-css { background-image: url(../images/files/css.png); }
949 .icon-file.text-css { background-image: url(../images/files/css.png); }
949 .icon-file.text-html { background-image: url(../images/files/html.png); }
950 .icon-file.text-html { background-image: url(../images/files/html.png); }
950 .icon-file.image-gif { background-image: url(../images/files/image.png); }
951 .icon-file.image-gif { background-image: url(../images/files/image.png); }
951 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
952 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
952 .icon-file.image-png { background-image: url(../images/files/image.png); }
953 .icon-file.image-png { background-image: url(../images/files/image.png); }
953 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
954 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
954 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
955 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
955 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
956 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
956 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
957 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
957
958
958 img.gravatar {
959 img.gravatar {
959 padding: 2px;
960 padding: 2px;
960 border: solid 1px #d5d5d5;
961 border: solid 1px #d5d5d5;
961 background: #fff;
962 background: #fff;
962 vertical-align: middle;
963 vertical-align: middle;
963 }
964 }
964
965
965 div.issue img.gravatar {
966 div.issue img.gravatar {
966 float: left;
967 float: left;
967 margin: 0 6px 0 0;
968 margin: 0 6px 0 0;
968 padding: 5px;
969 padding: 5px;
969 }
970 }
970
971
971 div.issue table img.gravatar {
972 div.issue table img.gravatar {
972 height: 14px;
973 height: 14px;
973 width: 14px;
974 width: 14px;
974 padding: 2px;
975 padding: 2px;
975 float: left;
976 float: left;
976 margin: 0 0.5em 0 0;
977 margin: 0 0.5em 0 0;
977 }
978 }
978
979
979 h2 img.gravatar {
980 h2 img.gravatar {
980 margin: -2px 4px -4px 0;
981 margin: -2px 4px -4px 0;
981 }
982 }
982
983
983 h3 img.gravatar {
984 h3 img.gravatar {
984 margin: -4px 4px -4px 0;
985 margin: -4px 4px -4px 0;
985 }
986 }
986
987
987 h4 img.gravatar {
988 h4 img.gravatar {
988 margin: -6px 4px -4px 0;
989 margin: -6px 4px -4px 0;
989 }
990 }
990
991
991 td.username img.gravatar {
992 td.username img.gravatar {
992 margin: 0 0.5em 0 0;
993 margin: 0 0.5em 0 0;
993 vertical-align: top;
994 vertical-align: top;
994 }
995 }
995
996
996 #activity dt img.gravatar {
997 #activity dt img.gravatar {
997 float: left;
998 float: left;
998 margin: 0 1em 1em 0;
999 margin: 0 1em 1em 0;
999 }
1000 }
1000
1001
1001 /* Used on 12px Gravatar img tags without the icon background */
1002 /* Used on 12px Gravatar img tags without the icon background */
1002 .icon-gravatar {
1003 .icon-gravatar {
1003 float: left;
1004 float: left;
1004 margin-right: 4px;
1005 margin-right: 4px;
1005 }
1006 }
1006
1007
1007 #activity dt,
1008 #activity dt,
1008 .journal {
1009 .journal {
1009 clear: left;
1010 clear: left;
1010 }
1011 }
1011
1012
1012 .journal-link {
1013 .journal-link {
1013 float: right;
1014 float: right;
1014 }
1015 }
1015
1016
1016 h2 img { vertical-align:middle; }
1017 h2 img { vertical-align:middle; }
1017
1018
1018 .hascontextmenu { cursor: context-menu; }
1019 .hascontextmenu { cursor: context-menu; }
1019
1020
1020 /***** Media print specific styles *****/
1021 /***** Media print specific styles *****/
1021 @media print {
1022 @media print {
1022 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1023 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1023 #main { background: #fff; }
1024 #main { background: #fff; }
1024 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1025 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1025 #wiki_add_attachment { display:none; }
1026 #wiki_add_attachment { display:none; }
1026 .hide-when-print { display: none; }
1027 .hide-when-print { display: none; }
1027 .autoscroll {overflow-x: visible;}
1028 .autoscroll {overflow-x: visible;}
1028 table.list {margin-top:0.5em;}
1029 table.list {margin-top:0.5em;}
1029 table.list th, table.list td {border: 1px solid #aaa;}
1030 table.list th, table.list td {border: 1px solid #aaa;}
1030 }
1031 }
1031
1032
1032 /* Accessibility specific styles */
1033 /* Accessibility specific styles */
1033 .hidden-for-sighted {
1034 .hidden-for-sighted {
1034 position:absolute;
1035 position:absolute;
1035 left:-10000px;
1036 left:-10000px;
1036 top:auto;
1037 top:auto;
1037 width:1px;
1038 width:1px;
1038 height:1px;
1039 height:1px;
1039 overflow:hidden;
1040 overflow:hidden;
1040 }
1041 }
@@ -1,5 +1,5
1 class Repository < ActiveRecord::Base
1 class Repository < ActiveRecord::Base
2 generator_for :type => 'Subversion'
2 generator_for :type => 'Subversion'
3 generator_for :url, :start => 'file:///test/svn'
3 generator_for :url, :start => 'file:///test/svn'
4
4 generator_for :identifier, :start => 'repo1'
5 end
5 end
@@ -1,17 +1,19
1 ---
1 ---
2 repositories_001:
2 repositories_001:
3 project_id: 1
3 project_id: 1
4 url: file:///<%= Rails.root %>/tmp/test/subversion_repository
4 url: file:///<%= Rails.root %>/tmp/test/subversion_repository
5 id: 10
5 id: 10
6 root_url: file:///<%= Rails.root %>/tmp/test/subversion_repository
6 root_url: file:///<%= Rails.root %>/tmp/test/subversion_repository
7 password: ""
7 password: ""
8 login: ""
8 login: ""
9 type: Subversion
9 type: Subversion
10 is_default: true
10 repositories_002:
11 repositories_002:
11 project_id: 2
12 project_id: 2
12 url: svn://localhost/test
13 url: svn://localhost/test
13 id: 11
14 id: 11
14 root_url: svn://localhost
15 root_url: svn://localhost
15 password: ""
16 password: ""
16 login: ""
17 login: ""
17 type: Subversion
18 type: Subversion
19 is_default: true
@@ -1,194 +1,203
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'repositories_controller'
19 require 'repositories_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class RepositoriesController; def rescue_action(e) raise e end; end
22 class RepositoriesController; def rescue_action(e) raise e end; end
23
23
24 class RepositoriesControllerTest < ActionController::TestCase
24 class RepositoriesControllerTest < ActionController::TestCase
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
26 :repositories, :issues, :issue_statuses, :changesets, :changes,
26 :repositories, :issues, :issue_statuses, :changesets, :changes,
27 :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
27 :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
28
28
29 def setup
29 def setup
30 @controller = RepositoriesController.new
30 @controller = RepositoriesController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 User.current = nil
33 User.current = nil
34 end
34 end
35
35
36 def test_new
36 def test_new
37 @request.session[:user_id] = 1
37 @request.session[:user_id] = 1
38 get :new, :project_id => 'subproject1'
38 get :new, :project_id => 'subproject1'
39 assert_response :success
39 assert_response :success
40 assert_template 'new'
40 assert_template 'new'
41 assert_kind_of Repository::Subversion, assigns(:repository)
41 assert_kind_of Repository::Subversion, assigns(:repository)
42 assert assigns(:repository).new_record?
42 assert assigns(:repository).new_record?
43 assert_tag 'input', :attributes => {:name => 'repository[url]'}
43 assert_tag 'input', :attributes => {:name => 'repository[url]'}
44 end
44 end
45
45
46 # TODO: remove it when multiple SCM support is added
47 def test_new_with_existing_repository
48 @request.session[:user_id] = 1
49 get :new, :project_id => 'ecookbook'
50 assert_response 302
51 end
52
53 def test_create
46 def test_create
54 @request.session[:user_id] = 1
47 @request.session[:user_id] = 1
55 assert_difference 'Repository.count' do
48 assert_difference 'Repository.count' do
56 post :create, :project_id => 'subproject1',
49 post :create, :project_id => 'subproject1',
57 :repository_scm => 'Subversion',
50 :repository_scm => 'Subversion',
58 :repository => {:url => 'file:///test'}
51 :repository => {:url => 'file:///test', :is_default => '1', :identifier => ''}
59 end
52 end
60 assert_response 302
53 assert_response 302
61 repository = Repository.first(:order => 'id DESC')
54 repository = Repository.first(:order => 'id DESC')
62 assert_kind_of Repository::Subversion, repository
55 assert_kind_of Repository::Subversion, repository
63 assert_equal 'file:///test', repository.url
56 assert_equal 'file:///test', repository.url
64 end
57 end
65
58
66 def test_create_with_failure
59 def test_create_with_failure
67 @request.session[:user_id] = 1
60 @request.session[:user_id] = 1
68 assert_no_difference 'Repository.count' do
61 assert_no_difference 'Repository.count' do
69 post :create, :project_id => 'subproject1',
62 post :create, :project_id => 'subproject1',
70 :repository_scm => 'Subversion',
63 :repository_scm => 'Subversion',
71 :repository => {:url => 'invalid'}
64 :repository => {:url => 'invalid'}
72 end
65 end
73 assert_response :success
66 assert_response :success
74 assert_template 'new'
67 assert_template 'new'
75 assert_kind_of Repository::Subversion, assigns(:repository)
68 assert_kind_of Repository::Subversion, assigns(:repository)
76 assert assigns(:repository).new_record?
69 assert assigns(:repository).new_record?
77 end
70 end
78
71
79 def test_edit
72 def test_edit
80 @request.session[:user_id] = 1
73 @request.session[:user_id] = 1
81 get :edit, :id => 11
74 get :edit, :id => 11
82 assert_response :success
75 assert_response :success
83 assert_template 'edit'
76 assert_template 'edit'
84 assert_equal Repository.find(11), assigns(:repository)
77 assert_equal Repository.find(11), assigns(:repository)
85 assert_tag 'input', :attributes => {:name => 'repository[url]', :value => 'svn://localhost/test'}
78 assert_tag 'input', :attributes => {:name => 'repository[url]', :value => 'svn://localhost/test'}
86 end
79 end
87
80
88 def test_update
81 def test_update
89 @request.session[:user_id] = 1
82 @request.session[:user_id] = 1
90 put :update, :id => 11, :repository => {:password => 'test_update'}
83 put :update, :id => 11, :repository => {:password => 'test_update'}
91 assert_response 302
84 assert_response 302
92 assert_equal 'test_update', Repository.find(11).password
85 assert_equal 'test_update', Repository.find(11).password
93 end
86 end
94
87
95 def test_update_with_failure
88 def test_update_with_failure
96 @request.session[:user_id] = 1
89 @request.session[:user_id] = 1
97 put :update, :id => 11, :repository => {:password => 'x'*260}
90 put :update, :id => 11, :repository => {:password => 'x'*260}
98 assert_response :success
91 assert_response :success
99 assert_template 'edit'
92 assert_template 'edit'
100 assert_equal Repository.find(11), assigns(:repository)
93 assert_equal Repository.find(11), assigns(:repository)
101 end
94 end
102
95
103 def test_destroy
96 def test_destroy
104 @request.session[:user_id] = 1
97 @request.session[:user_id] = 1
105 assert_difference 'Repository.count', -1 do
98 assert_difference 'Repository.count', -1 do
106 delete :destroy, :id => 11
99 delete :destroy, :id => 11
107 end
100 end
108 assert_response 302
101 assert_response 302
109 assert_nil Repository.find_by_id(11)
102 assert_nil Repository.find_by_id(11)
110 end
103 end
111
104
112 def test_revisions
105 def test_revisions
113 get :revisions, :id => 1
106 get :revisions, :id => 1
114 assert_response :success
107 assert_response :success
115 assert_template 'revisions'
108 assert_template 'revisions'
109 assert_equal Repository.find(10), assigns(:repository)
110 assert_not_nil assigns(:changesets)
111 end
112
113 def test_revisions_for_other_repository
114 repository = Repository::Subversion.create!(:project_id => 1, :identifier => 'foo', :url => 'file:///foo')
115
116 get :revisions, :id => 1, :repository_id => 'foo'
117 assert_response :success
118 assert_template 'revisions'
119 assert_equal repository, assigns(:repository)
116 assert_not_nil assigns(:changesets)
120 assert_not_nil assigns(:changesets)
117 end
121 end
118
122
123 def test_revisions_for_invalid_repository
124 get :revisions, :id => 1, :repository_id => 'foo'
125 assert_response 404
126 end
127
119 def test_revision
128 def test_revision
120 get :revision, :id => 1, :rev => 1
129 get :revision, :id => 1, :rev => 1
121 assert_response :success
130 assert_response :success
122 assert_not_nil assigns(:changeset)
131 assert_not_nil assigns(:changeset)
123 assert_equal "1", assigns(:changeset).revision
132 assert_equal "1", assigns(:changeset).revision
124 end
133 end
125
134
126 def test_revision_with_before_nil_and_afer_normal
135 def test_revision_with_before_nil_and_afer_normal
127 get :revision, {:id => 1, :rev => 1}
136 get :revision, {:id => 1, :rev => 1}
128 assert_response :success
137 assert_response :success
129 assert_template 'revision'
138 assert_template 'revision'
130 assert_no_tag :tag => "div", :attributes => { :class => "contextual" },
139 assert_no_tag :tag => "div", :attributes => { :class => "contextual" },
131 :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/0'}
140 :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/0'}
132 }
141 }
133 assert_tag :tag => "div", :attributes => { :class => "contextual" },
142 assert_tag :tag => "div", :attributes => { :class => "contextual" },
134 :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/2'}
143 :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/2'}
135 }
144 }
136 end
145 end
137
146
138 def test_graph_commits_per_month
147 def test_graph_commits_per_month
139 get :graph, :id => 1, :graph => 'commits_per_month'
148 get :graph, :id => 1, :graph => 'commits_per_month'
140 assert_response :success
149 assert_response :success
141 assert_equal 'image/svg+xml', @response.content_type
150 assert_equal 'image/svg+xml', @response.content_type
142 end
151 end
143
152
144 def test_graph_commits_per_author
153 def test_graph_commits_per_author
145 get :graph, :id => 1, :graph => 'commits_per_author'
154 get :graph, :id => 1, :graph => 'commits_per_author'
146 assert_response :success
155 assert_response :success
147 assert_equal 'image/svg+xml', @response.content_type
156 assert_equal 'image/svg+xml', @response.content_type
148 end
157 end
149
158
150 def test_get_committers
159 def test_get_committers
151 @request.session[:user_id] = 2
160 @request.session[:user_id] = 2
152 # add a commit with an unknown user
161 # add a commit with an unknown user
153 Changeset.create!(
162 Changeset.create!(
154 :repository => Project.find(1).repository,
163 :repository => Project.find(1).repository,
155 :committer => 'foo',
164 :committer => 'foo',
156 :committed_on => Time.now,
165 :committed_on => Time.now,
157 :revision => 100,
166 :revision => 100,
158 :comments => 'Committed by foo.'
167 :comments => 'Committed by foo.'
159 )
168 )
160
169
161 get :committers, :id => 10
170 get :committers, :id => 10
162 assert_response :success
171 assert_response :success
163 assert_template 'committers'
172 assert_template 'committers'
164
173
165 assert_tag :td, :content => 'dlopper',
174 assert_tag :td, :content => 'dlopper',
166 :sibling => { :tag => 'td',
175 :sibling => { :tag => 'td',
167 :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} },
176 :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} },
168 :child => { :tag => 'option', :content => 'Dave Lopper',
177 :child => { :tag => 'option', :content => 'Dave Lopper',
169 :attributes => { :value => '3', :selected => 'selected' }}}}
178 :attributes => { :value => '3', :selected => 'selected' }}}}
170 assert_tag :td, :content => 'foo',
179 assert_tag :td, :content => 'foo',
171 :sibling => { :tag => 'td',
180 :sibling => { :tag => 'td',
172 :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} }}}
181 :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} }}}
173 assert_no_tag :td, :content => 'foo',
182 assert_no_tag :td, :content => 'foo',
174 :sibling => { :tag => 'td',
183 :sibling => { :tag => 'td',
175 :descendant => { :tag => 'option', :attributes => { :selected => 'selected' }}}
184 :descendant => { :tag => 'option', :attributes => { :selected => 'selected' }}}
176 end
185 end
177
186
178 def test_post_committers
187 def test_post_committers
179 @request.session[:user_id] = 2
188 @request.session[:user_id] = 2
180 # add a commit with an unknown user
189 # add a commit with an unknown user
181 c = Changeset.create!(
190 c = Changeset.create!(
182 :repository => Project.find(1).repository,
191 :repository => Project.find(1).repository,
183 :committer => 'foo',
192 :committer => 'foo',
184 :committed_on => Time.now,
193 :committed_on => Time.now,
185 :revision => 100,
194 :revision => 100,
186 :comments => 'Committed by foo.'
195 :comments => 'Committed by foo.'
187 )
196 )
188 assert_no_difference "Changeset.count(:conditions => 'user_id = 3')" do
197 assert_no_difference "Changeset.count(:conditions => 'user_id = 3')" do
189 post :committers, :id => 10, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']}
198 post :committers, :id => 10, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']}
190 assert_response 302
199 assert_response 302
191 assert_equal User.find(2), c.reload.user
200 assert_equal User.find(2), c.reload.user
192 end
201 end
193 end
202 end
194 end
203 end
@@ -1,203 +1,346
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class RoutingRepositoriesTest < ActionController::IntegrationTest
20 class RoutingRepositoriesTest < ActionController::IntegrationTest
21 def setup
21 def setup
22 @path_hash = repository_path_hash(%w[path to file.c])
22 @path_hash = repository_path_hash(%w[path to file.c])
23 assert_equal "path/to/file.c", @path_hash[:path]
23 assert_equal "path/to/file.c", @path_hash[:path]
24 assert_equal %w[path to file.c], @path_hash[:param]
24 assert_equal %w[path to file.c], @path_hash[:param]
25 end
25 end
26
26
27 def test_repositories_resources
27 def test_repositories_resources
28 assert_routing(
28 assert_routing(
29 { :method => 'get',
29 { :method => 'get',
30 :path => "/projects/redmine/repositories/new" },
30 :path => "/projects/redmine/repositories/new" },
31 { :controller => 'repositories', :action => 'new', :project_id => 'redmine' }
31 { :controller => 'repositories', :action => 'new', :project_id => 'redmine' }
32 )
32 )
33 assert_routing(
33 assert_routing(
34 { :method => 'post',
34 { :method => 'post',
35 :path => "/projects/redmine/repositories" },
35 :path => "/projects/redmine/repositories" },
36 { :controller => 'repositories', :action => 'create', :project_id => 'redmine' }
36 { :controller => 'repositories', :action => 'create', :project_id => 'redmine' }
37 )
37 )
38 assert_routing(
38 assert_routing(
39 { :method => 'get',
39 { :method => 'get',
40 :path => "/repositories/1/edit" },
40 :path => "/repositories/1/edit" },
41 { :controller => 'repositories', :action => 'edit', :id => '1' }
41 { :controller => 'repositories', :action => 'edit', :id => '1' }
42 )
42 )
43 assert_routing(
43 assert_routing(
44 { :method => 'put',
44 { :method => 'put',
45 :path => "/repositories/1" },
45 :path => "/repositories/1" },
46 { :controller => 'repositories', :action => 'update', :id => '1' }
46 { :controller => 'repositories', :action => 'update', :id => '1' }
47 )
47 )
48 assert_routing(
48 assert_routing(
49 { :method => 'delete',
49 { :method => 'delete',
50 :path => "/repositories/1" },
50 :path => "/repositories/1" },
51 { :controller => 'repositories', :action => 'destroy', :id => '1' }
51 { :controller => 'repositories', :action => 'destroy', :id => '1' }
52 )
52 )
53 ["get", "post"].each do |method|
53 ["get", "post"].each do |method|
54 assert_routing(
54 assert_routing(
55 { :method => method,
55 { :method => method,
56 :path => "/repositories/1/committers" },
56 :path => "/repositories/1/committers" },
57 { :controller => 'repositories', :action => 'committers', :id => '1' }
57 { :controller => 'repositories', :action => 'committers', :id => '1' }
58 )
58 )
59 end
59 end
60 end
60 end
61
61
62 def test_repositories
62 def test_repositories
63 assert_routing(
63 assert_routing(
64 { :method => 'get',
64 { :method => 'get',
65 :path => "/projects/redmine/repository" },
65 :path => "/projects/redmine/repository" },
66 { :controller => 'repositories', :action => 'show', :id => 'redmine' }
66 { :controller => 'repositories', :action => 'show', :id => 'redmine' }
67 )
67 )
68 assert_routing(
68 assert_routing(
69 { :method => 'get',
69 { :method => 'get',
70 :path => "/projects/redmine/repository/statistics" },
70 :path => "/projects/redmine/repository/statistics" },
71 { :controller => 'repositories', :action => 'stats', :id => 'redmine' }
71 { :controller => 'repositories', :action => 'stats', :id => 'redmine' }
72 )
72 )
73 assert_routing(
74 { :method => 'get',
75 :path => "/projects/redmine/repository/graph" },
76 { :controller => 'repositories', :action => 'graph', :id => 'redmine' }
77 )
78 end
79
80 def test_repositories_with_repository_id
81 assert_routing(
82 { :method => 'get',
83 :path => "/projects/redmine/repository/foo" },
84 { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo' }
85 )
86 assert_routing(
87 { :method => 'get',
88 :path => "/projects/redmine/repository/foo/statistics" },
89 { :controller => 'repositories', :action => 'stats', :id => 'redmine', :repository_id => 'foo' }
90 )
91 assert_routing(
92 { :method => 'get',
93 :path => "/projects/redmine/repository/foo/graph" },
94 { :controller => 'repositories', :action => 'graph', :id => 'redmine', :repository_id => 'foo' }
95 )
73 end
96 end
74
97
75 def test_repositories_revisions
98 def test_repositories_revisions
76 empty_path_param = []
99 empty_path_param = []
77 assert_routing(
100 assert_routing(
78 { :method => 'get',
101 { :method => 'get',
79 :path => "/projects/redmine/repository/revisions" },
102 :path => "/projects/redmine/repository/revisions" },
80 { :controller => 'repositories', :action => 'revisions', :id => 'redmine' }
103 { :controller => 'repositories', :action => 'revisions', :id => 'redmine' }
81 )
104 )
82 assert_routing(
105 assert_routing(
83 { :method => 'get',
106 { :method => 'get',
84 :path => "/projects/redmine/repository/revisions.atom" },
107 :path => "/projects/redmine/repository/revisions.atom" },
85 { :controller => 'repositories', :action => 'revisions', :id => 'redmine',
108 { :controller => 'repositories', :action => 'revisions', :id => 'redmine',
86 :format => 'atom' }
109 :format => 'atom' }
87 )
110 )
88 assert_routing(
111 assert_routing(
89 { :method => 'get',
112 { :method => 'get',
90 :path => "/projects/redmine/repository/revisions/2457" },
113 :path => "/projects/redmine/repository/revisions/2457" },
91 { :controller => 'repositories', :action => 'revision', :id => 'redmine',
114 { :controller => 'repositories', :action => 'revision', :id => 'redmine',
92 :rev => '2457' }
115 :rev => '2457' }
93 )
116 )
94 assert_routing(
117 assert_routing(
95 { :method => 'get',
118 { :method => 'get',
96 :path => "/projects/redmine/repository/revisions/2457/show" },
119 :path => "/projects/redmine/repository/revisions/2457/show" },
97 { :controller => 'repositories', :action => 'show', :id => 'redmine',
120 { :controller => 'repositories', :action => 'show', :id => 'redmine',
98 :path => empty_path_param, :rev => '2457' }
121 :path => empty_path_param, :rev => '2457' }
99 )
122 )
100 assert_routing(
123 assert_routing(
101 { :method => 'get',
124 { :method => 'get',
102 :path => "/projects/redmine/repository/revisions/2457/show/#{@path_hash[:path]}" },
125 :path => "/projects/redmine/repository/revisions/2457/show/#{@path_hash[:path]}" },
103 { :controller => 'repositories', :action => 'show', :id => 'redmine',
126 { :controller => 'repositories', :action => 'show', :id => 'redmine',
104 :path => @path_hash[:param] , :rev => '2457'}
127 :path => @path_hash[:param] , :rev => '2457'}
105 )
128 )
106 assert_routing(
129 assert_routing(
107 { :method => 'get',
130 { :method => 'get',
108 :path => "/projects/redmine/repository/revisions/2457/changes" },
131 :path => "/projects/redmine/repository/revisions/2457/changes" },
109 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
132 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
110 :path => empty_path_param, :rev => '2457' }
133 :path => empty_path_param, :rev => '2457' }
111 )
134 )
112 assert_routing(
135 assert_routing(
113 { :method => 'get',
136 { :method => 'get',
114 :path => "/projects/redmine/repository/revisions/2457/changes/#{@path_hash[:path]}" },
137 :path => "/projects/redmine/repository/revisions/2457/changes/#{@path_hash[:path]}" },
115 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
138 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
116 :path => @path_hash[:param] , :rev => '2457'}
139 :path => @path_hash[:param] , :rev => '2457'}
117 )
140 )
118 assert_routing(
141 assert_routing(
119 { :method => 'get',
142 { :method => 'get',
120 :path => "/projects/redmine/repository/revisions/2457/diff" },
143 :path => "/projects/redmine/repository/revisions/2457/diff" },
121 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
144 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
122 :rev => '2457' }
145 :rev => '2457' }
123 )
146 )
124 assert_routing(
147 assert_routing(
125 { :method => 'get',
148 { :method => 'get',
126 :path => "/projects/redmine/repository/revisions/2457/diff.diff" },
149 :path => "/projects/redmine/repository/revisions/2457/diff.diff" },
127 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
150 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
128 :rev => '2457', :format => 'diff' }
151 :rev => '2457', :format => 'diff' }
129 )
152 )
130 assert_routing(
153 assert_routing(
131 { :method => 'get',
154 { :method => 'get',
132 :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" },
155 :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" },
133 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
156 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
134 :path => @path_hash[:param], :rev => '2' }
157 :path => @path_hash[:param], :rev => '2' }
135 )
158 )
136 assert_routing(
159 assert_routing(
137 { :method => 'get',
160 { :method => 'get',
138 :path => "/projects/redmine/repository/revisions/2/entry/#{@path_hash[:path]}" },
161 :path => "/projects/redmine/repository/revisions/2/entry/#{@path_hash[:path]}" },
139 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
162 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
140 :path => @path_hash[:param], :rev => '2' }
163 :path => @path_hash[:param], :rev => '2' }
141 )
164 )
142 assert_routing(
165 assert_routing(
143 { :method => 'get',
166 { :method => 'get',
144 :path => "/projects/redmine/repository/revisions/2/raw/#{@path_hash[:path]}" },
167 :path => "/projects/redmine/repository/revisions/2/raw/#{@path_hash[:path]}" },
145 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
168 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
146 :path => @path_hash[:param], :rev => '2', :format => 'raw' }
169 :path => @path_hash[:param], :rev => '2', :format => 'raw' }
147 )
170 )
148 assert_routing(
171 assert_routing(
149 { :method => 'get',
172 { :method => 'get',
150 :path => "/projects/redmine/repository/revisions/2/annotate/#{@path_hash[:path]}" },
173 :path => "/projects/redmine/repository/revisions/2/annotate/#{@path_hash[:path]}" },
151 { :controller => 'repositories', :action => 'annotate', :id => 'redmine',
174 { :controller => 'repositories', :action => 'annotate', :id => 'redmine',
152 :path => @path_hash[:param], :rev => '2' }
175 :path => @path_hash[:param], :rev => '2' }
153 )
176 )
154 end
177 end
155
178
179 def test_repositories_revisions_with_repository_id
180 empty_path_param = []
181 assert_routing(
182 { :method => 'get',
183 :path => "/projects/redmine/repository/foo/revisions" },
184 { :controller => 'repositories', :action => 'revisions', :id => 'redmine', :repository_id => 'foo' }
185 )
186 assert_routing(
187 { :method => 'get',
188 :path => "/projects/redmine/repository/foo/revisions.atom" },
189 { :controller => 'repositories', :action => 'revisions', :id => 'redmine', :repository_id => 'foo',
190 :format => 'atom' }
191 )
192 assert_routing(
193 { :method => 'get',
194 :path => "/projects/redmine/repository/foo/revisions/2457" },
195 { :controller => 'repositories', :action => 'revision', :id => 'redmine', :repository_id => 'foo',
196 :rev => '2457' }
197 )
198 assert_routing(
199 { :method => 'get',
200 :path => "/projects/redmine/repository/foo/revisions/2457/show" },
201 { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo',
202 :path => empty_path_param, :rev => '2457' }
203 )
204 assert_routing(
205 { :method => 'get',
206 :path => "/projects/redmine/repository/foo/revisions/2457/show/#{@path_hash[:path]}" },
207 { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo',
208 :path => @path_hash[:param] , :rev => '2457'}
209 )
210 assert_routing(
211 { :method => 'get',
212 :path => "/projects/redmine/repository/foo/revisions/2457/changes" },
213 { :controller => 'repositories', :action => 'changes', :id => 'redmine', :repository_id => 'foo',
214 :path => empty_path_param, :rev => '2457' }
215 )
216 assert_routing(
217 { :method => 'get',
218 :path => "/projects/redmine/repository/foo/revisions/2457/changes/#{@path_hash[:path]}" },
219 { :controller => 'repositories', :action => 'changes', :id => 'redmine', :repository_id => 'foo',
220 :path => @path_hash[:param] , :rev => '2457'}
221 )
222 assert_routing(
223 { :method => 'get',
224 :path => "/projects/redmine/repository/foo/revisions/2457/diff" },
225 { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo',
226 :rev => '2457' }
227 )
228 assert_routing(
229 { :method => 'get',
230 :path => "/projects/redmine/repository/foo/revisions/2457/diff.diff" },
231 { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo',
232 :rev => '2457', :format => 'diff' }
233 )
234 assert_routing(
235 { :method => 'get',
236 :path => "/projects/redmine/repository/foo/revisions/2/diff/#{@path_hash[:path]}" },
237 { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo',
238 :path => @path_hash[:param], :rev => '2' }
239 )
240 assert_routing(
241 { :method => 'get',
242 :path => "/projects/redmine/repository/foo/revisions/2/entry/#{@path_hash[:path]}" },
243 { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo',
244 :path => @path_hash[:param], :rev => '2' }
245 )
246 assert_routing(
247 { :method => 'get',
248 :path => "/projects/redmine/repository/foo/revisions/2/raw/#{@path_hash[:path]}" },
249 { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo',
250 :path => @path_hash[:param], :rev => '2', :format => 'raw' }
251 )
252 assert_routing(
253 { :method => 'get',
254 :path => "/projects/redmine/repository/foo/revisions/2/annotate/#{@path_hash[:path]}" },
255 { :controller => 'repositories', :action => 'annotate', :id => 'redmine', :repository_id => 'foo',
256 :path => @path_hash[:param], :rev => '2' }
257 )
258 end
259
156 def test_repositories_non_revisions_path
260 def test_repositories_non_revisions_path
157 assert_routing(
261 assert_routing(
158 { :method => 'get',
262 { :method => 'get',
159 :path => "/projects/redmine/repository/diff/#{@path_hash[:path]}" },
263 :path => "/projects/redmine/repository/diff/#{@path_hash[:path]}" },
160 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
264 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
161 :path => @path_hash[:param] }
265 :path => @path_hash[:param] }
162 )
266 )
163 assert_routing(
267 assert_routing(
164 { :method => 'get',
268 { :method => 'get',
165 :path => "/projects/redmine/repository/browse/#{@path_hash[:path]}" },
269 :path => "/projects/redmine/repository/browse/#{@path_hash[:path]}" },
166 { :controller => 'repositories', :action => 'browse', :id => 'redmine',
270 { :controller => 'repositories', :action => 'browse', :id => 'redmine',
167 :path => @path_hash[:param] }
271 :path => @path_hash[:param] }
168 )
272 )
169 assert_routing(
273 assert_routing(
170 { :method => 'get',
274 { :method => 'get',
171 :path => "/projects/redmine/repository/entry/#{@path_hash[:path]}" },
275 :path => "/projects/redmine/repository/entry/#{@path_hash[:path]}" },
172 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
276 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
173 :path => @path_hash[:param] }
277 :path => @path_hash[:param] }
174 )
278 )
175 assert_routing(
279 assert_routing(
176 { :method => 'get',
280 { :method => 'get',
177 :path => "/projects/redmine/repository/raw/#{@path_hash[:path]}" },
281 :path => "/projects/redmine/repository/raw/#{@path_hash[:path]}" },
178 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
282 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
179 :path => @path_hash[:param], :format => 'raw' }
283 :path => @path_hash[:param], :format => 'raw' }
180 )
284 )
181 assert_routing(
285 assert_routing(
182 { :method => 'get',
286 { :method => 'get',
183 :path => "/projects/redmine/repository/annotate/#{@path_hash[:path]}" },
287 :path => "/projects/redmine/repository/annotate/#{@path_hash[:path]}" },
184 { :controller => 'repositories', :action => 'annotate', :id => 'redmine',
288 { :controller => 'repositories', :action => 'annotate', :id => 'redmine',
185 :path => @path_hash[:param] }
289 :path => @path_hash[:param] }
186 )
290 )
187 assert_routing(
291 assert_routing(
188 { :method => 'get',
292 { :method => 'get',
189 :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" },
293 :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" },
190 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
294 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
191 :path => @path_hash[:param] }
295 :path => @path_hash[:param] }
192 )
296 )
193 end
297 end
194
298
299 def test_repositories_non_revisions_path_with_repository_id
300 assert_routing(
301 { :method => 'get',
302 :path => "/projects/redmine/repository/foo/diff/#{@path_hash[:path]}" },
303 { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo',
304 :path => @path_hash[:param] }
305 )
306 assert_routing(
307 { :method => 'get',
308 :path => "/projects/redmine/repository/foo/browse/#{@path_hash[:path]}" },
309 { :controller => 'repositories', :action => 'browse', :id => 'redmine', :repository_id => 'foo',
310 :path => @path_hash[:param] }
311 )
312 assert_routing(
313 { :method => 'get',
314 :path => "/projects/redmine/repository/foo/entry/#{@path_hash[:path]}" },
315 { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo',
316 :path => @path_hash[:param] }
317 )
318 assert_routing(
319 { :method => 'get',
320 :path => "/projects/redmine/repository/foo/raw/#{@path_hash[:path]}" },
321 { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo',
322 :path => @path_hash[:param], :format => 'raw' }
323 )
324 assert_routing(
325 { :method => 'get',
326 :path => "/projects/redmine/repository/foo/annotate/#{@path_hash[:path]}" },
327 { :controller => 'repositories', :action => 'annotate', :id => 'redmine', :repository_id => 'foo',
328 :path => @path_hash[:param] }
329 )
330 assert_routing(
331 { :method => 'get',
332 :path => "/projects/redmine/repository/foo/changes/#{@path_hash[:path]}" },
333 { :controller => 'repositories', :action => 'changes', :id => 'redmine', :repository_id => 'foo',
334 :path => @path_hash[:param] }
335 )
336 end
337
195 private
338 private
196
339
197 def repository_path_hash(arr)
340 def repository_path_hash(arr)
198 hs = {}
341 hs = {}
199 hs[:path] = arr.join("/")
342 hs[:path] = arr.join("/")
200 hs[:param] = arr
343 hs[:param] = arr
201 hs
344 hs
202 end
345 end
203 end
346 end
@@ -1,256 +1,269
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RepositoryTest < ActiveSupport::TestCase
20 class RepositoryTest < ActiveSupport::TestCase
21 fixtures :projects,
21 fixtures :projects,
22 :trackers,
22 :trackers,
23 :projects_trackers,
23 :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :repositories,
25 :repositories,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :issue_categories,
28 :issue_categories,
29 :changesets,
29 :changesets,
30 :changes,
30 :changes,
31 :users,
31 :users,
32 :members,
32 :members,
33 :member_roles,
33 :member_roles,
34 :roles,
34 :roles,
35 :enumerations
35 :enumerations
36
36
37 def setup
37 def setup
38 @repository = Project.find(1).repository
38 @repository = Project.find(1).repository
39 end
39 end
40
40
41 def test_create
41 def test_create
42 repository = Repository::Subversion.new(:project => Project.find(3))
42 repository = Repository::Subversion.new(:project => Project.find(3))
43 assert !repository.save
43 assert !repository.save
44
44
45 repository.url = "svn://localhost"
45 repository.url = "svn://localhost"
46 assert repository.save
46 assert repository.save
47 repository.reload
47 repository.reload
48
48
49 project = Project.find(3)
49 project = Project.find(3)
50 assert_equal repository, project.repository
50 assert_equal repository, project.repository
51 end
51 end
52
52
53 def test_first_repository_should_be_set_as_default
54 repository1 = Repository::Subversion.new(:project => Project.find(3), :identifier => 'svn1', :url => 'file:///svn1')
55 assert repository1.save
56 assert repository1.is_default?
57
58 repository2 = Repository::Subversion.new(:project => Project.find(3), :identifier => 'svn2', :url => 'file:///svn2')
59 assert repository2.save
60 assert !repository2.is_default?
61
62 assert_equal repository1, Project.find(3).repository
63 assert_equal [repository1, repository2], Project.find(3).repositories.sort
64 end
65
53 def test_destroy
66 def test_destroy
54 changesets = Changeset.count(:all, :conditions => "repository_id = 10")
67 changesets = Changeset.count(:all, :conditions => "repository_id = 10")
55 changes = Change.count(:all, :conditions => "repository_id = 10",
68 changes = Change.count(:all, :conditions => "repository_id = 10",
56 :include => :changeset)
69 :include => :changeset)
57 assert_difference 'Changeset.count', -changesets do
70 assert_difference 'Changeset.count', -changesets do
58 assert_difference 'Change.count', -changes do
71 assert_difference 'Change.count', -changes do
59 Repository.find(10).destroy
72 Repository.find(10).destroy
60 end
73 end
61 end
74 end
62 end
75 end
63
76
64 def test_should_not_create_with_disabled_scm
77 def test_should_not_create_with_disabled_scm
65 # disable Subversion
78 # disable Subversion
66 with_settings :enabled_scm => ['Darcs', 'Git'] do
79 with_settings :enabled_scm => ['Darcs', 'Git'] do
67 repository = Repository::Subversion.new(
80 repository = Repository::Subversion.new(
68 :project => Project.find(3), :url => "svn://localhost")
81 :project => Project.find(3), :url => "svn://localhost")
69 assert !repository.save
82 assert !repository.save
70 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
83 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
71 repository.errors[:type].to_s
84 repository.errors[:type].to_s
72 end
85 end
73 end
86 end
74
87
75 def test_scan_changesets_for_issue_ids
88 def test_scan_changesets_for_issue_ids
76 Setting.default_language = 'en'
89 Setting.default_language = 'en'
77 Setting.notified_events = ['issue_added','issue_updated']
90 Setting.notified_events = ['issue_added','issue_updated']
78
91
79 # choosing a status to apply to fix issues
92 # choosing a status to apply to fix issues
80 Setting.commit_fix_status_id = IssueStatus.find(
93 Setting.commit_fix_status_id = IssueStatus.find(
81 :first,
94 :first,
82 :conditions => ["is_closed = ?", true]).id
95 :conditions => ["is_closed = ?", true]).id
83 Setting.commit_fix_done_ratio = "90"
96 Setting.commit_fix_done_ratio = "90"
84 Setting.commit_ref_keywords = 'refs , references, IssueID'
97 Setting.commit_ref_keywords = 'refs , references, IssueID'
85 Setting.commit_fix_keywords = 'fixes , closes'
98 Setting.commit_fix_keywords = 'fixes , closes'
86 Setting.default_language = 'en'
99 Setting.default_language = 'en'
87 ActionMailer::Base.deliveries.clear
100 ActionMailer::Base.deliveries.clear
88
101
89 # make sure issue 1 is not already closed
102 # make sure issue 1 is not already closed
90 fixed_issue = Issue.find(1)
103 fixed_issue = Issue.find(1)
91 assert !fixed_issue.status.is_closed?
104 assert !fixed_issue.status.is_closed?
92 old_status = fixed_issue.status
105 old_status = fixed_issue.status
93
106
94 Repository.scan_changesets_for_issue_ids
107 Repository.scan_changesets_for_issue_ids
95 assert_equal [101, 102], Issue.find(3).changeset_ids
108 assert_equal [101, 102], Issue.find(3).changeset_ids
96
109
97 # fixed issues
110 # fixed issues
98 fixed_issue.reload
111 fixed_issue.reload
99 assert fixed_issue.status.is_closed?
112 assert fixed_issue.status.is_closed?
100 assert_equal 90, fixed_issue.done_ratio
113 assert_equal 90, fixed_issue.done_ratio
101 assert_equal [101], fixed_issue.changeset_ids
114 assert_equal [101], fixed_issue.changeset_ids
102
115
103 # issue change
116 # issue change
104 journal = fixed_issue.journals.find(:first, :order => 'created_on desc')
117 journal = fixed_issue.journals.find(:first, :order => 'created_on desc')
105 assert_equal User.find_by_login('dlopper'), journal.user
118 assert_equal User.find_by_login('dlopper'), journal.user
106 assert_equal 'Applied in changeset r2.', journal.notes
119 assert_equal 'Applied in changeset r2.', journal.notes
107
120
108 # 2 email notifications
121 # 2 email notifications
109 assert_equal 2, ActionMailer::Base.deliveries.size
122 assert_equal 2, ActionMailer::Base.deliveries.size
110 mail = ActionMailer::Base.deliveries.first
123 mail = ActionMailer::Base.deliveries.first
111 assert_kind_of TMail::Mail, mail
124 assert_kind_of TMail::Mail, mail
112 assert mail.subject.starts_with?(
125 assert mail.subject.starts_with?(
113 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
126 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
114 assert mail.body.include?(
127 assert mail.body.include?(
115 "Status changed from #{old_status} to #{fixed_issue.status}")
128 "Status changed from #{old_status} to #{fixed_issue.status}")
116
129
117 # ignoring commits referencing an issue of another project
130 # ignoring commits referencing an issue of another project
118 assert_equal [], Issue.find(4).changesets
131 assert_equal [], Issue.find(4).changesets
119 end
132 end
120
133
121 def test_for_changeset_comments_strip
134 def test_for_changeset_comments_strip
122 repository = Repository::Mercurial.create(
135 repository = Repository::Mercurial.create(
123 :project => Project.find( 4 ),
136 :project => Project.find( 4 ),
124 :url => '/foo/bar/baz' )
137 :url => '/foo/bar/baz' )
125 comment = <<-COMMENT
138 comment = <<-COMMENT
126 This is a loooooooooooooooooooooooooooong comment
139 This is a loooooooooooooooooooooooooooong comment
127
140
128
141
129 COMMENT
142 COMMENT
130 changeset = Changeset.new(
143 changeset = Changeset.new(
131 :comments => comment, :commit_date => Time.now,
144 :comments => comment, :commit_date => Time.now,
132 :revision => 0, :scmid => 'f39b7922fb3c',
145 :revision => 0, :scmid => 'f39b7922fb3c',
133 :committer => 'foo <foo@example.com>',
146 :committer => 'foo <foo@example.com>',
134 :committed_on => Time.now, :repository => repository )
147 :committed_on => Time.now, :repository => repository )
135 assert( changeset.save )
148 assert( changeset.save )
136 assert_not_equal( comment, changeset.comments )
149 assert_not_equal( comment, changeset.comments )
137 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
150 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
138 changeset.comments )
151 changeset.comments )
139 end
152 end
140
153
141 def test_for_urls_strip_cvs
154 def test_for_urls_strip_cvs
142 repository = Repository::Cvs.create(
155 repository = Repository::Cvs.create(
143 :project => Project.find(4),
156 :project => Project.find(4),
144 :url => ' :pserver:login:password@host:/path/to/the/repository',
157 :url => ' :pserver:login:password@host:/path/to/the/repository',
145 :root_url => 'foo ',
158 :root_url => 'foo ',
146 :log_encoding => 'UTF-8')
159 :log_encoding => 'UTF-8')
147 assert repository.save
160 assert repository.save
148 repository.reload
161 repository.reload
149 assert_equal ':pserver:login:password@host:/path/to/the/repository',
162 assert_equal ':pserver:login:password@host:/path/to/the/repository',
150 repository.url
163 repository.url
151 assert_equal 'foo', repository.root_url
164 assert_equal 'foo', repository.root_url
152 end
165 end
153
166
154 def test_for_urls_strip_subversion
167 def test_for_urls_strip_subversion
155 repository = Repository::Subversion.create(
168 repository = Repository::Subversion.create(
156 :project => Project.find(4),
169 :project => Project.find(4),
157 :url => ' file:///dummy ')
170 :url => ' file:///dummy ')
158 assert repository.save
171 assert repository.save
159 repository.reload
172 repository.reload
160 assert_equal 'file:///dummy', repository.url
173 assert_equal 'file:///dummy', repository.url
161 end
174 end
162
175
163 def test_for_urls_strip_git
176 def test_for_urls_strip_git
164 repository = Repository::Git.create(
177 repository = Repository::Git.create(
165 :project => Project.find(4),
178 :project => Project.find(4),
166 :url => ' c:\dummy ')
179 :url => ' c:\dummy ')
167 assert repository.save
180 assert repository.save
168 repository.reload
181 repository.reload
169 assert_equal 'c:\dummy', repository.url
182 assert_equal 'c:\dummy', repository.url
170 end
183 end
171
184
172 def test_manual_user_mapping
185 def test_manual_user_mapping
173 assert_no_difference "Changeset.count(:conditions => 'user_id <> 2')" do
186 assert_no_difference "Changeset.count(:conditions => 'user_id <> 2')" do
174 c = Changeset.create!(
187 c = Changeset.create!(
175 :repository => @repository,
188 :repository => @repository,
176 :committer => 'foo',
189 :committer => 'foo',
177 :committed_on => Time.now,
190 :committed_on => Time.now,
178 :revision => 100,
191 :revision => 100,
179 :comments => 'Committed by foo.'
192 :comments => 'Committed by foo.'
180 )
193 )
181 assert_nil c.user
194 assert_nil c.user
182 @repository.committer_ids = {'foo' => '2'}
195 @repository.committer_ids = {'foo' => '2'}
183 assert_equal User.find(2), c.reload.user
196 assert_equal User.find(2), c.reload.user
184 # committer is now mapped
197 # committer is now mapped
185 c = Changeset.create!(
198 c = Changeset.create!(
186 :repository => @repository,
199 :repository => @repository,
187 :committer => 'foo',
200 :committer => 'foo',
188 :committed_on => Time.now,
201 :committed_on => Time.now,
189 :revision => 101,
202 :revision => 101,
190 :comments => 'Another commit by foo.'
203 :comments => 'Another commit by foo.'
191 )
204 )
192 assert_equal User.find(2), c.user
205 assert_equal User.find(2), c.user
193 end
206 end
194 end
207 end
195
208
196 def test_auto_user_mapping_by_username
209 def test_auto_user_mapping_by_username
197 c = Changeset.create!(
210 c = Changeset.create!(
198 :repository => @repository,
211 :repository => @repository,
199 :committer => 'jsmith',
212 :committer => 'jsmith',
200 :committed_on => Time.now,
213 :committed_on => Time.now,
201 :revision => 100,
214 :revision => 100,
202 :comments => 'Committed by john.'
215 :comments => 'Committed by john.'
203 )
216 )
204 assert_equal User.find(2), c.user
217 assert_equal User.find(2), c.user
205 end
218 end
206
219
207 def test_auto_user_mapping_by_email
220 def test_auto_user_mapping_by_email
208 c = Changeset.create!(
221 c = Changeset.create!(
209 :repository => @repository,
222 :repository => @repository,
210 :committer => 'john <jsmith@somenet.foo>',
223 :committer => 'john <jsmith@somenet.foo>',
211 :committed_on => Time.now,
224 :committed_on => Time.now,
212 :revision => 100,
225 :revision => 100,
213 :comments => 'Committed by john.'
226 :comments => 'Committed by john.'
214 )
227 )
215 assert_equal User.find(2), c.user
228 assert_equal User.find(2), c.user
216 end
229 end
217
230
218 def test_filesystem_avaialbe
231 def test_filesystem_avaialbe
219 klass = Repository::Filesystem
232 klass = Repository::Filesystem
220 assert klass.scm_adapter_class
233 assert klass.scm_adapter_class
221 assert_equal true, klass.scm_available
234 assert_equal true, klass.scm_available
222 end
235 end
223
236
224 def test_merge_extra_info
237 def test_merge_extra_info
225 repo = Repository::Subversion.new(:project => Project.find(3))
238 repo = Repository::Subversion.new(:project => Project.find(3))
226 assert !repo.save
239 assert !repo.save
227 repo.url = "svn://localhost"
240 repo.url = "svn://localhost"
228 assert repo.save
241 assert repo.save
229 repo.reload
242 repo.reload
230 project = Project.find(3)
243 project = Project.find(3)
231 assert_equal repo, project.repository
244 assert_equal repo, project.repository
232 assert_nil repo.extra_info
245 assert_nil repo.extra_info
233 h1 = {"test_1" => {"test_11" => "test_value_11"}}
246 h1 = {"test_1" => {"test_11" => "test_value_11"}}
234 repo.merge_extra_info(h1)
247 repo.merge_extra_info(h1)
235 assert_equal h1, repo.extra_info
248 assert_equal h1, repo.extra_info
236 h2 = {"test_2" => {
249 h2 = {"test_2" => {
237 "test_21" => "test_value_21",
250 "test_21" => "test_value_21",
238 "test_22" => "test_value_22",
251 "test_22" => "test_value_22",
239 }}
252 }}
240 repo.merge_extra_info(h2)
253 repo.merge_extra_info(h2)
241 assert_equal (h = {"test_11" => "test_value_11"}),
254 assert_equal (h = {"test_11" => "test_value_11"}),
242 repo.extra_info["test_1"]
255 repo.extra_info["test_1"]
243 assert_equal "test_value_21",
256 assert_equal "test_value_21",
244 repo.extra_info["test_2"]["test_21"]
257 repo.extra_info["test_2"]["test_21"]
245 h3 = {"test_2" => {
258 h3 = {"test_2" => {
246 "test_23" => "test_value_23",
259 "test_23" => "test_value_23",
247 "test_24" => "test_value_24",
260 "test_24" => "test_value_24",
248 }}
261 }}
249 repo.merge_extra_info(h3)
262 repo.merge_extra_info(h3)
250 assert_equal (h = {"test_11" => "test_value_11"}),
263 assert_equal (h = {"test_11" => "test_value_11"}),
251 repo.extra_info["test_1"]
264 repo.extra_info["test_1"]
252 assert_nil repo.extra_info["test_2"]["test_21"]
265 assert_nil repo.extra_info["test_2"]["test_21"]
253 assert_equal "test_value_23",
266 assert_equal "test_value_23",
254 repo.extra_info["test_2"]["test_23"]
267 repo.extra_info["test_2"]["test_23"]
255 end
268 end
256 end
269 end
@@ -1,882 +1,881
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class UserTest < ActiveSupport::TestCase
20 class UserTest < ActiveSupport::TestCase
21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources,
21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources,
22 :trackers, :issue_statuses,
22 :trackers, :issue_statuses,
23 :projects_trackers,
23 :projects_trackers,
24 :watchers,
24 :watchers,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :journals, :journal_details,
26 :journals, :journal_details,
27 :groups_users,
27 :groups_users,
28 :enabled_modules,
28 :enabled_modules,
29 :workflows
29 :workflows
30
30
31 def setup
31 def setup
32 @admin = User.find(1)
32 @admin = User.find(1)
33 @jsmith = User.find(2)
33 @jsmith = User.find(2)
34 @dlopper = User.find(3)
34 @dlopper = User.find(3)
35 end
35 end
36
36
37 test 'object_daddy creation' do
37 test 'object_daddy creation' do
38 User.generate_with_protected!(:firstname => 'Testing connection')
38 User.generate_with_protected!(:firstname => 'Testing connection')
39 User.generate_with_protected!(:firstname => 'Testing connection')
39 User.generate_with_protected!(:firstname => 'Testing connection')
40 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
40 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
41 end
41 end
42
42
43 def test_truth
43 def test_truth
44 assert_kind_of User, @jsmith
44 assert_kind_of User, @jsmith
45 end
45 end
46
46
47 def test_mail_should_be_stripped
47 def test_mail_should_be_stripped
48 u = User.new
48 u = User.new
49 u.mail = " foo@bar.com "
49 u.mail = " foo@bar.com "
50 assert_equal "foo@bar.com", u.mail
50 assert_equal "foo@bar.com", u.mail
51 end
51 end
52
52
53 def test_mail_validation
53 def test_mail_validation
54 u = User.new
54 u = User.new
55 u.mail = ''
55 u.mail = ''
56 assert !u.valid?
56 assert !u.valid?
57 assert_equal I18n.translate('activerecord.errors.messages.blank'),
57 assert_equal I18n.translate('activerecord.errors.messages.blank'),
58 u.errors[:mail].to_s
58 u.errors[:mail].to_s
59 end
59 end
60
60
61 def test_create
61 def test_create
62 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
62 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
63
63
64 user.login = "jsmith"
64 user.login = "jsmith"
65 user.password, user.password_confirmation = "password", "password"
65 user.password, user.password_confirmation = "password", "password"
66 # login uniqueness
66 # login uniqueness
67 assert !user.save
67 assert !user.save
68 assert_equal 1, user.errors.count
68 assert_equal 1, user.errors.count
69
69
70 user.login = "newuser"
70 user.login = "newuser"
71 user.password, user.password_confirmation = "passwd", "password"
71 user.password, user.password_confirmation = "passwd", "password"
72 # password confirmation
72 # password confirmation
73 assert !user.save
73 assert !user.save
74 assert_equal 1, user.errors.count
74 assert_equal 1, user.errors.count
75
75
76 user.password, user.password_confirmation = "password", "password"
76 user.password, user.password_confirmation = "password", "password"
77 assert user.save
77 assert user.save
78 end
78 end
79
79
80 context "User#before_create" do
80 context "User#before_create" do
81 should "set the mail_notification to the default Setting" do
81 should "set the mail_notification to the default Setting" do
82 @user1 = User.generate_with_protected!
82 @user1 = User.generate_with_protected!
83 assert_equal 'only_my_events', @user1.mail_notification
83 assert_equal 'only_my_events', @user1.mail_notification
84
84
85 with_settings :default_notification_option => 'all' do
85 with_settings :default_notification_option => 'all' do
86 @user2 = User.generate_with_protected!
86 @user2 = User.generate_with_protected!
87 assert_equal 'all', @user2.mail_notification
87 assert_equal 'all', @user2.mail_notification
88 end
88 end
89 end
89 end
90 end
90 end
91
91
92 context "User.login" do
92 context "User.login" do
93 should "be case-insensitive." do
93 should "be case-insensitive." do
94 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
94 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
95 u.login = 'newuser'
95 u.login = 'newuser'
96 u.password, u.password_confirmation = "password", "password"
96 u.password, u.password_confirmation = "password", "password"
97 assert u.save
97 assert u.save
98
98
99 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
99 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
100 u.login = 'NewUser'
100 u.login = 'NewUser'
101 u.password, u.password_confirmation = "password", "password"
101 u.password, u.password_confirmation = "password", "password"
102 assert !u.save
102 assert !u.save
103 assert_equal I18n.translate('activerecord.errors.messages.taken'),
103 assert_equal I18n.translate('activerecord.errors.messages.taken'),
104 u.errors[:login].to_s
104 u.errors[:login].to_s
105 end
105 end
106 end
106 end
107
107
108 def test_mail_uniqueness_should_not_be_case_sensitive
108 def test_mail_uniqueness_should_not_be_case_sensitive
109 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
109 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
110 u.login = 'newuser1'
110 u.login = 'newuser1'
111 u.password, u.password_confirmation = "password", "password"
111 u.password, u.password_confirmation = "password", "password"
112 assert u.save
112 assert u.save
113
113
114 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
114 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
115 u.login = 'newuser2'
115 u.login = 'newuser2'
116 u.password, u.password_confirmation = "password", "password"
116 u.password, u.password_confirmation = "password", "password"
117 assert !u.save
117 assert !u.save
118 assert_equal I18n.translate('activerecord.errors.messages.taken'),
118 assert_equal I18n.translate('activerecord.errors.messages.taken'),
119 u.errors[:mail].to_s
119 u.errors[:mail].to_s
120 end
120 end
121
121
122 def test_update
122 def test_update
123 assert_equal "admin", @admin.login
123 assert_equal "admin", @admin.login
124 @admin.login = "john"
124 @admin.login = "john"
125 assert @admin.save, @admin.errors.full_messages.join("; ")
125 assert @admin.save, @admin.errors.full_messages.join("; ")
126 @admin.reload
126 @admin.reload
127 assert_equal "john", @admin.login
127 assert_equal "john", @admin.login
128 end
128 end
129
129
130 def test_destroy_should_delete_members_and_roles
130 def test_destroy_should_delete_members_and_roles
131 members = Member.find_all_by_user_id(2)
131 members = Member.find_all_by_user_id(2)
132 ms = members.size
132 ms = members.size
133 rs = members.collect(&:roles).flatten.size
133 rs = members.collect(&:roles).flatten.size
134
134
135 assert_difference 'Member.count', - ms do
135 assert_difference 'Member.count', - ms do
136 assert_difference 'MemberRole.count', - rs do
136 assert_difference 'MemberRole.count', - rs do
137 User.find(2).destroy
137 User.find(2).destroy
138 end
138 end
139 end
139 end
140
140
141 assert_nil User.find_by_id(2)
141 assert_nil User.find_by_id(2)
142 assert Member.find_all_by_user_id(2).empty?
142 assert Member.find_all_by_user_id(2).empty?
143 end
143 end
144
144
145 def test_destroy_should_update_attachments
145 def test_destroy_should_update_attachments
146 attachment = Attachment.create!(:container => Project.find(1),
146 attachment = Attachment.create!(:container => Project.find(1),
147 :file => uploaded_test_file("testfile.txt", "text/plain"),
147 :file => uploaded_test_file("testfile.txt", "text/plain"),
148 :author_id => 2)
148 :author_id => 2)
149
149
150 User.find(2).destroy
150 User.find(2).destroy
151 assert_nil User.find_by_id(2)
151 assert_nil User.find_by_id(2)
152 assert_equal User.anonymous, attachment.reload.author
152 assert_equal User.anonymous, attachment.reload.author
153 end
153 end
154
154
155 def test_destroy_should_update_comments
155 def test_destroy_should_update_comments
156 comment = Comment.create!(
156 comment = Comment.create!(
157 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
157 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
158 :author => User.find(2),
158 :author => User.find(2),
159 :comments => 'foo'
159 :comments => 'foo'
160 )
160 )
161
161
162 User.find(2).destroy
162 User.find(2).destroy
163 assert_nil User.find_by_id(2)
163 assert_nil User.find_by_id(2)
164 assert_equal User.anonymous, comment.reload.author
164 assert_equal User.anonymous, comment.reload.author
165 end
165 end
166
166
167 def test_destroy_should_update_issues
167 def test_destroy_should_update_issues
168 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
168 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
169
169
170 User.find(2).destroy
170 User.find(2).destroy
171 assert_nil User.find_by_id(2)
171 assert_nil User.find_by_id(2)
172 assert_equal User.anonymous, issue.reload.author
172 assert_equal User.anonymous, issue.reload.author
173 end
173 end
174
174
175 def test_destroy_should_unassign_issues
175 def test_destroy_should_unassign_issues
176 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
176 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
177
177
178 User.find(2).destroy
178 User.find(2).destroy
179 assert_nil User.find_by_id(2)
179 assert_nil User.find_by_id(2)
180 assert_nil issue.reload.assigned_to
180 assert_nil issue.reload.assigned_to
181 end
181 end
182
182
183 def test_destroy_should_update_journals
183 def test_destroy_should_update_journals
184 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
184 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
185 issue.init_journal(User.find(2), "update")
185 issue.init_journal(User.find(2), "update")
186 issue.save!
186 issue.save!
187
187
188 User.find(2).destroy
188 User.find(2).destroy
189 assert_nil User.find_by_id(2)
189 assert_nil User.find_by_id(2)
190 assert_equal User.anonymous, issue.journals.first.reload.user
190 assert_equal User.anonymous, issue.journals.first.reload.user
191 end
191 end
192
192
193 def test_destroy_should_update_journal_details_old_value
193 def test_destroy_should_update_journal_details_old_value
194 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
194 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
195 issue.init_journal(User.find(1), "update")
195 issue.init_journal(User.find(1), "update")
196 issue.assigned_to_id = nil
196 issue.assigned_to_id = nil
197 assert_difference 'JournalDetail.count' do
197 assert_difference 'JournalDetail.count' do
198 issue.save!
198 issue.save!
199 end
199 end
200 journal_detail = JournalDetail.first(:order => 'id DESC')
200 journal_detail = JournalDetail.first(:order => 'id DESC')
201 assert_equal '2', journal_detail.old_value
201 assert_equal '2', journal_detail.old_value
202
202
203 User.find(2).destroy
203 User.find(2).destroy
204 assert_nil User.find_by_id(2)
204 assert_nil User.find_by_id(2)
205 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
205 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
206 end
206 end
207
207
208 def test_destroy_should_update_journal_details_value
208 def test_destroy_should_update_journal_details_value
209 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
209 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
210 issue.init_journal(User.find(1), "update")
210 issue.init_journal(User.find(1), "update")
211 issue.assigned_to_id = 2
211 issue.assigned_to_id = 2
212 assert_difference 'JournalDetail.count' do
212 assert_difference 'JournalDetail.count' do
213 issue.save!
213 issue.save!
214 end
214 end
215 journal_detail = JournalDetail.first(:order => 'id DESC')
215 journal_detail = JournalDetail.first(:order => 'id DESC')
216 assert_equal '2', journal_detail.value
216 assert_equal '2', journal_detail.value
217
217
218 User.find(2).destroy
218 User.find(2).destroy
219 assert_nil User.find_by_id(2)
219 assert_nil User.find_by_id(2)
220 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
220 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
221 end
221 end
222
222
223 def test_destroy_should_update_messages
223 def test_destroy_should_update_messages
224 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
224 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
225 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
225 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
226
226
227 User.find(2).destroy
227 User.find(2).destroy
228 assert_nil User.find_by_id(2)
228 assert_nil User.find_by_id(2)
229 assert_equal User.anonymous, message.reload.author
229 assert_equal User.anonymous, message.reload.author
230 end
230 end
231
231
232 def test_destroy_should_update_news
232 def test_destroy_should_update_news
233 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
233 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
234
234
235 User.find(2).destroy
235 User.find(2).destroy
236 assert_nil User.find_by_id(2)
236 assert_nil User.find_by_id(2)
237 assert_equal User.anonymous, news.reload.author
237 assert_equal User.anonymous, news.reload.author
238 end
238 end
239
239
240 def test_destroy_should_delete_private_queries
240 def test_destroy_should_delete_private_queries
241 query = Query.new(:name => 'foo', :is_public => false)
241 query = Query.new(:name => 'foo', :is_public => false)
242 query.project_id = 1
242 query.project_id = 1
243 query.user_id = 2
243 query.user_id = 2
244 query.save!
244 query.save!
245
245
246 User.find(2).destroy
246 User.find(2).destroy
247 assert_nil User.find_by_id(2)
247 assert_nil User.find_by_id(2)
248 assert_nil Query.find_by_id(query.id)
248 assert_nil Query.find_by_id(query.id)
249 end
249 end
250
250
251 def test_destroy_should_update_public_queries
251 def test_destroy_should_update_public_queries
252 query = Query.new(:name => 'foo', :is_public => true)
252 query = Query.new(:name => 'foo', :is_public => true)
253 query.project_id = 1
253 query.project_id = 1
254 query.user_id = 2
254 query.user_id = 2
255 query.save!
255 query.save!
256
256
257 User.find(2).destroy
257 User.find(2).destroy
258 assert_nil User.find_by_id(2)
258 assert_nil User.find_by_id(2)
259 assert_equal User.anonymous, query.reload.user
259 assert_equal User.anonymous, query.reload.user
260 end
260 end
261
261
262 def test_destroy_should_update_time_entries
262 def test_destroy_should_update_time_entries
263 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
263 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
264 entry.project_id = 1
264 entry.project_id = 1
265 entry.user_id = 2
265 entry.user_id = 2
266 entry.save!
266 entry.save!
267
267
268 User.find(2).destroy
268 User.find(2).destroy
269 assert_nil User.find_by_id(2)
269 assert_nil User.find_by_id(2)
270 assert_equal User.anonymous, entry.reload.user
270 assert_equal User.anonymous, entry.reload.user
271 end
271 end
272
272
273 def test_destroy_should_delete_tokens
273 def test_destroy_should_delete_tokens
274 token = Token.create!(:user_id => 2, :value => 'foo')
274 token = Token.create!(:user_id => 2, :value => 'foo')
275
275
276 User.find(2).destroy
276 User.find(2).destroy
277 assert_nil User.find_by_id(2)
277 assert_nil User.find_by_id(2)
278 assert_nil Token.find_by_id(token.id)
278 assert_nil Token.find_by_id(token.id)
279 end
279 end
280
280
281 def test_destroy_should_delete_watchers
281 def test_destroy_should_delete_watchers
282 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
282 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
283 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
283 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
284
284
285 User.find(2).destroy
285 User.find(2).destroy
286 assert_nil User.find_by_id(2)
286 assert_nil User.find_by_id(2)
287 assert_nil Watcher.find_by_id(watcher.id)
287 assert_nil Watcher.find_by_id(watcher.id)
288 end
288 end
289
289
290 def test_destroy_should_update_wiki_contents
290 def test_destroy_should_update_wiki_contents
291 wiki_content = WikiContent.create!(
291 wiki_content = WikiContent.create!(
292 :text => 'foo',
292 :text => 'foo',
293 :author_id => 2,
293 :author_id => 2,
294 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
294 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
295 )
295 )
296 wiki_content.text = 'bar'
296 wiki_content.text = 'bar'
297 assert_difference 'WikiContent::Version.count' do
297 assert_difference 'WikiContent::Version.count' do
298 wiki_content.save!
298 wiki_content.save!
299 end
299 end
300
300
301 User.find(2).destroy
301 User.find(2).destroy
302 assert_nil User.find_by_id(2)
302 assert_nil User.find_by_id(2)
303 assert_equal User.anonymous, wiki_content.reload.author
303 assert_equal User.anonymous, wiki_content.reload.author
304 wiki_content.versions.each do |version|
304 wiki_content.versions.each do |version|
305 assert_equal User.anonymous, version.reload.author
305 assert_equal User.anonymous, version.reload.author
306 end
306 end
307 end
307 end
308
308
309 def test_destroy_should_nullify_issue_categories
309 def test_destroy_should_nullify_issue_categories
310 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
310 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
311
311
312 User.find(2).destroy
312 User.find(2).destroy
313 assert_nil User.find_by_id(2)
313 assert_nil User.find_by_id(2)
314 assert_nil category.reload.assigned_to_id
314 assert_nil category.reload.assigned_to_id
315 end
315 end
316
316
317 def test_destroy_should_nullify_changesets
317 def test_destroy_should_nullify_changesets
318 changeset = Changeset.create!(
318 changeset = Changeset.create!(
319 :repository => Repository::Subversion.create!(
319 :repository => Repository::Subversion.generate!(
320 :project_id => 1,
320 :project_id => 1
321 :url => 'file:///var/svn'
322 ),
321 ),
323 :revision => '12',
322 :revision => '12',
324 :committed_on => Time.now,
323 :committed_on => Time.now,
325 :committer => 'jsmith'
324 :committer => 'jsmith'
326 )
325 )
327 assert_equal 2, changeset.user_id
326 assert_equal 2, changeset.user_id
328
327
329 User.find(2).destroy
328 User.find(2).destroy
330 assert_nil User.find_by_id(2)
329 assert_nil User.find_by_id(2)
331 assert_nil changeset.reload.user_id
330 assert_nil changeset.reload.user_id
332 end
331 end
333
332
334 def test_anonymous_user_should_not_be_destroyable
333 def test_anonymous_user_should_not_be_destroyable
335 assert_no_difference 'User.count' do
334 assert_no_difference 'User.count' do
336 assert_equal false, User.anonymous.destroy
335 assert_equal false, User.anonymous.destroy
337 end
336 end
338 end
337 end
339
338
340 def test_validate_login_presence
339 def test_validate_login_presence
341 @admin.login = ""
340 @admin.login = ""
342 assert !@admin.save
341 assert !@admin.save
343 assert_equal 1, @admin.errors.count
342 assert_equal 1, @admin.errors.count
344 end
343 end
345
344
346 def test_validate_mail_notification_inclusion
345 def test_validate_mail_notification_inclusion
347 u = User.new
346 u = User.new
348 u.mail_notification = 'foo'
347 u.mail_notification = 'foo'
349 u.save
348 u.save
350 assert_not_nil u.errors[:mail_notification]
349 assert_not_nil u.errors[:mail_notification]
351 end
350 end
352
351
353 context "User#try_to_login" do
352 context "User#try_to_login" do
354 should "fall-back to case-insensitive if user login is not found as-typed." do
353 should "fall-back to case-insensitive if user login is not found as-typed." do
355 user = User.try_to_login("AdMin", "admin")
354 user = User.try_to_login("AdMin", "admin")
356 assert_kind_of User, user
355 assert_kind_of User, user
357 assert_equal "admin", user.login
356 assert_equal "admin", user.login
358 end
357 end
359
358
360 should "select the exact matching user first" do
359 should "select the exact matching user first" do
361 case_sensitive_user = User.generate_with_protected!(
360 case_sensitive_user = User.generate_with_protected!(
362 :login => 'changed', :password => 'admin',
361 :login => 'changed', :password => 'admin',
363 :password_confirmation => 'admin')
362 :password_confirmation => 'admin')
364 # bypass validations to make it appear like existing data
363 # bypass validations to make it appear like existing data
365 case_sensitive_user.update_attribute(:login, 'ADMIN')
364 case_sensitive_user.update_attribute(:login, 'ADMIN')
366
365
367 user = User.try_to_login("ADMIN", "admin")
366 user = User.try_to_login("ADMIN", "admin")
368 assert_kind_of User, user
367 assert_kind_of User, user
369 assert_equal "ADMIN", user.login
368 assert_equal "ADMIN", user.login
370
369
371 end
370 end
372 end
371 end
373
372
374 def test_password
373 def test_password
375 user = User.try_to_login("admin", "admin")
374 user = User.try_to_login("admin", "admin")
376 assert_kind_of User, user
375 assert_kind_of User, user
377 assert_equal "admin", user.login
376 assert_equal "admin", user.login
378 user.password = "hello"
377 user.password = "hello"
379 assert user.save
378 assert user.save
380
379
381 user = User.try_to_login("admin", "hello")
380 user = User.try_to_login("admin", "hello")
382 assert_kind_of User, user
381 assert_kind_of User, user
383 assert_equal "admin", user.login
382 assert_equal "admin", user.login
384 end
383 end
385
384
386 def test_validate_password_length
385 def test_validate_password_length
387 with_settings :password_min_length => '100' do
386 with_settings :password_min_length => '100' do
388 user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo")
387 user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo")
389 user.login = "newuser100"
388 user.login = "newuser100"
390 user.password, user.password_confirmation = "password100", "password100"
389 user.password, user.password_confirmation = "password100", "password100"
391 assert !user.save
390 assert !user.save
392 assert_equal 1, user.errors.count
391 assert_equal 1, user.errors.count
393 end
392 end
394 end
393 end
395
394
396 def test_name_format
395 def test_name_format
397 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
396 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
398 with_settings :user_format => :firstname_lastname do
397 with_settings :user_format => :firstname_lastname do
399 assert_equal 'John Smith', @jsmith.reload.name
398 assert_equal 'John Smith', @jsmith.reload.name
400 end
399 end
401 with_settings :user_format => :username do
400 with_settings :user_format => :username do
402 assert_equal 'jsmith', @jsmith.reload.name
401 assert_equal 'jsmith', @jsmith.reload.name
403 end
402 end
404 end
403 end
405
404
406 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
405 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
407 with_settings :user_format => 'lastname_coma_firstname' do
406 with_settings :user_format => 'lastname_coma_firstname' do
408 assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement
407 assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement
409 end
408 end
410 end
409 end
411
410
412 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
411 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
413 with_settings :user_format => 'lastname_firstname' do
412 with_settings :user_format => 'lastname_firstname' do
414 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors')
413 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors')
415 end
414 end
416 end
415 end
417
416
418 def test_fields_for_order_statement_with_blank_format_should_return_default
417 def test_fields_for_order_statement_with_blank_format_should_return_default
419 with_settings :user_format => '' do
418 with_settings :user_format => '' do
420 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
419 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
421 end
420 end
422 end
421 end
423
422
424 def test_fields_for_order_statement_with_invalid_format_should_return_default
423 def test_fields_for_order_statement_with_invalid_format_should_return_default
425 with_settings :user_format => 'foo' do
424 with_settings :user_format => 'foo' do
426 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
425 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
427 end
426 end
428 end
427 end
429
428
430 def test_lock
429 def test_lock
431 user = User.try_to_login("jsmith", "jsmith")
430 user = User.try_to_login("jsmith", "jsmith")
432 assert_equal @jsmith, user
431 assert_equal @jsmith, user
433
432
434 @jsmith.status = User::STATUS_LOCKED
433 @jsmith.status = User::STATUS_LOCKED
435 assert @jsmith.save
434 assert @jsmith.save
436
435
437 user = User.try_to_login("jsmith", "jsmith")
436 user = User.try_to_login("jsmith", "jsmith")
438 assert_equal nil, user
437 assert_equal nil, user
439 end
438 end
440
439
441 context ".try_to_login" do
440 context ".try_to_login" do
442 context "with good credentials" do
441 context "with good credentials" do
443 should "return the user" do
442 should "return the user" do
444 user = User.try_to_login("admin", "admin")
443 user = User.try_to_login("admin", "admin")
445 assert_kind_of User, user
444 assert_kind_of User, user
446 assert_equal "admin", user.login
445 assert_equal "admin", user.login
447 end
446 end
448 end
447 end
449
448
450 context "with wrong credentials" do
449 context "with wrong credentials" do
451 should "return nil" do
450 should "return nil" do
452 assert_nil User.try_to_login("admin", "foo")
451 assert_nil User.try_to_login("admin", "foo")
453 end
452 end
454 end
453 end
455 end
454 end
456
455
457 if ldap_configured?
456 if ldap_configured?
458 context "#try_to_login using LDAP" do
457 context "#try_to_login using LDAP" do
459 context "with failed connection to the LDAP server" do
458 context "with failed connection to the LDAP server" do
460 should "return nil" do
459 should "return nil" do
461 @auth_source = AuthSourceLdap.find(1)
460 @auth_source = AuthSourceLdap.find(1)
462 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
461 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
463
462
464 assert_equal nil, User.try_to_login('edavis', 'wrong')
463 assert_equal nil, User.try_to_login('edavis', 'wrong')
465 end
464 end
466 end
465 end
467
466
468 context "with an unsuccessful authentication" do
467 context "with an unsuccessful authentication" do
469 should "return nil" do
468 should "return nil" do
470 assert_equal nil, User.try_to_login('edavis', 'wrong')
469 assert_equal nil, User.try_to_login('edavis', 'wrong')
471 end
470 end
472 end
471 end
473
472
474 context "on the fly registration" do
473 context "on the fly registration" do
475 setup do
474 setup do
476 @auth_source = AuthSourceLdap.find(1)
475 @auth_source = AuthSourceLdap.find(1)
477 end
476 end
478
477
479 context "with a successful authentication" do
478 context "with a successful authentication" do
480 should "create a new user account if it doesn't exist" do
479 should "create a new user account if it doesn't exist" do
481 assert_difference('User.count') do
480 assert_difference('User.count') do
482 user = User.try_to_login('edavis', '123456')
481 user = User.try_to_login('edavis', '123456')
483 assert !user.admin?
482 assert !user.admin?
484 end
483 end
485 end
484 end
486
485
487 should "retrieve existing user" do
486 should "retrieve existing user" do
488 user = User.try_to_login('edavis', '123456')
487 user = User.try_to_login('edavis', '123456')
489 user.admin = true
488 user.admin = true
490 user.save!
489 user.save!
491
490
492 assert_no_difference('User.count') do
491 assert_no_difference('User.count') do
493 user = User.try_to_login('edavis', '123456')
492 user = User.try_to_login('edavis', '123456')
494 assert user.admin?
493 assert user.admin?
495 end
494 end
496 end
495 end
497 end
496 end
498 end
497 end
499 end
498 end
500
499
501 else
500 else
502 puts "Skipping LDAP tests."
501 puts "Skipping LDAP tests."
503 end
502 end
504
503
505 def test_create_anonymous
504 def test_create_anonymous
506 AnonymousUser.delete_all
505 AnonymousUser.delete_all
507 anon = User.anonymous
506 anon = User.anonymous
508 assert !anon.new_record?
507 assert !anon.new_record?
509 assert_kind_of AnonymousUser, anon
508 assert_kind_of AnonymousUser, anon
510 end
509 end
511
510
512 def test_ensure_single_anonymous_user
511 def test_ensure_single_anonymous_user
513 AnonymousUser.delete_all
512 AnonymousUser.delete_all
514 anon1 = User.anonymous
513 anon1 = User.anonymous
515 assert !anon1.new_record?
514 assert !anon1.new_record?
516 assert_kind_of AnonymousUser, anon1
515 assert_kind_of AnonymousUser, anon1
517 anon2 = AnonymousUser.create(
516 anon2 = AnonymousUser.create(
518 :lastname => 'Anonymous', :firstname => '',
517 :lastname => 'Anonymous', :firstname => '',
519 :mail => '', :login => '', :status => 0)
518 :mail => '', :login => '', :status => 0)
520 assert_equal 1, anon2.errors.count
519 assert_equal 1, anon2.errors.count
521 end
520 end
522
521
523 should_have_one :rss_token
522 should_have_one :rss_token
524
523
525 def test_rss_key
524 def test_rss_key
526 assert_nil @jsmith.rss_token
525 assert_nil @jsmith.rss_token
527 key = @jsmith.rss_key
526 key = @jsmith.rss_key
528 assert_equal 40, key.length
527 assert_equal 40, key.length
529
528
530 @jsmith.reload
529 @jsmith.reload
531 assert_equal key, @jsmith.rss_key
530 assert_equal key, @jsmith.rss_key
532 end
531 end
533
532
534
533
535 should_have_one :api_token
534 should_have_one :api_token
536
535
537 context "User#api_key" do
536 context "User#api_key" do
538 should "generate a new one if the user doesn't have one" do
537 should "generate a new one if the user doesn't have one" do
539 user = User.generate_with_protected!(:api_token => nil)
538 user = User.generate_with_protected!(:api_token => nil)
540 assert_nil user.api_token
539 assert_nil user.api_token
541
540
542 key = user.api_key
541 key = user.api_key
543 assert_equal 40, key.length
542 assert_equal 40, key.length
544 user.reload
543 user.reload
545 assert_equal key, user.api_key
544 assert_equal key, user.api_key
546 end
545 end
547
546
548 should "return the existing api token value" do
547 should "return the existing api token value" do
549 user = User.generate_with_protected!
548 user = User.generate_with_protected!
550 token = Token.generate!(:action => 'api')
549 token = Token.generate!(:action => 'api')
551 user.api_token = token
550 user.api_token = token
552 assert user.save
551 assert user.save
553
552
554 assert_equal token.value, user.api_key
553 assert_equal token.value, user.api_key
555 end
554 end
556 end
555 end
557
556
558 context "User#find_by_api_key" do
557 context "User#find_by_api_key" do
559 should "return nil if no matching key is found" do
558 should "return nil if no matching key is found" do
560 assert_nil User.find_by_api_key('zzzzzzzzz')
559 assert_nil User.find_by_api_key('zzzzzzzzz')
561 end
560 end
562
561
563 should "return nil if the key is found for an inactive user" do
562 should "return nil if the key is found for an inactive user" do
564 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
563 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
565 token = Token.generate!(:action => 'api')
564 token = Token.generate!(:action => 'api')
566 user.api_token = token
565 user.api_token = token
567 user.save
566 user.save
568
567
569 assert_nil User.find_by_api_key(token.value)
568 assert_nil User.find_by_api_key(token.value)
570 end
569 end
571
570
572 should "return the user if the key is found for an active user" do
571 should "return the user if the key is found for an active user" do
573 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
572 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
574 token = Token.generate!(:action => 'api')
573 token = Token.generate!(:action => 'api')
575 user.api_token = token
574 user.api_token = token
576 user.save
575 user.save
577
576
578 assert_equal user, User.find_by_api_key(token.value)
577 assert_equal user, User.find_by_api_key(token.value)
579 end
578 end
580 end
579 end
581
580
582 def test_roles_for_project
581 def test_roles_for_project
583 # user with a role
582 # user with a role
584 roles = @jsmith.roles_for_project(Project.find(1))
583 roles = @jsmith.roles_for_project(Project.find(1))
585 assert_kind_of Role, roles.first
584 assert_kind_of Role, roles.first
586 assert_equal "Manager", roles.first.name
585 assert_equal "Manager", roles.first.name
587
586
588 # user with no role
587 # user with no role
589 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
588 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
590 end
589 end
591
590
592 def test_projects_by_role_for_user_with_role
591 def test_projects_by_role_for_user_with_role
593 user = User.find(2)
592 user = User.find(2)
594 assert_kind_of Hash, user.projects_by_role
593 assert_kind_of Hash, user.projects_by_role
595 assert_equal 2, user.projects_by_role.size
594 assert_equal 2, user.projects_by_role.size
596 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
595 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
597 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
596 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
598 end
597 end
599
598
600 def test_projects_by_role_for_user_with_no_role
599 def test_projects_by_role_for_user_with_no_role
601 user = User.generate!
600 user = User.generate!
602 assert_equal({}, user.projects_by_role)
601 assert_equal({}, user.projects_by_role)
603 end
602 end
604
603
605 def test_projects_by_role_for_anonymous
604 def test_projects_by_role_for_anonymous
606 assert_equal({}, User.anonymous.projects_by_role)
605 assert_equal({}, User.anonymous.projects_by_role)
607 end
606 end
608
607
609 def test_valid_notification_options
608 def test_valid_notification_options
610 # without memberships
609 # without memberships
611 assert_equal 5, User.find(7).valid_notification_options.size
610 assert_equal 5, User.find(7).valid_notification_options.size
612 # with memberships
611 # with memberships
613 assert_equal 6, User.find(2).valid_notification_options.size
612 assert_equal 6, User.find(2).valid_notification_options.size
614 end
613 end
615
614
616 def test_valid_notification_options_class_method
615 def test_valid_notification_options_class_method
617 assert_equal 5, User.valid_notification_options.size
616 assert_equal 5, User.valid_notification_options.size
618 assert_equal 5, User.valid_notification_options(User.find(7)).size
617 assert_equal 5, User.valid_notification_options(User.find(7)).size
619 assert_equal 6, User.valid_notification_options(User.find(2)).size
618 assert_equal 6, User.valid_notification_options(User.find(2)).size
620 end
619 end
621
620
622 def test_mail_notification_all
621 def test_mail_notification_all
623 @jsmith.mail_notification = 'all'
622 @jsmith.mail_notification = 'all'
624 @jsmith.notified_project_ids = []
623 @jsmith.notified_project_ids = []
625 @jsmith.save
624 @jsmith.save
626 @jsmith.reload
625 @jsmith.reload
627 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
626 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
628 end
627 end
629
628
630 def test_mail_notification_selected
629 def test_mail_notification_selected
631 @jsmith.mail_notification = 'selected'
630 @jsmith.mail_notification = 'selected'
632 @jsmith.notified_project_ids = [1]
631 @jsmith.notified_project_ids = [1]
633 @jsmith.save
632 @jsmith.save
634 @jsmith.reload
633 @jsmith.reload
635 assert Project.find(1).recipients.include?(@jsmith.mail)
634 assert Project.find(1).recipients.include?(@jsmith.mail)
636 end
635 end
637
636
638 def test_mail_notification_only_my_events
637 def test_mail_notification_only_my_events
639 @jsmith.mail_notification = 'only_my_events'
638 @jsmith.mail_notification = 'only_my_events'
640 @jsmith.notified_project_ids = []
639 @jsmith.notified_project_ids = []
641 @jsmith.save
640 @jsmith.save
642 @jsmith.reload
641 @jsmith.reload
643 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
642 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
644 end
643 end
645
644
646 def test_comments_sorting_preference
645 def test_comments_sorting_preference
647 assert !@jsmith.wants_comments_in_reverse_order?
646 assert !@jsmith.wants_comments_in_reverse_order?
648 @jsmith.pref.comments_sorting = 'asc'
647 @jsmith.pref.comments_sorting = 'asc'
649 assert !@jsmith.wants_comments_in_reverse_order?
648 assert !@jsmith.wants_comments_in_reverse_order?
650 @jsmith.pref.comments_sorting = 'desc'
649 @jsmith.pref.comments_sorting = 'desc'
651 assert @jsmith.wants_comments_in_reverse_order?
650 assert @jsmith.wants_comments_in_reverse_order?
652 end
651 end
653
652
654 def test_find_by_mail_should_be_case_insensitive
653 def test_find_by_mail_should_be_case_insensitive
655 u = User.find_by_mail('JSmith@somenet.foo')
654 u = User.find_by_mail('JSmith@somenet.foo')
656 assert_not_nil u
655 assert_not_nil u
657 assert_equal 'jsmith@somenet.foo', u.mail
656 assert_equal 'jsmith@somenet.foo', u.mail
658 end
657 end
659
658
660 def test_random_password
659 def test_random_password
661 u = User.new
660 u = User.new
662 u.random_password
661 u.random_password
663 assert !u.password.blank?
662 assert !u.password.blank?
664 assert !u.password_confirmation.blank?
663 assert !u.password_confirmation.blank?
665 end
664 end
666
665
667 context "#change_password_allowed?" do
666 context "#change_password_allowed?" do
668 should "be allowed if no auth source is set" do
667 should "be allowed if no auth source is set" do
669 user = User.generate_with_protected!
668 user = User.generate_with_protected!
670 assert user.change_password_allowed?
669 assert user.change_password_allowed?
671 end
670 end
672
671
673 should "delegate to the auth source" do
672 should "delegate to the auth source" do
674 user = User.generate_with_protected!
673 user = User.generate_with_protected!
675
674
676 allowed_auth_source = AuthSource.generate!
675 allowed_auth_source = AuthSource.generate!
677 def allowed_auth_source.allow_password_changes?; true; end
676 def allowed_auth_source.allow_password_changes?; true; end
678
677
679 denied_auth_source = AuthSource.generate!
678 denied_auth_source = AuthSource.generate!
680 def denied_auth_source.allow_password_changes?; false; end
679 def denied_auth_source.allow_password_changes?; false; end
681
680
682 assert user.change_password_allowed?
681 assert user.change_password_allowed?
683
682
684 user.auth_source = allowed_auth_source
683 user.auth_source = allowed_auth_source
685 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
684 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
686
685
687 user.auth_source = denied_auth_source
686 user.auth_source = denied_auth_source
688 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
687 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
689 end
688 end
690
689
691 end
690 end
692
691
693 context "#allowed_to?" do
692 context "#allowed_to?" do
694 context "with a unique project" do
693 context "with a unique project" do
695 should "return false if project is archived" do
694 should "return false if project is archived" do
696 project = Project.find(1)
695 project = Project.find(1)
697 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
696 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
698 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
697 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
699 end
698 end
700
699
701 should "return false if related module is disabled" do
700 should "return false if related module is disabled" do
702 project = Project.find(1)
701 project = Project.find(1)
703 project.enabled_module_names = ["issue_tracking"]
702 project.enabled_module_names = ["issue_tracking"]
704 assert @admin.allowed_to?(:add_issues, project)
703 assert @admin.allowed_to?(:add_issues, project)
705 assert ! @admin.allowed_to?(:view_wiki_pages, project)
704 assert ! @admin.allowed_to?(:view_wiki_pages, project)
706 end
705 end
707
706
708 should "authorize nearly everything for admin users" do
707 should "authorize nearly everything for admin users" do
709 project = Project.find(1)
708 project = Project.find(1)
710 assert ! @admin.member_of?(project)
709 assert ! @admin.member_of?(project)
711 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
710 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
712 assert @admin.allowed_to?(p.to_sym, project)
711 assert @admin.allowed_to?(p.to_sym, project)
713 end
712 end
714 end
713 end
715
714
716 should "authorize normal users depending on their roles" do
715 should "authorize normal users depending on their roles" do
717 project = Project.find(1)
716 project = Project.find(1)
718 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
717 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
719 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
718 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
720 end
719 end
721 end
720 end
722
721
723 context "with multiple projects" do
722 context "with multiple projects" do
724 should "return false if array is empty" do
723 should "return false if array is empty" do
725 assert ! @admin.allowed_to?(:view_project, [])
724 assert ! @admin.allowed_to?(:view_project, [])
726 end
725 end
727
726
728 should "return true only if user has permission on all these projects" do
727 should "return true only if user has permission on all these projects" do
729 assert @admin.allowed_to?(:view_project, Project.all)
728 assert @admin.allowed_to?(:view_project, Project.all)
730 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
729 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
731 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
730 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
732 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
731 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
733 end
732 end
734
733
735 should "behave correctly with arrays of 1 project" do
734 should "behave correctly with arrays of 1 project" do
736 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
735 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
737 end
736 end
738 end
737 end
739
738
740 context "with options[:global]" do
739 context "with options[:global]" do
741 should "authorize if user has at least one role that has this permission" do
740 should "authorize if user has at least one role that has this permission" do
742 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
741 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
743 @anonymous = User.find(6)
742 @anonymous = User.find(6)
744 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
743 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
745 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
744 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
746 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
745 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
747 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
746 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
748 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
747 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
749 end
748 end
750 end
749 end
751 end
750 end
752
751
753 context "User#notify_about?" do
752 context "User#notify_about?" do
754 context "Issues" do
753 context "Issues" do
755 setup do
754 setup do
756 @project = Project.find(1)
755 @project = Project.find(1)
757 @author = User.generate_with_protected!
756 @author = User.generate_with_protected!
758 @assignee = User.generate_with_protected!
757 @assignee = User.generate_with_protected!
759 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
758 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
760 end
759 end
761
760
762 should "be true for a user with :all" do
761 should "be true for a user with :all" do
763 @author.update_attribute(:mail_notification, 'all')
762 @author.update_attribute(:mail_notification, 'all')
764 assert @author.notify_about?(@issue)
763 assert @author.notify_about?(@issue)
765 end
764 end
766
765
767 should "be false for a user with :none" do
766 should "be false for a user with :none" do
768 @author.update_attribute(:mail_notification, 'none')
767 @author.update_attribute(:mail_notification, 'none')
769 assert ! @author.notify_about?(@issue)
768 assert ! @author.notify_about?(@issue)
770 end
769 end
771
770
772 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
771 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
773 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
772 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
774 Member.create!(:user => @user, :project => @project, :role_ids => [1])
773 Member.create!(:user => @user, :project => @project, :role_ids => [1])
775 assert ! @user.notify_about?(@issue)
774 assert ! @user.notify_about?(@issue)
776 end
775 end
777
776
778 should "be true for a user with :only_my_events and is the author" do
777 should "be true for a user with :only_my_events and is the author" do
779 @author.update_attribute(:mail_notification, 'only_my_events')
778 @author.update_attribute(:mail_notification, 'only_my_events')
780 assert @author.notify_about?(@issue)
779 assert @author.notify_about?(@issue)
781 end
780 end
782
781
783 should "be true for a user with :only_my_events and is the assignee" do
782 should "be true for a user with :only_my_events and is the assignee" do
784 @assignee.update_attribute(:mail_notification, 'only_my_events')
783 @assignee.update_attribute(:mail_notification, 'only_my_events')
785 assert @assignee.notify_about?(@issue)
784 assert @assignee.notify_about?(@issue)
786 end
785 end
787
786
788 should "be true for a user with :only_assigned and is the assignee" do
787 should "be true for a user with :only_assigned and is the assignee" do
789 @assignee.update_attribute(:mail_notification, 'only_assigned')
788 @assignee.update_attribute(:mail_notification, 'only_assigned')
790 assert @assignee.notify_about?(@issue)
789 assert @assignee.notify_about?(@issue)
791 end
790 end
792
791
793 should "be false for a user with :only_assigned and is not the assignee" do
792 should "be false for a user with :only_assigned and is not the assignee" do
794 @author.update_attribute(:mail_notification, 'only_assigned')
793 @author.update_attribute(:mail_notification, 'only_assigned')
795 assert ! @author.notify_about?(@issue)
794 assert ! @author.notify_about?(@issue)
796 end
795 end
797
796
798 should "be true for a user with :only_owner and is the author" do
797 should "be true for a user with :only_owner and is the author" do
799 @author.update_attribute(:mail_notification, 'only_owner')
798 @author.update_attribute(:mail_notification, 'only_owner')
800 assert @author.notify_about?(@issue)
799 assert @author.notify_about?(@issue)
801 end
800 end
802
801
803 should "be false for a user with :only_owner and is not the author" do
802 should "be false for a user with :only_owner and is not the author" do
804 @assignee.update_attribute(:mail_notification, 'only_owner')
803 @assignee.update_attribute(:mail_notification, 'only_owner')
805 assert ! @assignee.notify_about?(@issue)
804 assert ! @assignee.notify_about?(@issue)
806 end
805 end
807
806
808 should "be true for a user with :selected and is the author" do
807 should "be true for a user with :selected and is the author" do
809 @author.update_attribute(:mail_notification, 'selected')
808 @author.update_attribute(:mail_notification, 'selected')
810 assert @author.notify_about?(@issue)
809 assert @author.notify_about?(@issue)
811 end
810 end
812
811
813 should "be true for a user with :selected and is the assignee" do
812 should "be true for a user with :selected and is the assignee" do
814 @assignee.update_attribute(:mail_notification, 'selected')
813 @assignee.update_attribute(:mail_notification, 'selected')
815 assert @assignee.notify_about?(@issue)
814 assert @assignee.notify_about?(@issue)
816 end
815 end
817
816
818 should "be false for a user with :selected and is not the author or assignee" do
817 should "be false for a user with :selected and is not the author or assignee" do
819 @user = User.generate_with_protected!(:mail_notification => 'selected')
818 @user = User.generate_with_protected!(:mail_notification => 'selected')
820 Member.create!(:user => @user, :project => @project, :role_ids => [1])
819 Member.create!(:user => @user, :project => @project, :role_ids => [1])
821 assert ! @user.notify_about?(@issue)
820 assert ! @user.notify_about?(@issue)
822 end
821 end
823 end
822 end
824
823
825 context "other events" do
824 context "other events" do
826 should 'be added and tested'
825 should 'be added and tested'
827 end
826 end
828 end
827 end
829
828
830 def test_salt_unsalted_passwords
829 def test_salt_unsalted_passwords
831 # Restore a user with an unsalted password
830 # Restore a user with an unsalted password
832 user = User.find(1)
831 user = User.find(1)
833 user.salt = nil
832 user.salt = nil
834 user.hashed_password = User.hash_password("unsalted")
833 user.hashed_password = User.hash_password("unsalted")
835 user.save!
834 user.save!
836
835
837 User.salt_unsalted_passwords!
836 User.salt_unsalted_passwords!
838
837
839 user.reload
838 user.reload
840 # Salt added
839 # Salt added
841 assert !user.salt.blank?
840 assert !user.salt.blank?
842 # Password still valid
841 # Password still valid
843 assert user.check_password?("unsalted")
842 assert user.check_password?("unsalted")
844 assert_equal user, User.try_to_login(user.login, "unsalted")
843 assert_equal user, User.try_to_login(user.login, "unsalted")
845 end
844 end
846
845
847 if Object.const_defined?(:OpenID)
846 if Object.const_defined?(:OpenID)
848
847
849 def test_setting_identity_url
848 def test_setting_identity_url
850 normalized_open_id_url = 'http://example.com/'
849 normalized_open_id_url = 'http://example.com/'
851 u = User.new( :identity_url => 'http://example.com/' )
850 u = User.new( :identity_url => 'http://example.com/' )
852 assert_equal normalized_open_id_url, u.identity_url
851 assert_equal normalized_open_id_url, u.identity_url
853 end
852 end
854
853
855 def test_setting_identity_url_without_trailing_slash
854 def test_setting_identity_url_without_trailing_slash
856 normalized_open_id_url = 'http://example.com/'
855 normalized_open_id_url = 'http://example.com/'
857 u = User.new( :identity_url => 'http://example.com' )
856 u = User.new( :identity_url => 'http://example.com' )
858 assert_equal normalized_open_id_url, u.identity_url
857 assert_equal normalized_open_id_url, u.identity_url
859 end
858 end
860
859
861 def test_setting_identity_url_without_protocol
860 def test_setting_identity_url_without_protocol
862 normalized_open_id_url = 'http://example.com/'
861 normalized_open_id_url = 'http://example.com/'
863 u = User.new( :identity_url => 'example.com' )
862 u = User.new( :identity_url => 'example.com' )
864 assert_equal normalized_open_id_url, u.identity_url
863 assert_equal normalized_open_id_url, u.identity_url
865 end
864 end
866
865
867 def test_setting_blank_identity_url
866 def test_setting_blank_identity_url
868 u = User.new( :identity_url => 'example.com' )
867 u = User.new( :identity_url => 'example.com' )
869 u.identity_url = ''
868 u.identity_url = ''
870 assert u.identity_url.blank?
869 assert u.identity_url.blank?
871 end
870 end
872
871
873 def test_setting_invalid_identity_url
872 def test_setting_invalid_identity_url
874 u = User.new( :identity_url => 'this is not an openid url' )
873 u = User.new( :identity_url => 'this is not an openid url' )
875 assert u.identity_url.blank?
874 assert u.identity_url.blank?
876 end
875 end
877
876
878 else
877 else
879 puts "Skipping openid tests."
878 puts "Skipping openid tests."
880 end
879 end
881
880
882 end
881 end
General Comments 0
You need to be logged in to leave comments. Login now