##// 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 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21
22 22 class ChangesetNotFound < Exception; end
23 23 class InvalidRevisionParam < Exception; end
24 24
25 25 class RepositoriesController < ApplicationController
26 26 menu_item :repository
27 27 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
28 28 default_search_scope :changesets
29 29
30 30 before_filter :find_project_by_project_id, :only => [:new, :create]
31 before_filter :check_repository_uniqueness, :only => [:new, :create]
32 31 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
33 32 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 33 before_filter :authorize
35 34 accept_rss_auth :revisions
36 35
37 36 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
38 37
39 38 def new
40 39 scm = params[:repository_scm] || Redmine::Scm::Base.all.first
41 40 @repository = Repository.factory(scm)
41 @repository.is_default = @project.repository.nil?
42 42 @repository.project = @project
43 43 render :layout => !request.xhr?
44 44 end
45 45
46 46 def create
47 47 @repository = Repository.factory(params[:repository_scm], params[:repository])
48 48 @repository.project = @project
49 49 if request.post? && @repository.save
50 50 redirect_to settings_project_path(@project, :tab => 'repositories')
51 51 else
52 52 render :action => 'new'
53 53 end
54 54 end
55 55
56 56 def edit
57 57 end
58 58
59 59 def update
60 60 @repository.attributes = params[:repository]
61 61 @repository.project = @project
62 62 if request.put? && @repository.save
63 63 redirect_to settings_project_path(@project, :tab => 'repositories')
64 64 else
65 65 render :action => 'edit'
66 66 end
67 67 end
68 68
69 69 def committers
70 70 @committers = @repository.committers
71 71 @users = @project.users
72 72 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
73 73 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
74 74 @users.compact!
75 75 @users.sort!
76 76 if request.post? && params[:committers].is_a?(Hash)
77 77 # Build a hash with repository usernames as keys and corresponding user ids as values
78 78 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
79 79 flash[:notice] = l(:notice_successful_update)
80 80 redirect_to settings_project_path(@project, :tab => 'repositories')
81 81 end
82 82 end
83 83
84 84 def destroy
85 85 @repository.destroy if request.delete?
86 86 redirect_to settings_project_path(@project, :tab => 'repositories')
87 87 end
88 88
89 89 def show
90 90 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
91 91
92 92 @entries = @repository.entries(@path, @rev)
93 93 @changeset = @repository.find_changeset_by_name(@rev)
94 94 if request.xhr?
95 95 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
96 96 else
97 97 (show_error_not_found; return) unless @entries
98 98 @changesets = @repository.latest_changesets(@path, @rev)
99 99 @properties = @repository.properties(@path, @rev)
100 @repositories = @project.repositories
100 101 render :action => 'show'
101 102 end
102 103 end
103 104
104 105 alias_method :browse, :show
105 106
106 107 def changes
107 108 @entry = @repository.entry(@path, @rev)
108 109 (show_error_not_found; return) unless @entry
109 110 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
110 111 @properties = @repository.properties(@path, @rev)
111 112 @changeset = @repository.find_changeset_by_name(@rev)
112 113 end
113 114
114 115 def revisions
115 116 @changeset_count = @repository.changesets.count
116 117 @changeset_pages = Paginator.new self, @changeset_count,
117 118 per_page_option,
118 119 params['page']
119 120 @changesets = @repository.changesets.find(:all,
120 121 :limit => @changeset_pages.items_per_page,
121 122 :offset => @changeset_pages.current.offset,
122 123 :include => [:user, :repository, :parents])
123 124
124 125 respond_to do |format|
125 126 format.html { render :layout => false if request.xhr? }
126 127 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
127 128 end
128 129 end
129 130
130 131 def entry
131 132 @entry = @repository.entry(@path, @rev)
132 133 (show_error_not_found; return) unless @entry
133 134
134 135 # If the entry is a dir, show the browser
135 136 (show; return) if @entry.is_dir?
136 137
137 138 @content = @repository.cat(@path, @rev)
138 139 (show_error_not_found; return) unless @content
139 140 if 'raw' == params[:format] ||
140 141 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
141 142 ! is_entry_text_data?(@content, @path)
142 143 # Force the download
143 144 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
144 145 send_type = Redmine::MimeType.of(@path)
145 146 send_opt[:type] = send_type.to_s if send_type
146 147 send_data @content, send_opt
147 148 else
148 149 # Prevent empty lines when displaying a file with Windows style eol
149 150 # TODO: UTF-16
150 151 # Is this needs? AttachmentsController reads file simply.
151 152 @content.gsub!("\r\n", "\n")
152 153 @changeset = @repository.find_changeset_by_name(@rev)
153 154 end
154 155 end
155 156
156 157 def is_entry_text_data?(ent, path)
157 158 # UTF-16 contains "\x00".
158 159 # It is very strict that file contains less than 30% of ascii symbols
159 160 # in non Western Europe.
160 161 return true if Redmine::MimeType.is_type?('text', path)
161 162 # Ruby 1.8.6 has a bug of integer divisions.
162 163 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
163 164 return false if ent.is_binary_data?
164 165 true
165 166 end
166 167 private :is_entry_text_data?
167 168
168 169 def annotate
169 170 @entry = @repository.entry(@path, @rev)
170 171 (show_error_not_found; return) unless @entry
171 172
172 173 @annotate = @repository.scm.annotate(@path, @rev)
173 174 if @annotate.nil? || @annotate.empty?
174 175 (render_error l(:error_scm_annotate); return)
175 176 end
176 177 ann_buf_size = 0
177 178 @annotate.lines.each do |buf|
178 179 ann_buf_size += buf.size
179 180 end
180 181 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
181 182 (render_error l(:error_scm_annotate_big_text_file); return)
182 183 end
183 184 @changeset = @repository.find_changeset_by_name(@rev)
184 185 end
185 186
186 187 def revision
187 188 raise ChangesetNotFound if @rev.blank?
188 189 @changeset = @repository.find_changeset_by_name(@rev)
189 190 raise ChangesetNotFound unless @changeset
190 191
191 192 respond_to do |format|
192 193 format.html
193 194 format.js {render :layout => false}
194 195 end
195 196 rescue ChangesetNotFound
196 197 show_error_not_found
197 198 end
198 199
199 200 def diff
200 201 if params[:format] == 'diff'
201 202 @diff = @repository.diff(@path, @rev, @rev_to)
202 203 (show_error_not_found; return) unless @diff
203 204 filename = "changeset_r#{@rev}"
204 205 filename << "_r#{@rev_to}" if @rev_to
205 206 send_data @diff.join, :filename => "#{filename}.diff",
206 207 :type => 'text/x-patch',
207 208 :disposition => 'attachment'
208 209 else
209 210 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
210 211 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
211 212
212 213 # Save diff type as user preference
213 214 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
214 215 User.current.pref[:diff_type] = @diff_type
215 216 User.current.preference.save
216 217 end
217 218 @cache_key = "repositories/diff/#{@repository.id}/" +
218 219 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
219 220 unless read_fragment(@cache_key)
220 221 @diff = @repository.diff(@path, @rev, @rev_to)
221 222 show_error_not_found unless @diff
222 223 end
223 224
224 225 @changeset = @repository.find_changeset_by_name(@rev)
225 226 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
226 227 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
227 228 end
228 229 end
229 230
230 231 def stats
231 232 end
232 233
233 234 def graph
234 235 data = nil
235 236 case params[:graph]
236 237 when "commits_per_month"
237 238 data = graph_commits_per_month(@repository)
238 239 when "commits_per_author"
239 240 data = graph_commits_per_author(@repository)
240 241 end
241 242 if data
242 243 headers["Content-Type"] = "image/svg+xml"
243 244 send_data(data, :type => "image/svg+xml", :disposition => "inline")
244 245 else
245 246 render_404
246 247 end
247 248 end
248 249
249 250 private
250 251
251 252 def find_repository
252 253 @repository = Repository.find(params[:id])
253 254 @project = @repository.project
254 255 rescue ActiveRecord::RecordNotFound
255 256 render_404
256 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 259 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
266 260
267 261 def find_project_repository
268 262 @project = Project.find(params[:id])
269 @repository = @project.repository
263 if params[:repository_id].present?
264 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
265 else
266 @repository = @project.repository
267 end
270 268 (render_404; return false) unless @repository
271 269 @path = params[:path].join('/') unless params[:path].nil?
272 270 @path ||= ''
273 271 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
274 272 @rev_to = params[:rev_to]
275 273
276 274 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
277 275 if @repository.branches.blank?
278 276 raise InvalidRevisionParam
279 277 end
280 278 end
281 279 rescue ActiveRecord::RecordNotFound
282 280 render_404
283 281 rescue InvalidRevisionParam
284 282 show_error_not_found
285 283 end
286 284
287 285 def show_error_not_found
288 286 render_error :message => l(:error_scm_not_found), :status => 404
289 287 end
290 288
291 289 # Handler for Redmine::Scm::Adapters::CommandFailed exception
292 290 def show_error_command_failed(exception)
293 291 render_error l(:error_scm_command_failed, exception.message)
294 292 end
295 293
296 294 def graph_commits_per_month(repository)
297 295 @date_to = Date.today
298 296 @date_from = @date_to << 11
299 297 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
300 298 commits_by_day = repository.changesets.count(
301 299 :all, :group => :commit_date,
302 300 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
303 301 commits_by_month = [0] * 12
304 302 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
305 303
306 304 changes_by_day = repository.changes.count(
307 305 :all, :group => :commit_date,
308 306 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
309 307 changes_by_month = [0] * 12
310 308 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
311 309
312 310 fields = []
313 311 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
314 312
315 313 graph = SVG::Graph::Bar.new(
316 314 :height => 300,
317 315 :width => 800,
318 316 :fields => fields.reverse,
319 317 :stack => :side,
320 318 :scale_integers => true,
321 319 :step_x_labels => 2,
322 320 :show_data_values => false,
323 321 :graph_title => l(:label_commits_per_month),
324 322 :show_graph_title => true
325 323 )
326 324
327 325 graph.add_data(
328 326 :data => commits_by_month[0..11].reverse,
329 327 :title => l(:label_revision_plural)
330 328 )
331 329
332 330 graph.add_data(
333 331 :data => changes_by_month[0..11].reverse,
334 332 :title => l(:label_change_plural)
335 333 )
336 334
337 335 graph.burn
338 336 end
339 337
340 338 def graph_commits_per_author(repository)
341 339 commits_by_author = repository.changesets.count(:all, :group => :committer)
342 340 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
343 341
344 342 changes_by_author = repository.changes.count(:all, :group => :committer)
345 343 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
346 344
347 345 fields = commits_by_author.collect {|r| r.first}
348 346 commits_data = commits_by_author.collect {|r| r.last}
349 347 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
350 348
351 349 fields = fields + [""]*(10 - fields.length) if fields.length<10
352 350 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
353 351 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
354 352
355 353 # Remove email adress in usernames
356 354 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
357 355
358 356 graph = SVG::Graph::BarHorizontal.new(
359 357 :height => 400,
360 358 :width => 800,
361 359 :fields => fields,
362 360 :stack => :side,
363 361 :scale_integers => true,
364 362 :show_data_values => false,
365 363 :rotate_y_labels => false,
366 364 :graph_title => l(:label_commits_per_author),
367 365 :show_graph_title => true
368 366 )
369 367 graph.add_data(
370 368 :data => commits_data,
371 369 :title => l(:label_revision_plural)
372 370 )
373 371 graph.add_data(
374 372 :data => changes_data,
375 373 :title => l(:label_change_plural)
376 374 )
377 375 graph.burn
378 376 end
379 377 end
380 378
@@ -1,68 +1,68
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class SysController < ActionController::Base
19 19 before_filter :check_enabled
20 20
21 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 23 # extra_info attribute from repository breaks activeresource client
24 24 render :xml => p.to_xml(:only => [:id, :identifier, :name, :is_public, :status], :include => {:repository => {:only => [:id, :url]}})
25 25 end
26 26
27 27 def create_project_repository
28 28 project = Project.find(params[:id])
29 29 if project.repository
30 30 render :nothing => true, :status => 409
31 31 else
32 32 logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
33 33 project.repository = Repository.factory(params[:vendor], params[:repository])
34 34 if project.repository && project.repository.save
35 35 render :xml => project.repository.to_xml(:only => [:id, :url]), :status => 201
36 36 else
37 37 render :nothing => true, :status => 422
38 38 end
39 39 end
40 40 end
41 41
42 42 def fetch_changesets
43 43 projects = []
44 44 if params[:id]
45 45 projects << Project.active.has_module(:repository).find(params[:id])
46 46 else
47 projects = Project.active.has_module(:repository).find(:all, :include => :repository)
47 projects = Project.active.has_module(:repository).all
48 48 end
49 49 projects.each do |project|
50 if project.repository
51 project.repository.fetch_changesets
50 project.repositories.each do |repository|
51 repository.fetch_changesets
52 52 end
53 53 end
54 54 render :nothing => true, :status => 200
55 55 rescue ActiveRecord::RecordNotFound
56 56 render :nothing => true, :status => 404
57 57 end
58 58
59 59 protected
60 60
61 61 def check_enabled
62 62 User.current = nil
63 63 unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
64 64 render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
65 65 return false
66 66 end
67 67 end
68 68 end
@@ -1,1086 +1,1089
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 #
38 38 # @param [String] name Anchor text (passed to link_to)
39 39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 40 # @param [optional, Hash] html_options Options passed to link_to
41 41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 44 end
45 45
46 46 # Display a link to remote if user is authorized
47 47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 48 url = options[:url] || {}
49 49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 50 end
51 51
52 52 # Displays a link to user's account page if active
53 53 def link_to_user(user, options={})
54 54 if user.is_a?(User)
55 55 name = h(user.name(options[:format]))
56 56 if user.active?
57 57 link_to name, :controller => 'users', :action => 'show', :id => user
58 58 else
59 59 name
60 60 end
61 61 else
62 62 h(user.to_s)
63 63 end
64 64 end
65 65
66 66 # Displays a link to +issue+ with its subject.
67 67 # Examples:
68 68 #
69 69 # link_to_issue(issue) # => Defect #6: This is the subject
70 70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 71 # link_to_issue(issue, :subject => false) # => Defect #6
72 72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 if options[:subject] == false
78 78 title = truncate(issue.subject, :length => 60)
79 79 else
80 80 subject = issue.subject
81 81 if options[:truncate]
82 82 subject = truncate(subject, :length => options[:truncate])
83 83 end
84 84 end
85 85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 86 :class => issue.css_classes,
87 87 :title => title
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 action = options.delete(:download) ? 'download' : 'show'
100 100 link_to(h(text),
101 101 {:controller => 'attachments', :action => action,
102 102 :id => attachment, :filename => attachment.filename },
103 103 options)
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 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 113 text = options.delete(:text) || format_revision(revision)
111 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 115 link_to(
113 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 118 :title => l(:label_revision_id, format_revision(revision))
116 119 )
117 120 end
118 121
119 122 # Generates a link to a message
120 123 def link_to_message(message, options={}, html_options = nil)
121 124 link_to(
122 125 h(truncate(message.subject, :length => 60)),
123 126 { :controller => 'messages', :action => 'show',
124 127 :board_id => message.board_id,
125 128 :id => message.root,
126 129 :r => (message.parent_id && message.id),
127 130 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
128 131 }.merge(options),
129 132 html_options
130 133 )
131 134 end
132 135
133 136 # Generates a link to a project if active
134 137 # Examples:
135 138 #
136 139 # link_to_project(project) # => link to the specified project overview
137 140 # link_to_project(project, :action=>'settings') # => link to project settings
138 141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 143 #
141 144 def link_to_project(project, options={}, html_options = nil)
142 145 if project.active?
143 146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
144 147 link_to(h(project), url, html_options)
145 148 else
146 149 h(project)
147 150 end
148 151 end
149 152
150 153 def toggle_link(name, id, options={})
151 154 onclick = "Element.toggle('#{id}'); "
152 155 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
153 156 onclick << "return false;"
154 157 link_to(name, "#", :onclick => onclick)
155 158 end
156 159
157 160 def image_to_function(name, function, html_options = {})
158 161 html_options.symbolize_keys!
159 162 tag(:input, html_options.merge({
160 163 :type => "image", :src => image_path(name),
161 164 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
162 165 }))
163 166 end
164 167
165 168 def prompt_to_remote(name, text, param, url, html_options = {})
166 169 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
167 170 link_to name, {}, html_options
168 171 end
169 172
170 173 def format_activity_title(text)
171 174 h(truncate_single_line(text, :length => 100))
172 175 end
173 176
174 177 def format_activity_day(date)
175 178 date == Date.today ? l(:label_today).titleize : format_date(date)
176 179 end
177 180
178 181 def format_activity_description(text)
179 182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
180 183 ).gsub(/[\r\n]+/, "<br />").html_safe
181 184 end
182 185
183 186 def format_version_name(version)
184 187 if version.project == @project
185 188 h(version)
186 189 else
187 190 h("#{version.project} - #{version}")
188 191 end
189 192 end
190 193
191 194 def due_date_distance_in_words(date)
192 195 if date
193 196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
194 197 end
195 198 end
196 199
197 200 def render_page_hierarchy(pages, node=nil, options={})
198 201 content = ''
199 202 if pages[node]
200 203 content << "<ul class=\"pages-hierarchy\">\n"
201 204 pages[node].each do |page|
202 205 content << "<li>"
203 206 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
204 207 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
205 208 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
206 209 content << "</li>\n"
207 210 end
208 211 content << "</ul>\n"
209 212 end
210 213 content.html_safe
211 214 end
212 215
213 216 # Renders flash messages
214 217 def render_flash_messages
215 218 s = ''
216 219 flash.each do |k,v|
217 220 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
218 221 end
219 222 s.html_safe
220 223 end
221 224
222 225 # Renders tabs and their content
223 226 def render_tabs(tabs)
224 227 if tabs.any?
225 228 render :partial => 'common/tabs', :locals => {:tabs => tabs}
226 229 else
227 230 content_tag 'p', l(:label_no_data), :class => "nodata"
228 231 end
229 232 end
230 233
231 234 # Renders the project quick-jump box
232 235 def render_project_jump_box
233 236 return unless User.current.logged?
234 237 projects = User.current.memberships.collect(&:project).compact.uniq
235 238 if projects.any?
236 239 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
237 240 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
238 241 '<option value="" disabled="disabled">---</option>'
239 242 s << project_tree_options_for_select(projects, :selected => @project) do |p|
240 243 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
241 244 end
242 245 s << '</select>'
243 246 s.html_safe
244 247 end
245 248 end
246 249
247 250 def project_tree_options_for_select(projects, options = {})
248 251 s = ''
249 252 project_tree(projects) do |project, level|
250 253 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
251 254 tag_options = {:value => project.id}
252 255 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
253 256 tag_options[:selected] = 'selected'
254 257 else
255 258 tag_options[:selected] = nil
256 259 end
257 260 tag_options.merge!(yield(project)) if block_given?
258 261 s << content_tag('option', name_prefix + h(project), tag_options)
259 262 end
260 263 s.html_safe
261 264 end
262 265
263 266 # Yields the given block for each project with its level in the tree
264 267 #
265 268 # Wrapper for Project#project_tree
266 269 def project_tree(projects, &block)
267 270 Project.project_tree(projects, &block)
268 271 end
269 272
270 273 def project_nested_ul(projects, &block)
271 274 s = ''
272 275 if projects.any?
273 276 ancestors = []
274 277 projects.sort_by(&:lft).each do |project|
275 278 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
276 279 s << "<ul>\n"
277 280 else
278 281 ancestors.pop
279 282 s << "</li>"
280 283 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
281 284 ancestors.pop
282 285 s << "</ul></li>\n"
283 286 end
284 287 end
285 288 s << "<li>"
286 289 s << yield(project).to_s
287 290 ancestors << project
288 291 end
289 292 s << ("</li></ul>\n" * ancestors.size)
290 293 end
291 294 s.html_safe
292 295 end
293 296
294 297 def principals_check_box_tags(name, principals)
295 298 s = ''
296 299 principals.sort.each do |principal|
297 300 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
298 301 end
299 302 s.html_safe
300 303 end
301 304
302 305 # Returns a string for users/groups option tags
303 306 def principals_options_for_select(collection, selected=nil)
304 307 s = ''
305 308 groups = ''
306 309 collection.sort.each do |element|
307 310 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
308 311 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
309 312 end
310 313 unless groups.empty?
311 314 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
312 315 end
313 316 s
314 317 end
315 318
316 319 # Truncates and returns the string as a single line
317 320 def truncate_single_line(string, *args)
318 321 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
319 322 end
320 323
321 324 # Truncates at line break after 250 characters or options[:length]
322 325 def truncate_lines(string, options={})
323 326 length = options[:length] || 250
324 327 if string.to_s =~ /\A(.{#{length}}.*?)$/m
325 328 "#{$1}..."
326 329 else
327 330 string
328 331 end
329 332 end
330 333
331 334 def anchor(text)
332 335 text.to_s.gsub(' ', '_')
333 336 end
334 337
335 338 def html_hours(text)
336 339 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
337 340 end
338 341
339 342 def authoring(created, author, options={})
340 343 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
341 344 end
342 345
343 346 def time_tag(time)
344 347 text = distance_of_time_in_words(Time.now, time)
345 348 if @project
346 349 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
347 350 else
348 351 content_tag('acronym', text, :title => format_time(time))
349 352 end
350 353 end
351 354
352 355 def syntax_highlight(name, content)
353 356 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
354 357 end
355 358
356 359 def to_path_param(path)
357 360 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
358 361 end
359 362
360 363 def pagination_links_full(paginator, count=nil, options={})
361 364 page_param = options.delete(:page_param) || :page
362 365 per_page_links = options.delete(:per_page_links)
363 366 url_param = params.dup
364 367
365 368 html = ''
366 369 if paginator.current.previous
367 370 # \xc2\xab(utf-8) = &#171;
368 371 html << link_to_content_update(
369 372 "\xc2\xab " + l(:label_previous),
370 373 url_param.merge(page_param => paginator.current.previous)) + ' '
371 374 end
372 375
373 376 html << (pagination_links_each(paginator, options) do |n|
374 377 link_to_content_update(n.to_s, url_param.merge(page_param => n))
375 378 end || '')
376 379
377 380 if paginator.current.next
378 381 # \xc2\xbb(utf-8) = &#187;
379 382 html << ' ' + link_to_content_update(
380 383 (l(:label_next) + " \xc2\xbb"),
381 384 url_param.merge(page_param => paginator.current.next))
382 385 end
383 386
384 387 unless count.nil?
385 388 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
386 389 if per_page_links != false && links = per_page_links(paginator.items_per_page)
387 390 html << " | #{links}"
388 391 end
389 392 end
390 393
391 394 html.html_safe
392 395 end
393 396
394 397 def per_page_links(selected=nil)
395 398 links = Setting.per_page_options_array.collect do |n|
396 399 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
397 400 end
398 401 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
399 402 end
400 403
401 404 def reorder_links(name, url, method = :post)
402 405 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
403 406 url.merge({"#{name}[move_to]" => 'highest'}),
404 407 :method => method, :title => l(:label_sort_highest)) +
405 408 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
406 409 url.merge({"#{name}[move_to]" => 'higher'}),
407 410 :method => method, :title => l(:label_sort_higher)) +
408 411 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
409 412 url.merge({"#{name}[move_to]" => 'lower'}),
410 413 :method => method, :title => l(:label_sort_lower)) +
411 414 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
412 415 url.merge({"#{name}[move_to]" => 'lowest'}),
413 416 :method => method, :title => l(:label_sort_lowest))
414 417 end
415 418
416 419 def breadcrumb(*args)
417 420 elements = args.flatten
418 421 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
419 422 end
420 423
421 424 def other_formats_links(&block)
422 425 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
423 426 yield Redmine::Views::OtherFormatsBuilder.new(self)
424 427 concat('</p>'.html_safe)
425 428 end
426 429
427 430 def page_header_title
428 431 if @project.nil? || @project.new_record?
429 432 h(Setting.app_title)
430 433 else
431 434 b = []
432 435 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
433 436 if ancestors.any?
434 437 root = ancestors.shift
435 438 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
436 439 if ancestors.size > 2
437 440 b << "\xe2\x80\xa6"
438 441 ancestors = ancestors[-2, 2]
439 442 end
440 443 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
441 444 end
442 445 b << h(@project)
443 446 b.join(" \xc2\xbb ").html_safe
444 447 end
445 448 end
446 449
447 450 def html_title(*args)
448 451 if args.empty?
449 452 title = @html_title || []
450 453 title << @project.name if @project
451 454 title << Setting.app_title unless Setting.app_title == title.last
452 455 title.select {|t| !t.blank? }.join(' - ')
453 456 else
454 457 @html_title ||= []
455 458 @html_title += args
456 459 end
457 460 end
458 461
459 462 # Returns the theme, controller name, and action as css classes for the
460 463 # HTML body.
461 464 def body_css_classes
462 465 css = []
463 466 if theme = Redmine::Themes.theme(Setting.ui_theme)
464 467 css << 'theme-' + theme.name
465 468 end
466 469
467 470 css << 'controller-' + params[:controller]
468 471 css << 'action-' + params[:action]
469 472 css.join(' ')
470 473 end
471 474
472 475 def accesskey(s)
473 476 Redmine::AccessKeys.key_for s
474 477 end
475 478
476 479 # Formats text according to system settings.
477 480 # 2 ways to call this method:
478 481 # * with a String: textilizable(text, options)
479 482 # * with an object and one of its attribute: textilizable(issue, :description, options)
480 483 def textilizable(*args)
481 484 options = args.last.is_a?(Hash) ? args.pop : {}
482 485 case args.size
483 486 when 1
484 487 obj = options[:object]
485 488 text = args.shift
486 489 when 2
487 490 obj = args.shift
488 491 attr = args.shift
489 492 text = obj.send(attr).to_s
490 493 else
491 494 raise ArgumentError, 'invalid arguments to textilizable'
492 495 end
493 496 return '' if text.blank?
494 497 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
495 498 only_path = options.delete(:only_path) == false ? false : true
496 499
497 500 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
498 501
499 502 @parsed_headings = []
500 503 @current_section = 0 if options[:edit_section_links]
501 504 text = parse_non_pre_blocks(text) do |text|
502 505 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
503 506 send method_name, text, project, obj, attr, only_path, options
504 507 end
505 508 end
506 509
507 510 if @parsed_headings.any?
508 511 replace_toc(text, @parsed_headings)
509 512 end
510 513
511 514 text.html_safe
512 515 end
513 516
514 517 def parse_non_pre_blocks(text)
515 518 s = StringScanner.new(text)
516 519 tags = []
517 520 parsed = ''
518 521 while !s.eos?
519 522 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
520 523 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
521 524 if tags.empty?
522 525 yield text
523 526 end
524 527 parsed << text
525 528 if tag
526 529 if closing
527 530 if tags.last == tag.downcase
528 531 tags.pop
529 532 end
530 533 else
531 534 tags << tag.downcase
532 535 end
533 536 parsed << full_tag
534 537 end
535 538 end
536 539 # Close any non closing tags
537 540 while tag = tags.pop
538 541 parsed << "</#{tag}>"
539 542 end
540 543 parsed.html_safe
541 544 end
542 545
543 546 def parse_inline_attachments(text, project, obj, attr, only_path, options)
544 547 # when using an image link, try to use an attachment, if possible
545 548 if options[:attachments] || (obj && obj.respond_to?(:attachments))
546 549 attachments = options[:attachments] || obj.attachments
547 550 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
548 551 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
549 552 # search for the picture in attachments
550 553 if found = Attachment.latest_attach(attachments, filename)
551 554 image_url = url_for :only_path => only_path, :controller => 'attachments',
552 555 :action => 'download', :id => found
553 556 desc = found.description.to_s.gsub('"', '')
554 557 if !desc.blank? && alttext.blank?
555 558 alt = " title=\"#{desc}\" alt=\"#{desc}\""
556 559 end
557 560 "src=\"#{image_url}\"#{alt}".html_safe
558 561 else
559 562 m.html_safe
560 563 end
561 564 end
562 565 end
563 566 end
564 567
565 568 # Wiki links
566 569 #
567 570 # Examples:
568 571 # [[mypage]]
569 572 # [[mypage|mytext]]
570 573 # wiki links can refer other project wikis, using project name or identifier:
571 574 # [[project:]] -> wiki starting page
572 575 # [[project:|mytext]]
573 576 # [[project:mypage]]
574 577 # [[project:mypage|mytext]]
575 578 def parse_wiki_links(text, project, obj, attr, only_path, options)
576 579 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
577 580 link_project = project
578 581 esc, all, page, title = $1, $2, $3, $5
579 582 if esc.nil?
580 583 if page =~ /^([^\:]+)\:(.*)$/
581 584 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
582 585 page = $2
583 586 title ||= $1 if page.blank?
584 587 end
585 588
586 589 if link_project && link_project.wiki
587 590 # extract anchor
588 591 anchor = nil
589 592 if page =~ /^(.+?)\#(.+)$/
590 593 page, anchor = $1, $2
591 594 end
592 595 anchor = sanitize_anchor_name(anchor) if anchor.present?
593 596 # check if page exists
594 597 wiki_page = link_project.wiki.find_page(page)
595 598 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
596 599 "##{anchor}"
597 600 else
598 601 case options[:wiki_links]
599 602 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
600 603 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
601 604 else
602 605 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
603 606 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
604 607 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
605 608 :id => wiki_page_id, :anchor => anchor, :parent => parent)
606 609 end
607 610 end
608 611 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
609 612 else
610 613 # project or wiki doesn't exist
611 614 all.html_safe
612 615 end
613 616 else
614 617 all.html_safe
615 618 end
616 619 end
617 620 end
618 621
619 622 # Redmine links
620 623 #
621 624 # Examples:
622 625 # Issues:
623 626 # #52 -> Link to issue #52
624 627 # Changesets:
625 628 # r52 -> Link to revision 52
626 629 # commit:a85130f -> Link to scmid starting with a85130f
627 630 # Documents:
628 631 # document#17 -> Link to document with id 17
629 632 # document:Greetings -> Link to the document with title "Greetings"
630 633 # document:"Some document" -> Link to the document with title "Some document"
631 634 # Versions:
632 635 # version#3 -> Link to version with id 3
633 636 # version:1.0.0 -> Link to version named "1.0.0"
634 637 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
635 638 # Attachments:
636 639 # attachment:file.zip -> Link to the attachment of the current object named file.zip
637 640 # Source files:
638 641 # source:some/file -> Link to the file located at /some/file in the project's repository
639 642 # source:some/file@52 -> Link to the file's revision 52
640 643 # source:some/file#L120 -> Link to line 120 of the file
641 644 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
642 645 # export:some/file -> Force the download of the file
643 646 # Forum messages:
644 647 # message#1218 -> Link to message with id 1218
645 648 #
646 649 # Links can refer other objects from other projects, using project identifier:
647 650 # identifier:r52
648 651 # identifier:document:"Some document"
649 652 # identifier:version:1.0.0
650 653 # identifier:source:some/file
651 654 def parse_redmine_links(text, project, obj, attr, only_path, options)
652 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 656 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
654 657 link = nil
655 658 if project_identifier
656 659 project = Project.visible.find_by_identifier(project_identifier)
657 660 end
658 661 if esc.nil?
659 662 if prefix.nil? && sep == 'r'
660 663 # project.changesets.visible raises an SQL error because of a double join on repositories
661 664 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
662 665 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
663 666 :class => 'changeset',
664 667 :title => truncate_single_line(changeset.comments, :length => 100))
665 668 end
666 669 elsif sep == '#'
667 670 oid = identifier.to_i
668 671 case prefix
669 672 when nil
670 673 if issue = Issue.visible.find_by_id(oid, :include => :status)
671 674 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
672 675 :class => issue.css_classes,
673 676 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
674 677 end
675 678 when 'document'
676 679 if document = Document.visible.find_by_id(oid)
677 680 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
678 681 :class => 'document'
679 682 end
680 683 when 'version'
681 684 if version = Version.visible.find_by_id(oid)
682 685 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
683 686 :class => 'version'
684 687 end
685 688 when 'message'
686 689 if message = Message.visible.find_by_id(oid, :include => :parent)
687 690 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
688 691 end
689 692 when 'forum'
690 693 if board = Board.visible.find_by_id(oid)
691 694 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
692 695 :class => 'board'
693 696 end
694 697 when 'news'
695 698 if news = News.visible.find_by_id(oid)
696 699 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
697 700 :class => 'news'
698 701 end
699 702 when 'project'
700 703 if p = Project.visible.find_by_id(oid)
701 704 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
702 705 end
703 706 end
704 707 elsif sep == ':'
705 708 # removes the double quotes if any
706 709 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
707 710 case prefix
708 711 when 'document'
709 712 if project && document = project.documents.visible.find_by_title(name)
710 713 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
711 714 :class => 'document'
712 715 end
713 716 when 'version'
714 717 if project && version = project.versions.visible.find_by_name(name)
715 718 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
716 719 :class => 'version'
717 720 end
718 721 when 'forum'
719 722 if project && board = project.boards.visible.find_by_name(name)
720 723 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
721 724 :class => 'board'
722 725 end
723 726 when 'news'
724 727 if project && news = project.news.visible.find_by_title(name)
725 728 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
726 729 :class => 'news'
727 730 end
728 731 when 'commit'
729 732 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
730 733 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
731 734 :class => 'changeset',
732 735 :title => truncate_single_line(h(changeset.comments), :length => 100)
733 736 end
734 737 when 'source', 'export'
735 738 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
736 739 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
737 740 path, rev, anchor = $1, $3, $5
738 741 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
739 742 :path => to_path_param(path),
740 743 :rev => rev,
741 744 :anchor => anchor,
742 745 :format => (prefix == 'export' ? 'raw' : nil)},
743 746 :class => (prefix == 'export' ? 'source download' : 'source')
744 747 end
745 748 when 'attachment'
746 749 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
747 750 if attachments && attachment = attachments.detect {|a| a.filename == name }
748 751 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
749 752 :class => 'attachment'
750 753 end
751 754 when 'project'
752 755 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
753 756 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
754 757 end
755 758 end
756 759 end
757 760 end
758 761 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
759 762 end
760 763 end
761 764
762 765 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
763 766
764 767 def parse_sections(text, project, obj, attr, only_path, options)
765 768 return unless options[:edit_section_links]
766 769 text.gsub!(HEADING_RE) do
767 770 @current_section += 1
768 771 if @current_section > 1
769 772 content_tag('div',
770 773 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
771 774 :class => 'contextual',
772 775 :title => l(:button_edit_section)) + $1
773 776 else
774 777 $1
775 778 end
776 779 end
777 780 end
778 781
779 782 # Headings and TOC
780 783 # Adds ids and links to headings unless options[:headings] is set to false
781 784 def parse_headings(text, project, obj, attr, only_path, options)
782 785 return if options[:headings] == false
783 786
784 787 text.gsub!(HEADING_RE) do
785 788 level, attrs, content = $2.to_i, $3, $4
786 789 item = strip_tags(content).strip
787 790 anchor = sanitize_anchor_name(item)
788 791 # used for single-file wiki export
789 792 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
790 793 @parsed_headings << [level, anchor, item]
791 794 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
792 795 end
793 796 end
794 797
795 798 MACROS_RE = /
796 799 (!)? # escaping
797 800 (
798 801 \{\{ # opening tag
799 802 ([\w]+) # macro name
800 803 (\(([^\}]*)\))? # optional arguments
801 804 \}\} # closing tag
802 805 )
803 806 /x unless const_defined?(:MACROS_RE)
804 807
805 808 # Macros substitution
806 809 def parse_macros(text, project, obj, attr, only_path, options)
807 810 text.gsub!(MACROS_RE) do
808 811 esc, all, macro = $1, $2, $3.downcase
809 812 args = ($5 || '').split(',').each(&:strip)
810 813 if esc.nil?
811 814 begin
812 815 exec_macro(macro, obj, args)
813 816 rescue => e
814 817 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
815 818 end || all
816 819 else
817 820 all
818 821 end
819 822 end
820 823 end
821 824
822 825 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
823 826
824 827 # Renders the TOC with given headings
825 828 def replace_toc(text, headings)
826 829 text.gsub!(TOC_RE) do
827 830 if headings.empty?
828 831 ''
829 832 else
830 833 div_class = 'toc'
831 834 div_class << ' right' if $1 == '>'
832 835 div_class << ' left' if $1 == '<'
833 836 out = "<ul class=\"#{div_class}\"><li>"
834 837 root = headings.map(&:first).min
835 838 current = root
836 839 started = false
837 840 headings.each do |level, anchor, item|
838 841 if level > current
839 842 out << '<ul><li>' * (level - current)
840 843 elsif level < current
841 844 out << "</li></ul>\n" * (current - level) + "</li><li>"
842 845 elsif started
843 846 out << '</li><li>'
844 847 end
845 848 out << "<a href=\"##{anchor}\">#{item}</a>"
846 849 current = level
847 850 started = true
848 851 end
849 852 out << '</li></ul>' * (current - root)
850 853 out << '</li></ul>'
851 854 end
852 855 end
853 856 end
854 857
855 858 # Same as Rails' simple_format helper without using paragraphs
856 859 def simple_format_without_paragraph(text)
857 860 text.to_s.
858 861 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
859 862 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
860 863 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
861 864 html_safe
862 865 end
863 866
864 867 def lang_options_for_select(blank=true)
865 868 (blank ? [["(auto)", ""]] : []) +
866 869 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
867 870 end
868 871
869 872 def label_tag_for(name, option_tags = nil, options = {})
870 873 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
871 874 content_tag("label", label_text)
872 875 end
873 876
874 877 def labelled_tabular_form_for(*args, &proc)
875 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 879 args << {} unless args.last.is_a?(Hash)
877 880 options = args.last
878 881 options[:html] ||= {}
879 882 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
880 883 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
881 884 form_for(*args, &proc)
882 885 end
883 886
884 887 def labelled_form_for(*args, &proc)
885 888 args << {} unless args.last.is_a?(Hash)
886 889 options = args.last
887 890 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
888 891 form_for(*args, &proc)
889 892 end
890 893
891 894 def labelled_fields_for(*args, &proc)
892 895 args << {} unless args.last.is_a?(Hash)
893 896 options = args.last
894 897 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
895 898 fields_for(*args, &proc)
896 899 end
897 900
898 901 def labelled_remote_form_for(*args, &proc)
899 902 args << {} unless args.last.is_a?(Hash)
900 903 options = args.last
901 904 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
902 905 remote_form_for(*args, &proc)
903 906 end
904 907
905 908 def back_url_hidden_field_tag
906 909 back_url = params[:back_url] || request.env['HTTP_REFERER']
907 910 back_url = CGI.unescape(back_url.to_s)
908 911 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
909 912 end
910 913
911 914 def check_all_links(form_name)
912 915 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
913 916 " | ".html_safe +
914 917 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
915 918 end
916 919
917 920 def progress_bar(pcts, options={})
918 921 pcts = [pcts, pcts] unless pcts.is_a?(Array)
919 922 pcts = pcts.collect(&:round)
920 923 pcts[1] = pcts[1] - pcts[0]
921 924 pcts << (100 - pcts[1] - pcts[0])
922 925 width = options[:width] || '100px;'
923 926 legend = options[:legend] || ''
924 927 content_tag('table',
925 928 content_tag('tr',
926 929 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
927 930 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
928 931 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
929 932 ), :class => 'progress', :style => "width: #{width};").html_safe +
930 933 content_tag('p', legend, :class => 'pourcent').html_safe
931 934 end
932 935
933 936 def checked_image(checked=true)
934 937 if checked
935 938 image_tag 'toggle_check.png'
936 939 end
937 940 end
938 941
939 942 def context_menu(url)
940 943 unless @context_menu_included
941 944 content_for :header_tags do
942 945 javascript_include_tag('context_menu') +
943 946 stylesheet_link_tag('context_menu')
944 947 end
945 948 if l(:direction) == 'rtl'
946 949 content_for :header_tags do
947 950 stylesheet_link_tag('context_menu_rtl')
948 951 end
949 952 end
950 953 @context_menu_included = true
951 954 end
952 955 javascript_tag "new ContextMenu('#{ url_for(url) }')"
953 956 end
954 957
955 958 def context_menu_link(name, url, options={})
956 959 options[:class] ||= ''
957 960 if options.delete(:selected)
958 961 options[:class] << ' icon-checked disabled'
959 962 options[:disabled] = true
960 963 end
961 964 if options.delete(:disabled)
962 965 options.delete(:method)
963 966 options.delete(:confirm)
964 967 options.delete(:onclick)
965 968 options[:class] << ' disabled'
966 969 url = '#'
967 970 end
968 971 link_to h(name), url, options
969 972 end
970 973
971 974 def calendar_for(field_id)
972 975 include_calendar_headers_tags
973 976 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
974 977 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
975 978 end
976 979
977 980 def include_calendar_headers_tags
978 981 unless @calendar_headers_tags_included
979 982 @calendar_headers_tags_included = true
980 983 content_for :header_tags do
981 984 start_of_week = case Setting.start_of_week.to_i
982 985 when 1
983 986 'Calendar._FD = 1;' # Monday
984 987 when 7
985 988 'Calendar._FD = 0;' # Sunday
986 989 when 6
987 990 'Calendar._FD = 6;' # Saturday
988 991 else
989 992 '' # use language
990 993 end
991 994
992 995 javascript_include_tag('calendar/calendar') +
993 996 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
994 997 javascript_tag(start_of_week) +
995 998 javascript_include_tag('calendar/calendar-setup') +
996 999 stylesheet_link_tag('calendar')
997 1000 end
998 1001 end
999 1002 end
1000 1003
1001 1004 def content_for(name, content = nil, &block)
1002 1005 @has_content ||= {}
1003 1006 @has_content[name] = true
1004 1007 super(name, content, &block)
1005 1008 end
1006 1009
1007 1010 def has_content?(name)
1008 1011 (@has_content && @has_content[name]) || false
1009 1012 end
1010 1013
1011 1014 def email_delivery_enabled?
1012 1015 !!ActionMailer::Base.perform_deliveries
1013 1016 end
1014 1017
1015 1018 # Returns the avatar image tag for the given +user+ if avatars are enabled
1016 1019 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1017 1020 def avatar(user, options = { })
1018 1021 if Setting.gravatar_enabled?
1019 1022 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1020 1023 email = nil
1021 1024 if user.respond_to?(:mail)
1022 1025 email = user.mail
1023 1026 elsif user.to_s =~ %r{<(.+?)>}
1024 1027 email = $1
1025 1028 end
1026 1029 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1027 1030 else
1028 1031 ''
1029 1032 end
1030 1033 end
1031 1034
1032 1035 def sanitize_anchor_name(anchor)
1033 1036 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1034 1037 end
1035 1038
1036 1039 # Returns the javascript tags that are included in the html layout head
1037 1040 def javascript_heads
1038 1041 tags = javascript_include_tag(:defaults)
1039 1042 unless User.current.pref.warn_on_leaving_unsaved == '0'
1040 1043 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1041 1044 end
1042 1045 tags
1043 1046 end
1044 1047
1045 1048 def favicon
1046 1049 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1047 1050 end
1048 1051
1049 1052 def robot_exclusion_tag
1050 1053 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1051 1054 end
1052 1055
1053 1056 # Returns true if arg is expected in the API response
1054 1057 def include_in_api_response?(arg)
1055 1058 unless @included_in_api_response
1056 1059 param = params[:include]
1057 1060 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1058 1061 @included_in_api_response.collect!(&:strip)
1059 1062 end
1060 1063 @included_in_api_response.include?(arg.to_s)
1061 1064 end
1062 1065
1063 1066 # Returns options or nil if nometa param or X-Redmine-Nometa header
1064 1067 # was set in the request
1065 1068 def api_meta(options)
1066 1069 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1067 1070 # compatibility mode for activeresource clients that raise
1068 1071 # an error when unserializing an array with attributes
1069 1072 nil
1070 1073 else
1071 1074 options
1072 1075 end
1073 1076 end
1074 1077
1075 1078 private
1076 1079
1077 1080 def wiki_helper
1078 1081 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1079 1082 extend helper
1080 1083 return self
1081 1084 end
1082 1085
1083 1086 def link_to_content_update(text, url_params = {}, html_options = {})
1084 1087 link_to(text, url_params, html_options)
1085 1088 end
1086 1089 end
@@ -1,311 +1,314
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'iconv'
21 21 require 'redmine/codeset_util'
22 22
23 23 module RepositoriesHelper
24 24 def format_revision(revision)
25 25 if revision.respond_to? :format_identifier
26 26 revision.format_identifier
27 27 else
28 28 revision.to_s
29 29 end
30 30 end
31 31
32 32 def truncate_at_line_break(text, length = 255)
33 33 if text
34 34 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
35 35 end
36 36 end
37 37
38 38 def render_properties(properties)
39 39 unless properties.nil? || properties.empty?
40 40 content = ''
41 41 properties.keys.sort.each do |property|
42 42 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
43 43 end
44 44 content_tag('ul', content.html_safe, :class => 'properties')
45 45 end
46 46 end
47 47
48 48 def render_changeset_changes
49 49 changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
50 50 case change.action
51 51 when 'A'
52 52 # Detects moved/copied files
53 53 if !change.from_path.blank?
54 54 change.action =
55 55 @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
56 56 end
57 57 change
58 58 when 'D'
59 59 @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
60 60 else
61 61 change
62 62 end
63 63 end.compact
64 64
65 65 tree = { }
66 66 changes.each do |change|
67 67 p = tree
68 68 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
69 69 path = ''
70 70 dirs.each do |dir|
71 71 path += '/' + dir
72 72 p[:s] ||= {}
73 73 p = p[:s]
74 74 p[path] ||= {}
75 75 p = p[path]
76 76 end
77 77 p[:c] = change
78 78 end
79 79 render_changes_tree(tree[:s])
80 80 end
81 81
82 82 def render_changes_tree(tree)
83 83 return '' if tree.nil?
84 84 output = ''
85 85 output << '<ul>'
86 86 tree.keys.sort.each do |file|
87 87 style = 'change'
88 88 text = File.basename(h(file))
89 89 if s = tree[file][:s]
90 90 style << ' folder'
91 91 path_param = to_path_param(@repository.relative_path(file))
92 92 text = link_to(h(text), :controller => 'repositories',
93 93 :action => 'show',
94 94 :id => @project,
95 :repository_id => @repository.identifier_param,
95 96 :path => path_param,
96 97 :rev => @changeset.identifier)
97 98 output << "<li class='#{style}'>#{text}"
98 99 output << render_changes_tree(s)
99 100 output << "</li>"
100 101 elsif c = tree[file][:c]
101 102 style << " change-#{c.action}"
102 103 path_param = to_path_param(@repository.relative_path(c.path))
103 104 text = link_to(h(text), :controller => 'repositories',
104 105 :action => 'entry',
105 106 :id => @project,
107 :repository_id => @repository.identifier_param,
106 108 :path => path_param,
107 109 :rev => @changeset.identifier) unless c.action == 'D'
108 110 text << " - #{h(c.revision)}" unless c.revision.blank?
109 111 text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
110 112 :action => 'diff',
111 113 :id => @project,
114 :repository_id => @repository.identifier_param,
112 115 :path => path_param,
113 116 :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
114 117 text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
115 118 output << "<li class='#{style}'>#{text}</li>"
116 119 end
117 120 end
118 121 output << '</ul>'
119 122 output.html_safe
120 123 end
121 124
122 125 def repository_field_tags(form, repository)
123 126 method = repository.class.name.demodulize.underscore + "_field_tags"
124 127 if repository.is_a?(Repository) &&
125 128 respond_to?(method) && method != 'repository_field_tags'
126 129 send(method, form, repository)
127 130 end
128 131 end
129 132
130 133 def scm_select_tag(repository)
131 134 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
132 135 Redmine::Scm::Base.all.each do |scm|
133 136 if Setting.enabled_scm.include?(scm) ||
134 137 (repository && repository.class.name.demodulize == scm)
135 138 scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
136 139 end
137 140 end
138 141 select_tag('repository_scm',
139 142 options_for_select(scm_options, repository.class.name.demodulize),
140 143 :disabled => (repository && !repository.new_record?),
141 144 :onchange => remote_function(
142 145 :url => new_project_repository_path(@project),
143 146 :method => :get,
144 147 :update => 'content',
145 148 :with => "Form.serialize(this.form)")
146 149 )
147 150 end
148 151
149 152 def with_leading_slash(path)
150 153 path.to_s.starts_with?('/') ? path : "/#{path}"
151 154 end
152 155
153 156 def without_leading_slash(path)
154 157 path.gsub(%r{^/+}, '')
155 158 end
156 159
157 160 def subversion_field_tags(form, repository)
158 161 content_tag('p', form.text_field(:url, :size => 60, :required => true,
159 162 :disabled => (repository && !repository.root_url.blank?)) +
160 163 '<br />'.html_safe +
161 164 '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
162 165 content_tag('p', form.text_field(:login, :size => 30)) +
163 166 content_tag('p', form.password_field(
164 167 :password, :size => 30, :name => 'ignore',
165 168 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
166 169 :onfocus => "this.value=''; this.name='repository[password]';",
167 170 :onchange => "this.name='repository[password]';"))
168 171 end
169 172
170 173 def darcs_field_tags(form, repository)
171 174 content_tag('p', form.text_field(
172 175 :url, :label => l(:field_path_to_repository),
173 176 :size => 60, :required => true,
174 177 :disabled => (repository && !repository.new_record?))) +
175 178 content_tag('p', form.select(
176 179 :log_encoding, [nil] + Setting::ENCODINGS,
177 180 :label => l(:field_commit_logs_encoding), :required => true))
178 181 end
179 182
180 183 def mercurial_field_tags(form, repository)
181 184 content_tag('p', form.text_field(
182 185 :url, :label => l(:field_path_to_repository),
183 186 :size => 60, :required => true,
184 187 :disabled => (repository && !repository.root_url.blank?)
185 188 ) +
186 189 '<br />'.html_safe + l(:text_mercurial_repository_note)) +
187 190 content_tag('p', form.select(
188 191 :path_encoding, [nil] + Setting::ENCODINGS,
189 192 :label => l(:field_scm_path_encoding)
190 193 ) +
191 194 '<br />'.html_safe + l(:text_scm_path_encoding_note))
192 195 end
193 196
194 197 def git_field_tags(form, repository)
195 198 content_tag('p', form.text_field(
196 199 :url, :label => l(:field_path_to_repository),
197 200 :size => 60, :required => true,
198 201 :disabled => (repository && !repository.root_url.blank?)
199 202 ) +
200 203 '<br />'.html_safe +
201 204 l(:text_git_repository_note)) +
202 205 content_tag('p', form.select(
203 206 :path_encoding, [nil] + Setting::ENCODINGS,
204 207 :label => l(:field_scm_path_encoding)
205 208 ) +
206 209 '<br />'.html_safe + l(:text_scm_path_encoding_note)) +
207 210 content_tag('p', form.check_box(
208 211 :extra_report_last_commit,
209 212 :label => l(:label_git_report_last_commit)
210 213 ))
211 214 end
212 215
213 216 def cvs_field_tags(form, repository)
214 217 content_tag('p', form.text_field(
215 218 :root_url,
216 219 :label => l(:field_cvsroot),
217 220 :size => 60, :required => true,
218 221 :disabled => !repository.new_record?)) +
219 222 content_tag('p', form.text_field(
220 223 :url,
221 224 :label => l(:field_cvs_module),
222 225 :size => 30, :required => true,
223 226 :disabled => !repository.new_record?)) +
224 227 content_tag('p', form.select(
225 228 :log_encoding, [nil] + Setting::ENCODINGS,
226 229 :label => l(:field_commit_logs_encoding), :required => true)) +
227 230 content_tag('p', form.select(
228 231 :path_encoding, [nil] + Setting::ENCODINGS,
229 232 :label => l(:field_scm_path_encoding)
230 233 ) +
231 234 '<br />'.html_safe + l(:text_scm_path_encoding_note))
232 235 end
233 236
234 237 def bazaar_field_tags(form, repository)
235 238 content_tag('p', form.text_field(
236 239 :url, :label => l(:field_path_to_repository),
237 240 :size => 60, :required => true,
238 241 :disabled => (repository && !repository.new_record?))) +
239 242 content_tag('p', form.select(
240 243 :log_encoding, [nil] + Setting::ENCODINGS,
241 244 :label => l(:field_commit_logs_encoding), :required => true))
242 245 end
243 246
244 247 def filesystem_field_tags(form, repository)
245 248 content_tag('p', form.text_field(
246 249 :url, :label => l(:field_root_directory),
247 250 :size => 60, :required => true,
248 251 :disabled => (repository && !repository.root_url.blank?))) +
249 252 content_tag('p', form.select(
250 253 :path_encoding, [nil] + Setting::ENCODINGS,
251 254 :label => l(:field_scm_path_encoding)
252 255 ) +
253 256 '<br />'.html_safe + l(:text_scm_path_encoding_note))
254 257 end
255 258
256 259 def index_commits(commits, heads, href_proc = nil)
257 260 return nil if commits.nil? or commits.first.parents.nil?
258 261 map = {}
259 262 commit_hashes = []
260 263 refs_map = {}
261 264 href_proc ||= Proc.new {|x|x}
262 265 heads.each{|r| refs_map[r.scmid] ||= []; refs_map[r.scmid] << r}
263 266 commits.reverse.each_with_index do |c, i|
264 267 h = {}
265 268 h[:parents] = c.parents.collect do |p|
266 269 [p.scmid, 0, 0]
267 270 end
268 271 h[:rdmid] = i
269 272 h[:space] = 0
270 273 h[:refs] = refs_map[c.scmid].join(" ") if refs_map.include? c.scmid
271 274 h[:scmid] = c.scmid
272 275 h[:href] = href_proc.call(c.scmid)
273 276 commit_hashes << h
274 277 map[c.scmid] = h
275 278 end
276 279 heads.sort! do |a,b|
277 280 a.to_s <=> b.to_s
278 281 end
279 282 j = 0
280 283 heads.each do |h|
281 284 if map.include? h.scmid then
282 285 j = mark_chain(j += 1, map[h.scmid], map)
283 286 end
284 287 end
285 288 # when no head matched anything use first commit
286 289 if j == 0 then
287 290 mark_chain(j += 1, map.values.first, map)
288 291 end
289 292 map
290 293 end
291 294
292 295 def mark_chain(mark, commit, map)
293 296 stack = [[mark, commit]]
294 297 markmax = mark
295 298 until stack.empty?
296 299 current = stack.pop
297 300 m, commit = current
298 301 commit[:space] = m if commit[:space] == 0
299 302 m1 = m - 1
300 303 commit[:parents].each_with_index do |p, i|
301 304 psha = p[0]
302 305 if map.include? psha and map[psha][:space] == 0 then
303 306 stack << [m1 += 1, map[psha]] if i == 0
304 307 stack = [[m1 += 1, map[psha]]] + stack if i > 0
305 308 end
306 309 end
307 310 markmax = m1 if markmax < m1
308 311 end
309 312 markmax
310 313 end
311 314 end
@@ -1,888 +1,889
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_ARCHIVED = 9
24 24
25 25 # Maximum length for project identifiers
26 26 IDENTIFIER_MAX_LENGTH = 100
27 27
28 28 # Specific overidden Activities
29 29 has_many :time_entry_activities
30 30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 31 has_many :memberships, :class_name => 'Member'
32 32 has_many :member_principals, :class_name => 'Member',
33 33 :include => :principal,
34 34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 35 has_many :users, :through => :members
36 36 has_many :principals, :through => :member_principals, :source => :principal
37 37
38 38 has_many :enabled_modules, :dependent => :delete_all
39 39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 41 has_many :issue_changes, :through => :issues, :source => :journals
42 42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 43 has_many :time_entries, :dependent => :delete_all
44 44 has_many :queries, :dependent => :delete_all
45 45 has_many :documents, :dependent => :destroy
46 46 has_many :news, :dependent => :destroy, :include => :author
47 47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 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 51 has_many :changesets, :through => :repository
51 52 has_one :wiki, :dependent => :destroy
52 53 # Custom field for the project issues
53 54 has_and_belongs_to_many :issue_custom_fields,
54 55 :class_name => 'IssueCustomField',
55 56 :order => "#{CustomField.table_name}.position",
56 57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 58 :association_foreign_key => 'custom_field_id'
58 59
59 60 acts_as_nested_set :order => 'name', :dependent => :destroy
60 61 acts_as_attachable :view_permission => :view_files,
61 62 :delete_permission => :manage_files
62 63
63 64 acts_as_customizable
64 65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 68 :author => nil
68 69
69 70 attr_protected :status
70 71
71 72 validates_presence_of :name, :identifier
72 73 validates_uniqueness_of :identifier
73 74 validates_associated :repository, :wiki
74 75 validates_length_of :name, :maximum => 255
75 76 validates_length_of :homepage, :maximum => 255
76 77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 78 # donwcase letters, digits, dashes but not digits only
78 79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 80 # reserved words
80 81 validates_exclusion_of :identifier, :in => %w( new )
81 82
82 83 before_destroy :delete_all_members
83 84
84 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 86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
87 88 named_scope :all_public, { :conditions => { :is_public => true } }
88 89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
89 90 named_scope :like, lambda {|arg|
90 91 if arg.blank?
91 92 {}
92 93 else
93 94 pattern = "%#{arg.to_s.strip.downcase}%"
94 95 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
95 96 end
96 97 }
97 98
98 99 def initialize(attributes=nil, *args)
99 100 super
100 101
101 102 initialized = (attributes || {}).stringify_keys
102 103 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
103 104 self.identifier = Project.next_identifier
104 105 end
105 106 if !initialized.key?('is_public')
106 107 self.is_public = Setting.default_projects_public?
107 108 end
108 109 if !initialized.key?('enabled_module_names')
109 110 self.enabled_module_names = Setting.default_projects_modules
110 111 end
111 112 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
112 113 self.trackers = Tracker.all
113 114 end
114 115 end
115 116
116 117 def identifier=(identifier)
117 118 super unless identifier_frozen?
118 119 end
119 120
120 121 def identifier_frozen?
121 122 errors[:identifier].nil? && !(new_record? || identifier.blank?)
122 123 end
123 124
124 125 # returns latest created projects
125 126 # non public projects will be returned only if user is a member of those
126 127 def self.latest(user=nil, count=5)
127 128 visible(user).find(:all, :limit => count, :order => "created_on DESC")
128 129 end
129 130
130 131 # Returns true if the project is visible to +user+ or to the current user.
131 132 def visible?(user=User.current)
132 133 user.allowed_to?(:view_project, self)
133 134 end
134 135
135 136 # Returns a SQL conditions string used to find all projects visible by the specified user.
136 137 #
137 138 # Examples:
138 139 # Project.visible_condition(admin) => "projects.status = 1"
139 140 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
140 141 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
141 142 def self.visible_condition(user, options={})
142 143 allowed_to_condition(user, :view_project, options)
143 144 end
144 145
145 146 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
146 147 #
147 148 # Valid options:
148 149 # * :project => limit the condition to project
149 150 # * :with_subprojects => limit the condition to project and its subprojects
150 151 # * :member => limit the condition to the user projects
151 152 def self.allowed_to_condition(user, permission, options={})
152 153 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
153 154 if perm = Redmine::AccessControl.permission(permission)
154 155 unless perm.project_module.nil?
155 156 # If the permission belongs to a project module, make sure the module is enabled
156 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 158 end
158 159 end
159 160 if options[:project]
160 161 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
161 162 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
162 163 base_statement = "(#{project_statement}) AND (#{base_statement})"
163 164 end
164 165
165 166 if user.admin?
166 167 base_statement
167 168 else
168 169 statement_by_role = {}
169 170 unless options[:member]
170 171 role = user.logged? ? Role.non_member : Role.anonymous
171 172 if role.allowed_to?(permission)
172 173 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
173 174 end
174 175 end
175 176 if user.logged?
176 177 user.projects_by_role.each do |role, projects|
177 178 if role.allowed_to?(permission)
178 179 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
179 180 end
180 181 end
181 182 end
182 183 if statement_by_role.empty?
183 184 "1=0"
184 185 else
185 186 if block_given?
186 187 statement_by_role.each do |role, statement|
187 188 if s = yield(role, user)
188 189 statement_by_role[role] = "(#{statement} AND (#{s}))"
189 190 end
190 191 end
191 192 end
192 193 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
193 194 end
194 195 end
195 196 end
196 197
197 198 # Returns the Systemwide and project specific activities
198 199 def activities(include_inactive=false)
199 200 if include_inactive
200 201 return all_activities
201 202 else
202 203 return active_activities
203 204 end
204 205 end
205 206
206 207 # Will create a new Project specific Activity or update an existing one
207 208 #
208 209 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
209 210 # does not successfully save.
210 211 def update_or_create_time_entry_activity(id, activity_hash)
211 212 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
212 213 self.create_time_entry_activity_if_needed(activity_hash)
213 214 else
214 215 activity = project.time_entry_activities.find_by_id(id.to_i)
215 216 activity.update_attributes(activity_hash) if activity
216 217 end
217 218 end
218 219
219 220 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
220 221 #
221 222 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
222 223 # does not successfully save.
223 224 def create_time_entry_activity_if_needed(activity)
224 225 if activity['parent_id']
225 226
226 227 parent_activity = TimeEntryActivity.find(activity['parent_id'])
227 228 activity['name'] = parent_activity.name
228 229 activity['position'] = parent_activity.position
229 230
230 231 if Enumeration.overridding_change?(activity, parent_activity)
231 232 project_activity = self.time_entry_activities.create(activity)
232 233
233 234 if project_activity.new_record?
234 235 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
235 236 else
236 237 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
237 238 end
238 239 end
239 240 end
240 241 end
241 242
242 243 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
243 244 #
244 245 # Examples:
245 246 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
246 247 # project.project_condition(false) => "projects.id = 1"
247 248 def project_condition(with_subprojects)
248 249 cond = "#{Project.table_name}.id = #{id}"
249 250 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
250 251 cond
251 252 end
252 253
253 254 def self.find(*args)
254 255 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
255 256 project = find_by_identifier(*args)
256 257 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
257 258 project
258 259 else
259 260 super
260 261 end
261 262 end
262 263
263 264 def to_param
264 265 # id is used for projects with a numeric identifier (compatibility)
265 266 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
266 267 end
267 268
268 269 def active?
269 270 self.status == STATUS_ACTIVE
270 271 end
271 272
272 273 def archived?
273 274 self.status == STATUS_ARCHIVED
274 275 end
275 276
276 277 # Archives the project and its descendants
277 278 def archive
278 279 # Check that there is no issue of a non descendant project that is assigned
279 280 # to one of the project or descendant versions
280 281 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
281 282 if v_ids.any? && Issue.find(:first, :include => :project,
282 283 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
283 284 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
284 285 return false
285 286 end
286 287 Project.transaction do
287 288 archive!
288 289 end
289 290 true
290 291 end
291 292
292 293 # Unarchives the project
293 294 # All its ancestors must be active
294 295 def unarchive
295 296 return false if ancestors.detect {|a| !a.active?}
296 297 update_attribute :status, STATUS_ACTIVE
297 298 end
298 299
299 300 # Returns an array of projects the project can be moved to
300 301 # by the current user
301 302 def allowed_parents
302 303 return @allowed_parents if @allowed_parents
303 304 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
304 305 @allowed_parents = @allowed_parents - self_and_descendants
305 306 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
306 307 @allowed_parents << nil
307 308 end
308 309 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
309 310 @allowed_parents << parent
310 311 end
311 312 @allowed_parents
312 313 end
313 314
314 315 # Sets the parent of the project with authorization check
315 316 def set_allowed_parent!(p)
316 317 unless p.nil? || p.is_a?(Project)
317 318 if p.to_s.blank?
318 319 p = nil
319 320 else
320 321 p = Project.find_by_id(p)
321 322 return false unless p
322 323 end
323 324 end
324 325 if p.nil?
325 326 if !new_record? && allowed_parents.empty?
326 327 return false
327 328 end
328 329 elsif !allowed_parents.include?(p)
329 330 return false
330 331 end
331 332 set_parent!(p)
332 333 end
333 334
334 335 # Sets the parent of the project
335 336 # Argument can be either a Project, a String, a Fixnum or nil
336 337 def set_parent!(p)
337 338 unless p.nil? || p.is_a?(Project)
338 339 if p.to_s.blank?
339 340 p = nil
340 341 else
341 342 p = Project.find_by_id(p)
342 343 return false unless p
343 344 end
344 345 end
345 346 if p == parent && !p.nil?
346 347 # Nothing to do
347 348 true
348 349 elsif p.nil? || (p.active? && move_possible?(p))
349 350 # Insert the project so that target's children or root projects stay alphabetically sorted
350 351 sibs = (p.nil? ? self.class.roots : p.children)
351 352 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
352 353 if to_be_inserted_before
353 354 move_to_left_of(to_be_inserted_before)
354 355 elsif p.nil?
355 356 if sibs.empty?
356 357 # move_to_root adds the project in first (ie. left) position
357 358 move_to_root
358 359 else
359 360 move_to_right_of(sibs.last) unless self == sibs.last
360 361 end
361 362 else
362 363 # move_to_child_of adds the project in last (ie.right) position
363 364 move_to_child_of(p)
364 365 end
365 366 Issue.update_versions_from_hierarchy_change(self)
366 367 true
367 368 else
368 369 # Can not move to the given target
369 370 false
370 371 end
371 372 end
372 373
373 374 # Returns an array of the trackers used by the project and its active sub projects
374 375 def rolled_up_trackers
375 376 @rolled_up_trackers ||=
376 377 Tracker.find(:all, :joins => :projects,
377 378 :select => "DISTINCT #{Tracker.table_name}.*",
378 379 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
379 380 :order => "#{Tracker.table_name}.position")
380 381 end
381 382
382 383 # Closes open and locked project versions that are completed
383 384 def close_completed_versions
384 385 Version.transaction do
385 386 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
386 387 if version.completed?
387 388 version.update_attribute(:status, 'closed')
388 389 end
389 390 end
390 391 end
391 392 end
392 393
393 394 # Returns a scope of the Versions on subprojects
394 395 def rolled_up_versions
395 396 @rolled_up_versions ||=
396 397 Version.scoped(:include => :project,
397 398 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
398 399 end
399 400
400 401 # Returns a scope of the Versions used by the project
401 402 def shared_versions
402 403 @shared_versions ||= begin
403 404 r = root? ? self : root
404 405 Version.scoped(:include => :project,
405 406 :conditions => "#{Project.table_name}.id = #{id}" +
406 407 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
407 408 " #{Version.table_name}.sharing = 'system'" +
408 409 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
409 410 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
410 411 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
411 412 "))")
412 413 end
413 414 end
414 415
415 416 # Returns a hash of project users grouped by role
416 417 def users_by_role
417 418 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
418 419 m.roles.each do |r|
419 420 h[r] ||= []
420 421 h[r] << m.user
421 422 end
422 423 h
423 424 end
424 425 end
425 426
426 427 # Deletes all project's members
427 428 def delete_all_members
428 429 me, mr = Member.table_name, MemberRole.table_name
429 430 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
430 431 Member.delete_all(['project_id = ?', id])
431 432 end
432 433
433 434 # Users/groups issues can be assigned to
434 435 def assignable_users
435 436 assignable = Setting.issue_group_assignment? ? member_principals : members
436 437 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
437 438 end
438 439
439 440 # Returns the mail adresses of users that should be always notified on project events
440 441 def recipients
441 442 notified_users.collect {|user| user.mail}
442 443 end
443 444
444 445 # Returns the users that should be notified on project events
445 446 def notified_users
446 447 # TODO: User part should be extracted to User#notify_about?
447 448 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
448 449 end
449 450
450 451 # Returns an array of all custom fields enabled for project issues
451 452 # (explictly associated custom fields and custom fields enabled for all projects)
452 453 def all_issue_custom_fields
453 454 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
454 455 end
455 456
456 457 # Returns an array of all custom fields enabled for project time entries
457 458 # (explictly associated custom fields and custom fields enabled for all projects)
458 459 def all_time_entry_custom_fields
459 460 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
460 461 end
461 462
462 463 def project
463 464 self
464 465 end
465 466
466 467 def <=>(project)
467 468 name.downcase <=> project.name.downcase
468 469 end
469 470
470 471 def to_s
471 472 name
472 473 end
473 474
474 475 # Returns a short description of the projects (first lines)
475 476 def short_description(length = 255)
476 477 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
477 478 end
478 479
479 480 def css_classes
480 481 s = 'project'
481 482 s << ' root' if root?
482 483 s << ' child' if child?
483 484 s << (leaf? ? ' leaf' : ' parent')
484 485 s
485 486 end
486 487
487 488 # The earliest start date of a project, based on it's issues and versions
488 489 def start_date
489 490 [
490 491 issues.minimum('start_date'),
491 492 shared_versions.collect(&:effective_date),
492 493 shared_versions.collect(&:start_date)
493 494 ].flatten.compact.min
494 495 end
495 496
496 497 # The latest due date of an issue or version
497 498 def due_date
498 499 [
499 500 issues.maximum('due_date'),
500 501 shared_versions.collect(&:effective_date),
501 502 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
502 503 ].flatten.compact.max
503 504 end
504 505
505 506 def overdue?
506 507 active? && !due_date.nil? && (due_date < Date.today)
507 508 end
508 509
509 510 # Returns the percent completed for this project, based on the
510 511 # progress on it's versions.
511 512 def completed_percent(options={:include_subprojects => false})
512 513 if options.delete(:include_subprojects)
513 514 total = self_and_descendants.collect(&:completed_percent).sum
514 515
515 516 total / self_and_descendants.count
516 517 else
517 518 if versions.count > 0
518 519 total = versions.collect(&:completed_pourcent).sum
519 520
520 521 total / versions.count
521 522 else
522 523 100
523 524 end
524 525 end
525 526 end
526 527
527 528 # Return true if this project is allowed to do the specified action.
528 529 # action can be:
529 530 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
530 531 # * a permission Symbol (eg. :edit_project)
531 532 def allows_to?(action)
532 533 if action.is_a? Hash
533 534 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
534 535 else
535 536 allowed_permissions.include? action
536 537 end
537 538 end
538 539
539 540 def module_enabled?(module_name)
540 541 module_name = module_name.to_s
541 542 enabled_modules.detect {|m| m.name == module_name}
542 543 end
543 544
544 545 def enabled_module_names=(module_names)
545 546 if module_names && module_names.is_a?(Array)
546 547 module_names = module_names.collect(&:to_s).reject(&:blank?)
547 548 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
548 549 else
549 550 enabled_modules.clear
550 551 end
551 552 end
552 553
553 554 # Returns an array of the enabled modules names
554 555 def enabled_module_names
555 556 enabled_modules.collect(&:name)
556 557 end
557 558
558 559 # Enable a specific module
559 560 #
560 561 # Examples:
561 562 # project.enable_module!(:issue_tracking)
562 563 # project.enable_module!("issue_tracking")
563 564 def enable_module!(name)
564 565 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
565 566 end
566 567
567 568 # Disable a module if it exists
568 569 #
569 570 # Examples:
570 571 # project.disable_module!(:issue_tracking)
571 572 # project.disable_module!("issue_tracking")
572 573 # project.disable_module!(project.enabled_modules.first)
573 574 def disable_module!(target)
574 575 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
575 576 target.destroy unless target.blank?
576 577 end
577 578
578 579 safe_attributes 'name',
579 580 'description',
580 581 'homepage',
581 582 'is_public',
582 583 'identifier',
583 584 'custom_field_values',
584 585 'custom_fields',
585 586 'tracker_ids',
586 587 'issue_custom_field_ids'
587 588
588 589 safe_attributes 'enabled_module_names',
589 590 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
590 591
591 592 # Returns an array of projects that are in this project's hierarchy
592 593 #
593 594 # Example: parents, children, siblings
594 595 def hierarchy
595 596 parents = project.self_and_ancestors || []
596 597 descendants = project.descendants || []
597 598 project_hierarchy = parents | descendants # Set union
598 599 end
599 600
600 601 # Returns an auto-generated project identifier based on the last identifier used
601 602 def self.next_identifier
602 603 p = Project.find(:first, :order => 'created_on DESC')
603 604 p.nil? ? nil : p.identifier.to_s.succ
604 605 end
605 606
606 607 # Copies and saves the Project instance based on the +project+.
607 608 # Duplicates the source project's:
608 609 # * Wiki
609 610 # * Versions
610 611 # * Categories
611 612 # * Issues
612 613 # * Members
613 614 # * Queries
614 615 #
615 616 # Accepts an +options+ argument to specify what to copy
616 617 #
617 618 # Examples:
618 619 # project.copy(1) # => copies everything
619 620 # project.copy(1, :only => 'members') # => copies members only
620 621 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
621 622 def copy(project, options={})
622 623 project = project.is_a?(Project) ? project : Project.find(project)
623 624
624 625 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
625 626 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
626 627
627 628 Project.transaction do
628 629 if save
629 630 reload
630 631 to_be_copied.each do |name|
631 632 send "copy_#{name}", project
632 633 end
633 634 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
634 635 save
635 636 end
636 637 end
637 638 end
638 639
639 640
640 641 # Copies +project+ and returns the new instance. This will not save
641 642 # the copy
642 643 def self.copy_from(project)
643 644 begin
644 645 project = project.is_a?(Project) ? project : Project.find(project)
645 646 if project
646 647 # clear unique attributes
647 648 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
648 649 copy = Project.new(attributes)
649 650 copy.enabled_modules = project.enabled_modules
650 651 copy.trackers = project.trackers
651 652 copy.custom_values = project.custom_values.collect {|v| v.clone}
652 653 copy.issue_custom_fields = project.issue_custom_fields
653 654 return copy
654 655 else
655 656 return nil
656 657 end
657 658 rescue ActiveRecord::RecordNotFound
658 659 return nil
659 660 end
660 661 end
661 662
662 663 # Yields the given block for each project with its level in the tree
663 664 def self.project_tree(projects, &block)
664 665 ancestors = []
665 666 projects.sort_by(&:lft).each do |project|
666 667 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
667 668 ancestors.pop
668 669 end
669 670 yield project, ancestors.size
670 671 ancestors << project
671 672 end
672 673 end
673 674
674 675 private
675 676
676 677 # Copies wiki from +project+
677 678 def copy_wiki(project)
678 679 # Check that the source project has a wiki first
679 680 unless project.wiki.nil?
680 681 self.wiki ||= Wiki.new
681 682 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
682 683 wiki_pages_map = {}
683 684 project.wiki.pages.each do |page|
684 685 # Skip pages without content
685 686 next if page.content.nil?
686 687 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
687 688 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
688 689 new_wiki_page.content = new_wiki_content
689 690 wiki.pages << new_wiki_page
690 691 wiki_pages_map[page.id] = new_wiki_page
691 692 end
692 693 wiki.save
693 694 # Reproduce page hierarchy
694 695 project.wiki.pages.each do |page|
695 696 if page.parent_id && wiki_pages_map[page.id]
696 697 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
697 698 wiki_pages_map[page.id].save
698 699 end
699 700 end
700 701 end
701 702 end
702 703
703 704 # Copies versions from +project+
704 705 def copy_versions(project)
705 706 project.versions.each do |version|
706 707 new_version = Version.new
707 708 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
708 709 self.versions << new_version
709 710 end
710 711 end
711 712
712 713 # Copies issue categories from +project+
713 714 def copy_issue_categories(project)
714 715 project.issue_categories.each do |issue_category|
715 716 new_issue_category = IssueCategory.new
716 717 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
717 718 self.issue_categories << new_issue_category
718 719 end
719 720 end
720 721
721 722 # Copies issues from +project+
722 723 # Note: issues assigned to a closed version won't be copied due to validation rules
723 724 def copy_issues(project)
724 725 # Stores the source issue id as a key and the copied issues as the
725 726 # value. Used to map the two togeather for issue relations.
726 727 issues_map = {}
727 728
728 729 # Get issues sorted by root_id, lft so that parent issues
729 730 # get copied before their children
730 731 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
731 732 new_issue = Issue.new
732 733 new_issue.copy_from(issue)
733 734 new_issue.project = self
734 735 # Reassign fixed_versions by name, since names are unique per
735 736 # project and the versions for self are not yet saved
736 737 if issue.fixed_version
737 738 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
738 739 end
739 740 # Reassign the category by name, since names are unique per
740 741 # project and the categories for self are not yet saved
741 742 if issue.category
742 743 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
743 744 end
744 745 # Parent issue
745 746 if issue.parent_id
746 747 if copied_parent = issues_map[issue.parent_id]
747 748 new_issue.parent_issue_id = copied_parent.id
748 749 end
749 750 end
750 751
751 752 self.issues << new_issue
752 753 if new_issue.new_record?
753 754 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
754 755 else
755 756 issues_map[issue.id] = new_issue unless new_issue.new_record?
756 757 end
757 758 end
758 759
759 760 # Relations after in case issues related each other
760 761 project.issues.each do |issue|
761 762 new_issue = issues_map[issue.id]
762 763 unless new_issue
763 764 # Issue was not copied
764 765 next
765 766 end
766 767
767 768 # Relations
768 769 issue.relations_from.each do |source_relation|
769 770 new_issue_relation = IssueRelation.new
770 771 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
771 772 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
772 773 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
773 774 new_issue_relation.issue_to = source_relation.issue_to
774 775 end
775 776 new_issue.relations_from << new_issue_relation
776 777 end
777 778
778 779 issue.relations_to.each do |source_relation|
779 780 new_issue_relation = IssueRelation.new
780 781 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
781 782 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
782 783 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
783 784 new_issue_relation.issue_from = source_relation.issue_from
784 785 end
785 786 new_issue.relations_to << new_issue_relation
786 787 end
787 788 end
788 789 end
789 790
790 791 # Copies members from +project+
791 792 def copy_members(project)
792 793 # Copy users first, then groups to handle members with inherited and given roles
793 794 members_to_copy = []
794 795 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
795 796 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
796 797
797 798 members_to_copy.each do |member|
798 799 new_member = Member.new
799 800 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
800 801 # only copy non inherited roles
801 802 # inherited roles will be added when copying the group membership
802 803 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
803 804 next if role_ids.empty?
804 805 new_member.role_ids = role_ids
805 806 new_member.project = self
806 807 self.members << new_member
807 808 end
808 809 end
809 810
810 811 # Copies queries from +project+
811 812 def copy_queries(project)
812 813 project.queries.each do |query|
813 814 new_query = ::Query.new
814 815 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
815 816 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
816 817 new_query.project = self
817 818 new_query.user_id = query.user_id
818 819 self.queries << new_query
819 820 end
820 821 end
821 822
822 823 # Copies boards from +project+
823 824 def copy_boards(project)
824 825 project.boards.each do |board|
825 826 new_board = Board.new
826 827 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
827 828 new_board.project = self
828 829 self.boards << new_board
829 830 end
830 831 end
831 832
832 833 def allowed_permissions
833 834 @allowed_permissions ||= begin
834 835 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
835 836 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
836 837 end
837 838 end
838 839
839 840 def allowed_actions
840 841 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
841 842 end
842 843
843 844 # Returns all the active Systemwide and project specific activities
844 845 def active_activities
845 846 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
846 847
847 848 if overridden_activity_ids.empty?
848 849 return TimeEntryActivity.shared.active
849 850 else
850 851 return system_activities_and_project_overrides
851 852 end
852 853 end
853 854
854 855 # Returns all the Systemwide and project specific activities
855 856 # (inactive and active)
856 857 def all_activities
857 858 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
858 859
859 860 if overridden_activity_ids.empty?
860 861 return TimeEntryActivity.shared
861 862 else
862 863 return system_activities_and_project_overrides(true)
863 864 end
864 865 end
865 866
866 867 # Returns the systemwide active activities merged with the project specific overrides
867 868 def system_activities_and_project_overrides(include_inactive=false)
868 869 if include_inactive
869 870 return TimeEntryActivity.shared.
870 871 find(:all,
871 872 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
872 873 self.time_entry_activities
873 874 else
874 875 return TimeEntryActivity.shared.active.
875 876 find(:all,
876 877 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
877 878 self.time_entry_activities.active
878 879 end
879 880 end
880 881
881 882 # Archives subprojects recursively
882 883 def archive!
883 884 children.each do |subproject|
884 885 subproject.send :archive!
885 886 end
886 887 update_attribute :status, STATUS_ARCHIVED
887 888 end
888 889 end
@@ -1,345 +1,408
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ScmFetchError < Exception; end
19 19
20 20 class Repository < ActiveRecord::Base
21 21 include Redmine::Ciphering
22 22
23 23 belongs_to :project
24 24 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
25 25 has_many :changes, :through => :changesets
26 26
27 27 serialize :extra_info
28 28
29 before_save :check_default
30
29 31 # Raw SQL to delete changesets and changes in the database
30 32 # has_many :changesets, :dependent => :destroy is too slow for big repositories
31 33 before_destroy :clear_changesets
32 34
33 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 42 # Checks if the SCM is enabled when creating a repository
35 43 validate :repo_create_validation, :on => :create
36 44
37 45 def repo_create_validation
38 46 unless Setting.enabled_scm.include?(self.class.name.demodulize)
39 47 errors.add(:type, :invalid)
40 48 end
41 49 end
42 50
43 51 def self.human_attribute_name(attribute_key_name, *args)
44 52 attr_name = attribute_key_name
45 53 if attr_name == "log_encoding"
46 54 attr_name = "commit_logs_encoding"
47 55 end
48 56 super(attr_name, *args)
49 57 end
50 58
51 59 alias :attributes_without_extra_info= :attributes=
52 60 def attributes=(new_attributes, guard_protected_attributes = true)
53 61 return if new_attributes.nil?
54 62 attributes = new_attributes.dup
55 63 attributes.stringify_keys!
56 64
57 65 p = {}
58 66 p_extra = {}
59 67 attributes.each do |k, v|
60 68 if k =~ /^extra_/
61 69 p_extra[k] = v
62 70 else
63 71 p[k] = v
64 72 end
65 73 end
66 74
67 75 send :attributes_without_extra_info=, p, guard_protected_attributes
68 merge_extra_info(p_extra)
76 if p_extra.keys.any?
77 merge_extra_info(p_extra)
78 end
69 79 end
70 80
71 81 # Removes leading and trailing whitespace
72 82 def url=(arg)
73 83 write_attribute(:url, arg ? arg.to_s.strip : nil)
74 84 end
75 85
76 86 # Removes leading and trailing whitespace
77 87 def root_url=(arg)
78 88 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
79 89 end
80 90
81 91 def password
82 92 read_ciphered_attribute(:password)
83 93 end
84 94
85 95 def password=(arg)
86 96 write_ciphered_attribute(:password, arg)
87 97 end
88 98
89 99 def scm_adapter
90 100 self.class.scm_adapter_class
91 101 end
92 102
93 103 def scm
94 104 @scm ||= self.scm_adapter.new(url, root_url,
95 105 login, password, path_encoding)
96 106 update_attribute(:root_url, @scm.root_url) if root_url.blank?
97 107 @scm
98 108 end
99 109
100 110 def scm_name
101 111 self.class.scm_name
102 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 152 def merge_extra_info(arg)
105 153 h = extra_info || {}
106 154 return h if arg.nil?
107 155 h.merge!(arg)
108 156 write_attribute(:extra_info, h)
109 157 end
110 158
111 159 def report_last_commit
112 160 true
113 161 end
114 162
115 163 def supports_cat?
116 164 scm.supports_cat?
117 165 end
118 166
119 167 def supports_annotate?
120 168 scm.supports_annotate?
121 169 end
122 170
123 171 def supports_all_revisions?
124 172 true
125 173 end
126 174
127 175 def supports_directory_revisions?
128 176 false
129 177 end
130 178
131 179 def supports_revision_graph?
132 180 false
133 181 end
134 182
135 183 def entry(path=nil, identifier=nil)
136 184 scm.entry(path, identifier)
137 185 end
138 186
139 187 def entries(path=nil, identifier=nil)
140 188 scm.entries(path, identifier)
141 189 end
142 190
143 191 def branches
144 192 scm.branches
145 193 end
146 194
147 195 def tags
148 196 scm.tags
149 197 end
150 198
151 199 def default_branch
152 200 nil
153 201 end
154 202
155 203 def properties(path, identifier=nil)
156 204 scm.properties(path, identifier)
157 205 end
158 206
159 207 def cat(path, identifier=nil)
160 208 scm.cat(path, identifier)
161 209 end
162 210
163 211 def diff(path, rev, rev_to)
164 212 scm.diff(path, rev, rev_to)
165 213 end
166 214
167 215 def diff_format_revisions(cs, cs_to, sep=':')
168 216 text = ""
169 217 text << cs_to.format_identifier + sep if cs_to
170 218 text << cs.format_identifier if cs
171 219 text
172 220 end
173 221
174 222 # Returns a path relative to the url of the repository
175 223 def relative_path(path)
176 224 path
177 225 end
178 226
179 227 # Finds and returns a revision with a number or the beginning of a hash
180 228 def find_changeset_by_name(name)
181 229 return nil if name.blank?
182 230 changesets.find(:first, :conditions => (name.match(/^\d*$/) ?
183 231 ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
184 232 end
185 233
186 234 def latest_changeset
187 235 @latest_changeset ||= changesets.find(:first)
188 236 end
189 237
190 238 # Returns the latest changesets for +path+
191 239 # Default behaviour is to search in cached changesets
192 240 def latest_changesets(path, rev, limit=10)
193 241 if path.blank?
194 242 changesets.find(
195 243 :all,
196 244 :include => :user,
197 245 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
198 246 :limit => limit)
199 247 else
200 248 changes.find(
201 249 :all,
202 250 :include => {:changeset => :user},
203 251 :conditions => ["path = ?", path.with_leading_slash],
204 252 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
205 253 :limit => limit
206 254 ).collect(&:changeset)
207 255 end
208 256 end
209 257
210 258 def scan_changesets_for_issue_ids
211 259 self.changesets.each(&:scan_comment_for_issue_ids)
212 260 end
213 261
214 262 # Returns an array of committers usernames and associated user_id
215 263 def committers
216 264 @committers ||= Changeset.connection.select_rows(
217 265 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
218 266 end
219 267
220 268 # Maps committers username to a user ids
221 269 def committer_ids=(h)
222 270 if h.is_a?(Hash)
223 271 committers.each do |committer, user_id|
224 272 new_user_id = h[committer]
225 273 if new_user_id && (new_user_id.to_i != user_id.to_i)
226 274 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
227 275 Changeset.update_all(
228 276 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
229 277 ["repository_id = ? AND committer = ?", id, committer])
230 278 end
231 279 end
232 280 @committers = nil
233 281 @found_committer_users = nil
234 282 true
235 283 else
236 284 false
237 285 end
238 286 end
239 287
240 288 # Returns the Redmine User corresponding to the given +committer+
241 289 # It will return nil if the committer is not yet mapped and if no User
242 290 # with the same username or email was found
243 291 def find_committer_user(committer)
244 292 unless committer.blank?
245 293 @found_committer_users ||= {}
246 294 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
247 295
248 296 user = nil
249 297 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
250 298 if c && c.user
251 299 user = c.user
252 300 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
253 301 username, email = $1.strip, $3
254 302 u = User.find_by_login(username)
255 303 u ||= User.find_by_mail(email) unless email.blank?
256 304 user = u
257 305 end
258 306 @found_committer_users[committer] = user
259 307 user
260 308 end
261 309 end
262 310
263 311 def repo_log_encoding
264 312 encoding = log_encoding.to_s.strip
265 313 encoding.blank? ? 'UTF-8' : encoding
266 314 end
267 315
268 316 # Fetches new changesets for all repositories of active projects
269 317 # Can be called periodically by an external script
270 318 # eg. ruby script/runner "Repository.fetch_changesets"
271 319 def self.fetch_changesets
272 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
273 if project.repository
320 Project.active.has_module(:repository).all.each do |project|
321 project.repositories.each do |repository|
274 322 begin
275 project.repository.fetch_changesets
323 repository.fetch_changesets
276 324 rescue Redmine::Scm::Adapters::CommandFailed => e
277 325 logger.error "scm: error during fetching changesets: #{e.message}"
278 326 end
279 327 end
280 328 end
281 329 end
282 330
283 331 # scan changeset comments to find related and fixed issues for all repositories
284 332 def self.scan_changesets_for_issue_ids
285 333 find(:all).each(&:scan_changesets_for_issue_ids)
286 334 end
287 335
288 336 def self.scm_name
289 337 'Abstract'
290 338 end
291 339
292 340 def self.available_scm
293 341 subclasses.collect {|klass| [klass.scm_name, klass.name]}
294 342 end
295 343
296 344 def self.factory(klass_name, *args)
297 345 klass = "Repository::#{klass_name}".constantize
298 346 klass.new(*args)
299 347 rescue
300 348 nil
301 349 end
302 350
303 351 def self.scm_adapter_class
304 352 nil
305 353 end
306 354
307 355 def self.scm_command
308 356 ret = ""
309 357 begin
310 358 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
311 359 rescue Exception => e
312 360 logger.error "scm: error during get command: #{e.message}"
313 361 end
314 362 ret
315 363 end
316 364
317 365 def self.scm_version_string
318 366 ret = ""
319 367 begin
320 368 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
321 369 rescue Exception => e
322 370 logger.error "scm: error during get version string: #{e.message}"
323 371 end
324 372 ret
325 373 end
326 374
327 375 def self.scm_available
328 376 ret = false
329 377 begin
330 378 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
331 379 rescue Exception => e
332 380 logger.error "scm: error during get scm available: #{e.message}"
333 381 end
334 382 ret
335 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 400 private
338 401
339 402 def clear_changesets
340 403 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
341 404 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
342 405 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
343 406 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
344 407 end
345 408 end
@@ -1,10 +1,10
1 1 <% changesets.each do |changeset| %>
2 2 <div class="changeset <%= cycle('odd', 'even') %>">
3 <p><%= link_to_revision(changeset, changeset.project,
3 <p><%= link_to_revision(changeset, changeset.repository,
4 4 :text => "#{l(:label_revision)} #{changeset.format_identifier}") %><br />
5 5 <span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p>
6 6 <div class="wiki">
7 7 <%= textilizable(changeset, :comments) %>
8 8 </div>
9 9 </div>
10 10 <% end %>
@@ -1,36 +1,41
1 <% if @project.repository %>
1 <% if @project.repositories.any? %>
2 2 <table class="list">
3 3 <thead>
4 4 <tr>
5 5 <th><%= l(:label_scm) %></th>
6 <th><%= l(:field_identifier) %></th>
7 <th><%= l(:field_repository_is_default) %></th>
6 8 <th><%= l(:label_repository) %></th>
7 9 <th></th>
8 10 </tr>
9 11 </thead>
10 12 <tbody>
11 <% repository = @project.repository %>
13 <% @project.repositories.each do |repository| %>
12 14 <tr class="<%= cycle 'odd', 'even' %>">
13 15 <td><%=h repository.scm_name %></td>
16 <td><%=h repository.identifier %></td>
17 <td align="center"><%= checked_image repository.is_default? %></td>
14 18 <td><%=h repository.url %></td>
15 19 <td class="buttons">
16 20 <% if User.current.allowed_to?(:manage_repository, @project) %>
17 21 <%= link_to(l(:label_user_plural), committers_repository_path(repository),
18 22 :class => 'icon icon-user') %>
19 23 <%= link_to(l(:button_edit), edit_repository_path(repository),
20 24 :class => 'icon icon-edit') %>
21 25 <%= link_to(l(:button_delete), repository_path(repository),
22 26 :confirm => l(:text_are_you_sure),
23 27 :method => :delete,
24 28 :class => 'icon icon-del') %>
25 29 <% end %>
26 30 </td>
27 31 </tr>
32 <% end %>
28 33 </tbody>
29 34 </table>
30 35 <% else %>
31 36 <p class="nodata"><%= l(:label_no_data) %></p>
32 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 40 <p><%= link_to l(:label_repository_new), new_project_repository_path(@project), :class => 'icon icon-add' %></p>
36 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 3 dirs = path.split('/')
4 4 if 'file' == kind
5 5 filename = dirs.pop
6 6 end
7 7 link_path = ''
8 8 dirs.each do |dir|
9 9 next if dir.blank?
10 10 link_path << '/' unless link_path.empty?
11 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 14 :path => to_path_param(link_path), :rev => @rev %>
15 15 <% end %>
16 16 <% if filename %>
17 17 / <%= link_to h(filename),
18 :action => 'changes', :id => @project,
18 :action => 'changes', :id => @project, :repository_id => @repository.identifier_param,
19 19 :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
20 20 <% end %>
21 21 <%
22 22 # @rev is revsion or Git and Mercurial branch or tag.
23 23 # For Mercurial *tip*, @rev and @changeset are nil.
24 24 rev_text = @changeset.nil? ? @rev : format_revision(@changeset)
25 25 %>
26 26 <%= "@ #{h rev_text}" unless rev_text.blank? %>
27 27
28 28 <% html_title(with_leading_slash(path)) -%>
@@ -1,39 +1,40
1 1 <% @entries.each do |entry| %>
2 2 <% tr_id = Digest::MD5.hexdigest(entry.path)
3 3 depth = params[:depth].to_i %>
4 4 <% ent_path = Redmine::CodesetUtil.replace_invalid_utf8(entry.path) %>
5 5 <% ent_name = Redmine::CodesetUtil.replace_invalid_utf8(entry.name) %>
6 6 <tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= entry.kind %>">
7 7 <td style="padding-left: <%=18 * depth%>px;" class="<%=
8 8 @repository.report_last_commit ? "filename" : "filename_no_report" %>";>
9 9 <% if entry.is_dir? %>
10 10 <span class="expander" onclick="<%= remote_function(
11 11 :url => {
12 12 :action => 'show',
13 13 :id => @project,
14 :repository_id => @repository.identifier_param,
14 15 :path => to_path_param(ent_path),
15 16 :rev => @rev,
16 17 :depth => (depth + 1),
17 18 :parent_id => tr_id
18 19 },
19 20 :method => :get,
20 21 :update => { :success => tr_id },
21 22 :position => :after,
22 23 :success => "scmEntryLoaded('#{tr_id}')",
23 24 :condition => "scmEntryClick('#{tr_id}')"
24 25 ) %>">&nbsp</span>
25 26 <% end %>
26 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 29 :class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
29 30 </td>
30 31 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
31 32 <% changeset = @project.repository.find_changeset_by_name(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
32 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 35 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
35 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 37 <td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
37 38 <% end %>
38 39 </tr>
39 40 <% end %>
@@ -1,22 +1,25
1 1 <%= error_messages_for 'repository' %>
2 2
3 3 <div class="box tabular">
4 4 <p>
5 5 <%= label_tag('repository_scm', l(:label_scm)) %><%= scm_select_tag(@repository) %>
6 6 <% if @repository && ! @repository.class.scm_available %>
7 7 <br />
8 8 <em><%= content_tag 'span', l(:text_scm_command_not_available), :class => 'error' %></em>
9 9 <% end %>
10 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 15 <% button_disabled = true %>
13 16 <% if @repository %>
14 17 <% button_disabled = ! @repository.class.scm_available %>
15 18 <%= repository_field_tags(f, @repository)%>
16 19 <% end %>
17 20 </div>
18 21
19 22 <p>
20 23 <%= submit_tag(@repository.new_record? ? l(:button_create) : l(:button_save), :disabled => button_disabled) %>
21 24 <%= link_to l(:button_cancel), settings_project_path(@project, :tab => 'repositories') %>
22 25 </p> No newline at end of file
@@ -1,31 +1,32
1 1 <% content_for :header_tags do %>
2 2 <%= javascript_include_tag 'repository_navigation' %>
3 3 <% end %>
4 4
5 5 <%= link_to l(:label_statistics),
6 {:action => 'stats', :id => @project},
6 {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param},
7 7 :class => 'icon icon-stats' %>
8 8
9 9 <% form_tag({:action => controller.action_name,
10 10 :id => @project,
11 :repository_id => @repository.identifier_param,
11 12 :path => to_path_param(@path),
12 13 :rev => ''},
13 14 {:method => :get, :id => 'revision_selector'}) do -%>
14 15 <!-- Branches Dropdown -->
15 16 <% if !@repository.branches.nil? && @repository.branches.length > 0 -%>
16 17 | <%= l(:label_branch) %>:
17 18 <%= select_tag :branch,
18 19 options_for_select([''] + @repository.branches, @rev),
19 20 :id => 'branch' %>
20 21 <% end -%>
21 22
22 23 <% if !@repository.tags.nil? && @repository.tags.length > 0 -%>
23 24 | <%= l(:label_tag) %>:
24 25 <%= select_tag :tag,
25 26 options_for_select([''] + @repository.tags, @rev),
26 27 :id => 'tag' %>
27 28 <% end -%>
28 29
29 30 | <%= l(:label_revision) %>:
30 31 <%= text_field_tag 'rev', @rev, :size => 8 %>
31 32 <% end -%>
@@ -1,56 +1,57
1 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 3 <table class="list changesets">
4 4 <thead><tr>
5 5 <% if show_revision_graph %>
6 6 <th></th>
7 7 <% end %>
8 8 <th>#</th>
9 9 <th></th>
10 10 <th></th>
11 11 <th><%= l(:label_date) %></th>
12 12 <th><%= l(:field_author) %></th>
13 13 <th><%= l(:field_comments) %></th>
14 14 </tr></thead>
15 15 <tbody>
16 16 <% show_diff = revisions.size > 1 %>
17 17 <% line_num = 1 %>
18 18 <% revisions.each do |changeset| %>
19 19 <tr class="changeset <%= cycle 'odd', 'even' %>">
20 20 <% if show_revision_graph %>
21 21 <% if line_num == 1 %>
22 22 <td class="revision_graph" rowspan="<%= revisions.size %>">
23 23 <% href_base = Proc.new {|x| url_for(:controller => 'repositories',
24 24 :action => 'revision',
25 25 :id => project,
26 :repository_id => @repository.identifier_param,
26 27 :rev => x) } %>
27 28 <%= render :partial => 'revision_graph',
28 29 :locals => {
29 30 :commits => index_commits(
30 31 revisions,
31 32 @repository.branches,
32 33 href_base
33 34 )
34 35 } %>
35 36 </td>
36 37 <% end %>
37 38 <% end %>
38 <td class="id"><%= link_to_revision(changeset, project) %></td>
39 <td class="id"><%= link_to_revision(changeset, @repository) %></td>
39 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 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 42 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
42 43 <td class="author"><%= h truncate(changeset.author.to_s, :length => 30) %></td>
43 44 <% if show_revision_graph %>
44 45 <td class="comments_nowrap">
45 46 <%= textilizable(truncate(truncate_at_line_break(changeset.comments, 0), :length => 90)) %>
46 47 </td>
47 48 <% else %>
48 49 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
49 50 <% end %>
50 51 </tr>
51 52 <% line_num += 1 %>
52 53 <% end %>
53 54 </tbody>
54 55 </table>
55 56 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
56 57 <% end %>
@@ -1,36 +1,36
1 1 <%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
2 2
3 3 <div class="contextual">
4 4 <%= render :partial => 'navigation' %>
5 5 </div>
6 6
7 7 <h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
8 8
9 9 <p><%= render :partial => 'link_to_functions' %></p>
10 10
11 11 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
12 12
13 13 <div class="autoscroll">
14 14 <table class="filecontent annotate syntaxhl">
15 15 <tbody>
16 16 <% line_num = 1 %>
17 17 <% syntax_highlight(@path, Redmine::CodesetUtil.to_utf8_by_setting(@annotate.content)).each_line do |line| %>
18 18 <% revision = @annotate.revisions[line_num - 1] %>
19 19 <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
20 20 <th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
21 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 23 <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
24 24 <td class="line-code"><pre><%= line %></pre></td>
25 25 </tr>
26 26 <% line_num += 1 %>
27 27 <% end %>
28 28 </tbody>
29 29 </table>
30 30 </div>
31 31
32 32 <% html_title(l(:button_annotate)) -%>
33 33
34 34 <% content_for :header_tags do %>
35 35 <%= stylesheet_link_tag 'scm' %>
36 36 <% end %>
@@ -1,100 +1,102
1 1 <div class="contextual">
2 2 &#171;
3 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 5 <% else -%>
6 6 <%= l(:label_previous) %>
7 7 <% end -%>
8 8 |
9 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 11 <% else -%>
12 12 <%= l(:label_next) %>
13 13 <% end -%>
14 14 &#187;&nbsp;
15 15
16 16 <% form_tag({:controller => 'repositories',
17 17 :action => 'revision',
18 18 :id => @project,
19 :repository_id => @repository.identifier_param,
19 20 :rev => nil},
20 21 :method => :get) do %>
21 22 <%= text_field_tag 'rev', @rev, :size => 8 %>
22 23 <%= submit_tag 'OK', :name => nil %>
23 24 <% end %>
24 25 </div>
25 26
26 27 <h2><%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
27 28
28 29 <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %>
29 30 <table class="revision-info">
30 31 <% if @changeset.scmid.present? %>
31 32 <tr>
32 33 <td>ID</td><td><%= h(@changeset.scmid) %></td>
33 34 </tr>
34 35 <% end %>
35 36 <% if @changeset.parents.present? %>
36 37 <tr>
37 38 <td><%= l(:label_parent_revision) %></td>
38 39 <td>
39 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 42 }.join(", ").html_safe %>
42 43 </td>
43 44 </tr>
44 45 <% end %>
45 46 <% if @changeset.children.present? %>
46 47 <tr>
47 48 <td><%= l(:label_child_revision) %></td>
48 49 <td>
49 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 52 }.join(", ").html_safe %>
52 53 </td>
53 54 </tr>
54 55 <% end %>
55 56 </table>
56 57 <% end %>
57 58
58 59 <p>
59 60 <span class="author">
60 61 <%= authoring(@changeset.committed_on, @changeset.author) %>
61 62 </span>
62 63 </p>
63 64
64 65 <%= textilizable @changeset.comments %>
65 66
66 67 <% if @changeset.issues.visible.any? %>
67 68 <h3><%= l(:label_related_issues) %></h3>
68 69 <ul>
69 70 <% @changeset.issues.visible.each do |issue| %>
70 71 <li><%= link_to_issue issue %></li>
71 72 <% end %>
72 73 </ul>
73 74 <% end %>
74 75
75 76 <% if User.current.allowed_to?(:browse_repository, @project) %>
76 77 <h3><%= l(:label_attachment_plural) %></h3>
77 78 <ul id="changes-legend">
78 79 <li class="change change-A"><%= l(:label_added) %></li>
79 80 <li class="change change-M"><%= l(:label_modified) %></li>
80 81 <li class="change change-C"><%= l(:label_copied) %></li>
81 82 <li class="change change-R"><%= l(:label_renamed) %></li>
82 83 <li class="change change-D"><%= l(:label_deleted) %></li>
83 84 </ul>
84 85
85 86 <p><%= link_to(l(:label_view_diff),
86 87 :action => 'diff',
87 88 :id => @project,
89 :repository_id => @repository.identifier_param,
88 90 :path => "",
89 91 :rev => @changeset.identifier) if @changeset.changes.any? %></p>
90 92
91 93 <div class="changeset-changes">
92 94 <%= render_changeset_changes %>
93 95 </div>
94 96 <% end %>
95 97
96 98 <% content_for :header_tags do %>
97 99 <%= stylesheet_link_tag "scm" %>
98 100 <% end %>
99 101
100 102 <% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%>
@@ -1,30 +1,30
1 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 3 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
4 4 <%= submit_tag 'OK' %>
5 5 <% end %>
6 6 </div>
7 7
8 8 <h2><%= l(:label_revision_plural) %></h2>
9 9
10 10 <%= render :partial => 'revisions',
11 11 :locals => {:project => @project,
12 12 :path => '',
13 13 :revisions => @changesets,
14 14 :entry => nil } %>
15 15
16 16 <p class="pagination"><%= pagination_links_full @changeset_pages,@changeset_count %></p>
17 17
18 18 <% content_for :header_tags do %>
19 19 <%= stylesheet_link_tag "scm" %>
20 20 <%= auto_discovery_link_tag(
21 21 :atom,
22 22 params.merge(
23 23 {:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
24 24 <% end %>
25 25
26 26 <% other_formats_links do |f| %>
27 27 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
28 28 <% end %>
29 29
30 30 <% html_title(l(:label_revision_plural)) -%>
@@ -1,64 +1,76
1 1 <%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
2 2
3 3 <div class="contextual">
4 4 <%= render :partial => 'navigation' %>
5 5 </div>
6 6
7 7 <h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2>
8 8
9 9 <% if !@entries.nil? && authorize_for('repositories', 'browse') %>
10 10 <%= render :partial => 'dir_list' %>
11 11 <% end %>
12 12
13 13 <%= render_properties(@properties) %>
14 14
15 15 <% if authorize_for('repositories', 'revisions') %>
16 16 <% if @changesets && !@changesets.empty? %>
17 17 <h3><%= l(:label_latest_revision_plural) %></h3>
18 18 <%= render :partial => 'revisions',
19 19 :locals => {:project => @project, :path => @path,
20 20 :revisions => @changesets, :entry => nil }%>
21 21 <% end %>
22 22 <p>
23 23 <%
24 24 has_branches = (!@repository.branches.nil? && @repository.branches.length > 0)
25 25 sep = ''
26 26 %>
27 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 29 <% sep = '|' %>
30 30 <% end %>
31 31 <%
32 32 if @repository.supports_directory_revisions? &&
33 33 ( has_branches || !@path.blank? || !@rev.blank? )
34 34 %>
35 35 <%= sep %>
36 36 <%=
37 37 link_to l(:label_view_revisions),
38 38 :action => 'changes',
39 39 :path => to_path_param(@path),
40 40 :id => @project,
41 :repository_id => @repository.identifier_param,
41 42 :rev => @rev
42 43 %>
43 44 <% end %>
44 45 </p>
45 46
46 47 <% if true # @path.blank? %>
47 48 <% content_for :header_tags do %>
48 49 <%= auto_discovery_link_tag(
49 50 :atom, params.merge(
50 51 {:format => 'atom', :action => 'revisions',
51 52 :id => @project, :page => nil, :key => User.current.rss_key})) %>
52 53 <% end %>
53 54
54 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 57 <% end %>
57 58 <% end %>
58 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 72 <% content_for :header_tags do %>
61 73 <%= stylesheet_link_tag "scm" %>
62 74 <% end %>
63 75
64 76 <% html_title(l(:label_repository)) -%>
@@ -1,12 +1,12
1 1 <h2><%= l(:label_statistics) %></h2>
2 2
3 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 5 </p>
6 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 8 </p>
9 9
10 10 <p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>
11 11
12 12 <% html_title(l(:label_repository), l(:label_statistics)) -%>
@@ -1,1012 +1,1013
1 1 en:
2 2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 3 direction: ltr
4 4 date:
5 5 formats:
6 6 # Use the strftime parameters for formats.
7 7 # When no format has been given, it uses default.
8 8 # You can provide other formats here if you like!
9 9 default: "%m/%d/%Y"
10 10 short: "%b %d"
11 11 long: "%B %d, %Y"
12 12
13 13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15 15
16 16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 19 # Used in date_select and datime_select.
20 20 order:
21 21 - :year
22 22 - :month
23 23 - :day
24 24
25 25 time:
26 26 formats:
27 27 default: "%m/%d/%Y %I:%M %p"
28 28 time: "%I:%M %p"
29 29 short: "%d %b %H:%M"
30 30 long: "%B %d, %Y %H:%M"
31 31 am: "am"
32 32 pm: "pm"
33 33
34 34 datetime:
35 35 distance_in_words:
36 36 half_a_minute: "half a minute"
37 37 less_than_x_seconds:
38 38 one: "less than 1 second"
39 39 other: "less than %{count} seconds"
40 40 x_seconds:
41 41 one: "1 second"
42 42 other: "%{count} seconds"
43 43 less_than_x_minutes:
44 44 one: "less than a minute"
45 45 other: "less than %{count} minutes"
46 46 x_minutes:
47 47 one: "1 minute"
48 48 other: "%{count} minutes"
49 49 about_x_hours:
50 50 one: "about 1 hour"
51 51 other: "about %{count} hours"
52 52 x_days:
53 53 one: "1 day"
54 54 other: "%{count} days"
55 55 about_x_months:
56 56 one: "about 1 month"
57 57 other: "about %{count} months"
58 58 x_months:
59 59 one: "1 month"
60 60 other: "%{count} months"
61 61 about_x_years:
62 62 one: "about 1 year"
63 63 other: "about %{count} years"
64 64 over_x_years:
65 65 one: "over 1 year"
66 66 other: "over %{count} years"
67 67 almost_x_years:
68 68 one: "almost 1 year"
69 69 other: "almost %{count} years"
70 70
71 71 number:
72 72 format:
73 73 separator: "."
74 74 delimiter: ""
75 75 precision: 3
76 76
77 77 human:
78 78 format:
79 79 delimiter: ""
80 80 precision: 1
81 81 storage_units:
82 82 format: "%n %u"
83 83 units:
84 84 byte:
85 85 one: "Byte"
86 86 other: "Bytes"
87 87 kb: "kB"
88 88 mb: "MB"
89 89 gb: "GB"
90 90 tb: "TB"
91 91
92 92 # Used in array.to_sentence.
93 93 support:
94 94 array:
95 95 sentence_connector: "and"
96 96 skip_last_comma: false
97 97
98 98 activerecord:
99 99 errors:
100 100 template:
101 101 header:
102 102 one: "1 error prohibited this %{model} from being saved"
103 103 other: "%{count} errors prohibited this %{model} from being saved"
104 104 messages:
105 105 inclusion: "is not included in the list"
106 106 exclusion: "is reserved"
107 107 invalid: "is invalid"
108 108 confirmation: "doesn't match confirmation"
109 109 accepted: "must be accepted"
110 110 empty: "can't be empty"
111 111 blank: "can't be blank"
112 112 too_long: "is too long (maximum is %{count} characters)"
113 113 too_short: "is too short (minimum is %{count} characters)"
114 114 wrong_length: "is the wrong length (should be %{count} characters)"
115 115 taken: "has already been taken"
116 116 not_a_number: "is not a number"
117 117 not_a_date: "is not a valid date"
118 118 greater_than: "must be greater than %{count}"
119 119 greater_than_or_equal_to: "must be greater than or equal to %{count}"
120 120 equal_to: "must be equal to %{count}"
121 121 less_than: "must be less than %{count}"
122 122 less_than_or_equal_to: "must be less than or equal to %{count}"
123 123 odd: "must be odd"
124 124 even: "must be even"
125 125 greater_than_start_date: "must be greater than start date"
126 126 not_same_project: "doesn't belong to the same project"
127 127 circular_dependency: "This relation would create a circular dependency"
128 128 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
129 129
130 130 actionview_instancetag_blank_option: Please select
131 131
132 132 general_text_No: 'No'
133 133 general_text_Yes: 'Yes'
134 134 general_text_no: 'no'
135 135 general_text_yes: 'yes'
136 136 general_lang_name: 'English'
137 137 general_csv_separator: ','
138 138 general_csv_decimal_separator: '.'
139 139 general_csv_encoding: ISO-8859-1
140 140 general_pdf_encoding: UTF-8
141 141 general_first_day_of_week: '7'
142 142
143 143 notice_account_updated: Account was successfully updated.
144 144 notice_account_invalid_creditentials: Invalid user or password
145 145 notice_account_password_updated: Password was successfully updated.
146 146 notice_account_wrong_password: Wrong password
147 147 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
148 148 notice_account_unknown_email: Unknown user.
149 149 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
150 150 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
151 151 notice_account_activated: Your account has been activated. You can now log in.
152 152 notice_successful_create: Successful creation.
153 153 notice_successful_update: Successful update.
154 154 notice_successful_delete: Successful deletion.
155 155 notice_successful_connection: Successful connection.
156 156 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
157 157 notice_locking_conflict: Data has been updated by another user.
158 158 notice_not_authorized: You are not authorized to access this page.
159 159 notice_not_authorized_archived_project: The project you're trying to access has been archived.
160 160 notice_email_sent: "An email was sent to %{value}"
161 161 notice_email_error: "An error occurred while sending mail (%{value})"
162 162 notice_feeds_access_key_reseted: Your RSS access key was reset.
163 163 notice_api_access_key_reseted: Your API access key was reset.
164 164 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
165 165 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
166 166 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
167 167 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
168 168 notice_account_pending: "Your account was created and is now pending administrator approval."
169 169 notice_default_data_loaded: Default configuration successfully loaded.
170 170 notice_unable_delete_version: Unable to delete version.
171 171 notice_unable_delete_time_entry: Unable to delete time log entry.
172 172 notice_issue_done_ratios_updated: Issue done ratios updated.
173 173 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
174 174 notice_issue_successful_create: "Issue %{id} created."
175 175
176 176 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
177 177 error_scm_not_found: "The entry or revision was not found in the repository."
178 178 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
179 179 error_scm_annotate: "The entry does not exist or cannot be annotated."
180 180 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
181 181 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
182 182 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
183 183 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
184 184 error_can_not_delete_custom_field: Unable to delete custom field
185 185 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
186 186 error_can_not_remove_role: "This role is in use and cannot be deleted."
187 187 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
188 188 error_can_not_archive_project: This project cannot be archived
189 189 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
190 190 error_workflow_copy_source: 'Please select a source tracker or role'
191 191 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
192 192 error_unable_delete_issue_status: 'Unable to delete issue status'
193 193 error_unable_to_connect: "Unable to connect (%{value})"
194 194 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
195 195 warning_attachments_not_saved: "%{count} file(s) could not be saved."
196 196
197 197 mail_subject_lost_password: "Your %{value} password"
198 198 mail_body_lost_password: 'To change your password, click on the following link:'
199 199 mail_subject_register: "Your %{value} account activation"
200 200 mail_body_register: 'To activate your account, click on the following link:'
201 201 mail_body_account_information_external: "You can use your %{value} account to log in."
202 202 mail_body_account_information: Your account information
203 203 mail_subject_account_activation_request: "%{value} account activation request"
204 204 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
205 205 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
206 206 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
207 207 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
208 208 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
209 209 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
210 210 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
211 211
212 212 gui_validation_error: 1 error
213 213 gui_validation_error_plural: "%{count} errors"
214 214
215 215 field_name: Name
216 216 field_description: Description
217 217 field_summary: Summary
218 218 field_is_required: Required
219 219 field_firstname: First name
220 220 field_lastname: Last name
221 221 field_mail: Email
222 222 field_filename: File
223 223 field_filesize: Size
224 224 field_downloads: Downloads
225 225 field_author: Author
226 226 field_created_on: Created
227 227 field_updated_on: Updated
228 228 field_field_format: Format
229 229 field_is_for_all: For all projects
230 230 field_possible_values: Possible values
231 231 field_regexp: Regular expression
232 232 field_min_length: Minimum length
233 233 field_max_length: Maximum length
234 234 field_value: Value
235 235 field_category: Category
236 236 field_title: Title
237 237 field_project: Project
238 238 field_issue: Issue
239 239 field_status: Status
240 240 field_notes: Notes
241 241 field_is_closed: Issue closed
242 242 field_is_default: Default value
243 243 field_tracker: Tracker
244 244 field_subject: Subject
245 245 field_due_date: Due date
246 246 field_assigned_to: Assignee
247 247 field_priority: Priority
248 248 field_fixed_version: Target version
249 249 field_user: User
250 250 field_principal: Principal
251 251 field_role: Role
252 252 field_homepage: Homepage
253 253 field_is_public: Public
254 254 field_parent: Subproject of
255 255 field_is_in_roadmap: Issues displayed in roadmap
256 256 field_login: Login
257 257 field_mail_notification: Email notifications
258 258 field_admin: Administrator
259 259 field_last_login_on: Last connection
260 260 field_language: Language
261 261 field_effective_date: Date
262 262 field_password: Password
263 263 field_new_password: New password
264 264 field_password_confirmation: Confirmation
265 265 field_version: Version
266 266 field_type: Type
267 267 field_host: Host
268 268 field_port: Port
269 269 field_account: Account
270 270 field_base_dn: Base DN
271 271 field_attr_login: Login attribute
272 272 field_attr_firstname: Firstname attribute
273 273 field_attr_lastname: Lastname attribute
274 274 field_attr_mail: Email attribute
275 275 field_onthefly: On-the-fly user creation
276 276 field_start_date: Start date
277 277 field_done_ratio: "% Done"
278 278 field_auth_source: Authentication mode
279 279 field_hide_mail: Hide my email address
280 280 field_comments: Comment
281 281 field_url: URL
282 282 field_start_page: Start page
283 283 field_subproject: Subproject
284 284 field_hours: Hours
285 285 field_activity: Activity
286 286 field_spent_on: Date
287 287 field_identifier: Identifier
288 288 field_is_filter: Used as a filter
289 289 field_issue_to: Related issue
290 290 field_delay: Delay
291 291 field_assignable: Issues can be assigned to this role
292 292 field_redirect_existing_links: Redirect existing links
293 293 field_estimated_hours: Estimated time
294 294 field_column_names: Columns
295 295 field_time_entries: Log time
296 296 field_time_zone: Time zone
297 297 field_searchable: Searchable
298 298 field_default_value: Default value
299 299 field_comments_sorting: Display comments
300 300 field_parent_title: Parent page
301 301 field_editable: Editable
302 302 field_watcher: Watcher
303 303 field_identity_url: OpenID URL
304 304 field_content: Content
305 305 field_group_by: Group results by
306 306 field_sharing: Sharing
307 307 field_parent_issue: Parent task
308 308 field_member_of_group: "Assignee's group"
309 309 field_assigned_to_role: "Assignee's role"
310 310 field_text: Text field
311 311 field_visible: Visible
312 312 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
313 313 field_issues_visibility: Issues visibility
314 314 field_is_private: Private
315 315 field_commit_logs_encoding: Commit messages encoding
316 316 field_scm_path_encoding: Path encoding
317 317 field_path_to_repository: Path to repository
318 318 field_root_directory: Root directory
319 319 field_cvsroot: CVSROOT
320 320 field_cvs_module: Module
321 field_repository_is_default: Main repository
321 322
322 323 setting_app_title: Application title
323 324 setting_app_subtitle: Application subtitle
324 325 setting_welcome_text: Welcome text
325 326 setting_default_language: Default language
326 327 setting_login_required: Authentication required
327 328 setting_self_registration: Self-registration
328 329 setting_attachment_max_size: Maximum attachment size
329 330 setting_issues_export_limit: Issues export limit
330 331 setting_mail_from: Emission email address
331 332 setting_bcc_recipients: Blind carbon copy recipients (bcc)
332 333 setting_plain_text_mail: Plain text mail (no HTML)
333 334 setting_host_name: Host name and path
334 335 setting_text_formatting: Text formatting
335 336 setting_wiki_compression: Wiki history compression
336 337 setting_feeds_limit: Maximum number of items in Atom feeds
337 338 setting_default_projects_public: New projects are public by default
338 339 setting_autofetch_changesets: Fetch commits automatically
339 340 setting_sys_api_enabled: Enable WS for repository management
340 341 setting_commit_ref_keywords: Referencing keywords
341 342 setting_commit_fix_keywords: Fixing keywords
342 343 setting_autologin: Autologin
343 344 setting_date_format: Date format
344 345 setting_time_format: Time format
345 346 setting_cross_project_issue_relations: Allow cross-project issue relations
346 347 setting_issue_list_default_columns: Default columns displayed on the issue list
347 348 setting_repositories_encodings: Attachments and repositories encodings
348 349 setting_emails_header: Emails header
349 350 setting_emails_footer: Emails footer
350 351 setting_protocol: Protocol
351 352 setting_per_page_options: Objects per page options
352 353 setting_user_format: Users display format
353 354 setting_activity_days_default: Days displayed on project activity
354 355 setting_display_subprojects_issues: Display subprojects issues on main projects by default
355 356 setting_enabled_scm: Enabled SCM
356 357 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
357 358 setting_mail_handler_api_enabled: Enable WS for incoming emails
358 359 setting_mail_handler_api_key: API key
359 360 setting_sequential_project_identifiers: Generate sequential project identifiers
360 361 setting_gravatar_enabled: Use Gravatar user icons
361 362 setting_gravatar_default: Default Gravatar image
362 363 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
363 364 setting_file_max_size_displayed: Maximum size of text files displayed inline
364 365 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
365 366 setting_openid: Allow OpenID login and registration
366 367 setting_password_min_length: Minimum password length
367 368 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
368 369 setting_default_projects_modules: Default enabled modules for new projects
369 370 setting_issue_done_ratio: Calculate the issue done ratio with
370 371 setting_issue_done_ratio_issue_field: Use the issue field
371 372 setting_issue_done_ratio_issue_status: Use the issue status
372 373 setting_start_of_week: Start calendars on
373 374 setting_rest_api_enabled: Enable REST web service
374 375 setting_cache_formatted_text: Cache formatted text
375 376 setting_default_notification_option: Default notification option
376 377 setting_commit_logtime_enabled: Enable time logging
377 378 setting_commit_logtime_activity_id: Activity for logged time
378 379 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
379 380 setting_issue_group_assignment: Allow issue assignment to groups
380 381 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
381 382
382 383 permission_add_project: Create project
383 384 permission_add_subprojects: Create subprojects
384 385 permission_edit_project: Edit project
385 386 permission_select_project_modules: Select project modules
386 387 permission_manage_members: Manage members
387 388 permission_manage_project_activities: Manage project activities
388 389 permission_manage_versions: Manage versions
389 390 permission_manage_categories: Manage issue categories
390 391 permission_view_issues: View Issues
391 392 permission_add_issues: Add issues
392 393 permission_edit_issues: Edit issues
393 394 permission_manage_issue_relations: Manage issue relations
394 395 permission_set_issues_private: Set issues public or private
395 396 permission_set_own_issues_private: Set own issues public or private
396 397 permission_add_issue_notes: Add notes
397 398 permission_edit_issue_notes: Edit notes
398 399 permission_edit_own_issue_notes: Edit own notes
399 400 permission_move_issues: Move issues
400 401 permission_delete_issues: Delete issues
401 402 permission_manage_public_queries: Manage public queries
402 403 permission_save_queries: Save queries
403 404 permission_view_gantt: View gantt chart
404 405 permission_view_calendar: View calendar
405 406 permission_view_issue_watchers: View watchers list
406 407 permission_add_issue_watchers: Add watchers
407 408 permission_delete_issue_watchers: Delete watchers
408 409 permission_log_time: Log spent time
409 410 permission_view_time_entries: View spent time
410 411 permission_edit_time_entries: Edit time logs
411 412 permission_edit_own_time_entries: Edit own time logs
412 413 permission_manage_news: Manage news
413 414 permission_comment_news: Comment news
414 415 permission_manage_documents: Manage documents
415 416 permission_view_documents: View documents
416 417 permission_manage_files: Manage files
417 418 permission_view_files: View files
418 419 permission_manage_wiki: Manage wiki
419 420 permission_rename_wiki_pages: Rename wiki pages
420 421 permission_delete_wiki_pages: Delete wiki pages
421 422 permission_view_wiki_pages: View wiki
422 423 permission_view_wiki_edits: View wiki history
423 424 permission_edit_wiki_pages: Edit wiki pages
424 425 permission_delete_wiki_pages_attachments: Delete attachments
425 426 permission_protect_wiki_pages: Protect wiki pages
426 427 permission_manage_repository: Manage repository
427 428 permission_browse_repository: Browse repository
428 429 permission_view_changesets: View changesets
429 430 permission_commit_access: Commit access
430 431 permission_manage_boards: Manage forums
431 432 permission_view_messages: View messages
432 433 permission_add_messages: Post messages
433 434 permission_edit_messages: Edit messages
434 435 permission_edit_own_messages: Edit own messages
435 436 permission_delete_messages: Delete messages
436 437 permission_delete_own_messages: Delete own messages
437 438 permission_export_wiki_pages: Export wiki pages
438 439 permission_manage_subtasks: Manage subtasks
439 440
440 441 project_module_issue_tracking: Issue tracking
441 442 project_module_time_tracking: Time tracking
442 443 project_module_news: News
443 444 project_module_documents: Documents
444 445 project_module_files: Files
445 446 project_module_wiki: Wiki
446 447 project_module_repository: Repository
447 448 project_module_boards: Forums
448 449 project_module_calendar: Calendar
449 450 project_module_gantt: Gantt
450 451
451 452 label_user: User
452 453 label_user_plural: Users
453 454 label_user_new: New user
454 455 label_user_anonymous: Anonymous
455 456 label_project: Project
456 457 label_project_new: New project
457 458 label_project_plural: Projects
458 459 label_x_projects:
459 460 zero: no projects
460 461 one: 1 project
461 462 other: "%{count} projects"
462 463 label_project_all: All Projects
463 464 label_project_latest: Latest projects
464 465 label_issue: Issue
465 466 label_issue_new: New issue
466 467 label_issue_plural: Issues
467 468 label_issue_view_all: View all issues
468 469 label_issues_by: "Issues by %{value}"
469 470 label_issue_added: Issue added
470 471 label_issue_updated: Issue updated
471 472 label_issue_note_added: Note added
472 473 label_issue_status_updated: Status updated
473 474 label_issue_priority_updated: Priority updated
474 475 label_document: Document
475 476 label_document_new: New document
476 477 label_document_plural: Documents
477 478 label_document_added: Document added
478 479 label_role: Role
479 480 label_role_plural: Roles
480 481 label_role_new: New role
481 482 label_role_and_permissions: Roles and permissions
482 483 label_role_anonymous: Anonymous
483 484 label_role_non_member: Non member
484 485 label_member: Member
485 486 label_member_new: New member
486 487 label_member_plural: Members
487 488 label_tracker: Tracker
488 489 label_tracker_plural: Trackers
489 490 label_tracker_new: New tracker
490 491 label_workflow: Workflow
491 492 label_issue_status: Issue status
492 493 label_issue_status_plural: Issue statuses
493 494 label_issue_status_new: New status
494 495 label_issue_category: Issue category
495 496 label_issue_category_plural: Issue categories
496 497 label_issue_category_new: New category
497 498 label_custom_field: Custom field
498 499 label_custom_field_plural: Custom fields
499 500 label_custom_field_new: New custom field
500 501 label_enumerations: Enumerations
501 502 label_enumeration_new: New value
502 503 label_information: Information
503 504 label_information_plural: Information
504 505 label_please_login: Please log in
505 506 label_register: Register
506 507 label_login_with_open_id_option: or login with OpenID
507 508 label_password_lost: Lost password
508 509 label_home: Home
509 510 label_my_page: My page
510 511 label_my_account: My account
511 512 label_my_projects: My projects
512 513 label_my_page_block: My page block
513 514 label_administration: Administration
514 515 label_login: Sign in
515 516 label_logout: Sign out
516 517 label_help: Help
517 518 label_reported_issues: Reported issues
518 519 label_assigned_to_me_issues: Issues assigned to me
519 520 label_last_login: Last connection
520 521 label_registered_on: Registered on
521 522 label_activity: Activity
522 523 label_overall_activity: Overall activity
523 524 label_user_activity: "%{value}'s activity"
524 525 label_new: New
525 526 label_logged_as: Logged in as
526 527 label_environment: Environment
527 528 label_authentication: Authentication
528 529 label_auth_source: Authentication mode
529 530 label_auth_source_new: New authentication mode
530 531 label_auth_source_plural: Authentication modes
531 532 label_subproject_plural: Subprojects
532 533 label_subproject_new: New subproject
533 534 label_and_its_subprojects: "%{value} and its subprojects"
534 535 label_min_max_length: Min - Max length
535 536 label_list: List
536 537 label_date: Date
537 538 label_integer: Integer
538 539 label_float: Float
539 540 label_boolean: Boolean
540 541 label_string: Text
541 542 label_text: Long text
542 543 label_attribute: Attribute
543 544 label_attribute_plural: Attributes
544 545 label_download: "%{count} Download"
545 546 label_download_plural: "%{count} Downloads"
546 547 label_no_data: No data to display
547 548 label_change_status: Change status
548 549 label_history: History
549 550 label_attachment: File
550 551 label_attachment_new: New file
551 552 label_attachment_delete: Delete file
552 553 label_attachment_plural: Files
553 554 label_file_added: File added
554 555 label_report: Report
555 556 label_report_plural: Reports
556 557 label_news: News
557 558 label_news_new: Add news
558 559 label_news_plural: News
559 560 label_news_latest: Latest news
560 561 label_news_view_all: View all news
561 562 label_news_added: News added
562 563 label_news_comment_added: Comment added to a news
563 564 label_settings: Settings
564 565 label_overview: Overview
565 566 label_version: Version
566 567 label_version_new: New version
567 568 label_version_plural: Versions
568 569 label_close_versions: Close completed versions
569 570 label_confirmation: Confirmation
570 571 label_export_to: 'Also available in:'
571 572 label_read: Read...
572 573 label_public_projects: Public projects
573 574 label_open_issues: open
574 575 label_open_issues_plural: open
575 576 label_closed_issues: closed
576 577 label_closed_issues_plural: closed
577 578 label_x_open_issues_abbr_on_total:
578 579 zero: 0 open / %{total}
579 580 one: 1 open / %{total}
580 581 other: "%{count} open / %{total}"
581 582 label_x_open_issues_abbr:
582 583 zero: 0 open
583 584 one: 1 open
584 585 other: "%{count} open"
585 586 label_x_closed_issues_abbr:
586 587 zero: 0 closed
587 588 one: 1 closed
588 589 other: "%{count} closed"
589 590 label_x_issues:
590 591 zero: 0 issues
591 592 one: 1 issue
592 593 other: "%{count} issues"
593 594 label_total: Total
594 595 label_permissions: Permissions
595 596 label_current_status: Current status
596 597 label_new_statuses_allowed: New statuses allowed
597 598 label_all: all
598 599 label_none: none
599 600 label_nobody: nobody
600 601 label_next: Next
601 602 label_previous: Previous
602 603 label_used_by: Used by
603 604 label_details: Details
604 605 label_add_note: Add a note
605 606 label_per_page: Per page
606 607 label_calendar: Calendar
607 608 label_months_from: months from
608 609 label_gantt: Gantt
609 610 label_internal: Internal
610 611 label_last_changes: "last %{count} changes"
611 612 label_change_view_all: View all changes
612 613 label_personalize_page: Personalize this page
613 614 label_comment: Comment
614 615 label_comment_plural: Comments
615 616 label_x_comments:
616 617 zero: no comments
617 618 one: 1 comment
618 619 other: "%{count} comments"
619 620 label_comment_add: Add a comment
620 621 label_comment_added: Comment added
621 622 label_comment_delete: Delete comments
622 623 label_query: Custom query
623 624 label_query_plural: Custom queries
624 625 label_query_new: New query
625 626 label_my_queries: My custom queries
626 627 label_filter_add: Add filter
627 628 label_filter_plural: Filters
628 629 label_equals: is
629 630 label_not_equals: is not
630 631 label_in_less_than: in less than
631 632 label_in_more_than: in more than
632 633 label_greater_or_equal: '>='
633 634 label_less_or_equal: '<='
634 635 label_between: between
635 636 label_in: in
636 637 label_today: today
637 638 label_all_time: all time
638 639 label_yesterday: yesterday
639 640 label_this_week: this week
640 641 label_last_week: last week
641 642 label_last_n_days: "last %{count} days"
642 643 label_this_month: this month
643 644 label_last_month: last month
644 645 label_this_year: this year
645 646 label_date_range: Date range
646 647 label_less_than_ago: less than days ago
647 648 label_more_than_ago: more than days ago
648 649 label_ago: days ago
649 650 label_contains: contains
650 651 label_not_contains: doesn't contain
651 652 label_day_plural: days
652 653 label_repository: Repository
653 654 label_repository_new: New repository
654 655 label_repository_plural: Repositories
655 656 label_browse: Browse
656 657 label_modification: "%{count} change"
657 658 label_modification_plural: "%{count} changes"
658 659 label_branch: Branch
659 660 label_tag: Tag
660 661 label_revision: Revision
661 662 label_revision_plural: Revisions
662 663 label_revision_id: "Revision %{value}"
663 664 label_associated_revisions: Associated revisions
664 665 label_added: added
665 666 label_modified: modified
666 667 label_copied: copied
667 668 label_renamed: renamed
668 669 label_deleted: deleted
669 670 label_latest_revision: Latest revision
670 671 label_latest_revision_plural: Latest revisions
671 672 label_view_revisions: View revisions
672 673 label_view_all_revisions: View all revisions
673 674 label_max_size: Maximum size
674 675 label_sort_highest: Move to top
675 676 label_sort_higher: Move up
676 677 label_sort_lower: Move down
677 678 label_sort_lowest: Move to bottom
678 679 label_roadmap: Roadmap
679 680 label_roadmap_due_in: "Due in %{value}"
680 681 label_roadmap_overdue: "%{value} late"
681 682 label_roadmap_no_issues: No issues for this version
682 683 label_search: Search
683 684 label_result_plural: Results
684 685 label_all_words: All words
685 686 label_wiki: Wiki
686 687 label_wiki_edit: Wiki edit
687 688 label_wiki_edit_plural: Wiki edits
688 689 label_wiki_page: Wiki page
689 690 label_wiki_page_plural: Wiki pages
690 691 label_index_by_title: Index by title
691 692 label_index_by_date: Index by date
692 693 label_current_version: Current version
693 694 label_preview: Preview
694 695 label_feed_plural: Feeds
695 696 label_changes_details: Details of all changes
696 697 label_issue_tracking: Issue tracking
697 698 label_spent_time: Spent time
698 699 label_overall_spent_time: Overall spent time
699 700 label_f_hour: "%{value} hour"
700 701 label_f_hour_plural: "%{value} hours"
701 702 label_time_tracking: Time tracking
702 703 label_change_plural: Changes
703 704 label_statistics: Statistics
704 705 label_commits_per_month: Commits per month
705 706 label_commits_per_author: Commits per author
706 707 label_diff: diff
707 708 label_view_diff: View differences
708 709 label_diff_inline: inline
709 710 label_diff_side_by_side: side by side
710 711 label_options: Options
711 712 label_copy_workflow_from: Copy workflow from
712 713 label_permissions_report: Permissions report
713 714 label_watched_issues: Watched issues
714 715 label_related_issues: Related issues
715 716 label_applied_status: Applied status
716 717 label_loading: Loading...
717 718 label_relation_new: New relation
718 719 label_relation_delete: Delete relation
719 720 label_relates_to: related to
720 721 label_duplicates: duplicates
721 722 label_duplicated_by: duplicated by
722 723 label_blocks: blocks
723 724 label_blocked_by: blocked by
724 725 label_precedes: precedes
725 726 label_follows: follows
726 727 label_end_to_start: end to start
727 728 label_end_to_end: end to end
728 729 label_start_to_start: start to start
729 730 label_start_to_end: start to end
730 731 label_stay_logged_in: Stay logged in
731 732 label_disabled: disabled
732 733 label_show_completed_versions: Show completed versions
733 734 label_me: me
734 735 label_board: Forum
735 736 label_board_new: New forum
736 737 label_board_plural: Forums
737 738 label_board_locked: Locked
738 739 label_board_sticky: Sticky
739 740 label_topic_plural: Topics
740 741 label_message_plural: Messages
741 742 label_message_last: Last message
742 743 label_message_new: New message
743 744 label_message_posted: Message added
744 745 label_reply_plural: Replies
745 746 label_send_information: Send account information to the user
746 747 label_year: Year
747 748 label_month: Month
748 749 label_week: Week
749 750 label_date_from: From
750 751 label_date_to: To
751 752 label_language_based: Based on user's language
752 753 label_sort_by: "Sort by %{value}"
753 754 label_send_test_email: Send a test email
754 755 label_feeds_access_key: RSS access key
755 756 label_missing_feeds_access_key: Missing a RSS access key
756 757 label_feeds_access_key_created_on: "RSS access key created %{value} ago"
757 758 label_module_plural: Modules
758 759 label_added_time_by: "Added by %{author} %{age} ago"
759 760 label_updated_time_by: "Updated by %{author} %{age} ago"
760 761 label_updated_time: "Updated %{value} ago"
761 762 label_jump_to_a_project: Jump to a project...
762 763 label_file_plural: Files
763 764 label_changeset_plural: Changesets
764 765 label_default_columns: Default columns
765 766 label_no_change_option: (No change)
766 767 label_bulk_edit_selected_issues: Bulk edit selected issues
767 768 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
768 769 label_theme: Theme
769 770 label_default: Default
770 771 label_search_titles_only: Search titles only
771 772 label_user_mail_option_all: "For any event on all my projects"
772 773 label_user_mail_option_selected: "For any event on the selected projects only..."
773 774 label_user_mail_option_none: "No events"
774 775 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
775 776 label_user_mail_option_only_assigned: "Only for things I am assigned to"
776 777 label_user_mail_option_only_owner: "Only for things I am the owner of"
777 778 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
778 779 label_registration_activation_by_email: account activation by email
779 780 label_registration_manual_activation: manual account activation
780 781 label_registration_automatic_activation: automatic account activation
781 782 label_display_per_page: "Per page: %{value}"
782 783 label_age: Age
783 784 label_change_properties: Change properties
784 785 label_general: General
785 786 label_more: More
786 787 label_scm: SCM
787 788 label_plugins: Plugins
788 789 label_ldap_authentication: LDAP authentication
789 790 label_downloads_abbr: D/L
790 791 label_optional_description: Optional description
791 792 label_add_another_file: Add another file
792 793 label_preferences: Preferences
793 794 label_chronological_order: In chronological order
794 795 label_reverse_chronological_order: In reverse chronological order
795 796 label_planning: Planning
796 797 label_incoming_emails: Incoming emails
797 798 label_generate_key: Generate a key
798 799 label_issue_watchers: Watchers
799 800 label_example: Example
800 801 label_display: Display
801 802 label_sort: Sort
802 803 label_ascending: Ascending
803 804 label_descending: Descending
804 805 label_date_from_to: From %{start} to %{end}
805 806 label_wiki_content_added: Wiki page added
806 807 label_wiki_content_updated: Wiki page updated
807 808 label_group: Group
808 809 label_group_plural: Groups
809 810 label_group_new: New group
810 811 label_time_entry_plural: Spent time
811 812 label_version_sharing_none: Not shared
812 813 label_version_sharing_descendants: With subprojects
813 814 label_version_sharing_hierarchy: With project hierarchy
814 815 label_version_sharing_tree: With project tree
815 816 label_version_sharing_system: With all projects
816 817 label_update_issue_done_ratios: Update issue done ratios
817 818 label_copy_source: Source
818 819 label_copy_target: Target
819 820 label_copy_same_as_target: Same as target
820 821 label_display_used_statuses_only: Only display statuses that are used by this tracker
821 822 label_api_access_key: API access key
822 823 label_missing_api_access_key: Missing an API access key
823 824 label_api_access_key_created_on: "API access key created %{value} ago"
824 825 label_profile: Profile
825 826 label_subtask_plural: Subtasks
826 827 label_project_copy_notifications: Send email notifications during the project copy
827 828 label_principal_search: "Search for user or group:"
828 829 label_user_search: "Search for user:"
829 830 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
830 831 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
831 832 label_issues_visibility_all: All issues
832 833 label_issues_visibility_public: All non private issues
833 834 label_issues_visibility_own: Issues created by or assigned to the user
834 835 label_git_report_last_commit: Report last commit for files and directories
835 836 label_parent_revision: Parent
836 837 label_child_revision: Child
837 838 label_export_options: "%{export_format} export options"
838 839
839 840 button_login: Login
840 841 button_submit: Submit
841 842 button_save: Save
842 843 button_check_all: Check all
843 844 button_uncheck_all: Uncheck all
844 845 button_collapse_all: Collapse all
845 846 button_expand_all: Expand all
846 847 button_delete: Delete
847 848 button_create: Create
848 849 button_create_and_continue: Create and continue
849 850 button_test: Test
850 851 button_edit: Edit
851 852 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
852 853 button_add: Add
853 854 button_change: Change
854 855 button_apply: Apply
855 856 button_clear: Clear
856 857 button_lock: Lock
857 858 button_unlock: Unlock
858 859 button_download: Download
859 860 button_list: List
860 861 button_view: View
861 862 button_move: Move
862 863 button_move_and_follow: Move and follow
863 864 button_back: Back
864 865 button_cancel: Cancel
865 866 button_activate: Activate
866 867 button_sort: Sort
867 868 button_log_time: Log time
868 869 button_rollback: Rollback to this version
869 870 button_watch: Watch
870 871 button_unwatch: Unwatch
871 872 button_reply: Reply
872 873 button_archive: Archive
873 874 button_unarchive: Unarchive
874 875 button_reset: Reset
875 876 button_rename: Rename
876 877 button_change_password: Change password
877 878 button_copy: Copy
878 879 button_copy_and_follow: Copy and follow
879 880 button_annotate: Annotate
880 881 button_update: Update
881 882 button_configure: Configure
882 883 button_quote: Quote
883 884 button_duplicate: Duplicate
884 885 button_show: Show
885 886 button_edit_section: Edit this section
886 887 button_export: Export
887 888
888 889 status_active: active
889 890 status_registered: registered
890 891 status_locked: locked
891 892
892 893 version_status_open: open
893 894 version_status_locked: locked
894 895 version_status_closed: closed
895 896
896 897 field_active: Active
897 898
898 899 text_select_mail_notifications: Select actions for which email notifications should be sent.
899 900 text_regexp_info: eg. ^[A-Z0-9]+$
900 901 text_min_max_length_info: 0 means no restriction
901 902 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
902 903 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
903 904 text_workflow_edit: Select a role and a tracker to edit the workflow
904 905 text_are_you_sure: Are you sure?
905 906 text_are_you_sure_with_children: "Delete issue and all child issues?"
906 907 text_journal_changed: "%{label} changed from %{old} to %{new}"
907 908 text_journal_changed_no_detail: "%{label} updated"
908 909 text_journal_set_to: "%{label} set to %{value}"
909 910 text_journal_deleted: "%{label} deleted (%{old})"
910 911 text_journal_added: "%{label} %{value} added"
911 912 text_tip_issue_begin_day: issue beginning this day
912 913 text_tip_issue_end_day: issue ending this day
913 914 text_tip_issue_begin_end_day: issue beginning and ending this day
914 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 916 text_caracters_maximum: "%{count} characters maximum."
916 917 text_caracters_minimum: "Must be at least %{count} characters long."
917 918 text_length_between: "Length between %{min} and %{max} characters."
918 919 text_tracker_no_workflow: No workflow defined for this tracker
919 920 text_unallowed_characters: Unallowed characters
920 921 text_comma_separated: Multiple values allowed (comma separated).
921 922 text_line_separated: Multiple values allowed (one line for each value).
922 923 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
923 924 text_issue_added: "Issue %{id} has been reported by %{author}."
924 925 text_issue_updated: "Issue %{id} has been updated by %{author}."
925 926 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
926 927 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
927 928 text_issue_category_destroy_assignments: Remove category assignments
928 929 text_issue_category_reassign_to: Reassign issues to this category
929 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 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 932 text_load_default_configuration: Load the default configuration
932 933 text_status_changed_by_changeset: "Applied in changeset %{value}."
933 934 text_time_logged_by_changeset: "Applied in changeset %{value}."
934 935 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
935 936 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
936 937 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
937 938 text_select_project_modules: 'Select modules to enable for this project:'
938 939 text_default_administrator_account_changed: Default administrator account changed
939 940 text_file_repository_writable: Attachments directory writable
940 941 text_plugin_assets_writable: Plugin assets directory writable
941 942 text_rmagick_available: RMagick available (optional)
942 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 944 text_destroy_time_entries: Delete reported hours
944 945 text_assign_time_entries_to_project: Assign reported hours to the project
945 946 text_reassign_time_entries: 'Reassign reported hours to this issue:'
946 947 text_user_wrote: "%{value} wrote:"
947 948 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
948 949 text_enumeration_category_reassign_to: 'Reassign them to this value:'
949 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 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 952 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
952 953 text_custom_field_possible_values_info: 'One line for each value'
953 954 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
954 955 text_wiki_page_nullify_children: "Keep child pages as root pages"
955 956 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
956 957 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
957 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 959 text_zoom_in: Zoom in
959 960 text_zoom_out: Zoom out
960 961 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
961 962 text_scm_path_encoding_note: "Default: UTF-8"
962 963 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
963 964 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
964 965 text_scm_command: Command
965 966 text_scm_command_version: Version
966 967 text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it.
967 968 text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel.
968 969
969 970 default_role_manager: Manager
970 971 default_role_developer: Developer
971 972 default_role_reporter: Reporter
972 973 default_tracker_bug: Bug
973 974 default_tracker_feature: Feature
974 975 default_tracker_support: Support
975 976 default_issue_status_new: New
976 977 default_issue_status_in_progress: In Progress
977 978 default_issue_status_resolved: Resolved
978 979 default_issue_status_feedback: Feedback
979 980 default_issue_status_closed: Closed
980 981 default_issue_status_rejected: Rejected
981 982 default_doc_category_user: User documentation
982 983 default_doc_category_tech: Technical documentation
983 984 default_priority_low: Low
984 985 default_priority_normal: Normal
985 986 default_priority_high: High
986 987 default_priority_urgent: Urgent
987 988 default_priority_immediate: Immediate
988 989 default_activity_design: Design
989 990 default_activity_development: Development
990 991
991 992 enumeration_issue_priorities: Issue priorities
992 993 enumeration_doc_categories: Document categories
993 994 enumeration_activities: Activities (time tracking)
994 995 enumeration_system_activity: System Activity
995 996 description_filter: Filter
996 997 description_search: Searchfield
997 998 description_choose_project: Projects
998 999 description_project_scope: Search scope
999 1000 description_notes: Notes
1000 1001 description_message_content: Message content
1001 1002 description_query_sort_criteria_attribute: Sort attribute
1002 1003 description_query_sort_criteria_direction: Sort direction
1003 1004 description_user_mail_notification: Mail notification settings
1004 1005 description_available_columns: Available Columns
1005 1006 description_selected_columns: Selected Columns
1006 1007 description_all_columns: All Columns
1007 1008 description_issue_category_reassign: Choose issue category
1008 1009 description_wiki_subpages_reassign: Choose new parent page
1009 1010 description_date_range_list: Choose range from list
1010 1011 description_date_range_interval: Choose range by selecting start and end date
1011 1012 description_date_from: Enter start date
1012 1013 description_date_to: Enter end date
@@ -1,1029 +1,1030
1 1 # French translations for Ruby on Rails
2 2 # by Christian Lescuyer (christian@flyingcoders.com)
3 3 # contributor: Sebastien Grosjean - ZenCocoon.com
4 4 # contributor: Thibaut Cuvelier - Developpez.com
5 5
6 6 fr:
7 7 direction: ltr
8 8 date:
9 9 formats:
10 10 default: "%d/%m/%Y"
11 11 short: "%e %b"
12 12 long: "%e %B %Y"
13 13 long_ordinal: "%e %B %Y"
14 14 only_day: "%e"
15 15
16 16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
17 17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
18 18 month_names: [~, janvier, fΓ©vrier, mars, avril, mai, juin, juillet, aoΓ»t, septembre, octobre, novembre, dΓ©cembre]
19 19 abbr_month_names: [~, jan., fΓ©v., mar., avr., mai, juin, juil., aoΓ»t, sept., oct., nov., dΓ©c.]
20 20 order:
21 21 - :day
22 22 - :month
23 23 - :year
24 24
25 25 time:
26 26 formats:
27 27 default: "%d/%m/%Y %H:%M"
28 28 time: "%H:%M"
29 29 short: "%d %b %H:%M"
30 30 long: "%A %d %B %Y %H:%M:%S %Z"
31 31 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
32 32 only_second: "%S"
33 33 am: 'am'
34 34 pm: 'pm'
35 35
36 36 datetime:
37 37 distance_in_words:
38 38 half_a_minute: "30 secondes"
39 39 less_than_x_seconds:
40 40 zero: "moins d'une seconde"
41 41 one: "moins d'uneΒ seconde"
42 42 other: "moins de %{count}Β secondes"
43 43 x_seconds:
44 44 one: "1Β seconde"
45 45 other: "%{count}Β secondes"
46 46 less_than_x_minutes:
47 47 zero: "moins d'une minute"
48 48 one: "moins d'uneΒ minute"
49 49 other: "moins de %{count}Β minutes"
50 50 x_minutes:
51 51 one: "1Β minute"
52 52 other: "%{count}Β minutes"
53 53 about_x_hours:
54 54 one: "environ une heure"
55 55 other: "environ %{count}Β heures"
56 56 x_days:
57 57 one: "unΒ jour"
58 58 other: "%{count}Β jours"
59 59 about_x_months:
60 60 one: "environ un mois"
61 61 other: "environ %{count}Β mois"
62 62 x_months:
63 63 one: "unΒ mois"
64 64 other: "%{count}Β mois"
65 65 about_x_years:
66 66 one: "environ un an"
67 67 other: "environ %{count}Β ans"
68 68 over_x_years:
69 69 one: "plus d'un an"
70 70 other: "plus de %{count}Β ans"
71 71 almost_x_years:
72 72 one: "presqu'un an"
73 73 other: "presque %{count} ans"
74 74 prompts:
75 75 year: "AnnΓ©e"
76 76 month: "Mois"
77 77 day: "Jour"
78 78 hour: "Heure"
79 79 minute: "Minute"
80 80 second: "Seconde"
81 81
82 82 number:
83 83 format:
84 84 precision: 3
85 85 separator: ','
86 86 delimiter: 'Β '
87 87 currency:
88 88 format:
89 89 unit: '€'
90 90 precision: 2
91 91 format: '%nΒ %u'
92 92 human:
93 93 format:
94 94 precision: 2
95 95 storage_units:
96 96 format: "%n %u"
97 97 units:
98 98 byte:
99 99 one: "octet"
100 100 other: "octet"
101 101 kb: "ko"
102 102 mb: "Mo"
103 103 gb: "Go"
104 104 tb: "To"
105 105
106 106 support:
107 107 array:
108 108 sentence_connector: 'et'
109 109 skip_last_comma: true
110 110 word_connector: ", "
111 111 two_words_connector: " et "
112 112 last_word_connector: " et "
113 113
114 114 activerecord:
115 115 errors:
116 116 template:
117 117 header:
118 118 one: "Impossible d'enregistrer %{model} : une erreur"
119 119 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
120 120 body: "Veuillez vΓ©rifier les champs suivantsΒ :"
121 121 messages:
122 122 inclusion: "n'est pas inclus(e) dans la liste"
123 123 exclusion: "n'est pas disponible"
124 124 invalid: "n'est pas valide"
125 125 confirmation: "ne concorde pas avec la confirmation"
126 126 accepted: "doit Γͺtre acceptΓ©(e)"
127 127 empty: "doit Γͺtre renseignΓ©(e)"
128 128 blank: "doit Γͺtre renseignΓ©(e)"
129 129 too_long: "est trop long (pas plus de %{count} caractères)"
130 130 too_short: "est trop court (au moins %{count} caractères)"
131 131 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
132 132 taken: "est dΓ©jΓ  utilisΓ©"
133 133 not_a_number: "n'est pas un nombre"
134 134 not_a_date: "n'est pas une date valide"
135 135 greater_than: "doit Γͺtre supΓ©rieur Γ  %{count}"
136 136 greater_than_or_equal_to: "doit Γͺtre supΓ©rieur ou Γ©gal Γ  %{count}"
137 137 equal_to: "doit Γͺtre Γ©gal Γ  %{count}"
138 138 less_than: "doit Γͺtre infΓ©rieur Γ  %{count}"
139 139 less_than_or_equal_to: "doit Γͺtre infΓ©rieur ou Γ©gal Γ  %{count}"
140 140 odd: "doit Γͺtre impair"
141 141 even: "doit Γͺtre pair"
142 142 greater_than_start_date: "doit Γͺtre postΓ©rieure Γ  la date de dΓ©but"
143 143 not_same_project: "n'appartient pas au mΓͺme projet"
144 144 circular_dependency: "Cette relation crΓ©erait une dΓ©pendance circulaire"
145 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 147 actionview_instancetag_blank_option: Choisir
148 148
149 149 general_text_No: 'Non'
150 150 general_text_Yes: 'Oui'
151 151 general_text_no: 'non'
152 152 general_text_yes: 'oui'
153 153 general_lang_name: 'FranΓ§ais'
154 154 general_csv_separator: ';'
155 155 general_csv_decimal_separator: ','
156 156 general_csv_encoding: ISO-8859-1
157 157 general_pdf_encoding: UTF-8
158 158 general_first_day_of_week: '1'
159 159
160 160 notice_account_updated: Le compte a été mis à jour avec succès.
161 161 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
162 162 notice_account_password_updated: Mot de passe mis à jour avec succès.
163 163 notice_account_wrong_password: Mot de passe incorrect
164 164 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a Γ©tΓ© envoyΓ©.
165 165 notice_account_unknown_email: Aucun compte ne correspond Γ  cette adresse.
166 166 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
167 167 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a Γ©tΓ© envoyΓ©.
168 168 notice_account_activated: Votre compte a Γ©tΓ© activΓ©. Vous pouvez Γ  prΓ©sent vous connecter.
169 169 notice_successful_create: Création effectuée avec succès.
170 170 notice_successful_update: Mise à jour effectuée avec succès.
171 171 notice_successful_delete: Suppression effectuée avec succès.
172 172 notice_successful_connection: Connexion rΓ©ussie.
173 173 notice_file_not_found: "La page Γ  laquelle vous souhaitez accΓ©der n'existe pas ou a Γ©tΓ© supprimΓ©e."
174 174 notice_locking_conflict: Les donnΓ©es ont Γ©tΓ© mises Γ  jour par un autre utilisateur. Mise Γ  jour impossible.
175 175 notice_not_authorized: "Vous n'Γͺtes pas autorisΓ© Γ  accΓ©der Γ  cette page."
176 176 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accΓ©der a Γ©tΓ© archivΓ©.
177 177 notice_email_sent: "Un email a Γ©tΓ© envoyΓ© Γ  %{value}"
178 178 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
179 179 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée."
180 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 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 182 notice_no_issue_selected: "Aucune demande sΓ©lectionnΓ©e ! Cochez les demandes que vous voulez mettre Γ  jour."
183 183 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
184 184 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
185 185 notice_unable_delete_version: Impossible de supprimer cette version.
186 186 notice_issue_done_ratios_updated: L'avancement des demandes a Γ©tΓ© mis Γ  jour.
187 187 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
188 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 189 notice_issue_successful_create: "La demande %{id} a été créée."
190 190
191 191 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramΓ©trage : %{value}"
192 192 error_scm_not_found: "L'entrΓ©e et/ou la rΓ©vision demandΓ©e n'existe pas dans le dΓ©pΓ΄t."
193 193 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
194 194 error_scm_annotate: "L'entrΓ©e n'existe pas ou ne peut pas Γͺtre annotΓ©e."
195 195 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas Γ  ce projet"
196 196 error_can_not_reopen_issue_on_closed_version: 'Une demande assignΓ©e Γ  une version fermΓ©e ne peut pas Γͺtre rΓ©ouverte'
197 197 error_can_not_archive_project: "Ce projet ne peut pas Γͺtre archivΓ©"
198 198 error_workflow_copy_source: 'Veuillez sΓ©lectionner un tracker et/ou un rΓ΄le source'
199 199 error_workflow_copy_target: 'Veuillez sΓ©lectionner les trackers et rΓ΄les cibles'
200 200 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu Γͺtre mis Γ  jour.
201 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 203 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu Γͺtre sauvegardΓ©s."
204 204
205 205 mail_subject_lost_password: "Votre mot de passe %{value}"
206 206 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
207 207 mail_subject_register: "Activation de votre compte %{value}"
208 208 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
209 209 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
210 210 mail_body_account_information: Paramètres de connexion de votre compte
211 211 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
212 212 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nΓ©cessite votre approbation :"
213 213 mail_subject_reminder: "%{count} demande(s) arrivent Γ  Γ©chΓ©ance (%{days})"
214 214 mail_body_reminder: "%{count} demande(s) qui vous sont assignΓ©es arrivent Γ  Γ©chΓ©ance dans les %{days} prochains jours :"
215 215 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutΓ©e"
216 216 mail_body_wiki_content_added: "La page wiki '%{id}' a Γ©tΓ© ajoutΓ©e par %{author}."
217 217 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise Γ  jour"
218 218 mail_body_wiki_content_updated: "La page wiki '%{id}' a Γ©tΓ© mise Γ  jour par %{author}."
219 219
220 220 gui_validation_error: 1 erreur
221 221 gui_validation_error_plural: "%{count} erreurs"
222 222
223 223 field_name: Nom
224 224 field_description: Description
225 225 field_summary: RΓ©sumΓ©
226 226 field_is_required: Obligatoire
227 227 field_firstname: PrΓ©nom
228 228 field_lastname: Nom
229 229 field_mail: "Email "
230 230 field_filename: Fichier
231 231 field_filesize: Taille
232 232 field_downloads: TΓ©lΓ©chargements
233 233 field_author: Auteur
234 234 field_created_on: "Créé "
235 235 field_updated_on: "Mis-Γ -jour "
236 236 field_field_format: Format
237 237 field_is_for_all: Pour tous les projets
238 238 field_possible_values: Valeurs possibles
239 239 field_regexp: Expression régulière
240 240 field_min_length: Longueur minimum
241 241 field_max_length: Longueur maximum
242 242 field_value: Valeur
243 243 field_category: CatΓ©gorie
244 244 field_title: Titre
245 245 field_project: Projet
246 246 field_issue: Demande
247 247 field_status: Statut
248 248 field_notes: Notes
249 249 field_is_closed: Demande fermΓ©e
250 250 field_is_default: Valeur par dΓ©faut
251 251 field_tracker: Tracker
252 252 field_subject: Sujet
253 253 field_due_date: EchΓ©ance
254 254 field_assigned_to: AssignΓ© Γ 
255 255 field_priority: PrioritΓ©
256 256 field_fixed_version: Version cible
257 257 field_user: Utilisateur
258 258 field_role: RΓ΄le
259 259 field_homepage: "Site web "
260 260 field_is_public: Public
261 261 field_parent: Sous-projet de
262 262 field_is_in_roadmap: Demandes affichΓ©es dans la roadmap
263 263 field_login: "Identifiant "
264 264 field_mail_notification: Notifications par mail
265 265 field_admin: Administrateur
266 266 field_last_login_on: "Dernière connexion "
267 267 field_language: Langue
268 268 field_effective_date: Date
269 269 field_password: Mot de passe
270 270 field_new_password: Nouveau mot de passe
271 271 field_password_confirmation: Confirmation
272 272 field_version: Version
273 273 field_type: Type
274 274 field_host: HΓ΄te
275 275 field_port: Port
276 276 field_account: Compte
277 277 field_base_dn: Base DN
278 278 field_attr_login: Attribut Identifiant
279 279 field_attr_firstname: Attribut PrΓ©nom
280 280 field_attr_lastname: Attribut Nom
281 281 field_attr_mail: Attribut Email
282 282 field_onthefly: CrΓ©ation des utilisateurs Γ  la volΓ©e
283 283 field_start_date: DΓ©but
284 284 field_done_ratio: "% rΓ©alisΓ©"
285 285 field_auth_source: Mode d'authentification
286 286 field_hide_mail: Cacher mon adresse mail
287 287 field_comments: Commentaire
288 288 field_url: URL
289 289 field_start_page: Page de dΓ©marrage
290 290 field_subproject: Sous-projet
291 291 field_hours: Heures
292 292 field_activity: ActivitΓ©
293 293 field_spent_on: Date
294 294 field_identifier: Identifiant
295 295 field_is_filter: UtilisΓ© comme filtre
296 296 field_issue_to: Demande liΓ©e
297 297 field_delay: Retard
298 298 field_assignable: Demandes assignables Γ  ce rΓ΄le
299 299 field_redirect_existing_links: Rediriger les liens existants
300 300 field_estimated_hours: Temps estimΓ©
301 301 field_column_names: Colonnes
302 302 field_time_zone: Fuseau horaire
303 303 field_searchable: UtilisΓ© pour les recherches
304 304 field_default_value: Valeur par dΓ©faut
305 305 field_comments_sorting: Afficher les commentaires
306 306 field_parent_title: Page parent
307 307 field_editable: Modifiable
308 308 field_watcher: Observateur
309 309 field_identity_url: URL OpenID
310 310 field_content: Contenu
311 311 field_group_by: Grouper par
312 312 field_sharing: Partage
313 313 field_active: Actif
314 314 field_parent_issue: TΓ’che parente
315 315 field_visible: Visible
316 316 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardΓ©"
317 317 field_issues_visibility: VisibilitΓ© des demandes
318 318 field_is_private: PrivΓ©e
319 319 field_commit_logs_encoding: Encodage des messages de commit
320 field_repository_is_default: DΓ©pΓ΄t principal
320 321
321 322 setting_app_title: Titre de l'application
322 323 setting_app_subtitle: Sous-titre de l'application
323 324 setting_welcome_text: Texte d'accueil
324 325 setting_default_language: Langue par dΓ©faut
325 326 setting_login_required: Authentification obligatoire
326 327 setting_self_registration: Inscription des nouveaux utilisateurs
327 328 setting_attachment_max_size: Taille maximale des fichiers
328 329 setting_issues_export_limit: Limite d'exportation des demandes
329 330 setting_mail_from: Adresse d'Γ©mission
330 331 setting_bcc_recipients: Destinataires en copie cachΓ©e (cci)
331 332 setting_plain_text_mail: Mail en texte brut (non HTML)
332 333 setting_host_name: Nom d'hΓ΄te et chemin
333 334 setting_text_formatting: Formatage du texte
334 335 setting_wiki_compression: Compression de l'historique des pages wiki
335 336 setting_feeds_limit: Nombre maximal d'Γ©lΓ©ments dans les flux Atom
336 337 setting_default_projects_public: DΓ©finir les nouveaux projets comme publics par dΓ©faut
337 338 setting_autofetch_changesets: RΓ©cupΓ©ration automatique des commits
338 339 setting_sys_api_enabled: Activer les WS pour la gestion des dΓ©pΓ΄ts
339 340 setting_commit_ref_keywords: Mots-clΓ©s de rΓ©fΓ©rencement
340 341 setting_commit_fix_keywords: Mots-clΓ©s de rΓ©solution
341 342 setting_autologin: DurΓ©e maximale de connexion automatique
342 343 setting_date_format: Format de date
343 344 setting_time_format: Format d'heure
344 345 setting_cross_project_issue_relations: Autoriser les relations entre demandes de diffΓ©rents projets
345 346 setting_issue_list_default_columns: Colonnes affichΓ©es par dΓ©faut sur la liste des demandes
346 347 setting_emails_footer: Pied-de-page des emails
347 348 setting_protocol: Protocole
348 349 setting_per_page_options: Options d'objets affichΓ©s par page
349 350 setting_user_format: Format d'affichage des utilisateurs
350 351 setting_activity_days_default: Nombre de jours affichΓ©s sur l'activitΓ© des projets
351 352 setting_display_subprojects_issues: Afficher par dΓ©faut les demandes des sous-projets sur les projets principaux
352 353 setting_enabled_scm: SCM activΓ©s
353 354 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
354 355 setting_mail_handler_api_enabled: "Activer le WS pour la rΓ©ception d'emails"
355 356 setting_mail_handler_api_key: ClΓ© de protection de l'API
356 357 setting_sequential_project_identifiers: GΓ©nΓ©rer des identifiants de projet sΓ©quentiels
357 358 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
358 359 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichΓ©es
359 360 setting_file_max_size_displayed: Taille maximum des fichiers texte affichΓ©s en ligne
360 361 setting_repository_log_display_limit: "Nombre maximum de rΓ©visions affichΓ©es sur l'historique d'un fichier"
361 362 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
362 363 setting_password_min_length: Longueur minimum des mots de passe
363 364 setting_new_project_user_role_id: RΓ΄le donnΓ© Γ  un utilisateur non-administrateur qui crΓ©e un projet
364 365 setting_default_projects_modules: Modules activΓ©s par dΓ©faut pour les nouveaux projets
365 366 setting_issue_done_ratio: Calcul de l'avancement des demandes
366 367 setting_issue_done_ratio_issue_status: Utiliser le statut
367 368 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectuΓ©'
368 369 setting_rest_api_enabled: Activer l'API REST
369 370 setting_gravatar_default: Image Gravatar par dΓ©faut
370 371 setting_start_of_week: Jour de dΓ©but des calendriers
371 372 setting_cache_formatted_text: Mettre en cache le texte formatΓ©
372 373 setting_commit_logtime_enabled: Permettre la saisie de temps
373 374 setting_commit_logtime_activity_id: ActivitΓ© pour le temps saisi
374 375 setting_gantt_items_limit: Nombre maximum d'Γ©lΓ©ments affichΓ©s sur le gantt
375 376 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
376 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 379 permission_add_project: CrΓ©er un projet
379 380 permission_add_subprojects: CrΓ©er des sous-projets
380 381 permission_edit_project: Modifier le projet
381 382 permission_select_project_modules: Choisir les modules
382 383 permission_manage_members: GΓ©rer les membres
383 384 permission_manage_versions: GΓ©rer les versions
384 385 permission_manage_categories: GΓ©rer les catΓ©gories de demandes
385 386 permission_view_issues: Voir les demandes
386 387 permission_add_issues: CrΓ©er des demandes
387 388 permission_edit_issues: Modifier les demandes
388 389 permission_manage_issue_relations: GΓ©rer les relations
389 390 permission_set_issues_private: Rendre les demandes publiques ou privΓ©es
390 391 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privΓ©es
391 392 permission_add_issue_notes: Ajouter des notes
392 393 permission_edit_issue_notes: Modifier les notes
393 394 permission_edit_own_issue_notes: Modifier ses propres notes
394 395 permission_move_issues: DΓ©placer les demandes
395 396 permission_delete_issues: Supprimer les demandes
396 397 permission_manage_public_queries: GΓ©rer les requΓͺtes publiques
397 398 permission_save_queries: Sauvegarder les requΓͺtes
398 399 permission_view_gantt: Voir le gantt
399 400 permission_view_calendar: Voir le calendrier
400 401 permission_view_issue_watchers: Voir la liste des observateurs
401 402 permission_add_issue_watchers: Ajouter des observateurs
402 403 permission_delete_issue_watchers: Supprimer des observateurs
403 404 permission_log_time: Saisir le temps passΓ©
404 405 permission_view_time_entries: Voir le temps passΓ©
405 406 permission_edit_time_entries: Modifier les temps passΓ©s
406 407 permission_edit_own_time_entries: Modifier son propre temps passΓ©
407 408 permission_manage_news: GΓ©rer les annonces
408 409 permission_comment_news: Commenter les annonces
409 410 permission_manage_documents: GΓ©rer les documents
410 411 permission_view_documents: Voir les documents
411 412 permission_manage_files: GΓ©rer les fichiers
412 413 permission_view_files: Voir les fichiers
413 414 permission_manage_wiki: GΓ©rer le wiki
414 415 permission_rename_wiki_pages: Renommer les pages
415 416 permission_delete_wiki_pages: Supprimer les pages
416 417 permission_view_wiki_pages: Voir le wiki
417 418 permission_view_wiki_edits: "Voir l'historique des modifications"
418 419 permission_edit_wiki_pages: Modifier les pages
419 420 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
420 421 permission_protect_wiki_pages: ProtΓ©ger les pages
421 422 permission_manage_repository: GΓ©rer le dΓ©pΓ΄t de sources
422 423 permission_browse_repository: Parcourir les sources
423 424 permission_view_changesets: Voir les rΓ©visions
424 425 permission_commit_access: Droit de commit
425 426 permission_manage_boards: GΓ©rer les forums
426 427 permission_view_messages: Voir les messages
427 428 permission_add_messages: Poster un message
428 429 permission_edit_messages: Modifier les messages
429 430 permission_edit_own_messages: Modifier ses propres messages
430 431 permission_delete_messages: Supprimer les messages
431 432 permission_delete_own_messages: Supprimer ses propres messages
432 433 permission_export_wiki_pages: Exporter les pages
433 434 permission_manage_project_activities: GΓ©rer les activitΓ©s
434 435 permission_manage_subtasks: GΓ©rer les sous-tΓ’ches
435 436
436 437 project_module_issue_tracking: Suivi des demandes
437 438 project_module_time_tracking: Suivi du temps passΓ©
438 439 project_module_news: Publication d'annonces
439 440 project_module_documents: Publication de documents
440 441 project_module_files: Publication de fichiers
441 442 project_module_wiki: Wiki
442 443 project_module_repository: DΓ©pΓ΄t de sources
443 444 project_module_boards: Forums de discussion
444 445
445 446 label_user: Utilisateur
446 447 label_user_plural: Utilisateurs
447 448 label_user_new: Nouvel utilisateur
448 449 label_user_anonymous: Anonyme
449 450 label_project: Projet
450 451 label_project_new: Nouveau projet
451 452 label_project_plural: Projets
452 453 label_x_projects:
453 454 zero: aucun projet
454 455 one: un projet
455 456 other: "%{count} projets"
456 457 label_project_all: Tous les projets
457 458 label_project_latest: Derniers projets
458 459 label_issue: Demande
459 460 label_issue_new: Nouvelle demande
460 461 label_issue_plural: Demandes
461 462 label_issue_view_all: Voir toutes les demandes
462 463 label_issue_added: Demande ajoutΓ©e
463 464 label_issue_updated: Demande mise Γ  jour
464 465 label_issue_note_added: Note ajoutΓ©e
465 466 label_issue_status_updated: Statut changΓ©
466 467 label_issue_priority_updated: PrioritΓ© changΓ©e
467 468 label_issues_by: "Demandes par %{value}"
468 469 label_document: Document
469 470 label_document_new: Nouveau document
470 471 label_document_plural: Documents
471 472 label_document_added: Document ajoutΓ©
472 473 label_role: RΓ΄le
473 474 label_role_plural: RΓ΄les
474 475 label_role_new: Nouveau rΓ΄le
475 476 label_role_and_permissions: RΓ΄les et permissions
476 477 label_role_anonymous: Anonyme
477 478 label_role_non_member: Non membre
478 479 label_member: Membre
479 480 label_member_new: Nouveau membre
480 481 label_member_plural: Membres
481 482 label_tracker: Tracker
482 483 label_tracker_plural: Trackers
483 484 label_tracker_new: Nouveau tracker
484 485 label_workflow: Workflow
485 486 label_issue_status: Statut de demandes
486 487 label_issue_status_plural: Statuts de demandes
487 488 label_issue_status_new: Nouveau statut
488 489 label_issue_category: CatΓ©gorie de demandes
489 490 label_issue_category_plural: CatΓ©gories de demandes
490 491 label_issue_category_new: Nouvelle catΓ©gorie
491 492 label_custom_field: Champ personnalisΓ©
492 493 label_custom_field_plural: Champs personnalisΓ©s
493 494 label_custom_field_new: Nouveau champ personnalisΓ©
494 495 label_enumerations: Listes de valeurs
495 496 label_enumeration_new: Nouvelle valeur
496 497 label_information: Information
497 498 label_information_plural: Informations
498 499 label_please_login: Identification
499 500 label_register: S'enregistrer
500 501 label_login_with_open_id_option: S'authentifier avec OpenID
501 502 label_password_lost: Mot de passe perdu
502 503 label_home: Accueil
503 504 label_my_page: Ma page
504 505 label_my_account: Mon compte
505 506 label_my_projects: Mes projets
506 507 label_my_page_block: Blocs disponibles
507 508 label_administration: Administration
508 509 label_login: Connexion
509 510 label_logout: DΓ©connexion
510 511 label_help: Aide
511 512 label_reported_issues: "Demandes soumises "
512 513 label_assigned_to_me_issues: Demandes qui me sont assignΓ©es
513 514 label_last_login: "Dernière connexion "
514 515 label_registered_on: "Inscrit le "
515 516 label_activity: ActivitΓ©
516 517 label_overall_activity: ActivitΓ© globale
517 518 label_user_activity: "ActivitΓ© de %{value}"
518 519 label_new: Nouveau
519 520 label_logged_as: ConnectΓ© en tant que
520 521 label_environment: Environnement
521 522 label_authentication: Authentification
522 523 label_auth_source: Mode d'authentification
523 524 label_auth_source_new: Nouveau mode d'authentification
524 525 label_auth_source_plural: Modes d'authentification
525 526 label_subproject_plural: Sous-projets
526 527 label_subproject_new: Nouveau sous-projet
527 528 label_and_its_subprojects: "%{value} et ses sous-projets"
528 529 label_min_max_length: Longueurs mini - maxi
529 530 label_list: Liste
530 531 label_date: Date
531 532 label_integer: Entier
532 533 label_float: Nombre dΓ©cimal
533 534 label_boolean: BoolΓ©en
534 535 label_string: Texte
535 536 label_text: Texte long
536 537 label_attribute: Attribut
537 538 label_attribute_plural: Attributs
538 539 label_download: "%{count} tΓ©lΓ©chargement"
539 540 label_download_plural: "%{count} tΓ©lΓ©chargements"
540 541 label_no_data: Aucune donnΓ©e Γ  afficher
541 542 label_change_status: Changer le statut
542 543 label_history: Historique
543 544 label_attachment: Fichier
544 545 label_attachment_new: Nouveau fichier
545 546 label_attachment_delete: Supprimer le fichier
546 547 label_attachment_plural: Fichiers
547 548 label_file_added: Fichier ajoutΓ©
548 549 label_report: Rapport
549 550 label_report_plural: Rapports
550 551 label_news: Annonce
551 552 label_news_new: Nouvelle annonce
552 553 label_news_plural: Annonces
553 554 label_news_latest: Dernières annonces
554 555 label_news_view_all: Voir toutes les annonces
555 556 label_news_added: Annonce ajoutΓ©e
556 557 label_news_comment_added: Commentaire ajoutΓ© Γ  une annonce
557 558 label_settings: Configuration
558 559 label_overview: AperΓ§u
559 560 label_version: Version
560 561 label_version_new: Nouvelle version
561 562 label_version_plural: Versions
562 563 label_confirmation: Confirmation
563 564 label_export_to: 'Formats disponibles :'
564 565 label_read: Lire...
565 566 label_public_projects: Projets publics
566 567 label_open_issues: ouvert
567 568 label_open_issues_plural: ouverts
568 569 label_closed_issues: fermΓ©
569 570 label_closed_issues_plural: fermΓ©s
570 571 label_x_open_issues_abbr_on_total:
571 572 zero: 0 ouverte sur %{total}
572 573 one: 1 ouverte sur %{total}
573 574 other: "%{count} ouvertes sur %{total}"
574 575 label_x_open_issues_abbr:
575 576 zero: 0 ouverte
576 577 one: 1 ouverte
577 578 other: "%{count} ouvertes"
578 579 label_x_closed_issues_abbr:
579 580 zero: 0 fermΓ©e
580 581 one: 1 fermΓ©e
581 582 other: "%{count} fermΓ©es"
582 583 label_x_issues:
583 584 zero: 0 demande
584 585 one: 1 demande
585 586 other: "%{count} demandes"
586 587 label_total: Total
587 588 label_permissions: Permissions
588 589 label_current_status: Statut actuel
589 590 label_new_statuses_allowed: Nouveaux statuts autorisΓ©s
590 591 label_all: tous
591 592 label_none: aucun
592 593 label_nobody: personne
593 594 label_next: Suivant
594 595 label_previous: PrΓ©cΓ©dent
595 596 label_used_by: UtilisΓ© par
596 597 label_details: DΓ©tails
597 598 label_add_note: Ajouter une note
598 599 label_per_page: Par page
599 600 label_calendar: Calendrier
600 601 label_months_from: mois depuis
601 602 label_gantt: Gantt
602 603 label_internal: Interne
603 604 label_last_changes: "%{count} derniers changements"
604 605 label_change_view_all: Voir tous les changements
605 606 label_personalize_page: Personnaliser cette page
606 607 label_comment: Commentaire
607 608 label_comment_plural: Commentaires
608 609 label_x_comments:
609 610 zero: aucun commentaire
610 611 one: un commentaire
611 612 other: "%{count} commentaires"
612 613 label_comment_add: Ajouter un commentaire
613 614 label_comment_added: Commentaire ajoutΓ©
614 615 label_comment_delete: Supprimer les commentaires
615 616 label_query: Rapport personnalisΓ©
616 617 label_query_plural: Rapports personnalisΓ©s
617 618 label_query_new: Nouveau rapport
618 619 label_my_queries: Mes rapports personnalisΓ©s
619 620 label_filter_add: "Ajouter le filtre "
620 621 label_filter_plural: Filtres
621 622 label_equals: Γ©gal
622 623 label_not_equals: diffΓ©rent
623 624 label_in_less_than: dans moins de
624 625 label_in_more_than: dans plus de
625 626 label_in: dans
626 627 label_today: aujourd'hui
627 628 label_all_time: toute la pΓ©riode
628 629 label_yesterday: hier
629 630 label_this_week: cette semaine
630 631 label_last_week: la semaine dernière
631 632 label_last_n_days: "les %{count} derniers jours"
632 633 label_this_month: ce mois-ci
633 634 label_last_month: le mois dernier
634 635 label_this_year: cette annΓ©e
635 636 label_date_range: PΓ©riode
636 637 label_less_than_ago: il y a moins de
637 638 label_more_than_ago: il y a plus de
638 639 label_ago: il y a
639 640 label_contains: contient
640 641 label_not_contains: ne contient pas
641 642 label_day_plural: jours
642 643 label_repository: DΓ©pΓ΄t
643 644 label_repository_new: Nouveau dΓ©pΓ΄t
644 645 label_repository_plural: DΓ©pΓ΄ts
645 646 label_browse: Parcourir
646 647 label_modification: "%{count} modification"
647 648 label_modification_plural: "%{count} modifications"
648 649 label_revision: "RΓ©vision "
649 650 label_revision_plural: RΓ©visions
650 651 label_associated_revisions: RΓ©visions associΓ©es
651 652 label_added: ajoutΓ©
652 653 label_modified: modifiΓ©
653 654 label_copied: copiΓ©
654 655 label_renamed: renommΓ©
655 656 label_deleted: supprimΓ©
656 657 label_latest_revision: Dernière révision
657 658 label_latest_revision_plural: Dernières révisions
658 659 label_view_revisions: Voir les rΓ©visions
659 660 label_max_size: Taille maximale
660 661 label_sort_highest: Remonter en premier
661 662 label_sort_higher: Remonter
662 663 label_sort_lower: Descendre
663 664 label_sort_lowest: Descendre en dernier
664 665 label_roadmap: Roadmap
665 666 label_roadmap_due_in: "Γ‰chΓ©ance dans %{value}"
666 667 label_roadmap_overdue: "En retard de %{value}"
667 668 label_roadmap_no_issues: Aucune demande pour cette version
668 669 label_search: "Recherche "
669 670 label_result_plural: RΓ©sultats
670 671 label_all_words: Tous les mots
671 672 label_wiki: Wiki
672 673 label_wiki_edit: RΓ©vision wiki
673 674 label_wiki_edit_plural: RΓ©visions wiki
674 675 label_wiki_page: Page wiki
675 676 label_wiki_page_plural: Pages wiki
676 677 label_index_by_title: Index par titre
677 678 label_index_by_date: Index par date
678 679 label_current_version: Version actuelle
679 680 label_preview: PrΓ©visualisation
680 681 label_feed_plural: Flux RSS
681 682 label_changes_details: DΓ©tails de tous les changements
682 683 label_issue_tracking: Suivi des demandes
683 684 label_spent_time: Temps passΓ©
684 685 label_f_hour: "%{value} heure"
685 686 label_f_hour_plural: "%{value} heures"
686 687 label_time_tracking: Suivi du temps
687 688 label_change_plural: Changements
688 689 label_statistics: Statistiques
689 690 label_commits_per_month: Commits par mois
690 691 label_commits_per_author: Commits par auteur
691 692 label_view_diff: Voir les diffΓ©rences
692 693 label_diff_inline: en ligne
693 694 label_diff_side_by_side: cΓ΄te Γ  cΓ΄te
694 695 label_options: Options
695 696 label_copy_workflow_from: Copier le workflow de
696 697 label_permissions_report: Synthèse des permissions
697 698 label_watched_issues: Demandes surveillΓ©es
698 699 label_related_issues: Demandes liΓ©es
699 700 label_applied_status: Statut appliquΓ©
700 701 label_loading: Chargement...
701 702 label_relation_new: Nouvelle relation
702 703 label_relation_delete: Supprimer la relation
703 704 label_relates_to: liΓ© Γ 
704 705 label_duplicates: duplique
705 706 label_duplicated_by: dupliquΓ© par
706 707 label_blocks: bloque
707 708 label_blocked_by: bloquΓ© par
708 709 label_precedes: précède
709 710 label_follows: suit
710 711 label_end_to_start: fin Γ  dΓ©but
711 712 label_end_to_end: fin Γ  fin
712 713 label_start_to_start: dΓ©but Γ  dΓ©but
713 714 label_start_to_end: dΓ©but Γ  fin
714 715 label_stay_logged_in: Rester connectΓ©
715 716 label_disabled: dΓ©sactivΓ©
716 717 label_show_completed_versions: Voir les versions passΓ©es
717 718 label_me: moi
718 719 label_board: Forum
719 720 label_board_new: Nouveau forum
720 721 label_board_plural: Forums
721 722 label_topic_plural: Discussions
722 723 label_message_plural: Messages
723 724 label_message_last: Dernier message
724 725 label_message_new: Nouveau message
725 726 label_message_posted: Message ajoutΓ©
726 727 label_reply_plural: RΓ©ponses
727 728 label_send_information: Envoyer les informations Γ  l'utilisateur
728 729 label_year: AnnΓ©e
729 730 label_month: Mois
730 731 label_week: Semaine
731 732 label_date_from: Du
732 733 label_date_to: Au
733 734 label_language_based: BasΓ© sur la langue de l'utilisateur
734 735 label_sort_by: "Trier par %{value}"
735 736 label_send_test_email: Envoyer un email de test
736 737 label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a %{value}"
737 738 label_module_plural: Modules
738 739 label_added_time_by: "AjoutΓ© par %{author} il y a %{age}"
739 740 label_updated_time_by: "Mis Γ  jour par %{author} il y a %{age}"
740 741 label_updated_time: "Mis Γ  jour il y a %{value}"
741 742 label_jump_to_a_project: Aller Γ  un projet...
742 743 label_file_plural: Fichiers
743 744 label_changeset_plural: RΓ©visions
744 745 label_default_columns: Colonnes par dΓ©faut
745 746 label_no_change_option: (Pas de changement)
746 747 label_bulk_edit_selected_issues: Modifier les demandes sΓ©lectionnΓ©es
747 748 label_theme: Thème
748 749 label_default: DΓ©faut
749 750 label_search_titles_only: Uniquement dans les titres
750 751 label_user_mail_option_all: "Pour tous les Γ©vΓ©nements de tous mes projets"
751 752 label_user_mail_option_selected: "Pour tous les Γ©vΓ©nements des projets sΓ©lectionnΓ©s..."
752 753 label_user_mail_no_self_notified: "Je ne veux pas Γͺtre notifiΓ© des changements que j'effectue"
753 754 label_registration_activation_by_email: activation du compte par email
754 755 label_registration_manual_activation: activation manuelle du compte
755 756 label_registration_automatic_activation: activation automatique du compte
756 757 label_display_per_page: "Par page : %{value}"
757 758 label_age: Γ‚ge
758 759 label_change_properties: Changer les propriΓ©tΓ©s
759 760 label_general: GΓ©nΓ©ral
760 761 label_more: Plus
761 762 label_scm: SCM
762 763 label_plugins: Plugins
763 764 label_ldap_authentication: Authentification LDAP
764 765 label_downloads_abbr: D/L
765 766 label_optional_description: Description facultative
766 767 label_add_another_file: Ajouter un autre fichier
767 768 label_preferences: PrΓ©fΓ©rences
768 769 label_chronological_order: Dans l'ordre chronologique
769 770 label_reverse_chronological_order: Dans l'ordre chronologique inverse
770 771 label_planning: Planning
771 772 label_incoming_emails: Emails entrants
772 773 label_generate_key: GΓ©nΓ©rer une clΓ©
773 774 label_issue_watchers: Observateurs
774 775 label_example: Exemple
775 776 label_display: Affichage
776 777 label_sort: Tri
777 778 label_ascending: Croissant
778 779 label_descending: DΓ©croissant
779 780 label_date_from_to: Du %{start} au %{end}
780 781 label_wiki_content_added: Page wiki ajoutΓ©e
781 782 label_wiki_content_updated: Page wiki mise Γ  jour
782 783 label_group_plural: Groupes
783 784 label_group: Groupe
784 785 label_group_new: Nouveau groupe
785 786 label_time_entry_plural: Temps passΓ©
786 787 label_version_sharing_none: Non partagΓ©
787 788 label_version_sharing_descendants: Avec les sous-projets
788 789 label_version_sharing_hierarchy: Avec toute la hiΓ©rarchie
789 790 label_version_sharing_tree: Avec tout l'arbre
790 791 label_version_sharing_system: Avec tous les projets
791 792 label_copy_source: Source
792 793 label_copy_target: Cible
793 794 label_copy_same_as_target: Comme la cible
794 795 label_update_issue_done_ratios: Mettre Γ  jour l'avancement des demandes
795 796 label_display_used_statuses_only: N'afficher que les statuts utilisΓ©s dans ce tracker
796 797 label_api_access_key: Clé d'accès API
797 798 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
798 799 label_feeds_access_key: Clé d'accès RSS
799 800 label_missing_api_access_key: Clé d'accès API manquante
800 801 label_missing_feeds_access_key: Clé d'accès RSS manquante
801 802 label_close_versions: Fermer les versions terminΓ©es
802 803 label_revision_id: Revision %{value}
803 804 label_profile: Profil
804 805 label_subtask_plural: Sous-tΓ’ches
805 806 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
806 807 label_principal_search: "Rechercher un utilisateur ou un groupe :"
807 808 label_user_search: "Rechercher un utilisateur :"
808 809 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
809 810 label_additional_workflow_transitions_for_assignee: Autorisations supplΓ©mentaires lorsque la demande est assignΓ©e Γ  l'utilisateur
810 811 label_issues_visibility_all: Toutes les demandes
811 812 label_issues_visibility_public: Toutes les demandes non privΓ©es
812 813 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
813 814 label_export_options: Options d'exportation %{export_format}
814 815
815 816 button_login: Connexion
816 817 button_submit: Soumettre
817 818 button_save: Sauvegarder
818 819 button_check_all: Tout cocher
819 820 button_uncheck_all: Tout dΓ©cocher
820 821 button_collapse_all: Plier tout
821 822 button_expand_all: DΓ©plier tout
822 823 button_delete: Supprimer
823 824 button_create: CrΓ©er
824 825 button_create_and_continue: CrΓ©er et continuer
825 826 button_test: Tester
826 827 button_edit: Modifier
827 828 button_add: Ajouter
828 829 button_change: Changer
829 830 button_apply: Appliquer
830 831 button_clear: Effacer
831 832 button_lock: Verrouiller
832 833 button_unlock: DΓ©verrouiller
833 834 button_download: TΓ©lΓ©charger
834 835 button_list: Lister
835 836 button_view: Voir
836 837 button_move: DΓ©placer
837 838 button_move_and_follow: DΓ©placer et suivre
838 839 button_back: Retour
839 840 button_cancel: Annuler
840 841 button_activate: Activer
841 842 button_sort: Trier
842 843 button_log_time: Saisir temps
843 844 button_rollback: Revenir Γ  cette version
844 845 button_watch: Surveiller
845 846 button_unwatch: Ne plus surveiller
846 847 button_reply: RΓ©pondre
847 848 button_archive: Archiver
848 849 button_unarchive: DΓ©sarchiver
849 850 button_reset: RΓ©initialiser
850 851 button_rename: Renommer
851 852 button_change_password: Changer de mot de passe
852 853 button_copy: Copier
853 854 button_copy_and_follow: Copier et suivre
854 855 button_annotate: Annoter
855 856 button_update: Mettre Γ  jour
856 857 button_configure: Configurer
857 858 button_quote: Citer
858 859 button_duplicate: Dupliquer
859 860 button_show: Afficher
860 861 button_edit_section: Modifier cette section
861 862 button_export: Exporter
862 863
863 864 status_active: actif
864 865 status_registered: enregistrΓ©
865 866 status_locked: verrouillΓ©
866 867
867 868 version_status_open: ouvert
868 869 version_status_locked: verrouillΓ©
869 870 version_status_closed: fermΓ©
870 871
871 872 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyΓ©e
872 873 text_regexp_info: ex. ^[A-Z0-9]+$
873 874 text_min_max_length_info: 0 pour aucune restriction
874 875 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
875 876 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront Γ©galement supprimΓ©s."
876 877 text_workflow_edit: SΓ©lectionner un tracker et un rΓ΄le pour Γ©diter le workflow
877 878 text_are_you_sure: Êtes-vous sûr ?
878 879 text_tip_issue_begin_day: tΓ’che commenΓ§ant ce jour
879 880 text_tip_issue_end_day: tΓ’che finissant ce jour
880 881 text_tip_issue_begin_end_day: tΓ’che commenΓ§ant et finissant ce jour
881 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 883 text_caracters_maximum: "%{count} caractères maximum."
883 884 text_caracters_minimum: "%{count} caractères minimum."
884 885 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
885 886 text_tracker_no_workflow: Aucun worflow n'est dΓ©fini pour ce tracker
886 887 text_unallowed_characters: Caractères non autorisés
887 888 text_comma_separated: Plusieurs valeurs possibles (sΓ©parΓ©es par des virgules).
888 889 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
889 890 text_issues_ref_in_commit_messages: RΓ©fΓ©rencement et rΓ©solution des demandes dans les commentaires de commits
890 891 text_issue_added: "La demande %{id} a Γ©tΓ© soumise par %{author}."
891 892 text_issue_updated: "La demande %{id} a Γ©tΓ© mise Γ  jour par %{author}."
892 893 text_wiki_destroy_confirmation: Etes-vous sΓ»r de vouloir supprimer ce wiki et tout son contenu ?
893 894 text_issue_category_destroy_question: "%{count} demandes sont affectΓ©es Γ  cette catΓ©gorie. Que voulez-vous faire ?"
894 895 text_issue_category_destroy_assignments: N'affecter les demandes Γ  aucune autre catΓ©gorie
895 896 text_issue_category_reassign_to: RΓ©affecter les demandes Γ  cette catΓ©gorie
896 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 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 899 text_load_default_configuration: Charger le paramΓ©trage par dΓ©faut
899 900 text_status_changed_by_changeset: "AppliquΓ© par commit %{value}."
900 901 text_time_logged_by_changeset: "AppliquΓ© par commit %{value}"
901 902 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
902 903 text_issues_destroy_descendants_confirmation: "Cela entrainera Γ©galement la suppression de %{count} sous-tΓ’che(s)."
903 904 text_select_project_modules: 'SΓ©lectionner les modules Γ  activer pour ce projet :'
904 905 text_default_administrator_account_changed: Compte administrateur par dΓ©faut changΓ©
905 906 text_file_repository_writable: RΓ©pertoire de stockage des fichiers accessible en Γ©criture
906 907 text_plugin_assets_writable: RΓ©pertoire public des plugins accessible en Γ©criture
907 908 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
908 909 text_destroy_time_entries_question: "%{hours} heures ont Γ©tΓ© enregistrΓ©es sur les demandes Γ  supprimer. Que voulez-vous faire ?"
909 910 text_destroy_time_entries: Supprimer les heures
910 911 text_assign_time_entries_to_project: Reporter les heures sur le projet
911 912 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
912 913 text_user_wrote: "%{value} a Γ©crit :"
913 914 text_enumeration_destroy_question: "Cette valeur est affectΓ©e Γ  %{count} objets."
914 915 text_enumeration_category_reassign_to: 'RΓ©affecter les objets Γ  cette valeur:'
915 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 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 918 text_diff_truncated: '... Ce diffΓ©rentiel a Γ©tΓ© tronquΓ© car il excΓ¨de la taille maximale pouvant Γͺtre affichΓ©e.'
918 919 text_custom_field_possible_values_info: 'Une ligne par valeur'
919 920 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
920 921 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
921 922 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
922 923 text_wiki_page_reassign_children: "RΓ©affecter les sous-pages Γ  cette page"
923 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 925 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardΓ© qui sera perdu si vous quittez la page."
925 926
926 927 default_role_manager: "Manager "
927 928 default_role_developer: "DΓ©veloppeur "
928 929 default_role_reporter: "Rapporteur "
929 930 default_tracker_bug: Anomalie
930 931 default_tracker_feature: Evolution
931 932 default_tracker_support: Assistance
932 933 default_issue_status_new: Nouveau
933 934 default_issue_status_in_progress: En cours
934 935 default_issue_status_resolved: RΓ©solu
935 936 default_issue_status_feedback: Commentaire
936 937 default_issue_status_closed: FermΓ©
937 938 default_issue_status_rejected: RejetΓ©
938 939 default_doc_category_user: Documentation utilisateur
939 940 default_doc_category_tech: Documentation technique
940 941 default_priority_low: Bas
941 942 default_priority_normal: Normal
942 943 default_priority_high: Haut
943 944 default_priority_urgent: Urgent
944 945 default_priority_immediate: ImmΓ©diat
945 946 default_activity_design: Conception
946 947 default_activity_development: DΓ©veloppement
947 948
948 949 enumeration_issue_priorities: PrioritΓ©s des demandes
949 950 enumeration_doc_categories: CatΓ©gories des documents
950 951 enumeration_activities: ActivitΓ©s (suivi du temps)
951 952 label_greater_or_equal: ">="
952 953 label_less_or_equal: "<="
953 954 label_between: entre
954 955 label_view_all_revisions: Voir toutes les rΓ©visions
955 956 label_tag: Tag
956 957 label_branch: Branche
957 958 error_no_tracker_in_project: "Aucun tracker n'est associΓ© Γ  ce projet. VΓ©rifier la configuration du projet."
958 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 960 text_journal_changed: "%{label} changΓ© de %{old} Γ  %{new}"
960 961 text_journal_changed_no_detail: "%{label} mis Γ  jour"
961 962 text_journal_set_to: "%{label} mis Γ  %{value}"
962 963 text_journal_deleted: "%{label} %{old} supprimΓ©"
963 964 text_journal_added: "%{label} %{value} ajoutΓ©"
964 965 enumeration_system_activity: Activité système
965 966 label_board_sticky: Sticky
966 967 label_board_locked: VerrouillΓ©
967 968 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
968 969 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisΓ©
969 970 error_unable_to_connect: Connexion impossible (%{value})
970 971 error_can_not_remove_role: Ce rΓ΄le est utilisΓ© et ne peut pas Γͺtre supprimΓ©.
971 972 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas Γͺtre supprimΓ©.
972 973 field_principal: Principal
973 974 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
974 975 text_zoom_out: Zoom arrière
975 976 text_zoom_in: Zoom avant
976 977 notice_unable_delete_time_entry: Impossible de supprimer le temps passΓ©.
977 978 label_overall_spent_time: Temps passΓ© global
978 979 field_time_entries: Temps passΓ©
979 980 project_module_gantt: Gantt
980 981 project_module_calendar: Calendrier
981 982 button_edit_associated_wikipage: "Modifier la page wiki associΓ©e: %{page_title}"
982 983 text_are_you_sure_with_children: Supprimer la demande et toutes ses sous-demandes ?
983 984 field_text: Champ texte
984 985 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
985 986 setting_default_notification_option: Option de notification par dΓ©faut
986 987 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
987 988 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assignΓ©
988 989 label_user_mail_option_none: Aucune notification
989 990 field_member_of_group: Groupe de l'assignΓ©
990 991 field_assigned_to_role: RΓ΄le de l'assignΓ©
991 992 setting_emails_header: En-tΓͺte des emails
992 993 label_bulk_edit_selected_time_entries: Modifier les temps passΓ©s sΓ©lectionnΓ©s
993 994 text_time_entries_destroy_confirmation: "Etes-vous sΓ»r de vouloir supprimer les temps passΓ©s sΓ©lectionnΓ©s ?"
994 995 field_scm_path_encoding: Encodage des chemins
995 996 text_scm_path_encoding_note: "DΓ©faut : UTF-8"
996 997 field_path_to_repository: Chemin du dΓ©pΓ΄t
997 998 field_root_directory: RΓ©pertoire racine
998 999 field_cvs_module: Module
999 1000 field_cvsroot: CVSROOT
1000 1001 text_mercurial_repository_note: "DΓ©pΓ΄t local (exemples : /hgrepo, c:\\hgrepo)"
1001 1002 text_scm_command: Commande
1002 1003 text_scm_command_version: Version
1003 1004 label_git_report_last_commit: Afficher le dernier commit des fichiers et rΓ©pertoires
1004 1005 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1005 1006 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1006 1007 label_diff: diff
1007 1008 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1008 1009 description_query_sort_criteria_direction: Ordre de tri
1009 1010 description_project_scope: Périmètre de recherche
1010 1011 description_filter: Filtre
1011 1012 description_user_mail_notification: Option de notification
1012 1013 description_date_from: Date de dΓ©but
1013 1014 description_message_content: Contenu du message
1014 1015 description_available_columns: Colonnes disponibles
1015 1016 description_all_columns: Toutes les colonnes
1016 1017 description_date_range_interval: Choisir une pΓ©riode
1017 1018 description_issue_category_reassign: Choisir une catΓ©gorie
1018 1019 description_search: Champ de recherche
1019 1020 description_notes: Notes
1020 1021 description_date_range_list: Choisir une pΓ©riode prΓ©dΓ©finie
1021 1022 description_choose_project: Projets
1022 1023 description_date_to: Date de fin
1023 1024 description_query_sort_criteria_attribute: Critère de tri
1024 1025 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1025 1026 description_selected_columns: Colonnes sΓ©lectionnΓ©es
1026 1027 label_parent_revision: Parent
1027 1028 label_child_revision: Enfant
1028 1029 error_scm_annotate_big_text_file: Cette entrΓ©e ne peut pas Γͺtre annotΓ©e car elle excΓ¨de la taille maximale.
1029 1030 setting_repositories_encodings: Encodages des fichiers et des dΓ©pΓ΄ts
@@ -1,392 +1,404
1 1 ActionController::Routing::Routes.draw do |map|
2 2 # Add your own custom routes here.
3 3 # The priority is based upon order of creation: first created -> highest priority.
4 4
5 5 # Here's a sample route:
6 6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 7 # Keep in mind you can assign values other than :controller and :action
8 8
9 9 map.home '', :controller => 'welcome', :conditions => {:method => :get}
10 10
11 11 map.signin 'login', :controller => 'account', :action => 'login',
12 12 :conditions => {:method => [:get, :post]}
13 13 map.signout 'logout', :controller => 'account', :action => 'logout',
14 14 :conditions => {:method => :get}
15 15 map.connect 'account/register', :controller => 'account', :action => 'register',
16 16 :conditions => {:method => [:get, :post]}
17 17 map.connect 'account/lost_password', :controller => 'account', :action => 'lost_password',
18 18 :conditions => {:method => [:get, :post]}
19 19 map.connect 'account/activate', :controller => 'account', :action => 'activate',
20 20 :conditions => {:method => :get}
21 21
22 22 map.connect 'projects/:id/wiki', :controller => 'wikis',
23 23 :action => 'edit', :conditions => {:method => :post}
24 24 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis',
25 25 :action => 'destroy', :conditions => {:method => [:get, :post]}
26 26
27 27 map.with_options :controller => 'messages' do |messages_routes|
28 28 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
29 29 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
30 30 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
31 31 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
32 32 end
33 33 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
34 34 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
35 35 messages_actions.connect 'boards/:board_id/topics/preview', :action => 'preview'
36 36 messages_actions.connect 'boards/:board_id/topics/quote/:id', :action => 'quote'
37 37 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
38 38 messages_actions.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
39 39 messages_actions.connect 'boards/:board_id/topics/:id/destroy', :action => 'destroy'
40 40 end
41 41 end
42 42
43 43 # Misc issue routes. TODO: move into resources
44 44 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes',
45 45 :action => 'issues', :conditions => { :method => :get }
46 46 # TODO: would look nicer as /issues/:id/preview
47 47 map.preview_issue '/issues/preview/:id', :controller => 'previews',
48 48 :action => 'issue'
49 49 map.issues_context_menu '/issues/context_menu',
50 50 :controller => 'context_menus', :action => 'issues'
51 51
52 52 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
53 53 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new',
54 54 :id => /\d+/, :conditions => { :method => :post }
55 55
56 56 map.connect '/journals/diff/:id', :controller => 'journals', :action => 'diff',
57 57 :id => /\d+/, :conditions => { :method => :get }
58 58 map.connect '/journals/edit/:id', :controller => 'journals', :action => 'edit',
59 59 :id => /\d+/, :conditions => { :method => [:get, :post] }
60 60
61 61 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
62 62 gantts_routes.connect '/projects/:project_id/issues/gantt'
63 63 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
64 64 gantts_routes.connect '/issues/gantt.:format'
65 65 end
66 66
67 67 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
68 68 calendars_routes.connect '/projects/:project_id/issues/calendar'
69 69 calendars_routes.connect '/issues/calendar'
70 70 end
71 71
72 72 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
73 73 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
74 74 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
75 75 end
76 76
77 77 map.connect 'my/account', :controller => 'my', :action => 'account',
78 78 :conditions => {:method => [:get, :post]}
79 79 map.connect 'my/page', :controller => 'my', :action => 'page',
80 80 :conditions => {:method => :get}
81 81 # Redirects to my/page
82 82 map.connect 'my', :controller => 'my', :action => 'index',
83 83 :conditions => {:method => :get}
84 84 map.connect 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key',
85 85 :conditions => {:method => :post}
86 86 map.connect 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key',
87 87 :conditions => {:method => :post}
88 88 map.connect 'my/password', :controller => 'my', :action => 'password',
89 89 :conditions => {:method => [:get, :post]}
90 90 map.connect 'my/page_layout', :controller => 'my', :action => 'page_layout',
91 91 :conditions => {:method => :get}
92 92 map.connect 'my/add_block', :controller => 'my', :action => 'add_block',
93 93 :conditions => {:method => :post}
94 94 map.connect 'my/remove_block', :controller => 'my', :action => 'remove_block',
95 95 :conditions => {:method => :post}
96 96 map.connect 'my/order_blocks', :controller => 'my', :action => 'order_blocks',
97 97 :conditions => {:method => :post}
98 98
99 99 map.connect 'projects/:id/members/new', :controller => 'members',
100 100 :action => 'new', :conditions => { :method => :post }
101 101 map.connect 'members/edit/:id', :controller => 'members',
102 102 :action => 'edit', :id => /\d+/, :conditions => { :method => :post }
103 103 map.connect 'members/destroy/:id', :controller => 'members',
104 104 :action => 'destroy', :id => /\d+/, :conditions => { :method => :post }
105 105 map.connect 'members/autocomplete_for_member/:id', :controller => 'members',
106 106 :action => 'autocomplete_for_member', :conditions => { :method => :post }
107 107
108 108 map.with_options :controller => 'users' do |users|
109 109 users.user_membership 'users/:id/memberships/:membership_id',
110 110 :action => 'edit_membership',
111 111 :conditions => {:method => :put}
112 112 users.connect 'users/:id/memberships/:membership_id',
113 113 :action => 'destroy_membership',
114 114 :conditions => {:method => :delete}
115 115 users.user_memberships 'users/:id/memberships',
116 116 :action => 'edit_membership',
117 117 :conditions => {:method => :post}
118 118 end
119 119 map.resources :users
120 120
121 121 # For nice "roadmap" in the url for the index action
122 122 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
123 123
124 124 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
125 125 map.connect 'news/:id/comments', :controller => 'comments',
126 126 :action => 'create', :conditions => {:method => :post}
127 127 map.connect 'news/:id/comments/:comment_id', :controller => 'comments',
128 128 :action => 'destroy', :conditions => {:method => :delete}
129 129
130 130 map.connect 'watchers/new', :controller=> 'watchers', :action => 'new',
131 131 :conditions => {:method => :get}
132 132 map.connect 'watchers', :controller=> 'watchers', :action => 'create',
133 133 :conditions => {:method => :post}
134 134 map.connect 'watchers/destroy', :controller=> 'watchers', :action => 'destroy',
135 135 :conditions => {:method => :post}
136 136 map.connect 'watchers/watch', :controller=> 'watchers', :action => 'watch',
137 137 :conditions => {:method => :post}
138 138 map.connect 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch',
139 139 :conditions => {:method => :post}
140 140 map.connect 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user',
141 141 :conditions => {:method => :get}
142 142
143 143 # TODO: port to be part of the resources route(s)
144 144 map.with_options :conditions => {:method => :get} do |project_views|
145 145 project_views.connect 'projects/:id/settings/:tab',
146 146 :controller => 'projects', :action => 'settings'
147 147 project_views.connect 'projects/:project_id/issues/:copy_from/copy',
148 148 :controller => 'issues', :action => 'new'
149 149 end
150 150
151 151 map.resources :projects, :member => {
152 152 :copy => [:get, :post],
153 153 :settings => :get,
154 154 :modules => :post,
155 155 :archive => :post,
156 156 :unarchive => :post
157 157 } do |project|
158 158 project.resource :enumerations, :controller => 'project_enumerations',
159 159 :only => [:update, :destroy]
160 160 # issue form update
161 161 project.issue_form 'issues/new', :controller => 'issues',
162 162 :action => 'new', :conditions => {:method => [:post, :put]}
163 163 project.resources :issues, :only => [:index, :new, :create] do |issues|
164 164 issues.resources :time_entries, :controller => 'timelog',
165 165 :collection => {:report => :get}
166 166 end
167 167
168 168 project.resources :files, :only => [:index, :new, :create]
169 169 project.resources :versions, :shallow => true,
170 170 :collection => {:close_completed => :put},
171 171 :member => {:status_by => :post}
172 172 project.resources :news, :shallow => true
173 173 project.resources :time_entries, :controller => 'timelog',
174 174 :collection => {:report => :get}
175 175 project.resources :queries, :only => [:new, :create]
176 176 project.resources :issue_categories, :shallow => true
177 177 project.resources :documents, :shallow => true, :member => {:add_attachment => :post}
178 178 project.resources :boards
179 179 project.resources :repositories, :shallow => true, :except => [:index, :show],
180 180 :member => {:committers => [:get, :post]}
181 181
182 182 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
183 183 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
184 184 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
185 185 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
186 186 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
187 187 project.resources :wiki, :except => [:new, :create], :member => {
188 188 :rename => [:get, :post],
189 189 :history => :get,
190 190 :preview => :any,
191 191 :protect => :post,
192 192 :add_attachment => :post
193 193 }, :collection => {
194 194 :export => :get,
195 195 :date_index => :get
196 196 }
197 197 end
198 198
199 199 map.connect 'news', :controller => 'news', :action => 'index'
200 200 map.connect 'news.:format', :controller => 'news', :action => 'index'
201 201
202 202 map.resources :queries, :except => [:show]
203 203 map.resources :issues,
204 204 :collection => {:bulk_edit => [:get, :post], :bulk_update => :post} do |issues|
205 205 issues.resources :time_entries, :controller => 'timelog',
206 206 :collection => {:report => :get}
207 207 issues.resources :relations, :shallow => true,
208 208 :controller => 'issue_relations',
209 209 :only => [:index, :show, :create, :destroy]
210 210 end
211 211 # Bulk deletion
212 212 map.connect '/issues', :controller => 'issues', :action => 'destroy',
213 213 :conditions => {:method => :delete}
214 214
215 215 map.connect '/time_entries/destroy',
216 216 :controller => 'timelog', :action => 'destroy',
217 217 :conditions => { :method => :delete }
218 218 map.time_entries_context_menu '/time_entries/context_menu',
219 219 :controller => 'context_menus', :action => 'time_entries'
220 220
221 221 map.resources :time_entries, :controller => 'timelog',
222 222 :collection => {:report => :get, :bulk_edit => :get, :bulk_update => :post}
223 223
224 224 map.with_options :controller => 'activities', :action => 'index',
225 225 :conditions => {:method => :get} do |activity|
226 226 activity.connect 'projects/:id/activity'
227 227 activity.connect 'projects/:id/activity.:format'
228 228 activity.connect 'activity', :id => nil
229 229 activity.connect 'activity.:format', :id => nil
230 230 end
231 231
232 232 map.with_options :controller => 'repositories' do |repositories|
233 233 repositories.with_options :conditions => {:method => :get} do |repository_views|
234 234 repository_views.connect 'projects/:id/repository',
235 235 :action => 'show'
236 236 repository_views.connect 'projects/:id/repository/statistics',
237 237 :action => 'stats'
238
238 repository_views.connect 'projects/:id/repository/graph',
239 :action => 'graph'
239 240 repository_views.connect 'projects/:id/repository/revisions',
240 241 :action => 'revisions'
241 242 repository_views.connect 'projects/:id/repository/revisions.:format',
242 243 :action => 'revisions'
243 244 repository_views.connect 'projects/:id/repository/revisions/:rev',
244 245 :action => 'revision'
245 246 repository_views.connect 'projects/:id/repository/revisions/:rev/diff',
246 247 :action => 'diff'
247 248 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format',
248 249 :action => 'diff'
249 250 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path',
250 :action => 'entry',
251 :format => 'raw',
252 :requirements => { :rev => /[a-z0-9\.\-_]+/ }
251 :action => 'entry', :format => 'raw'
253 252 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path',
254 :requirements => { :rev => /[a-z0-9\.\-_]+/ }
255
253 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
256 254 repository_views.connect 'projects/:id/repository/raw/*path',
257 255 :action => 'entry', :format => 'raw'
258 repository_views.connect 'projects/:id/repository/browse/*path',
259 :action => 'browse'
260 repository_views.connect 'projects/:id/repository/entry/*path',
261 :action => 'entry'
262 repository_views.connect 'projects/:id/repository/changes/*path',
263 :action => 'changes'
264 repository_views.connect 'projects/:id/repository/annotate/*path',
265 :action => 'annotate'
266 repository_views.connect 'projects/:id/repository/diff/*path',
256 repository_views.connect 'projects/:id/repository/:action/*path',
257 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
258
259 # Same routes with a repository_id
260 repository_views.connect 'projects/:id/repository/:repository_id/statistics',
261 :action => 'stats'
262 repository_views.connect 'projects/:id/repository/:repository_id/graph',
263 :action => 'graph'
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 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 283 :action => 'show'
270 repository_views.connect 'projects/:id/repository/graph',
271 :action => 'graph'
272 284 end
273 285
274 286 repositories.connect 'projects/:id/repository/revision',
275 287 :action => 'revision',
276 288 :conditions => {:method => [:get, :post]}
277 289 end
278 290
279 291 # additional routes for having the file name at the end of url
280 292 map.connect 'attachments/:id/:filename', :controller => 'attachments',
281 293 :action => 'show', :id => /\d+/, :filename => /.*/,
282 294 :conditions => {:method => :get}
283 295 map.connect 'attachments/download/:id/:filename', :controller => 'attachments',
284 296 :action => 'download', :id => /\d+/, :filename => /.*/,
285 297 :conditions => {:method => :get}
286 298 map.connect 'attachments/download/:id', :controller => 'attachments',
287 299 :action => 'download', :id => /\d+/,
288 300 :conditions => {:method => :get}
289 301 map.resources :attachments, :only => [:show, :destroy]
290 302
291 303 map.resources :groups, :member => {:autocomplete_for_user => :get}
292 304 map.group_users 'groups/:id/users', :controller => 'groups',
293 305 :action => 'add_users', :id => /\d+/,
294 306 :conditions => {:method => :post}
295 307 map.group_user 'groups/:id/users/:user_id', :controller => 'groups',
296 308 :action => 'remove_user', :id => /\d+/,
297 309 :conditions => {:method => :delete}
298 310 map.connect 'groups/destroy_membership/:id', :controller => 'groups',
299 311 :action => 'destroy_membership', :id => /\d+/,
300 312 :conditions => {:method => :post}
301 313 map.connect 'groups/edit_membership/:id', :controller => 'groups',
302 314 :action => 'edit_membership', :id => /\d+/,
303 315 :conditions => {:method => :post}
304 316
305 317 map.resources :trackers, :except => :show
306 318 map.resources :issue_statuses, :except => :show, :collection => {:update_issue_done_ratio => :post}
307 319 map.resources :custom_fields, :except => :show
308 320 map.resources :roles, :except => :show, :collection => {:permissions => [:get, :post]}
309 321 map.resources :enumerations, :except => :show
310 322
311 323 map.connect 'search', :controller => 'search', :action => 'index', :conditions => {:method => :get}
312 324
313 325 map.connect 'mail_handler', :controller => 'mail_handler',
314 326 :action => 'index', :conditions => {:method => :post}
315 327
316 328 map.connect 'admin', :controller => 'admin', :action => 'index',
317 329 :conditions => {:method => :get}
318 330 map.connect 'admin/projects', :controller => 'admin', :action => 'projects',
319 331 :conditions => {:method => :get}
320 332 map.connect 'admin/plugins', :controller => 'admin', :action => 'plugins',
321 333 :conditions => {:method => :get}
322 334 map.connect 'admin/info', :controller => 'admin', :action => 'info',
323 335 :conditions => {:method => :get}
324 336 map.connect 'admin/test_email', :controller => 'admin', :action => 'test_email',
325 337 :conditions => {:method => :get}
326 338 map.connect 'admin/default_configuration', :controller => 'admin',
327 339 :action => 'default_configuration', :conditions => {:method => :post}
328 340
329 341 # Used by AuthSourcesControllerTest
330 342 # TODO : refactor *AuthSourcesController to remove these routes
331 343 map.connect 'auth_sources', :controller => 'auth_sources',
332 344 :action => 'index', :conditions => {:method => :get}
333 345 map.connect 'auth_sources/new', :controller => 'auth_sources',
334 346 :action => 'new', :conditions => {:method => :get}
335 347 map.connect 'auth_sources/create', :controller => 'auth_sources',
336 348 :action => 'create', :conditions => {:method => :post}
337 349 map.connect 'auth_sources/destroy/:id', :controller => 'auth_sources',
338 350 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
339 351 map.connect 'auth_sources/test_connection/:id', :controller => 'auth_sources',
340 352 :action => 'test_connection', :conditions => {:method => :get}
341 353 map.connect 'auth_sources/edit/:id', :controller => 'auth_sources',
342 354 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
343 355 map.connect 'auth_sources/update/:id', :controller => 'auth_sources',
344 356 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
345 357
346 358 map.connect 'ldap_auth_sources', :controller => 'ldap_auth_sources',
347 359 :action => 'index', :conditions => {:method => :get}
348 360 map.connect 'ldap_auth_sources/new', :controller => 'ldap_auth_sources',
349 361 :action => 'new', :conditions => {:method => :get}
350 362 map.connect 'ldap_auth_sources/create', :controller => 'ldap_auth_sources',
351 363 :action => 'create', :conditions => {:method => :post}
352 364 map.connect 'ldap_auth_sources/destroy/:id', :controller => 'ldap_auth_sources',
353 365 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
354 366 map.connect 'ldap_auth_sources/test_connection/:id', :controller => 'ldap_auth_sources',
355 367 :action => 'test_connection', :conditions => {:method => :get}
356 368 map.connect 'ldap_auth_sources/edit/:id', :controller => 'ldap_auth_sources',
357 369 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
358 370 map.connect 'ldap_auth_sources/update/:id', :controller => 'ldap_auth_sources',
359 371 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
360 372
361 373 map.connect 'workflows', :controller => 'workflows',
362 374 :action => 'index', :conditions => {:method => :get}
363 375 map.connect 'workflows/edit', :controller => 'workflows',
364 376 :action => 'edit', :conditions => {:method => [:get, :post]}
365 377 map.connect 'workflows/copy', :controller => 'workflows',
366 378 :action => 'copy', :conditions => {:method => [:get, :post]}
367 379
368 380 map.connect 'settings', :controller => 'settings',
369 381 :action => 'index', :conditions => {:method => :get}
370 382 map.connect 'settings/edit', :controller => 'settings',
371 383 :action => 'edit', :conditions => {:method => [:get, :post]}
372 384 map.connect 'settings/plugin/:id', :controller => 'settings',
373 385 :action => 'plugin', :conditions => {:method => [:get, :post]}
374 386
375 387 map.with_options :controller => 'sys' do |sys|
376 388 sys.connect 'sys/projects.:format',
377 389 :action => 'projects',
378 390 :conditions => {:method => :get}
379 391 sys.connect 'sys/projects/:id/repository.:format',
380 392 :action => 'create_project_repository',
381 393 :conditions => {:method => :post}
382 394 sys.connect 'sys/fetch_changesets',
383 395 :action => 'fetch_changesets',
384 396 :conditions => {:method => :get}
385 397 end
386 398
387 399 map.connect 'robots.txt', :controller => 'welcome',
388 400 :action => 'robots', :conditions => {:method => :get}
389 401
390 402 # Used for OpenID
391 403 map.root :controller => 'account', :action => 'login'
392 404 end
@@ -1,1040 +1,1041
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
5 5 h1 {margin:0; padding:0; font-size: 24px;}
6 6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
8 8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
9 9
10 10 /***** Layout *****/
11 11 #wrapper {background: white;}
12 12
13 13 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 14 #top-menu ul {margin: 0; padding: 0;}
15 15 #top-menu li {
16 16 float:left;
17 17 list-style-type:none;
18 18 margin: 0px 0px 0px 0px;
19 19 padding: 0px 0px 0px 0px;
20 20 white-space:nowrap;
21 21 }
22 22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 24
25 25 #account {float:right;}
26 26
27 27 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 28 #header a {color:#f8f8f8;}
29 29 #header h1 a.ancestor { font-size: 80%; }
30 30 #quick-search {float:right;}
31 31
32 32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 33 #main-menu ul {margin: 0; padding: 0;}
34 34 #main-menu li {
35 35 float:left;
36 36 list-style-type:none;
37 37 margin: 0px 2px 0px 0px;
38 38 padding: 0px 0px 0px 0px;
39 39 white-space:nowrap;
40 40 }
41 41 #main-menu li a {
42 42 display: block;
43 43 color: #fff;
44 44 text-decoration: none;
45 45 font-weight: bold;
46 46 margin: 0;
47 47 padding: 4px 10px 4px 10px;
48 48 }
49 49 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 51
52 52 #admin-menu ul {margin: 0; padding: 0;}
53 53 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
54 54
55 55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 56 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 57 #admin-menu a.users { background-image: url(../images/user.png); }
58 58 #admin-menu a.groups { background-image: url(../images/group.png); }
59 59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 67 #admin-menu a.info { background-image: url(../images/help.png); }
68 68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 69
70 70 #main {background-color:#EEEEEE;}
71 71
72 72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 73 * html #sidebar{ width: 22%; }
74 74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 77 #sidebar .contextual { margin-right: 1em; }
78 78
79 79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 81 html>body #content { min-height: 600px; }
82 82 * html body #content { height: 600px; } /* IE */
83 83
84 84 #main.nosidebar #sidebar{ display: none; }
85 85 #main.nosidebar #content{ width: auto; border-right: 0; }
86 86
87 87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88 88
89 89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 90 #login-form table td {padding: 6px;}
91 91 #login-form label {font-weight: bold;}
92 92 #login-form input#username, #login-form input#password { width: 300px; }
93 93
94 94 #modalbg {position:absolute; top:0; left:0; width:100%; height:100%; background:#ccc; z-index:49; opacity:0.5;}
95 95 html>body #modalbg {position:fixed;}
96 96 div.modal { border-radius:5px; position:absolute; top:25%; background:#fff; border:2px solid #759FCF; z-index:50; padding:0px; padding:8px;}
97 97 div.modal h3.title {background:#759FCF; color:#fff; border:0; padding-left:8px; margin:-8px; margin-bottom: 1em; border-top-left-radius:2px;border-top-right-radius:2px;}
98 98 div.modal p.buttons {text-align:right; margin-bottom:0;}
99 99 html>body div.modal {position:fixed;}
100 100
101 101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
102 102
103 103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
104 104
105 105 /***** Links *****/
106 106 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
107 107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
108 108 a img{ border: 0; }
109 109
110 110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
111 a.repository.selected {font-weight:bold;}
111 112
112 113 /***** Tables *****/
113 114 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
114 115 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
115 116 table.list td { vertical-align: top; }
116 117 table.list td.id { width: 2%; text-align: center;}
117 118 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
118 119 table.list td.checkbox input {padding:0px;}
119 120 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
120 121 table.list td.buttons a { padding-right: 0.6em; }
121 122 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
122 123
123 124 tr.project td.name a { white-space:nowrap; }
124 125
125 126 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
126 127 tr.project.idnt-1 td.name {padding-left: 0.5em;}
127 128 tr.project.idnt-2 td.name {padding-left: 2em;}
128 129 tr.project.idnt-3 td.name {padding-left: 3.5em;}
129 130 tr.project.idnt-4 td.name {padding-left: 5em;}
130 131 tr.project.idnt-5 td.name {padding-left: 6.5em;}
131 132 tr.project.idnt-6 td.name {padding-left: 8em;}
132 133 tr.project.idnt-7 td.name {padding-left: 9.5em;}
133 134 tr.project.idnt-8 td.name {padding-left: 11em;}
134 135 tr.project.idnt-9 td.name {padding-left: 12.5em;}
135 136
136 137 tr.issue { text-align: center; white-space: nowrap; }
137 138 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
138 139 tr.issue td.subject { text-align: left; }
139 140 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
140 141
141 142 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
142 143 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
143 144 tr.issue.idnt-2 td.subject {padding-left: 2em;}
144 145 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
145 146 tr.issue.idnt-4 td.subject {padding-left: 5em;}
146 147 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
147 148 tr.issue.idnt-6 td.subject {padding-left: 8em;}
148 149 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
149 150 tr.issue.idnt-8 td.subject {padding-left: 11em;}
150 151 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
151 152
152 153 tr.entry { border: 1px solid #f8f8f8; }
153 154 tr.entry td { white-space: nowrap; }
154 155 tr.entry td.filename { width: 30%; }
155 156 tr.entry td.filename_no_report { width: 70%; }
156 157 tr.entry td.size { text-align: right; font-size: 90%; }
157 158 tr.entry td.revision, tr.entry td.author { text-align: center; }
158 159 tr.entry td.age { text-align: right; }
159 160 tr.entry.file td.filename a { margin-left: 16px; }
160 161 tr.entry.file td.filename_no_report a { margin-left: 16px; }
161 162
162 163 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
163 164 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
164 165
165 166 tr.changeset { height: 20px }
166 167 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
167 168 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
168 169 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
169 170 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
170 171 tr.changeset td.comments_nowrap { width: 45%; white-space:nowrap;}
171 172
172 173 table.files tr.file td { text-align: center; }
173 174 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
174 175 table.files tr.file td.digest { font-size: 80%; }
175 176
176 177 table.members td.roles, table.memberships td.roles { width: 45%; }
177 178
178 179 tr.message { height: 2.6em; }
179 180 tr.message td.subject { padding-left: 20px; }
180 181 tr.message td.created_on { white-space: nowrap; }
181 182 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
182 183 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
183 184 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
184 185
185 186 tr.version.closed, tr.version.closed a { color: #999; }
186 187 tr.version td.name { padding-left: 20px; }
187 188 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
188 189 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
189 190
190 191 tr.user td { width:13%; }
191 192 tr.user td.email { width:18%; }
192 193 tr.user td { white-space: nowrap; }
193 194 tr.user.locked, tr.user.registered { color: #aaa; }
194 195 tr.user.locked a, tr.user.registered a { color: #aaa; }
195 196
196 197 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
197 198
198 199 tr.time-entry { text-align: center; white-space: nowrap; }
199 200 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
200 201 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
201 202 td.hours .hours-dec { font-size: 0.9em; }
202 203
203 204 table.plugins td { vertical-align: middle; }
204 205 table.plugins td.configure { text-align: right; padding-right: 1em; }
205 206 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
206 207 table.plugins span.description { display: block; font-size: 0.9em; }
207 208 table.plugins span.url { display: block; font-size: 0.9em; }
208 209
209 210 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
210 211 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
211 212 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
212 213 tr.group:hover a.toggle-all { display:inline;}
213 214 a.toggle-all:hover {text-decoration:none;}
214 215
215 216 table.list tbody tr:hover { background-color:#ffffdd; }
216 217 table.list tbody tr.group:hover { background-color:inherit; }
217 218 table td {padding:2px;}
218 219 table p {margin:0;}
219 220 .odd {background-color:#f6f7f8;}
220 221 .even {background-color: #fff;}
221 222
222 223 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
223 224 a.sort.asc { background-image: url(../images/sort_asc.png); }
224 225 a.sort.desc { background-image: url(../images/sort_desc.png); }
225 226
226 227 table.attributes { width: 100% }
227 228 table.attributes th { vertical-align: top; text-align: left; }
228 229 table.attributes td { vertical-align: top; }
229 230
230 231 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
231 232
232 233 td.center {text-align:center;}
233 234
234 235 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
235 236
236 237 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
237 238 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
238 239 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
239 240 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
240 241
241 242 #watchers ul {margin: 0; padding: 0;}
242 243 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
243 244 #watchers select {width: 95%; display: block;}
244 245 #watchers a.delete {opacity: 0.4;}
245 246 #watchers a.delete:hover {opacity: 1;}
246 247 #watchers img.gravatar {margin: 0 4px 2px 0;}
247 248
248 249 .highlight { background-color: #FCFD8D;}
249 250 .highlight.token-1 { background-color: #faa;}
250 251 .highlight.token-2 { background-color: #afa;}
251 252 .highlight.token-3 { background-color: #aaf;}
252 253
253 254 .box{
254 255 padding:6px;
255 256 margin-bottom: 10px;
256 257 background-color:#f6f6f6;
257 258 color:#505050;
258 259 line-height:1.5em;
259 260 border: 1px solid #e4e4e4;
260 261 }
261 262
262 263 div.square {
263 264 border: 1px solid #999;
264 265 float: left;
265 266 margin: .3em .4em 0 .4em;
266 267 overflow: hidden;
267 268 width: .6em; height: .6em;
268 269 }
269 270 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
270 271 .contextual input, .contextual select {font-size:0.9em;}
271 272 .message .contextual { margin-top: 0; }
272 273
273 274 .splitcontentleft{float:left; width:49%;}
274 275 .splitcontentright{float:right; width:49%;}
275 276 form {display: inline;}
276 277 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
277 278 fieldset {border: 1px solid #e4e4e4; margin:0;}
278 279 legend {color: #484848;}
279 280 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
280 281 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
281 282 blockquote blockquote { margin-left: 0;}
282 283 acronym { border-bottom: 1px dotted; cursor: help; }
283 284 textarea.wiki-edit { width: 99%; }
284 285 li p {margin-top: 0;}
285 286 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
286 287 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
287 288 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
288 289 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
289 290
290 291 div.issue div.subject div div { padding-left: 16px; }
291 292 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
292 293 div.issue div.subject>div>p { margin-top: 0.5em; }
293 294 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
294 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 296 div.issue .next-prev-links {color:#999;}
296 297 div.issue table.attributes th {width:22%;}
297 298 div.issue table.attributes td {width:28%;}
298 299
299 300 #issue_tree table.issues, #relations table.issues { border: 0; }
300 301 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
301 302 #relations td.buttons {padding:0;}
302 303
303 304 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
304 305 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
305 306 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
306 307
307 308 fieldset#date-range p { margin: 2px 0 2px 0; }
308 309 fieldset#filters table { border-collapse: collapse; }
309 310 fieldset#filters table td { padding: 0; vertical-align: middle; }
310 311 fieldset#filters tr.filter { height: 2em; }
311 312 fieldset#filters td.field { width:200px; }
312 313 fieldset#filters td.operator { width:170px; }
313 314 fieldset#filters td.values { white-space:nowrap; }
314 315 fieldset#filters td.values img { vertical-align: middle; margin-left:1px; }
315 316 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
316 317 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
317 318
318 319 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
319 320 div#issue-changesets div.changeset { padding: 4px;}
320 321 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
321 322 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
322 323
323 324 div#activity dl, #search-results { margin-left: 2em; }
324 325 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
325 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 327 div#activity dt.me .time { border-bottom: 1px solid #999; }
327 328 div#activity dt .time { color: #777; font-size: 80%; }
328 329 div#activity dd .description, #search-results dd .description { font-style: italic; }
329 330 div#activity span.project:after, #search-results span.project:after { content: " -"; }
330 331 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
331 332
332 333 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
333 334
334 335 div#search-results-counts {float:right;}
335 336 div#search-results-counts ul { margin-top: 0.5em; }
336 337 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
337 338
338 339 dt.issue { background-image: url(../images/ticket.png); }
339 340 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
340 341 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
341 342 dt.issue-note { background-image: url(../images/ticket_note.png); }
342 343 dt.changeset { background-image: url(../images/changeset.png); }
343 344 dt.news { background-image: url(../images/news.png); }
344 345 dt.message { background-image: url(../images/message.png); }
345 346 dt.reply { background-image: url(../images/comments.png); }
346 347 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
347 348 dt.attachment { background-image: url(../images/attachment.png); }
348 349 dt.document { background-image: url(../images/document.png); }
349 350 dt.project { background-image: url(../images/projects.png); }
350 351 dt.time-entry { background-image: url(../images/time.png); }
351 352
352 353 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
353 354
354 355 div#roadmap .related-issues { margin-bottom: 1em; }
355 356 div#roadmap .related-issues td.checkbox { display: none; }
356 357 div#roadmap .wiki h1:first-child { display: none; }
357 358 div#roadmap .wiki h1 { font-size: 120%; }
358 359 div#roadmap .wiki h2 { font-size: 110%; }
359 360 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
360 361
361 362 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
362 363 div#version-summary fieldset { margin-bottom: 1em; }
363 364 div#version-summary fieldset.time-tracking table { width:100%; }
364 365 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
365 366
366 367 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
367 368 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
368 369 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
369 370 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
370 371 table#time-report .hours-dec { font-size: 0.9em; }
371 372
372 373 div.wiki-page .contextual a {opacity: 0.4}
373 374 div.wiki-page .contextual a:hover {opacity: 1}
374 375
375 376 form .attributes select { width: 60%; }
376 377 input#issue_subject { width: 99%; }
377 378 select#issue_done_ratio { width: 95px; }
378 379
379 380 ul.projects { margin: 0; padding-left: 1em; }
380 381 ul.projects.root { margin: 0; padding: 0; }
381 382 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
382 383 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
383 384 ul.projects li.child { list-style-type:none; margin-top: 1em;}
384 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 386 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
386 387
387 388 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
388 389 #tracker_project_ids li { list-style-type:none; }
389 390
390 391 ul.properties {padding:0; font-size: 0.9em; color: #777;}
391 392 ul.properties li {list-style-type:none;}
392 393 ul.properties li span {font-style:italic;}
393 394
394 395 .total-hours { font-size: 110%; font-weight: bold; }
395 396 .total-hours span.hours-int { font-size: 120%; }
396 397
397 398 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
398 399 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
399 400
400 401 #workflow_copy_form select { width: 200px; }
401 402
402 403 textarea#custom_field_possible_values {width: 99%}
403 404 input#content_comments {width: 99%}
404 405
405 406 .pagination {font-size: 90%}
406 407 p.pagination {margin-top:8px;}
407 408
408 409 /***** Tabular forms ******/
409 410 .tabular p{
410 411 margin: 0;
411 412 padding: 3px 0 3px 0;
412 413 padding-left: 180px; /* width of left column containing the label elements */
413 414 min-height: 1.8em;
414 415 clear:left;
415 416 }
416 417
417 418 html>body .tabular p {overflow:hidden;}
418 419
419 420 .tabular label{
420 421 font-weight: bold;
421 422 float: left;
422 423 text-align: right;
423 424 /* width of left column */
424 425 margin-left: -180px;
425 426 /* width of labels. Should be smaller than left column to create some right margin */
426 427 width: 175px;
427 428 }
428 429
429 430 .tabular label.floating{
430 431 font-weight: normal;
431 432 margin-left: 0px;
432 433 text-align: left;
433 434 width: 270px;
434 435 }
435 436
436 437 .tabular label.block{
437 438 font-weight: normal;
438 439 margin-left: 0px !important;
439 440 text-align: left;
440 441 float: none;
441 442 display: block;
442 443 width: auto;
443 444 }
444 445
445 446 .tabular label.inline{
446 447 float:none;
447 448 margin-left: 5px !important;
448 449 width: auto;
449 450 }
450 451
451 452 form em {font-style:normal;font-size:90%;color:#888;}
452 453
453 454 label.no-css {
454 455 font-weight: inherit;
455 456 float:none;
456 457 text-align:left;
457 458 margin-left:0px;
458 459 width:auto;
459 460 }
460 461 input#time_entry_comments { width: 90%;}
461 462
462 463 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
463 464
464 465 .tabular.settings p{ padding-left: 300px; }
465 466 .tabular.settings label{ margin-left: -300px; width: 295px; }
466 467 .tabular.settings textarea { width: 99%; }
467 468
468 469 .settings.enabled_scm table {width:100%}
469 470 .settings.enabled_scm td.scm_name{ font-weight: bold; }
470 471
471 472 fieldset.settings label { display: block; }
472 473 fieldset#notified_events .parent { padding-left: 20px; }
473 474
474 475 .required {color: #bb0000;}
475 476 .summary {font-style: italic;}
476 477
477 478 #attachments_fields input[type=text] {margin-left: 8px; }
478 479 #attachments_fields span {display:block; white-space:nowrap;}
479 480 #attachments_fields img {vertical-align: middle;}
480 481
481 482 div.attachments { margin-top: 12px; }
482 483 div.attachments p { margin:4px 0 2px 0; }
483 484 div.attachments img { vertical-align: middle; }
484 485 div.attachments span.author { font-size: 0.9em; color: #888; }
485 486
486 487 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
487 488 .other-formats span + span:before { content: "| "; }
488 489
489 490 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
490 491
491 492 /* Project members tab */
492 493 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
493 494 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
494 495 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
495 496 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
496 497 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
497 498 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
498 499
499 500 #users_for_watcher {height: 200px; overflow:auto;}
500 501 #users_for_watcher label {display: block;}
501 502
502 503 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
503 504
504 505 input#principal_search, input#user_search {width:100%}
505 506
506 507 * html div#tab-content-members fieldset div { height: 450px; }
507 508
508 509 /***** Flash & error messages ****/
509 510 #errorExplanation, div.flash, .nodata, .warning {
510 511 padding: 4px 4px 4px 30px;
511 512 margin-bottom: 12px;
512 513 font-size: 1.1em;
513 514 border: 2px solid;
514 515 }
515 516
516 517 div.flash {margin-top: 8px;}
517 518
518 519 div.flash.error, #errorExplanation {
519 520 background: url(../images/exclamation.png) 8px 50% no-repeat;
520 521 background-color: #ffe3e3;
521 522 border-color: #dd0000;
522 523 color: #880000;
523 524 }
524 525
525 526 div.flash.notice {
526 527 background: url(../images/true.png) 8px 5px no-repeat;
527 528 background-color: #dfffdf;
528 529 border-color: #9fcf9f;
529 530 color: #005f00;
530 531 }
531 532
532 533 div.flash.warning {
533 534 background: url(../images/warning.png) 8px 5px no-repeat;
534 535 background-color: #FFEBC1;
535 536 border-color: #FDBF3B;
536 537 color: #A6750C;
537 538 text-align: left;
538 539 }
539 540
540 541 .nodata, .warning {
541 542 text-align: center;
542 543 background-color: #FFEBC1;
543 544 border-color: #FDBF3B;
544 545 color: #A6750C;
545 546 }
546 547
547 548 span.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
548 549
549 550 #errorExplanation ul { font-size: 0.9em;}
550 551 #errorExplanation h2, #errorExplanation p { display: none; }
551 552
552 553 /***** Ajax indicator ******/
553 554 #ajax-indicator {
554 555 position: absolute; /* fixed not supported by IE */
555 556 background-color:#eee;
556 557 border: 1px solid #bbb;
557 558 top:35%;
558 559 left:40%;
559 560 width:20%;
560 561 font-weight:bold;
561 562 text-align:center;
562 563 padding:0.6em;
563 564 z-index:100;
564 565 opacity: 0.5;
565 566 }
566 567
567 568 html>body #ajax-indicator { position: fixed; }
568 569
569 570 #ajax-indicator span {
570 571 background-position: 0% 40%;
571 572 background-repeat: no-repeat;
572 573 background-image: url(../images/loading.gif);
573 574 padding-left: 26px;
574 575 vertical-align: bottom;
575 576 }
576 577
577 578 /***** Calendar *****/
578 579 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
579 580 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
580 581 table.cal thead th.week-number {width: auto;}
581 582 table.cal tbody tr {height: 100px;}
582 583 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
583 584 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
584 585 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
585 586 table.cal td.odd p.day-num {color: #bbb;}
586 587 table.cal td.today {background:#ffffdd;}
587 588 table.cal td.today p.day-num {font-weight: bold;}
588 589 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
589 590 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
590 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 592 p.cal.legend span {display:block;}
592 593
593 594 /***** Tooltips ******/
594 595 .tooltip{position:relative;z-index:24;}
595 596 .tooltip:hover{z-index:25;color:#000;}
596 597 .tooltip span.tip{display: none; text-align:left;}
597 598
598 599 div.tooltip:hover span.tip{
599 600 display:block;
600 601 position:absolute;
601 602 top:12px; left:24px; width:270px;
602 603 border:1px solid #555;
603 604 background-color:#fff;
604 605 padding: 4px;
605 606 font-size: 0.8em;
606 607 color:#505050;
607 608 }
608 609
609 610 /***** Progress bar *****/
610 611 table.progress {
611 612 border-collapse: collapse;
612 613 border-spacing: 0pt;
613 614 empty-cells: show;
614 615 text-align: center;
615 616 float:left;
616 617 margin: 1px 6px 1px 0px;
617 618 }
618 619
619 620 table.progress td { height: 1em; }
620 621 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
621 622 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
622 623 table.progress td.todo { background: #eee none repeat scroll 0%; }
623 624 p.pourcent {font-size: 80%;}
624 625 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
625 626
626 627 #roadmap table.progress td { height: 1.2em; }
627 628 /***** Tabs *****/
628 629 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
629 630 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
630 631 #content .tabs ul li {
631 632 float:left;
632 633 list-style-type:none;
633 634 white-space:nowrap;
634 635 margin-right:4px;
635 636 background:#fff;
636 637 position:relative;
637 638 margin-bottom:-1px;
638 639 }
639 640 #content .tabs ul li a{
640 641 display:block;
641 642 font-size: 0.9em;
642 643 text-decoration:none;
643 644 line-height:1.3em;
644 645 padding:4px 6px 4px 6px;
645 646 border: 1px solid #ccc;
646 647 border-bottom: 1px solid #bbbbbb;
647 648 background-color: #f6f6f6;
648 649 color:#999;
649 650 font-weight:bold;
650 651 border-top-left-radius:3px;
651 652 border-top-right-radius:3px;
652 653 }
653 654
654 655 #content .tabs ul li a:hover {
655 656 background-color: #ffffdd;
656 657 text-decoration:none;
657 658 }
658 659
659 660 #content .tabs ul li a.selected {
660 661 background-color: #fff;
661 662 border: 1px solid #bbbbbb;
662 663 border-bottom: 1px solid #fff;
663 664 color:#444;
664 665 }
665 666
666 667 #content .tabs ul li a.selected:hover {
667 668 background-color: #fff;
668 669 }
669 670
670 671 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
671 672
672 673 button.tab-left, button.tab-right {
673 674 font-size: 0.9em;
674 675 cursor: pointer;
675 676 height:24px;
676 677 border: 1px solid #ccc;
677 678 border-bottom: 1px solid #bbbbbb;
678 679 position:absolute;
679 680 padding:4px;
680 681 width: 20px;
681 682 bottom: -1px;
682 683 }
683 684
684 685 button.tab-left {
685 686 right: 20px;
686 687 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
687 688 border-top-left-radius:3px;
688 689 }
689 690
690 691 button.tab-right {
691 692 right: 0;
692 693 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
693 694 border-top-right-radius:3px;
694 695 }
695 696
696 697 /***** Auto-complete *****/
697 698 div.autocomplete {
698 699 position:absolute;
699 700 width:400px;
700 701 margin:0;
701 702 padding:0;
702 703 }
703 704 div.autocomplete ul {
704 705 list-style-type:none;
705 706 margin:0;
706 707 padding:0;
707 708 }
708 709 div.autocomplete ul li {
709 710 list-style-type:none;
710 711 display:block;
711 712 margin:-1px 0 0 0;
712 713 padding:2px;
713 714 cursor:pointer;
714 715 font-size: 90%;
715 716 border: 1px solid #ccc;
716 717 border-left: 1px solid #ccc;
717 718 border-right: 1px solid #ccc;
718 719 background-color:white;
719 720 }
720 721 div.autocomplete ul li.selected { background-color: #ffb;}
721 722 div.autocomplete ul li span.informal {
722 723 font-size: 80%;
723 724 color: #aaa;
724 725 }
725 726
726 727 #parent_issue_candidates ul li {width: 500px;}
727 728 #related_issue_candidates ul li {width: 500px;}
728 729
729 730 /***** Diff *****/
730 731 .diff_out { background: #fcc; }
731 732 .diff_out span { background: #faa; }
732 733 .diff_in { background: #cfc; }
733 734 .diff_in span { background: #afa; }
734 735
735 736 .text-diff {
736 737 padding: 1em;
737 738 background-color:#f6f6f6;
738 739 color:#505050;
739 740 border: 1px solid #e4e4e4;
740 741 }
741 742
742 743 /***** Wiki *****/
743 744 div.wiki table {
744 745 border: 1px solid #505050;
745 746 border-collapse: collapse;
746 747 margin-bottom: 1em;
747 748 }
748 749
749 750 div.wiki table, div.wiki td, div.wiki th {
750 751 border: 1px solid #bbb;
751 752 padding: 4px;
752 753 }
753 754
754 755 div.wiki .external {
755 756 background-position: 0% 60%;
756 757 background-repeat: no-repeat;
757 758 padding-left: 12px;
758 759 background-image: url(../images/external.png);
759 760 }
760 761
761 762 div.wiki a.new {
762 763 color: #b73535;
763 764 }
764 765
765 766 div.wiki ul, div.wiki ol {margin-bottom:1em;}
766 767
767 768 div.wiki pre {
768 769 margin: 1em 1em 1em 1.6em;
769 770 padding: 2px 2px 2px 0;
770 771 background-color: #fafafa;
771 772 border: 1px solid #dadada;
772 773 width:auto;
773 774 overflow-x: auto;
774 775 overflow-y: hidden;
775 776 }
776 777
777 778 div.wiki ul.toc {
778 779 background-color: #ffffdd;
779 780 border: 1px solid #e4e4e4;
780 781 padding: 4px;
781 782 line-height: 1.2em;
782 783 margin-bottom: 12px;
783 784 margin-right: 12px;
784 785 margin-left: 0;
785 786 display: table
786 787 }
787 788 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
788 789
789 790 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
790 791 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
791 792 div.wiki ul.toc ul { margin: 0; padding: 0; }
792 793 div.wiki ul.toc li { list-style-type:none; margin: 0;}
793 794 div.wiki ul.toc li li { margin-left: 1.5em; }
794 795 div.wiki ul.toc li li li { font-size: 0.8em; }
795 796
796 797 div.wiki ul.toc a {
797 798 font-size: 0.9em;
798 799 font-weight: normal;
799 800 text-decoration: none;
800 801 color: #606060;
801 802 }
802 803 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
803 804
804 805 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
805 806 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
806 807 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
807 808
808 809 div.wiki img { vertical-align: middle; }
809 810
810 811 /***** My page layout *****/
811 812 .block-receiver {
812 813 border:1px dashed #c0c0c0;
813 814 margin-bottom: 20px;
814 815 padding: 15px 0 15px 0;
815 816 }
816 817
817 818 .mypage-box {
818 819 margin:0 0 20px 0;
819 820 color:#505050;
820 821 line-height:1.5em;
821 822 }
822 823
823 824 .handle {
824 825 cursor: move;
825 826 }
826 827
827 828 a.close-icon {
828 829 display:block;
829 830 margin-top:3px;
830 831 overflow:hidden;
831 832 width:12px;
832 833 height:12px;
833 834 background-repeat: no-repeat;
834 835 cursor:pointer;
835 836 background-image:url('../images/close.png');
836 837 }
837 838
838 839 a.close-icon:hover {
839 840 background-image:url('../images/close_hl.png');
840 841 }
841 842
842 843 /***** Gantt chart *****/
843 844 .gantt_hdr {
844 845 position:absolute;
845 846 top:0;
846 847 height:16px;
847 848 border-top: 1px solid #c0c0c0;
848 849 border-bottom: 1px solid #c0c0c0;
849 850 border-right: 1px solid #c0c0c0;
850 851 text-align: center;
851 852 overflow: hidden;
852 853 }
853 854
854 855 .gantt_subjects { font-size: 0.8em; }
855 856 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
856 857
857 858 .task {
858 859 position: absolute;
859 860 height:8px;
860 861 font-size:0.8em;
861 862 color:#888;
862 863 padding:0;
863 864 margin:0;
864 865 line-height:16px;
865 866 white-space:nowrap;
866 867 }
867 868
868 869 .task.label {width:100%;}
869 870 .task.label.project, .task.label.version { font-weight: bold; }
870 871
871 872 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
872 873 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
873 874 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
874 875
875 876 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
876 877 .task_late.parent, .task_done.parent { height: 3px;}
877 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 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 881 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
881 882 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
882 883 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
883 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 886 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
886 887 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
887 888 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
888 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 891 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
891 892 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
892 893
893 894 /***** Icons *****/
894 895 .icon {
895 896 background-position: 0% 50%;
896 897 background-repeat: no-repeat;
897 898 padding-left: 20px;
898 899 padding-top: 2px;
899 900 padding-bottom: 3px;
900 901 }
901 902
902 903 .icon-add { background-image: url(../images/add.png); }
903 904 .icon-edit { background-image: url(../images/edit.png); }
904 905 .icon-copy { background-image: url(../images/copy.png); }
905 906 .icon-duplicate { background-image: url(../images/duplicate.png); }
906 907 .icon-del { background-image: url(../images/delete.png); }
907 908 .icon-move { background-image: url(../images/move.png); }
908 909 .icon-save { background-image: url(../images/save.png); }
909 910 .icon-cancel { background-image: url(../images/cancel.png); }
910 911 .icon-multiple { background-image: url(../images/table_multiple.png); }
911 912 .icon-folder { background-image: url(../images/folder.png); }
912 913 .open .icon-folder { background-image: url(../images/folder_open.png); }
913 914 .icon-package { background-image: url(../images/package.png); }
914 915 .icon-user { background-image: url(../images/user.png); }
915 916 .icon-projects { background-image: url(../images/projects.png); }
916 917 .icon-help { background-image: url(../images/help.png); }
917 918 .icon-attachment { background-image: url(../images/attachment.png); }
918 919 .icon-history { background-image: url(../images/history.png); }
919 920 .icon-time { background-image: url(../images/time.png); }
920 921 .icon-time-add { background-image: url(../images/time_add.png); }
921 922 .icon-stats { background-image: url(../images/stats.png); }
922 923 .icon-warning { background-image: url(../images/warning.png); }
923 924 .icon-fav { background-image: url(../images/fav.png); }
924 925 .icon-fav-off { background-image: url(../images/fav_off.png); }
925 926 .icon-reload { background-image: url(../images/reload.png); }
926 927 .icon-lock { background-image: url(../images/locked.png); }
927 928 .icon-unlock { background-image: url(../images/unlock.png); }
928 929 .icon-checked { background-image: url(../images/true.png); }
929 930 .icon-details { background-image: url(../images/zoom_in.png); }
930 931 .icon-report { background-image: url(../images/report.png); }
931 932 .icon-comment { background-image: url(../images/comment.png); }
932 933 .icon-summary { background-image: url(../images/lightning.png); }
933 934 .icon-server-authentication { background-image: url(../images/server_key.png); }
934 935 .icon-issue { background-image: url(../images/ticket.png); }
935 936 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
936 937 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
937 938 .icon-passwd { background-image: url(../images/textfield_key.png); }
938 939
939 940 .icon-file { background-image: url(../images/files/default.png); }
940 941 .icon-file.text-plain { background-image: url(../images/files/text.png); }
941 942 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
942 943 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
943 944 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
944 945 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
945 946 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
946 947 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
947 948 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
948 949 .icon-file.text-css { background-image: url(../images/files/css.png); }
949 950 .icon-file.text-html { background-image: url(../images/files/html.png); }
950 951 .icon-file.image-gif { background-image: url(../images/files/image.png); }
951 952 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
952 953 .icon-file.image-png { background-image: url(../images/files/image.png); }
953 954 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
954 955 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
955 956 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
956 957 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
957 958
958 959 img.gravatar {
959 960 padding: 2px;
960 961 border: solid 1px #d5d5d5;
961 962 background: #fff;
962 963 vertical-align: middle;
963 964 }
964 965
965 966 div.issue img.gravatar {
966 967 float: left;
967 968 margin: 0 6px 0 0;
968 969 padding: 5px;
969 970 }
970 971
971 972 div.issue table img.gravatar {
972 973 height: 14px;
973 974 width: 14px;
974 975 padding: 2px;
975 976 float: left;
976 977 margin: 0 0.5em 0 0;
977 978 }
978 979
979 980 h2 img.gravatar {
980 981 margin: -2px 4px -4px 0;
981 982 }
982 983
983 984 h3 img.gravatar {
984 985 margin: -4px 4px -4px 0;
985 986 }
986 987
987 988 h4 img.gravatar {
988 989 margin: -6px 4px -4px 0;
989 990 }
990 991
991 992 td.username img.gravatar {
992 993 margin: 0 0.5em 0 0;
993 994 vertical-align: top;
994 995 }
995 996
996 997 #activity dt img.gravatar {
997 998 float: left;
998 999 margin: 0 1em 1em 0;
999 1000 }
1000 1001
1001 1002 /* Used on 12px Gravatar img tags without the icon background */
1002 1003 .icon-gravatar {
1003 1004 float: left;
1004 1005 margin-right: 4px;
1005 1006 }
1006 1007
1007 1008 #activity dt,
1008 1009 .journal {
1009 1010 clear: left;
1010 1011 }
1011 1012
1012 1013 .journal-link {
1013 1014 float: right;
1014 1015 }
1015 1016
1016 1017 h2 img { vertical-align:middle; }
1017 1018
1018 1019 .hascontextmenu { cursor: context-menu; }
1019 1020
1020 1021 /***** Media print specific styles *****/
1021 1022 @media print {
1022 1023 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1023 1024 #main { background: #fff; }
1024 1025 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1025 1026 #wiki_add_attachment { display:none; }
1026 1027 .hide-when-print { display: none; }
1027 1028 .autoscroll {overflow-x: visible;}
1028 1029 table.list {margin-top:0.5em;}
1029 1030 table.list th, table.list td {border: 1px solid #aaa;}
1030 1031 }
1031 1032
1032 1033 /* Accessibility specific styles */
1033 1034 .hidden-for-sighted {
1034 1035 position:absolute;
1035 1036 left:-10000px;
1036 1037 top:auto;
1037 1038 width:1px;
1038 1039 height:1px;
1039 1040 overflow:hidden;
1040 1041 }
@@ -1,5 +1,5
1 1 class Repository < ActiveRecord::Base
2 2 generator_for :type => 'Subversion'
3 3 generator_for :url, :start => 'file:///test/svn'
4
4 generator_for :identifier, :start => 'repo1'
5 5 end
@@ -1,17 +1,19
1 1 ---
2 2 repositories_001:
3 3 project_id: 1
4 4 url: file:///<%= Rails.root %>/tmp/test/subversion_repository
5 5 id: 10
6 6 root_url: file:///<%= Rails.root %>/tmp/test/subversion_repository
7 7 password: ""
8 8 login: ""
9 9 type: Subversion
10 is_default: true
10 11 repositories_002:
11 12 project_id: 2
12 13 url: svn://localhost/test
13 14 id: 11
14 15 root_url: svn://localhost
15 16 password: ""
16 17 login: ""
17 18 type: Subversion
19 is_default: true
@@ -1,194 +1,203
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesControllerTest < ActionController::TestCase
25 25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
26 26 :repositories, :issues, :issue_statuses, :changesets, :changes,
27 27 :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
28 28
29 29 def setup
30 30 @controller = RepositoriesController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 User.current = nil
34 34 end
35 35
36 36 def test_new
37 37 @request.session[:user_id] = 1
38 38 get :new, :project_id => 'subproject1'
39 39 assert_response :success
40 40 assert_template 'new'
41 41 assert_kind_of Repository::Subversion, assigns(:repository)
42 42 assert assigns(:repository).new_record?
43 43 assert_tag 'input', :attributes => {:name => 'repository[url]'}
44 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 46 def test_create
54 47 @request.session[:user_id] = 1
55 48 assert_difference 'Repository.count' do
56 49 post :create, :project_id => 'subproject1',
57 50 :repository_scm => 'Subversion',
58 :repository => {:url => 'file:///test'}
51 :repository => {:url => 'file:///test', :is_default => '1', :identifier => ''}
59 52 end
60 53 assert_response 302
61 54 repository = Repository.first(:order => 'id DESC')
62 55 assert_kind_of Repository::Subversion, repository
63 56 assert_equal 'file:///test', repository.url
64 57 end
65 58
66 59 def test_create_with_failure
67 60 @request.session[:user_id] = 1
68 61 assert_no_difference 'Repository.count' do
69 62 post :create, :project_id => 'subproject1',
70 63 :repository_scm => 'Subversion',
71 64 :repository => {:url => 'invalid'}
72 65 end
73 66 assert_response :success
74 67 assert_template 'new'
75 68 assert_kind_of Repository::Subversion, assigns(:repository)
76 69 assert assigns(:repository).new_record?
77 70 end
78 71
79 72 def test_edit
80 73 @request.session[:user_id] = 1
81 74 get :edit, :id => 11
82 75 assert_response :success
83 76 assert_template 'edit'
84 77 assert_equal Repository.find(11), assigns(:repository)
85 78 assert_tag 'input', :attributes => {:name => 'repository[url]', :value => 'svn://localhost/test'}
86 79 end
87 80
88 81 def test_update
89 82 @request.session[:user_id] = 1
90 83 put :update, :id => 11, :repository => {:password => 'test_update'}
91 84 assert_response 302
92 85 assert_equal 'test_update', Repository.find(11).password
93 86 end
94 87
95 88 def test_update_with_failure
96 89 @request.session[:user_id] = 1
97 90 put :update, :id => 11, :repository => {:password => 'x'*260}
98 91 assert_response :success
99 92 assert_template 'edit'
100 93 assert_equal Repository.find(11), assigns(:repository)
101 94 end
102 95
103 96 def test_destroy
104 97 @request.session[:user_id] = 1
105 98 assert_difference 'Repository.count', -1 do
106 99 delete :destroy, :id => 11
107 100 end
108 101 assert_response 302
109 102 assert_nil Repository.find_by_id(11)
110 103 end
111 104
112 105 def test_revisions
113 106 get :revisions, :id => 1
114 107 assert_response :success
115 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 120 assert_not_nil assigns(:changesets)
117 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 128 def test_revision
120 129 get :revision, :id => 1, :rev => 1
121 130 assert_response :success
122 131 assert_not_nil assigns(:changeset)
123 132 assert_equal "1", assigns(:changeset).revision
124 133 end
125 134
126 135 def test_revision_with_before_nil_and_afer_normal
127 136 get :revision, {:id => 1, :rev => 1}
128 137 assert_response :success
129 138 assert_template 'revision'
130 139 assert_no_tag :tag => "div", :attributes => { :class => "contextual" },
131 140 :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/0'}
132 141 }
133 142 assert_tag :tag => "div", :attributes => { :class => "contextual" },
134 143 :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/2'}
135 144 }
136 145 end
137 146
138 147 def test_graph_commits_per_month
139 148 get :graph, :id => 1, :graph => 'commits_per_month'
140 149 assert_response :success
141 150 assert_equal 'image/svg+xml', @response.content_type
142 151 end
143 152
144 153 def test_graph_commits_per_author
145 154 get :graph, :id => 1, :graph => 'commits_per_author'
146 155 assert_response :success
147 156 assert_equal 'image/svg+xml', @response.content_type
148 157 end
149 158
150 159 def test_get_committers
151 160 @request.session[:user_id] = 2
152 161 # add a commit with an unknown user
153 162 Changeset.create!(
154 163 :repository => Project.find(1).repository,
155 164 :committer => 'foo',
156 165 :committed_on => Time.now,
157 166 :revision => 100,
158 167 :comments => 'Committed by foo.'
159 168 )
160 169
161 170 get :committers, :id => 10
162 171 assert_response :success
163 172 assert_template 'committers'
164 173
165 174 assert_tag :td, :content => 'dlopper',
166 175 :sibling => { :tag => 'td',
167 176 :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} },
168 177 :child => { :tag => 'option', :content => 'Dave Lopper',
169 178 :attributes => { :value => '3', :selected => 'selected' }}}}
170 179 assert_tag :td, :content => 'foo',
171 180 :sibling => { :tag => 'td',
172 181 :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} }}}
173 182 assert_no_tag :td, :content => 'foo',
174 183 :sibling => { :tag => 'td',
175 184 :descendant => { :tag => 'option', :attributes => { :selected => 'selected' }}}
176 185 end
177 186
178 187 def test_post_committers
179 188 @request.session[:user_id] = 2
180 189 # add a commit with an unknown user
181 190 c = Changeset.create!(
182 191 :repository => Project.find(1).repository,
183 192 :committer => 'foo',
184 193 :committed_on => Time.now,
185 194 :revision => 100,
186 195 :comments => 'Committed by foo.'
187 196 )
188 197 assert_no_difference "Changeset.count(:conditions => 'user_id = 3')" do
189 198 post :committers, :id => 10, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']}
190 199 assert_response 302
191 200 assert_equal User.find(2), c.reload.user
192 201 end
193 202 end
194 203 end
@@ -1,203 +1,346
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class RoutingRepositoriesTest < ActionController::IntegrationTest
21 21 def setup
22 22 @path_hash = repository_path_hash(%w[path to file.c])
23 23 assert_equal "path/to/file.c", @path_hash[:path]
24 24 assert_equal %w[path to file.c], @path_hash[:param]
25 25 end
26 26
27 27 def test_repositories_resources
28 28 assert_routing(
29 29 { :method => 'get',
30 30 :path => "/projects/redmine/repositories/new" },
31 31 { :controller => 'repositories', :action => 'new', :project_id => 'redmine' }
32 32 )
33 33 assert_routing(
34 34 { :method => 'post',
35 35 :path => "/projects/redmine/repositories" },
36 36 { :controller => 'repositories', :action => 'create', :project_id => 'redmine' }
37 37 )
38 38 assert_routing(
39 39 { :method => 'get',
40 40 :path => "/repositories/1/edit" },
41 41 { :controller => 'repositories', :action => 'edit', :id => '1' }
42 42 )
43 43 assert_routing(
44 44 { :method => 'put',
45 45 :path => "/repositories/1" },
46 46 { :controller => 'repositories', :action => 'update', :id => '1' }
47 47 )
48 48 assert_routing(
49 49 { :method => 'delete',
50 50 :path => "/repositories/1" },
51 51 { :controller => 'repositories', :action => 'destroy', :id => '1' }
52 52 )
53 53 ["get", "post"].each do |method|
54 54 assert_routing(
55 55 { :method => method,
56 56 :path => "/repositories/1/committers" },
57 57 { :controller => 'repositories', :action => 'committers', :id => '1' }
58 58 )
59 59 end
60 60 end
61 61
62 62 def test_repositories
63 63 assert_routing(
64 64 { :method => 'get',
65 65 :path => "/projects/redmine/repository" },
66 66 { :controller => 'repositories', :action => 'show', :id => 'redmine' }
67 67 )
68 68 assert_routing(
69 69 { :method => 'get',
70 70 :path => "/projects/redmine/repository/statistics" },
71 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 96 end
74 97
75 98 def test_repositories_revisions
76 99 empty_path_param = []
77 100 assert_routing(
78 101 { :method => 'get',
79 102 :path => "/projects/redmine/repository/revisions" },
80 103 { :controller => 'repositories', :action => 'revisions', :id => 'redmine' }
81 104 )
82 105 assert_routing(
83 106 { :method => 'get',
84 107 :path => "/projects/redmine/repository/revisions.atom" },
85 108 { :controller => 'repositories', :action => 'revisions', :id => 'redmine',
86 109 :format => 'atom' }
87 110 )
88 111 assert_routing(
89 112 { :method => 'get',
90 113 :path => "/projects/redmine/repository/revisions/2457" },
91 114 { :controller => 'repositories', :action => 'revision', :id => 'redmine',
92 115 :rev => '2457' }
93 116 )
94 117 assert_routing(
95 118 { :method => 'get',
96 119 :path => "/projects/redmine/repository/revisions/2457/show" },
97 120 { :controller => 'repositories', :action => 'show', :id => 'redmine',
98 121 :path => empty_path_param, :rev => '2457' }
99 122 )
100 123 assert_routing(
101 124 { :method => 'get',
102 125 :path => "/projects/redmine/repository/revisions/2457/show/#{@path_hash[:path]}" },
103 126 { :controller => 'repositories', :action => 'show', :id => 'redmine',
104 127 :path => @path_hash[:param] , :rev => '2457'}
105 128 )
106 129 assert_routing(
107 130 { :method => 'get',
108 131 :path => "/projects/redmine/repository/revisions/2457/changes" },
109 132 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
110 133 :path => empty_path_param, :rev => '2457' }
111 134 )
112 135 assert_routing(
113 136 { :method => 'get',
114 137 :path => "/projects/redmine/repository/revisions/2457/changes/#{@path_hash[:path]}" },
115 138 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
116 139 :path => @path_hash[:param] , :rev => '2457'}
117 140 )
118 141 assert_routing(
119 142 { :method => 'get',
120 143 :path => "/projects/redmine/repository/revisions/2457/diff" },
121 144 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
122 145 :rev => '2457' }
123 146 )
124 147 assert_routing(
125 148 { :method => 'get',
126 149 :path => "/projects/redmine/repository/revisions/2457/diff.diff" },
127 150 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
128 151 :rev => '2457', :format => 'diff' }
129 152 )
130 153 assert_routing(
131 154 { :method => 'get',
132 155 :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" },
133 156 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
134 157 :path => @path_hash[:param], :rev => '2' }
135 158 )
136 159 assert_routing(
137 160 { :method => 'get',
138 161 :path => "/projects/redmine/repository/revisions/2/entry/#{@path_hash[:path]}" },
139 162 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
140 163 :path => @path_hash[:param], :rev => '2' }
141 164 )
142 165 assert_routing(
143 166 { :method => 'get',
144 167 :path => "/projects/redmine/repository/revisions/2/raw/#{@path_hash[:path]}" },
145 168 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
146 169 :path => @path_hash[:param], :rev => '2', :format => 'raw' }
147 170 )
148 171 assert_routing(
149 172 { :method => 'get',
150 173 :path => "/projects/redmine/repository/revisions/2/annotate/#{@path_hash[:path]}" },
151 174 { :controller => 'repositories', :action => 'annotate', :id => 'redmine',
152 175 :path => @path_hash[:param], :rev => '2' }
153 176 )
154 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 260 def test_repositories_non_revisions_path
157 261 assert_routing(
158 262 { :method => 'get',
159 263 :path => "/projects/redmine/repository/diff/#{@path_hash[:path]}" },
160 264 { :controller => 'repositories', :action => 'diff', :id => 'redmine',
161 265 :path => @path_hash[:param] }
162 266 )
163 267 assert_routing(
164 268 { :method => 'get',
165 269 :path => "/projects/redmine/repository/browse/#{@path_hash[:path]}" },
166 270 { :controller => 'repositories', :action => 'browse', :id => 'redmine',
167 271 :path => @path_hash[:param] }
168 272 )
169 273 assert_routing(
170 274 { :method => 'get',
171 275 :path => "/projects/redmine/repository/entry/#{@path_hash[:path]}" },
172 276 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
173 277 :path => @path_hash[:param] }
174 278 )
175 279 assert_routing(
176 280 { :method => 'get',
177 281 :path => "/projects/redmine/repository/raw/#{@path_hash[:path]}" },
178 282 { :controller => 'repositories', :action => 'entry', :id => 'redmine',
179 283 :path => @path_hash[:param], :format => 'raw' }
180 284 )
181 285 assert_routing(
182 286 { :method => 'get',
183 287 :path => "/projects/redmine/repository/annotate/#{@path_hash[:path]}" },
184 288 { :controller => 'repositories', :action => 'annotate', :id => 'redmine',
185 289 :path => @path_hash[:param] }
186 290 )
187 291 assert_routing(
188 292 { :method => 'get',
189 293 :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" },
190 294 { :controller => 'repositories', :action => 'changes', :id => 'redmine',
191 295 :path => @path_hash[:param] }
192 296 )
193 297 end
194 298
195 private
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
338 private
196 339
197 340 def repository_path_hash(arr)
198 341 hs = {}
199 342 hs[:path] = arr.join("/")
200 343 hs[:param] = arr
201 344 hs
202 345 end
203 346 end
@@ -1,256 +1,269
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoryTest < ActiveSupport::TestCase
21 21 fixtures :projects,
22 22 :trackers,
23 23 :projects_trackers,
24 24 :enabled_modules,
25 25 :repositories,
26 26 :issues,
27 27 :issue_statuses,
28 28 :issue_categories,
29 29 :changesets,
30 30 :changes,
31 31 :users,
32 32 :members,
33 33 :member_roles,
34 34 :roles,
35 35 :enumerations
36 36
37 37 def setup
38 38 @repository = Project.find(1).repository
39 39 end
40 40
41 41 def test_create
42 42 repository = Repository::Subversion.new(:project => Project.find(3))
43 43 assert !repository.save
44 44
45 45 repository.url = "svn://localhost"
46 46 assert repository.save
47 47 repository.reload
48 48
49 49 project = Project.find(3)
50 50 assert_equal repository, project.repository
51 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 66 def test_destroy
54 67 changesets = Changeset.count(:all, :conditions => "repository_id = 10")
55 68 changes = Change.count(:all, :conditions => "repository_id = 10",
56 69 :include => :changeset)
57 70 assert_difference 'Changeset.count', -changesets do
58 71 assert_difference 'Change.count', -changes do
59 72 Repository.find(10).destroy
60 73 end
61 74 end
62 75 end
63 76
64 77 def test_should_not_create_with_disabled_scm
65 78 # disable Subversion
66 79 with_settings :enabled_scm => ['Darcs', 'Git'] do
67 80 repository = Repository::Subversion.new(
68 81 :project => Project.find(3), :url => "svn://localhost")
69 82 assert !repository.save
70 83 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
71 84 repository.errors[:type].to_s
72 85 end
73 86 end
74 87
75 88 def test_scan_changesets_for_issue_ids
76 89 Setting.default_language = 'en'
77 90 Setting.notified_events = ['issue_added','issue_updated']
78 91
79 92 # choosing a status to apply to fix issues
80 93 Setting.commit_fix_status_id = IssueStatus.find(
81 94 :first,
82 95 :conditions => ["is_closed = ?", true]).id
83 96 Setting.commit_fix_done_ratio = "90"
84 97 Setting.commit_ref_keywords = 'refs , references, IssueID'
85 98 Setting.commit_fix_keywords = 'fixes , closes'
86 99 Setting.default_language = 'en'
87 100 ActionMailer::Base.deliveries.clear
88 101
89 102 # make sure issue 1 is not already closed
90 103 fixed_issue = Issue.find(1)
91 104 assert !fixed_issue.status.is_closed?
92 105 old_status = fixed_issue.status
93 106
94 107 Repository.scan_changesets_for_issue_ids
95 108 assert_equal [101, 102], Issue.find(3).changeset_ids
96 109
97 110 # fixed issues
98 111 fixed_issue.reload
99 112 assert fixed_issue.status.is_closed?
100 113 assert_equal 90, fixed_issue.done_ratio
101 114 assert_equal [101], fixed_issue.changeset_ids
102 115
103 116 # issue change
104 117 journal = fixed_issue.journals.find(:first, :order => 'created_on desc')
105 118 assert_equal User.find_by_login('dlopper'), journal.user
106 119 assert_equal 'Applied in changeset r2.', journal.notes
107 120
108 121 # 2 email notifications
109 122 assert_equal 2, ActionMailer::Base.deliveries.size
110 123 mail = ActionMailer::Base.deliveries.first
111 124 assert_kind_of TMail::Mail, mail
112 125 assert mail.subject.starts_with?(
113 126 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
114 127 assert mail.body.include?(
115 128 "Status changed from #{old_status} to #{fixed_issue.status}")
116 129
117 130 # ignoring commits referencing an issue of another project
118 131 assert_equal [], Issue.find(4).changesets
119 132 end
120 133
121 134 def test_for_changeset_comments_strip
122 135 repository = Repository::Mercurial.create(
123 136 :project => Project.find( 4 ),
124 137 :url => '/foo/bar/baz' )
125 138 comment = <<-COMMENT
126 139 This is a loooooooooooooooooooooooooooong comment
127 140
128 141
129 142 COMMENT
130 143 changeset = Changeset.new(
131 144 :comments => comment, :commit_date => Time.now,
132 145 :revision => 0, :scmid => 'f39b7922fb3c',
133 146 :committer => 'foo <foo@example.com>',
134 147 :committed_on => Time.now, :repository => repository )
135 148 assert( changeset.save )
136 149 assert_not_equal( comment, changeset.comments )
137 150 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
138 151 changeset.comments )
139 152 end
140 153
141 154 def test_for_urls_strip_cvs
142 155 repository = Repository::Cvs.create(
143 156 :project => Project.find(4),
144 157 :url => ' :pserver:login:password@host:/path/to/the/repository',
145 158 :root_url => 'foo ',
146 159 :log_encoding => 'UTF-8')
147 160 assert repository.save
148 161 repository.reload
149 162 assert_equal ':pserver:login:password@host:/path/to/the/repository',
150 163 repository.url
151 164 assert_equal 'foo', repository.root_url
152 165 end
153 166
154 167 def test_for_urls_strip_subversion
155 168 repository = Repository::Subversion.create(
156 169 :project => Project.find(4),
157 170 :url => ' file:///dummy ')
158 171 assert repository.save
159 172 repository.reload
160 173 assert_equal 'file:///dummy', repository.url
161 174 end
162 175
163 176 def test_for_urls_strip_git
164 177 repository = Repository::Git.create(
165 178 :project => Project.find(4),
166 179 :url => ' c:\dummy ')
167 180 assert repository.save
168 181 repository.reload
169 182 assert_equal 'c:\dummy', repository.url
170 183 end
171 184
172 185 def test_manual_user_mapping
173 186 assert_no_difference "Changeset.count(:conditions => 'user_id <> 2')" do
174 187 c = Changeset.create!(
175 188 :repository => @repository,
176 189 :committer => 'foo',
177 190 :committed_on => Time.now,
178 191 :revision => 100,
179 192 :comments => 'Committed by foo.'
180 193 )
181 194 assert_nil c.user
182 195 @repository.committer_ids = {'foo' => '2'}
183 196 assert_equal User.find(2), c.reload.user
184 197 # committer is now mapped
185 198 c = Changeset.create!(
186 199 :repository => @repository,
187 200 :committer => 'foo',
188 201 :committed_on => Time.now,
189 202 :revision => 101,
190 203 :comments => 'Another commit by foo.'
191 204 )
192 205 assert_equal User.find(2), c.user
193 206 end
194 207 end
195 208
196 209 def test_auto_user_mapping_by_username
197 210 c = Changeset.create!(
198 211 :repository => @repository,
199 212 :committer => 'jsmith',
200 213 :committed_on => Time.now,
201 214 :revision => 100,
202 215 :comments => 'Committed by john.'
203 216 )
204 217 assert_equal User.find(2), c.user
205 218 end
206 219
207 220 def test_auto_user_mapping_by_email
208 221 c = Changeset.create!(
209 222 :repository => @repository,
210 223 :committer => 'john <jsmith@somenet.foo>',
211 224 :committed_on => Time.now,
212 225 :revision => 100,
213 226 :comments => 'Committed by john.'
214 227 )
215 228 assert_equal User.find(2), c.user
216 229 end
217 230
218 231 def test_filesystem_avaialbe
219 232 klass = Repository::Filesystem
220 233 assert klass.scm_adapter_class
221 234 assert_equal true, klass.scm_available
222 235 end
223 236
224 237 def test_merge_extra_info
225 238 repo = Repository::Subversion.new(:project => Project.find(3))
226 239 assert !repo.save
227 240 repo.url = "svn://localhost"
228 241 assert repo.save
229 242 repo.reload
230 243 project = Project.find(3)
231 244 assert_equal repo, project.repository
232 245 assert_nil repo.extra_info
233 246 h1 = {"test_1" => {"test_11" => "test_value_11"}}
234 247 repo.merge_extra_info(h1)
235 248 assert_equal h1, repo.extra_info
236 249 h2 = {"test_2" => {
237 250 "test_21" => "test_value_21",
238 251 "test_22" => "test_value_22",
239 252 }}
240 253 repo.merge_extra_info(h2)
241 254 assert_equal (h = {"test_11" => "test_value_11"}),
242 255 repo.extra_info["test_1"]
243 256 assert_equal "test_value_21",
244 257 repo.extra_info["test_2"]["test_21"]
245 258 h3 = {"test_2" => {
246 259 "test_23" => "test_value_23",
247 260 "test_24" => "test_value_24",
248 261 }}
249 262 repo.merge_extra_info(h3)
250 263 assert_equal (h = {"test_11" => "test_value_11"}),
251 264 repo.extra_info["test_1"]
252 265 assert_nil repo.extra_info["test_2"]["test_21"]
253 266 assert_equal "test_value_23",
254 267 repo.extra_info["test_2"]["test_23"]
255 268 end
256 269 end
@@ -1,882 +1,881
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class UserTest < ActiveSupport::TestCase
21 21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources,
22 22 :trackers, :issue_statuses,
23 23 :projects_trackers,
24 24 :watchers,
25 25 :issue_categories, :enumerations, :issues,
26 26 :journals, :journal_details,
27 27 :groups_users,
28 28 :enabled_modules,
29 29 :workflows
30 30
31 31 def setup
32 32 @admin = User.find(1)
33 33 @jsmith = User.find(2)
34 34 @dlopper = User.find(3)
35 35 end
36 36
37 37 test 'object_daddy creation' do
38 38 User.generate_with_protected!(:firstname => 'Testing connection')
39 39 User.generate_with_protected!(:firstname => 'Testing connection')
40 40 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
41 41 end
42 42
43 43 def test_truth
44 44 assert_kind_of User, @jsmith
45 45 end
46 46
47 47 def test_mail_should_be_stripped
48 48 u = User.new
49 49 u.mail = " foo@bar.com "
50 50 assert_equal "foo@bar.com", u.mail
51 51 end
52 52
53 53 def test_mail_validation
54 54 u = User.new
55 55 u.mail = ''
56 56 assert !u.valid?
57 57 assert_equal I18n.translate('activerecord.errors.messages.blank'),
58 58 u.errors[:mail].to_s
59 59 end
60 60
61 61 def test_create
62 62 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
63 63
64 64 user.login = "jsmith"
65 65 user.password, user.password_confirmation = "password", "password"
66 66 # login uniqueness
67 67 assert !user.save
68 68 assert_equal 1, user.errors.count
69 69
70 70 user.login = "newuser"
71 71 user.password, user.password_confirmation = "passwd", "password"
72 72 # password confirmation
73 73 assert !user.save
74 74 assert_equal 1, user.errors.count
75 75
76 76 user.password, user.password_confirmation = "password", "password"
77 77 assert user.save
78 78 end
79 79
80 80 context "User#before_create" do
81 81 should "set the mail_notification to the default Setting" do
82 82 @user1 = User.generate_with_protected!
83 83 assert_equal 'only_my_events', @user1.mail_notification
84 84
85 85 with_settings :default_notification_option => 'all' do
86 86 @user2 = User.generate_with_protected!
87 87 assert_equal 'all', @user2.mail_notification
88 88 end
89 89 end
90 90 end
91 91
92 92 context "User.login" do
93 93 should "be case-insensitive." do
94 94 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
95 95 u.login = 'newuser'
96 96 u.password, u.password_confirmation = "password", "password"
97 97 assert u.save
98 98
99 99 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
100 100 u.login = 'NewUser'
101 101 u.password, u.password_confirmation = "password", "password"
102 102 assert !u.save
103 103 assert_equal I18n.translate('activerecord.errors.messages.taken'),
104 104 u.errors[:login].to_s
105 105 end
106 106 end
107 107
108 108 def test_mail_uniqueness_should_not_be_case_sensitive
109 109 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
110 110 u.login = 'newuser1'
111 111 u.password, u.password_confirmation = "password", "password"
112 112 assert u.save
113 113
114 114 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
115 115 u.login = 'newuser2'
116 116 u.password, u.password_confirmation = "password", "password"
117 117 assert !u.save
118 118 assert_equal I18n.translate('activerecord.errors.messages.taken'),
119 119 u.errors[:mail].to_s
120 120 end
121 121
122 122 def test_update
123 123 assert_equal "admin", @admin.login
124 124 @admin.login = "john"
125 125 assert @admin.save, @admin.errors.full_messages.join("; ")
126 126 @admin.reload
127 127 assert_equal "john", @admin.login
128 128 end
129 129
130 130 def test_destroy_should_delete_members_and_roles
131 131 members = Member.find_all_by_user_id(2)
132 132 ms = members.size
133 133 rs = members.collect(&:roles).flatten.size
134 134
135 135 assert_difference 'Member.count', - ms do
136 136 assert_difference 'MemberRole.count', - rs do
137 137 User.find(2).destroy
138 138 end
139 139 end
140 140
141 141 assert_nil User.find_by_id(2)
142 142 assert Member.find_all_by_user_id(2).empty?
143 143 end
144 144
145 145 def test_destroy_should_update_attachments
146 146 attachment = Attachment.create!(:container => Project.find(1),
147 147 :file => uploaded_test_file("testfile.txt", "text/plain"),
148 148 :author_id => 2)
149 149
150 150 User.find(2).destroy
151 151 assert_nil User.find_by_id(2)
152 152 assert_equal User.anonymous, attachment.reload.author
153 153 end
154 154
155 155 def test_destroy_should_update_comments
156 156 comment = Comment.create!(
157 157 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
158 158 :author => User.find(2),
159 159 :comments => 'foo'
160 160 )
161 161
162 162 User.find(2).destroy
163 163 assert_nil User.find_by_id(2)
164 164 assert_equal User.anonymous, comment.reload.author
165 165 end
166 166
167 167 def test_destroy_should_update_issues
168 168 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
169 169
170 170 User.find(2).destroy
171 171 assert_nil User.find_by_id(2)
172 172 assert_equal User.anonymous, issue.reload.author
173 173 end
174 174
175 175 def test_destroy_should_unassign_issues
176 176 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
177 177
178 178 User.find(2).destroy
179 179 assert_nil User.find_by_id(2)
180 180 assert_nil issue.reload.assigned_to
181 181 end
182 182
183 183 def test_destroy_should_update_journals
184 184 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
185 185 issue.init_journal(User.find(2), "update")
186 186 issue.save!
187 187
188 188 User.find(2).destroy
189 189 assert_nil User.find_by_id(2)
190 190 assert_equal User.anonymous, issue.journals.first.reload.user
191 191 end
192 192
193 193 def test_destroy_should_update_journal_details_old_value
194 194 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
195 195 issue.init_journal(User.find(1), "update")
196 196 issue.assigned_to_id = nil
197 197 assert_difference 'JournalDetail.count' do
198 198 issue.save!
199 199 end
200 200 journal_detail = JournalDetail.first(:order => 'id DESC')
201 201 assert_equal '2', journal_detail.old_value
202 202
203 203 User.find(2).destroy
204 204 assert_nil User.find_by_id(2)
205 205 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
206 206 end
207 207
208 208 def test_destroy_should_update_journal_details_value
209 209 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
210 210 issue.init_journal(User.find(1), "update")
211 211 issue.assigned_to_id = 2
212 212 assert_difference 'JournalDetail.count' do
213 213 issue.save!
214 214 end
215 215 journal_detail = JournalDetail.first(:order => 'id DESC')
216 216 assert_equal '2', journal_detail.value
217 217
218 218 User.find(2).destroy
219 219 assert_nil User.find_by_id(2)
220 220 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
221 221 end
222 222
223 223 def test_destroy_should_update_messages
224 224 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
225 225 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
226 226
227 227 User.find(2).destroy
228 228 assert_nil User.find_by_id(2)
229 229 assert_equal User.anonymous, message.reload.author
230 230 end
231 231
232 232 def test_destroy_should_update_news
233 233 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
234 234
235 235 User.find(2).destroy
236 236 assert_nil User.find_by_id(2)
237 237 assert_equal User.anonymous, news.reload.author
238 238 end
239 239
240 240 def test_destroy_should_delete_private_queries
241 241 query = Query.new(:name => 'foo', :is_public => false)
242 242 query.project_id = 1
243 243 query.user_id = 2
244 244 query.save!
245 245
246 246 User.find(2).destroy
247 247 assert_nil User.find_by_id(2)
248 248 assert_nil Query.find_by_id(query.id)
249 249 end
250 250
251 251 def test_destroy_should_update_public_queries
252 252 query = Query.new(:name => 'foo', :is_public => true)
253 253 query.project_id = 1
254 254 query.user_id = 2
255 255 query.save!
256 256
257 257 User.find(2).destroy
258 258 assert_nil User.find_by_id(2)
259 259 assert_equal User.anonymous, query.reload.user
260 260 end
261 261
262 262 def test_destroy_should_update_time_entries
263 263 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
264 264 entry.project_id = 1
265 265 entry.user_id = 2
266 266 entry.save!
267 267
268 268 User.find(2).destroy
269 269 assert_nil User.find_by_id(2)
270 270 assert_equal User.anonymous, entry.reload.user
271 271 end
272 272
273 273 def test_destroy_should_delete_tokens
274 274 token = Token.create!(:user_id => 2, :value => 'foo')
275 275
276 276 User.find(2).destroy
277 277 assert_nil User.find_by_id(2)
278 278 assert_nil Token.find_by_id(token.id)
279 279 end
280 280
281 281 def test_destroy_should_delete_watchers
282 282 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
283 283 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
284 284
285 285 User.find(2).destroy
286 286 assert_nil User.find_by_id(2)
287 287 assert_nil Watcher.find_by_id(watcher.id)
288 288 end
289 289
290 290 def test_destroy_should_update_wiki_contents
291 291 wiki_content = WikiContent.create!(
292 292 :text => 'foo',
293 293 :author_id => 2,
294 294 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
295 295 )
296 296 wiki_content.text = 'bar'
297 297 assert_difference 'WikiContent::Version.count' do
298 298 wiki_content.save!
299 299 end
300 300
301 301 User.find(2).destroy
302 302 assert_nil User.find_by_id(2)
303 303 assert_equal User.anonymous, wiki_content.reload.author
304 304 wiki_content.versions.each do |version|
305 305 assert_equal User.anonymous, version.reload.author
306 306 end
307 307 end
308 308
309 309 def test_destroy_should_nullify_issue_categories
310 310 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
311 311
312 312 User.find(2).destroy
313 313 assert_nil User.find_by_id(2)
314 314 assert_nil category.reload.assigned_to_id
315 315 end
316 316
317 317 def test_destroy_should_nullify_changesets
318 318 changeset = Changeset.create!(
319 :repository => Repository::Subversion.create!(
320 :project_id => 1,
321 :url => 'file:///var/svn'
319 :repository => Repository::Subversion.generate!(
320 :project_id => 1
322 321 ),
323 322 :revision => '12',
324 323 :committed_on => Time.now,
325 324 :committer => 'jsmith'
326 325 )
327 326 assert_equal 2, changeset.user_id
328 327
329 328 User.find(2).destroy
330 329 assert_nil User.find_by_id(2)
331 330 assert_nil changeset.reload.user_id
332 331 end
333 332
334 333 def test_anonymous_user_should_not_be_destroyable
335 334 assert_no_difference 'User.count' do
336 335 assert_equal false, User.anonymous.destroy
337 336 end
338 337 end
339 338
340 339 def test_validate_login_presence
341 340 @admin.login = ""
342 341 assert !@admin.save
343 342 assert_equal 1, @admin.errors.count
344 343 end
345 344
346 345 def test_validate_mail_notification_inclusion
347 346 u = User.new
348 347 u.mail_notification = 'foo'
349 348 u.save
350 349 assert_not_nil u.errors[:mail_notification]
351 350 end
352 351
353 352 context "User#try_to_login" do
354 353 should "fall-back to case-insensitive if user login is not found as-typed." do
355 354 user = User.try_to_login("AdMin", "admin")
356 355 assert_kind_of User, user
357 356 assert_equal "admin", user.login
358 357 end
359 358
360 359 should "select the exact matching user first" do
361 360 case_sensitive_user = User.generate_with_protected!(
362 361 :login => 'changed', :password => 'admin',
363 362 :password_confirmation => 'admin')
364 363 # bypass validations to make it appear like existing data
365 364 case_sensitive_user.update_attribute(:login, 'ADMIN')
366 365
367 366 user = User.try_to_login("ADMIN", "admin")
368 367 assert_kind_of User, user
369 368 assert_equal "ADMIN", user.login
370 369
371 370 end
372 371 end
373 372
374 373 def test_password
375 374 user = User.try_to_login("admin", "admin")
376 375 assert_kind_of User, user
377 376 assert_equal "admin", user.login
378 377 user.password = "hello"
379 378 assert user.save
380 379
381 380 user = User.try_to_login("admin", "hello")
382 381 assert_kind_of User, user
383 382 assert_equal "admin", user.login
384 383 end
385 384
386 385 def test_validate_password_length
387 386 with_settings :password_min_length => '100' do
388 387 user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo")
389 388 user.login = "newuser100"
390 389 user.password, user.password_confirmation = "password100", "password100"
391 390 assert !user.save
392 391 assert_equal 1, user.errors.count
393 392 end
394 393 end
395 394
396 395 def test_name_format
397 396 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
398 397 with_settings :user_format => :firstname_lastname do
399 398 assert_equal 'John Smith', @jsmith.reload.name
400 399 end
401 400 with_settings :user_format => :username do
402 401 assert_equal 'jsmith', @jsmith.reload.name
403 402 end
404 403 end
405 404
406 405 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
407 406 with_settings :user_format => 'lastname_coma_firstname' do
408 407 assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement
409 408 end
410 409 end
411 410
412 411 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
413 412 with_settings :user_format => 'lastname_firstname' do
414 413 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors')
415 414 end
416 415 end
417 416
418 417 def test_fields_for_order_statement_with_blank_format_should_return_default
419 418 with_settings :user_format => '' do
420 419 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
421 420 end
422 421 end
423 422
424 423 def test_fields_for_order_statement_with_invalid_format_should_return_default
425 424 with_settings :user_format => 'foo' do
426 425 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
427 426 end
428 427 end
429 428
430 429 def test_lock
431 430 user = User.try_to_login("jsmith", "jsmith")
432 431 assert_equal @jsmith, user
433 432
434 433 @jsmith.status = User::STATUS_LOCKED
435 434 assert @jsmith.save
436 435
437 436 user = User.try_to_login("jsmith", "jsmith")
438 437 assert_equal nil, user
439 438 end
440 439
441 440 context ".try_to_login" do
442 441 context "with good credentials" do
443 442 should "return the user" do
444 443 user = User.try_to_login("admin", "admin")
445 444 assert_kind_of User, user
446 445 assert_equal "admin", user.login
447 446 end
448 447 end
449 448
450 449 context "with wrong credentials" do
451 450 should "return nil" do
452 451 assert_nil User.try_to_login("admin", "foo")
453 452 end
454 453 end
455 454 end
456 455
457 456 if ldap_configured?
458 457 context "#try_to_login using LDAP" do
459 458 context "with failed connection to the LDAP server" do
460 459 should "return nil" do
461 460 @auth_source = AuthSourceLdap.find(1)
462 461 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
463 462
464 463 assert_equal nil, User.try_to_login('edavis', 'wrong')
465 464 end
466 465 end
467 466
468 467 context "with an unsuccessful authentication" do
469 468 should "return nil" do
470 469 assert_equal nil, User.try_to_login('edavis', 'wrong')
471 470 end
472 471 end
473 472
474 473 context "on the fly registration" do
475 474 setup do
476 475 @auth_source = AuthSourceLdap.find(1)
477 476 end
478 477
479 478 context "with a successful authentication" do
480 479 should "create a new user account if it doesn't exist" do
481 480 assert_difference('User.count') do
482 481 user = User.try_to_login('edavis', '123456')
483 482 assert !user.admin?
484 483 end
485 484 end
486 485
487 486 should "retrieve existing user" do
488 487 user = User.try_to_login('edavis', '123456')
489 488 user.admin = true
490 489 user.save!
491 490
492 491 assert_no_difference('User.count') do
493 492 user = User.try_to_login('edavis', '123456')
494 493 assert user.admin?
495 494 end
496 495 end
497 496 end
498 497 end
499 498 end
500 499
501 500 else
502 501 puts "Skipping LDAP tests."
503 502 end
504 503
505 504 def test_create_anonymous
506 505 AnonymousUser.delete_all
507 506 anon = User.anonymous
508 507 assert !anon.new_record?
509 508 assert_kind_of AnonymousUser, anon
510 509 end
511 510
512 511 def test_ensure_single_anonymous_user
513 512 AnonymousUser.delete_all
514 513 anon1 = User.anonymous
515 514 assert !anon1.new_record?
516 515 assert_kind_of AnonymousUser, anon1
517 516 anon2 = AnonymousUser.create(
518 517 :lastname => 'Anonymous', :firstname => '',
519 518 :mail => '', :login => '', :status => 0)
520 519 assert_equal 1, anon2.errors.count
521 520 end
522 521
523 522 should_have_one :rss_token
524 523
525 524 def test_rss_key
526 525 assert_nil @jsmith.rss_token
527 526 key = @jsmith.rss_key
528 527 assert_equal 40, key.length
529 528
530 529 @jsmith.reload
531 530 assert_equal key, @jsmith.rss_key
532 531 end
533 532
534 533
535 534 should_have_one :api_token
536 535
537 536 context "User#api_key" do
538 537 should "generate a new one if the user doesn't have one" do
539 538 user = User.generate_with_protected!(:api_token => nil)
540 539 assert_nil user.api_token
541 540
542 541 key = user.api_key
543 542 assert_equal 40, key.length
544 543 user.reload
545 544 assert_equal key, user.api_key
546 545 end
547 546
548 547 should "return the existing api token value" do
549 548 user = User.generate_with_protected!
550 549 token = Token.generate!(:action => 'api')
551 550 user.api_token = token
552 551 assert user.save
553 552
554 553 assert_equal token.value, user.api_key
555 554 end
556 555 end
557 556
558 557 context "User#find_by_api_key" do
559 558 should "return nil if no matching key is found" do
560 559 assert_nil User.find_by_api_key('zzzzzzzzz')
561 560 end
562 561
563 562 should "return nil if the key is found for an inactive user" do
564 563 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
565 564 token = Token.generate!(:action => 'api')
566 565 user.api_token = token
567 566 user.save
568 567
569 568 assert_nil User.find_by_api_key(token.value)
570 569 end
571 570
572 571 should "return the user if the key is found for an active user" do
573 572 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
574 573 token = Token.generate!(:action => 'api')
575 574 user.api_token = token
576 575 user.save
577 576
578 577 assert_equal user, User.find_by_api_key(token.value)
579 578 end
580 579 end
581 580
582 581 def test_roles_for_project
583 582 # user with a role
584 583 roles = @jsmith.roles_for_project(Project.find(1))
585 584 assert_kind_of Role, roles.first
586 585 assert_equal "Manager", roles.first.name
587 586
588 587 # user with no role
589 588 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
590 589 end
591 590
592 591 def test_projects_by_role_for_user_with_role
593 592 user = User.find(2)
594 593 assert_kind_of Hash, user.projects_by_role
595 594 assert_equal 2, user.projects_by_role.size
596 595 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
597 596 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
598 597 end
599 598
600 599 def test_projects_by_role_for_user_with_no_role
601 600 user = User.generate!
602 601 assert_equal({}, user.projects_by_role)
603 602 end
604 603
605 604 def test_projects_by_role_for_anonymous
606 605 assert_equal({}, User.anonymous.projects_by_role)
607 606 end
608 607
609 608 def test_valid_notification_options
610 609 # without memberships
611 610 assert_equal 5, User.find(7).valid_notification_options.size
612 611 # with memberships
613 612 assert_equal 6, User.find(2).valid_notification_options.size
614 613 end
615 614
616 615 def test_valid_notification_options_class_method
617 616 assert_equal 5, User.valid_notification_options.size
618 617 assert_equal 5, User.valid_notification_options(User.find(7)).size
619 618 assert_equal 6, User.valid_notification_options(User.find(2)).size
620 619 end
621 620
622 621 def test_mail_notification_all
623 622 @jsmith.mail_notification = 'all'
624 623 @jsmith.notified_project_ids = []
625 624 @jsmith.save
626 625 @jsmith.reload
627 626 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
628 627 end
629 628
630 629 def test_mail_notification_selected
631 630 @jsmith.mail_notification = 'selected'
632 631 @jsmith.notified_project_ids = [1]
633 632 @jsmith.save
634 633 @jsmith.reload
635 634 assert Project.find(1).recipients.include?(@jsmith.mail)
636 635 end
637 636
638 637 def test_mail_notification_only_my_events
639 638 @jsmith.mail_notification = 'only_my_events'
640 639 @jsmith.notified_project_ids = []
641 640 @jsmith.save
642 641 @jsmith.reload
643 642 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
644 643 end
645 644
646 645 def test_comments_sorting_preference
647 646 assert !@jsmith.wants_comments_in_reverse_order?
648 647 @jsmith.pref.comments_sorting = 'asc'
649 648 assert !@jsmith.wants_comments_in_reverse_order?
650 649 @jsmith.pref.comments_sorting = 'desc'
651 650 assert @jsmith.wants_comments_in_reverse_order?
652 651 end
653 652
654 653 def test_find_by_mail_should_be_case_insensitive
655 654 u = User.find_by_mail('JSmith@somenet.foo')
656 655 assert_not_nil u
657 656 assert_equal 'jsmith@somenet.foo', u.mail
658 657 end
659 658
660 659 def test_random_password
661 660 u = User.new
662 661 u.random_password
663 662 assert !u.password.blank?
664 663 assert !u.password_confirmation.blank?
665 664 end
666 665
667 666 context "#change_password_allowed?" do
668 667 should "be allowed if no auth source is set" do
669 668 user = User.generate_with_protected!
670 669 assert user.change_password_allowed?
671 670 end
672 671
673 672 should "delegate to the auth source" do
674 673 user = User.generate_with_protected!
675 674
676 675 allowed_auth_source = AuthSource.generate!
677 676 def allowed_auth_source.allow_password_changes?; true; end
678 677
679 678 denied_auth_source = AuthSource.generate!
680 679 def denied_auth_source.allow_password_changes?; false; end
681 680
682 681 assert user.change_password_allowed?
683 682
684 683 user.auth_source = allowed_auth_source
685 684 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
686 685
687 686 user.auth_source = denied_auth_source
688 687 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
689 688 end
690 689
691 690 end
692 691
693 692 context "#allowed_to?" do
694 693 context "with a unique project" do
695 694 should "return false if project is archived" do
696 695 project = Project.find(1)
697 696 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
698 697 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
699 698 end
700 699
701 700 should "return false if related module is disabled" do
702 701 project = Project.find(1)
703 702 project.enabled_module_names = ["issue_tracking"]
704 703 assert @admin.allowed_to?(:add_issues, project)
705 704 assert ! @admin.allowed_to?(:view_wiki_pages, project)
706 705 end
707 706
708 707 should "authorize nearly everything for admin users" do
709 708 project = Project.find(1)
710 709 assert ! @admin.member_of?(project)
711 710 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
712 711 assert @admin.allowed_to?(p.to_sym, project)
713 712 end
714 713 end
715 714
716 715 should "authorize normal users depending on their roles" do
717 716 project = Project.find(1)
718 717 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
719 718 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
720 719 end
721 720 end
722 721
723 722 context "with multiple projects" do
724 723 should "return false if array is empty" do
725 724 assert ! @admin.allowed_to?(:view_project, [])
726 725 end
727 726
728 727 should "return true only if user has permission on all these projects" do
729 728 assert @admin.allowed_to?(:view_project, Project.all)
730 729 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
731 730 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
732 731 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
733 732 end
734 733
735 734 should "behave correctly with arrays of 1 project" do
736 735 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
737 736 end
738 737 end
739 738
740 739 context "with options[:global]" do
741 740 should "authorize if user has at least one role that has this permission" do
742 741 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
743 742 @anonymous = User.find(6)
744 743 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
745 744 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
746 745 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
747 746 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
748 747 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
749 748 end
750 749 end
751 750 end
752 751
753 752 context "User#notify_about?" do
754 753 context "Issues" do
755 754 setup do
756 755 @project = Project.find(1)
757 756 @author = User.generate_with_protected!
758 757 @assignee = User.generate_with_protected!
759 758 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
760 759 end
761 760
762 761 should "be true for a user with :all" do
763 762 @author.update_attribute(:mail_notification, 'all')
764 763 assert @author.notify_about?(@issue)
765 764 end
766 765
767 766 should "be false for a user with :none" do
768 767 @author.update_attribute(:mail_notification, 'none')
769 768 assert ! @author.notify_about?(@issue)
770 769 end
771 770
772 771 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
773 772 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
774 773 Member.create!(:user => @user, :project => @project, :role_ids => [1])
775 774 assert ! @user.notify_about?(@issue)
776 775 end
777 776
778 777 should "be true for a user with :only_my_events and is the author" do
779 778 @author.update_attribute(:mail_notification, 'only_my_events')
780 779 assert @author.notify_about?(@issue)
781 780 end
782 781
783 782 should "be true for a user with :only_my_events and is the assignee" do
784 783 @assignee.update_attribute(:mail_notification, 'only_my_events')
785 784 assert @assignee.notify_about?(@issue)
786 785 end
787 786
788 787 should "be true for a user with :only_assigned and is the assignee" do
789 788 @assignee.update_attribute(:mail_notification, 'only_assigned')
790 789 assert @assignee.notify_about?(@issue)
791 790 end
792 791
793 792 should "be false for a user with :only_assigned and is not the assignee" do
794 793 @author.update_attribute(:mail_notification, 'only_assigned')
795 794 assert ! @author.notify_about?(@issue)
796 795 end
797 796
798 797 should "be true for a user with :only_owner and is the author" do
799 798 @author.update_attribute(:mail_notification, 'only_owner')
800 799 assert @author.notify_about?(@issue)
801 800 end
802 801
803 802 should "be false for a user with :only_owner and is not the author" do
804 803 @assignee.update_attribute(:mail_notification, 'only_owner')
805 804 assert ! @assignee.notify_about?(@issue)
806 805 end
807 806
808 807 should "be true for a user with :selected and is the author" do
809 808 @author.update_attribute(:mail_notification, 'selected')
810 809 assert @author.notify_about?(@issue)
811 810 end
812 811
813 812 should "be true for a user with :selected and is the assignee" do
814 813 @assignee.update_attribute(:mail_notification, 'selected')
815 814 assert @assignee.notify_about?(@issue)
816 815 end
817 816
818 817 should "be false for a user with :selected and is not the author or assignee" do
819 818 @user = User.generate_with_protected!(:mail_notification => 'selected')
820 819 Member.create!(:user => @user, :project => @project, :role_ids => [1])
821 820 assert ! @user.notify_about?(@issue)
822 821 end
823 822 end
824 823
825 824 context "other events" do
826 825 should 'be added and tested'
827 826 end
828 827 end
829 828
830 829 def test_salt_unsalted_passwords
831 830 # Restore a user with an unsalted password
832 831 user = User.find(1)
833 832 user.salt = nil
834 833 user.hashed_password = User.hash_password("unsalted")
835 834 user.save!
836 835
837 836 User.salt_unsalted_passwords!
838 837
839 838 user.reload
840 839 # Salt added
841 840 assert !user.salt.blank?
842 841 # Password still valid
843 842 assert user.check_password?("unsalted")
844 843 assert_equal user, User.try_to_login(user.login, "unsalted")
845 844 end
846 845
847 846 if Object.const_defined?(:OpenID)
848 847
849 848 def test_setting_identity_url
850 849 normalized_open_id_url = 'http://example.com/'
851 850 u = User.new( :identity_url => 'http://example.com/' )
852 851 assert_equal normalized_open_id_url, u.identity_url
853 852 end
854 853
855 854 def test_setting_identity_url_without_trailing_slash
856 855 normalized_open_id_url = 'http://example.com/'
857 856 u = User.new( :identity_url => 'http://example.com' )
858 857 assert_equal normalized_open_id_url, u.identity_url
859 858 end
860 859
861 860 def test_setting_identity_url_without_protocol
862 861 normalized_open_id_url = 'http://example.com/'
863 862 u = User.new( :identity_url => 'example.com' )
864 863 assert_equal normalized_open_id_url, u.identity_url
865 864 end
866 865
867 866 def test_setting_blank_identity_url
868 867 u = User.new( :identity_url => 'example.com' )
869 868 u.identity_url = ''
870 869 assert u.identity_url.blank?
871 870 end
872 871
873 872 def test_setting_invalid_identity_url
874 873 u = User.new( :identity_url => 'this is not an openid url' )
875 874 assert u.identity_url.blank?
876 875 end
877 876
878 877 else
879 878 puts "Skipping openid tests."
880 879 end
881 880
882 881 end
General Comments 0
You need to be logged in to leave comments. Login now