##// END OF EJS Templates
Require password re-entry for sensitive actions (#19851)....
Jean-Philippe Lang -
r13951:d6f389658b9e
parent child
Show More
@@ -0,0 +1,10
1 <h2><%= l :label_api_access_key %></h2>
2
3 <div class="box">
4 <pre><%= @user.api_key %></pre>
5 </div>
6
7 <p><%= link_to l(:button_back), action: 'account' %></p>
8
9
10
@@ -0,0 +1,1
1 $('#api-access-key').html('<%= escape_javascript @user.api_key %>').toggle();
@@ -0,0 +1,19
1 <h3 class="title"><%= l(:label_password_required) %></h3>
2 <%= form_tag({}, remote: true) do %>
3
4 <%= hidden_field_tag '_method', request.request_method %>
5 <%= hash_to_hidden_fields @sudo_form.original_fields %>
6 <%= render_flash_messages %>
7 <div class="box tabular">
8 <p>
9 <label for="sudo_password"><%= l :field_password %><span class="required">*</span></label>
10 <%= password_field_tag :sudo_password, nil, size: 25 %>
11 </p>
12 </div>
13
14 <p class="buttons">
15 <%= submit_tag l(:button_confirm_password), onclick: "hideModal(this);" %>
16 <%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
17 </p>
18 <% end %>
19
@@ -0,0 +1,17
1 <h2><%= l :label_password_required %></h2>
2 <%= form_tag({}, class: 'tabular') do %>
3
4 <%= hidden_field_tag '_method', request.request_method %>
5 <%= hash_to_hidden_fields @sudo_form.original_fields %>
6
7 <div class="box">
8 <p>
9 <label for="sudo_password"><%= l :field_password %><span class="required">*</span></label>
10 <%= password_field_tag :sudo_password, nil, size: 25 %>
11 </p>
12 </div>
13 <%= submit_tag l(:button_confirm_password) %>
14 <% end %>
15 <%= javascript_tag "$('#sudo_password').focus();" %>
16
17
@@ -0,0 +1,4
1 $('#ajax-modal').html('<%= escape_javascript render partial: 'sudo_mode/new_modal' %>');
2 showModal('ajax-modal', '400px');
3 $('#sudo_password').focus();
4
@@ -0,0 +1,224
1 require 'active_support/core_ext/object/to_query'
2 require 'rack/utils'
3
4 module Redmine
5 module SudoMode
6
7 # timespan after which sudo mode expires when unused.
8 MAX_INACTIVITY = 15.minutes
9
10
11 class SudoRequired < StandardError
12 end
13
14
15 class Form
16 include ActiveModel::Validations
17
18 attr_accessor :password, :original_fields
19 validate :check_password
20
21 def initialize(password = nil)
22 self.password = password
23 end
24
25 def check_password
26 unless password.present? && User.current.check_password?(password)
27 errors[:password] << :invalid
28 end
29 end
30 end
31
32
33 module Helper
34 # Represents params data from hash as hidden fields
35 #
36 # taken from https://github.com/brianhempel/hash_to_hidden_fields
37 def hash_to_hidden_fields(hash)
38 cleaned_hash = hash.reject { |k, v| v.nil? }
39 pairs = cleaned_hash.to_query.split(Rack::Utils::DEFAULT_SEP)
40 tags = pairs.map do |pair|
41 key, value = pair.split('=', 2).map { |str| Rack::Utils.unescape(str) }
42 hidden_field_tag(key, value)
43 end
44 tags.join("\n").html_safe
45 end
46 end
47
48
49 module Controller
50 extend ActiveSupport::Concern
51
52 included do
53 around_filter :sudo_mode
54 end
55
56 # Sudo mode Around Filter
57 #
58 # Checks the 'last used' timestamp from session and sets the
59 # SudoMode::active? flag accordingly.
60 #
61 # After the request refreshes the timestamp if sudo mode was used during
62 # this request.
63 def sudo_mode
64 if api_request?
65 SudoMode.disable!
66 elsif sudo_timestamp_valid?
67 SudoMode.active!
68 end
69 yield
70 update_sudo_timestamp! if SudoMode.was_used?
71 end
72
73 # This renders the sudo mode form / handles sudo form submission.
74 #
75 # Call this method in controller actions if sudo permissions are required
76 # for processing this request. This approach is good in cases where the
77 # action needs to be protected in any case or where the check is simple.
78 #
79 # In cases where this decision depends on complex conditions in the model,
80 # consider the declarative approach using the require_sudo_mode class
81 # method and a corresponding declaration in the model that causes it to throw
82 # a SudoRequired Error when necessary.
83 #
84 # All parameter names given are included as hidden fields to be resubmitted
85 # along with the password.
86 #
87 # Returns true when processing the action should continue, false otherwise.
88 # If false is returned, render has already been called for display of the
89 # password form.
90 #
91 # if @user.mail_changed?
92 # require_sudo_mode :user or return
93 # end
94 #
95 def require_sudo_mode(*param_names)
96 return true if SudoMode.active?
97
98 if param_names.blank?
99 param_names = params.keys - %w(id action controller sudo_password)
100 end
101
102 process_sudo_form
103
104 if SudoMode.active?
105 true
106 else
107 render_sudo_form param_names
108 false
109 end
110 end
111
112 # display the sudo password form
113 def render_sudo_form(param_names)
114 @sudo_form ||= SudoMode::Form.new
115 @sudo_form.original_fields = params.slice( *param_names )
116 # a simple 'render "sudo_mode/new"' works when used directly inside an
117 # action, but not when called from a before_filter:
118 respond_to do |format|
119 format.html { render 'sudo_mode/new' }
120 format.js { render 'sudo_mode/new' }
121 end
122 end
123
124 # handle sudo password form submit
125 def process_sudo_form
126 if params[:sudo_password]
127 @sudo_form = SudoMode::Form.new(params[:sudo_password])
128 if @sudo_form.valid?
129 SudoMode.active!
130 else
131 flash.now[:error] = l(:notice_account_wrong_password)
132 end
133 end
134 end
135
136 def sudo_timestamp_valid?
137 session[:sudo_timestamp].to_i > MAX_INACTIVITY.ago.to_i
138 end
139
140 def update_sudo_timestamp!(new_value = Time.now.to_i)
141 session[:sudo_timestamp] = new_value
142 end
143
144 # Before Filter which is used by the require_sudo_mode class method.
145 class SudoRequestFilter < Struct.new(:parameters, :request_methods)
146 def before(controller)
147 method_matches = request_methods.blank? || request_methods.include?(controller.request.method_symbol)
148 if SudoMode.possible? && method_matches
149 controller.require_sudo_mode( *parameters )
150 else
151 true
152 end
153 end
154 end
155
156 module ClassMethods
157
158 # Handles sudo requirements for the given actions, preserving the named
159 # parameters, or any parameters if you omit the :parameters option.
160 #
161 # Sudo enforcement by default is active for all requests to an action
162 # but may be limited to a certain subset of request methods via the
163 # :only option.
164 #
165 # Examples:
166 #
167 # require_sudo_mode :account, only: :post
168 # require_sudo_mode :update, :create, parameters: %w(role)
169 # require_sudo_mode :destroy
170 #
171 def require_sudo_mode(*args)
172 actions = args.dup
173 options = actions.extract_options!
174 filter = SudoRequestFilter.new Array(options[:parameters]), Array(options[:only])
175 before_filter filter, only: actions
176 end
177 end
178 end
179
180
181 # true if the sudo mode state was queried during this request
182 def self.was_used?
183 !!RequestStore.store[:sudo_mode_was_used]
184 end
185
186 # true if sudo mode is currently active.
187 #
188 # Calling this method also turns was_used? to true, therefore
189 # it is important to only call this when sudo is actually needed, as the last
190 # condition to determine wether a change can be done or not.
191 #
192 # If you do it wrong, timeout of the sudo mode will happen too late or not at
193 # all.
194 def self.active?
195 if !!RequestStore.store[:sudo_mode]
196 RequestStore.store[:sudo_mode_was_used] = true
197 end
198 end
199
200 def self.active!
201 RequestStore.store[:sudo_mode] = true
202 end
203
204 def self.possible?
205 !disabled? && User.current.logged?
206 end
207
208 # Turn off sudo mode (never require password entry).
209 def self.disable!
210 RequestStore.store[:sudo_mode_disabled] = true
211 end
212
213 # Turn sudo mode back on
214 def self.enable!
215 RequestStore.store[:sudo_mode_disabled] = nil
216 end
217
218 def self.disabled?
219 !!RequestStore.store[:sudo_mode_disabled]
220 end
221
222 end
223 end
224
@@ -0,0 +1,126
1 require File.expand_path('../../test_helper', __FILE__)
2
3 class SudoTest < Redmine::IntegrationTest
4 fixtures :projects, :members, :member_roles, :roles, :users
5
6 def setup
7 Redmine::SudoMode.enable!
8 end
9
10 def teardown
11 Redmine::SudoMode.disable!
12 end
13
14 def test_create_member_xhr
15 log_user 'admin', 'admin'
16 get '/projects/ecookbook/settings/members'
17 assert_response :success
18
19 assert_no_difference 'Member.count' do
20 xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}
21 end
22
23 assert_no_difference 'Member.count' do
24 xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: ''
25 end
26
27 assert_no_difference 'Member.count' do
28 xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'wrong'
29 end
30
31 assert_difference 'Member.count' do
32 xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'admin'
33 end
34 assert User.find(7).member_of?(Project.find(1))
35 end
36
37 def test_create_member
38 log_user 'admin', 'admin'
39 get '/projects/ecookbook/settings/members'
40 assert_response :success
41
42 assert_no_difference 'Member.count' do
43 post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}
44 end
45
46 assert_no_difference 'Member.count' do
47 post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: ''
48 end
49
50 assert_no_difference 'Member.count' do
51 post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'wrong'
52 end
53
54 assert_difference 'Member.count' do
55 post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'admin'
56 end
57
58 assert_redirected_to '/projects/ecookbook/settings/members'
59 assert User.find(7).member_of?(Project.find(1))
60 end
61
62 def test_create_role
63 log_user 'admin', 'admin'
64 get '/roles'
65 assert_response :success
66
67 get '/roles/new'
68 assert_response :success
69
70 post '/roles', role: { }
71 assert_response :success
72 assert_select 'h2', 'Confirm your password to continue'
73 assert_select 'form[action="/roles"]'
74 assert assigns(:sudo_form).errors.blank?
75
76 post '/roles', role: { name: 'new role', issues_visibility: 'all' }
77 assert_response :success
78 assert_select 'h2', 'Confirm your password to continue'
79 assert_select 'form[action="/roles"]'
80 assert_match /"new role"/, response.body
81 assert assigns(:sudo_form).errors.blank?
82
83 post '/roles', role: { name: 'new role', issues_visibility: 'all' }, sudo_password: 'wrong'
84 assert_response :success
85 assert_select 'h2', 'Confirm your password to continue'
86 assert_select 'form[action="/roles"]'
87 assert_match /"new role"/, response.body
88 assert assigns(:sudo_form).errors[:password].present?
89
90 assert_difference 'Role.count' do
91 post '/roles', role: { name: 'new role', issues_visibility: 'all', assignable: '1', permissions: %w(view_calendar) }, sudo_password: 'admin'
92 end
93 assert_redirected_to '/roles'
94 end
95
96 def test_update_email_address
97 log_user 'jsmith', 'jsmith'
98 get '/my/account'
99 assert_response :success
100 post '/my/account', user: { mail: 'newmail@test.com' }
101 assert_response :success
102 assert_select 'h2', 'Confirm your password to continue'
103 assert_select 'form[action="/my/account"]'
104 assert_match /"newmail@test\.com"/, response.body
105 assert assigns(:sudo_form).errors.blank?
106
107 # wrong password
108 post '/my/account', user: { mail: 'newmail@test.com' }, sudo_password: 'wrong'
109 assert_response :success
110 assert_select 'h2', 'Confirm your password to continue'
111 assert_select 'form[action="/my/account"]'
112 assert_match /"newmail@test\.com"/, response.body
113 assert assigns(:sudo_form).errors[:password].present?
114
115 # correct password
116 post '/my/account', user: { mail: 'newmail@test.com' }, sudo_password: 'jsmith'
117 assert_redirected_to '/my/account'
118 assert_equal 'newmail@test.com', User.find_by_login('jsmith').mail
119
120 # sudo mode should now be active and not require password again
121 post '/my/account', user: { mail: 'even.newer.mail@test.com' }
122 assert_redirected_to '/my/account'
123 assert_equal 'even.newer.mail@test.com', User.find_by_login('jsmith').mail
124 end
125
126 end
@@ -1,660 +1,662
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25 25 include Redmine::Pagination
26 26 include RoutesHelper
27 27 helper :routes
28 28
29 29 class_attribute :accept_api_auth_actions
30 30 class_attribute :accept_rss_auth_actions
31 31 class_attribute :model_object
32 32
33 33 layout 'base'
34 34
35 35 protect_from_forgery
36 36
37 37 def verify_authenticity_token
38 38 unless api_request?
39 39 super
40 40 end
41 41 end
42 42
43 43 def handle_unverified_request
44 44 unless api_request?
45 45 super
46 46 cookies.delete(autologin_cookie_name)
47 47 self.logged_user = nil
48 48 set_localization
49 49 render_error :status => 422, :message => "Invalid form authenticity token."
50 50 end
51 51 end
52 52
53 53 before_filter :session_expiration, :user_setup, :force_logout_if_password_changed, :check_if_login_required, :check_password_change, :set_localization
54 54
55 55 rescue_from ::Unauthorized, :with => :deny_access
56 56 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
57 57
58 58 include Redmine::Search::Controller
59 59 include Redmine::MenuManager::MenuController
60 60 helper Redmine::MenuManager::MenuHelper
61 61
62 include Redmine::SudoMode::Controller
63
62 64 def session_expiration
63 65 if session[:user_id]
64 66 if session_expired? && !try_to_autologin
65 67 set_localization(User.active.find_by_id(session[:user_id]))
66 68 self.logged_user = nil
67 69 flash[:error] = l(:error_session_expired)
68 70 require_login
69 71 else
70 72 session[:atime] = Time.now.utc.to_i
71 73 end
72 74 end
73 75 end
74 76
75 77 def session_expired?
76 78 if Setting.session_lifetime?
77 79 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
78 80 return true
79 81 end
80 82 end
81 83 if Setting.session_timeout?
82 84 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
83 85 return true
84 86 end
85 87 end
86 88 false
87 89 end
88 90
89 91 def start_user_session(user)
90 92 session[:user_id] = user.id
91 93 session[:ctime] = Time.now.utc.to_i
92 94 session[:atime] = Time.now.utc.to_i
93 95 if user.must_change_password?
94 96 session[:pwd] = '1'
95 97 end
96 98 end
97 99
98 100 def user_setup
99 101 # Check the settings cache for each request
100 102 Setting.check_cache
101 103 # Find the current user
102 104 User.current = find_current_user
103 105 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
104 106 end
105 107
106 108 # Returns the current user or nil if no user is logged in
107 109 # and starts a session if needed
108 110 def find_current_user
109 111 user = nil
110 112 unless api_request?
111 113 if session[:user_id]
112 114 # existing session
113 115 user = (User.active.find(session[:user_id]) rescue nil)
114 116 elsif autologin_user = try_to_autologin
115 117 user = autologin_user
116 118 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
117 119 # RSS key authentication does not start a session
118 120 user = User.find_by_rss_key(params[:key])
119 121 end
120 122 end
121 123 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
122 124 if (key = api_key_from_request)
123 125 # Use API key
124 126 user = User.find_by_api_key(key)
125 127 elsif request.authorization.to_s =~ /\ABasic /i
126 128 # HTTP Basic, either username/password or API key/random
127 129 authenticate_with_http_basic do |username, password|
128 130 user = User.try_to_login(username, password) || User.find_by_api_key(username)
129 131 end
130 132 if user && user.must_change_password?
131 133 render_error :message => 'You must change your password', :status => 403
132 134 return
133 135 end
134 136 end
135 137 # Switch user if requested by an admin user
136 138 if user && user.admin? && (username = api_switch_user_from_request)
137 139 su = User.find_by_login(username)
138 140 if su && su.active?
139 141 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
140 142 user = su
141 143 else
142 144 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
143 145 end
144 146 end
145 147 end
146 148 user
147 149 end
148 150
149 151 def force_logout_if_password_changed
150 152 passwd_changed_on = User.current.passwd_changed_on || Time.at(0)
151 153 # Make sure we force logout only for web browser sessions, not API calls
152 154 # if the password was changed after the session creation.
153 155 if session[:user_id] && passwd_changed_on.utc.to_i > session[:ctime].to_i
154 156 reset_session
155 157 set_localization
156 158 flash[:error] = l(:error_session_expired)
157 159 redirect_to signin_url
158 160 end
159 161 end
160 162
161 163 def autologin_cookie_name
162 164 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
163 165 end
164 166
165 167 def try_to_autologin
166 168 if cookies[autologin_cookie_name] && Setting.autologin?
167 169 # auto-login feature starts a new session
168 170 user = User.try_to_autologin(cookies[autologin_cookie_name])
169 171 if user
170 172 reset_session
171 173 start_user_session(user)
172 174 end
173 175 user
174 176 end
175 177 end
176 178
177 179 # Sets the logged in user
178 180 def logged_user=(user)
179 181 reset_session
180 182 if user && user.is_a?(User)
181 183 User.current = user
182 184 start_user_session(user)
183 185 else
184 186 User.current = User.anonymous
185 187 end
186 188 end
187 189
188 190 # Logs out current user
189 191 def logout_user
190 192 if User.current.logged?
191 193 cookies.delete(autologin_cookie_name)
192 194 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
193 195 self.logged_user = nil
194 196 end
195 197 end
196 198
197 199 # check if login is globally required to access the application
198 200 def check_if_login_required
199 201 # no check needed if user is already logged in
200 202 return true if User.current.logged?
201 203 require_login if Setting.login_required?
202 204 end
203 205
204 206 def check_password_change
205 207 if session[:pwd]
206 208 if User.current.must_change_password?
207 209 flash[:error] = l(:error_password_expired)
208 210 redirect_to my_password_path
209 211 else
210 212 session.delete(:pwd)
211 213 end
212 214 end
213 215 end
214 216
215 217 def set_localization(user=User.current)
216 218 lang = nil
217 219 if user && user.logged?
218 220 lang = find_language(user.language)
219 221 end
220 222 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
221 223 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
222 224 if !accept_lang.blank?
223 225 accept_lang = accept_lang.downcase
224 226 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
225 227 end
226 228 end
227 229 lang ||= Setting.default_language
228 230 set_language_if_valid(lang)
229 231 end
230 232
231 233 def require_login
232 234 if !User.current.logged?
233 235 # Extract only the basic url parameters on non-GET requests
234 236 if request.get?
235 237 url = url_for(params)
236 238 else
237 239 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
238 240 end
239 241 respond_to do |format|
240 242 format.html {
241 243 if request.xhr?
242 244 head :unauthorized
243 245 else
244 246 redirect_to signin_path(:back_url => url)
245 247 end
246 248 }
247 249 format.any(:atom, :pdf, :csv) {
248 250 redirect_to signin_path(:back_url => url)
249 251 }
250 252 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
251 253 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
252 254 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
253 255 format.any { head :unauthorized }
254 256 end
255 257 return false
256 258 end
257 259 true
258 260 end
259 261
260 262 def require_admin
261 263 return unless require_login
262 264 if !User.current.admin?
263 265 render_403
264 266 return false
265 267 end
266 268 true
267 269 end
268 270
269 271 def deny_access
270 272 User.current.logged? ? render_403 : require_login
271 273 end
272 274
273 275 # Authorize the user for the requested action
274 276 def authorize(ctrl = params[:controller], action = params[:action], global = false)
275 277 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
276 278 if allowed
277 279 true
278 280 else
279 281 if @project && @project.archived?
280 282 render_403 :message => :notice_not_authorized_archived_project
281 283 else
282 284 deny_access
283 285 end
284 286 end
285 287 end
286 288
287 289 # Authorize the user for the requested action outside a project
288 290 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
289 291 authorize(ctrl, action, global)
290 292 end
291 293
292 294 # Find project of id params[:id]
293 295 def find_project
294 296 @project = Project.find(params[:id])
295 297 rescue ActiveRecord::RecordNotFound
296 298 render_404
297 299 end
298 300
299 301 # Find project of id params[:project_id]
300 302 def find_project_by_project_id
301 303 @project = Project.find(params[:project_id])
302 304 rescue ActiveRecord::RecordNotFound
303 305 render_404
304 306 end
305 307
306 308 # Find a project based on params[:project_id]
307 309 # TODO: some subclasses override this, see about merging their logic
308 310 def find_optional_project
309 311 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
310 312 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
311 313 allowed ? true : deny_access
312 314 rescue ActiveRecord::RecordNotFound
313 315 render_404
314 316 end
315 317
316 318 # Finds and sets @project based on @object.project
317 319 def find_project_from_association
318 320 render_404 unless @object.present?
319 321
320 322 @project = @object.project
321 323 end
322 324
323 325 def find_model_object
324 326 model = self.class.model_object
325 327 if model
326 328 @object = model.find(params[:id])
327 329 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
328 330 end
329 331 rescue ActiveRecord::RecordNotFound
330 332 render_404
331 333 end
332 334
333 335 def self.model_object(model)
334 336 self.model_object = model
335 337 end
336 338
337 339 # Find the issue whose id is the :id parameter
338 340 # Raises a Unauthorized exception if the issue is not visible
339 341 def find_issue
340 342 # Issue.visible.find(...) can not be used to redirect user to the login form
341 343 # if the issue actually exists but requires authentication
342 344 @issue = Issue.find(params[:id])
343 345 raise Unauthorized unless @issue.visible?
344 346 @project = @issue.project
345 347 rescue ActiveRecord::RecordNotFound
346 348 render_404
347 349 end
348 350
349 351 # Find issues with a single :id param or :ids array param
350 352 # Raises a Unauthorized exception if one of the issues is not visible
351 353 def find_issues
352 354 @issues = Issue.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a
353 355 raise ActiveRecord::RecordNotFound if @issues.empty?
354 356 raise Unauthorized unless @issues.all?(&:visible?)
355 357 @projects = @issues.collect(&:project).compact.uniq
356 358 @project = @projects.first if @projects.size == 1
357 359 rescue ActiveRecord::RecordNotFound
358 360 render_404
359 361 end
360 362
361 363 def find_attachments
362 364 if (attachments = params[:attachments]).present?
363 365 att = attachments.values.collect do |attachment|
364 366 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
365 367 end
366 368 att.compact!
367 369 end
368 370 @attachments = att || []
369 371 end
370 372
371 373 # make sure that the user is a member of the project (or admin) if project is private
372 374 # used as a before_filter for actions that do not require any particular permission on the project
373 375 def check_project_privacy
374 376 if @project && !@project.archived?
375 377 if @project.visible?
376 378 true
377 379 else
378 380 deny_access
379 381 end
380 382 else
381 383 @project = nil
382 384 render_404
383 385 false
384 386 end
385 387 end
386 388
387 389 def back_url
388 390 url = params[:back_url]
389 391 if url.nil? && referer = request.env['HTTP_REFERER']
390 392 url = CGI.unescape(referer.to_s)
391 393 end
392 394 url
393 395 end
394 396
395 397 def redirect_back_or_default(default, options={})
396 398 back_url = params[:back_url].to_s
397 399 if back_url.present? && valid_back_url?(back_url)
398 400 redirect_to(back_url)
399 401 return
400 402 elsif options[:referer]
401 403 redirect_to_referer_or default
402 404 return
403 405 end
404 406 redirect_to default
405 407 false
406 408 end
407 409
408 410 # Returns true if back_url is a valid url for redirection, otherwise false
409 411 def valid_back_url?(back_url)
410 412 if CGI.unescape(back_url).include?('..')
411 413 return false
412 414 end
413 415
414 416 begin
415 417 uri = URI.parse(back_url)
416 418 rescue URI::InvalidURIError
417 419 return false
418 420 end
419 421
420 422 if uri.host.present? && uri.host != request.host
421 423 return false
422 424 end
423 425
424 426 if uri.path.match(%r{/(login|account/register)})
425 427 return false
426 428 end
427 429
428 430 if relative_url_root.present? && !uri.path.starts_with?(relative_url_root)
429 431 return false
430 432 end
431 433
432 434 return true
433 435 end
434 436 private :valid_back_url?
435 437
436 438 # Redirects to the request referer if present, redirects to args or call block otherwise.
437 439 def redirect_to_referer_or(*args, &block)
438 440 redirect_to :back
439 441 rescue ::ActionController::RedirectBackError
440 442 if args.any?
441 443 redirect_to *args
442 444 elsif block_given?
443 445 block.call
444 446 else
445 447 raise "#redirect_to_referer_or takes arguments or a block"
446 448 end
447 449 end
448 450
449 451 def render_403(options={})
450 452 @project = nil
451 453 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
452 454 return false
453 455 end
454 456
455 457 def render_404(options={})
456 458 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
457 459 return false
458 460 end
459 461
460 462 # Renders an error response
461 463 def render_error(arg)
462 464 arg = {:message => arg} unless arg.is_a?(Hash)
463 465
464 466 @message = arg[:message]
465 467 @message = l(@message) if @message.is_a?(Symbol)
466 468 @status = arg[:status] || 500
467 469
468 470 respond_to do |format|
469 471 format.html {
470 472 render :template => 'common/error', :layout => use_layout, :status => @status
471 473 }
472 474 format.any { head @status }
473 475 end
474 476 end
475 477
476 478 # Handler for ActionView::MissingTemplate exception
477 479 def missing_template
478 480 logger.warn "Missing template, responding with 404"
479 481 @project = nil
480 482 render_404
481 483 end
482 484
483 485 # Filter for actions that provide an API response
484 486 # but have no HTML representation for non admin users
485 487 def require_admin_or_api_request
486 488 return true if api_request?
487 489 if User.current.admin?
488 490 true
489 491 elsif User.current.logged?
490 492 render_error(:status => 406)
491 493 else
492 494 deny_access
493 495 end
494 496 end
495 497
496 498 # Picks which layout to use based on the request
497 499 #
498 500 # @return [boolean, string] name of the layout to use or false for no layout
499 501 def use_layout
500 502 request.xhr? ? false : 'base'
501 503 end
502 504
503 505 def render_feed(items, options={})
504 506 @items = (items || []).to_a
505 507 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
506 508 @items = @items.slice(0, Setting.feeds_limit.to_i)
507 509 @title = options[:title] || Setting.app_title
508 510 render :template => "common/feed", :formats => [:atom], :layout => false,
509 511 :content_type => 'application/atom+xml'
510 512 end
511 513
512 514 def self.accept_rss_auth(*actions)
513 515 if actions.any?
514 516 self.accept_rss_auth_actions = actions
515 517 else
516 518 self.accept_rss_auth_actions || []
517 519 end
518 520 end
519 521
520 522 def accept_rss_auth?(action=action_name)
521 523 self.class.accept_rss_auth.include?(action.to_sym)
522 524 end
523 525
524 526 def self.accept_api_auth(*actions)
525 527 if actions.any?
526 528 self.accept_api_auth_actions = actions
527 529 else
528 530 self.accept_api_auth_actions || []
529 531 end
530 532 end
531 533
532 534 def accept_api_auth?(action=action_name)
533 535 self.class.accept_api_auth.include?(action.to_sym)
534 536 end
535 537
536 538 # Returns the number of objects that should be displayed
537 539 # on the paginated list
538 540 def per_page_option
539 541 per_page = nil
540 542 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
541 543 per_page = params[:per_page].to_s.to_i
542 544 session[:per_page] = per_page
543 545 elsif session[:per_page]
544 546 per_page = session[:per_page]
545 547 else
546 548 per_page = Setting.per_page_options_array.first || 25
547 549 end
548 550 per_page
549 551 end
550 552
551 553 # Returns offset and limit used to retrieve objects
552 554 # for an API response based on offset, limit and page parameters
553 555 def api_offset_and_limit(options=params)
554 556 if options[:offset].present?
555 557 offset = options[:offset].to_i
556 558 if offset < 0
557 559 offset = 0
558 560 end
559 561 end
560 562 limit = options[:limit].to_i
561 563 if limit < 1
562 564 limit = 25
563 565 elsif limit > 100
564 566 limit = 100
565 567 end
566 568 if offset.nil? && options[:page].present?
567 569 offset = (options[:page].to_i - 1) * limit
568 570 offset = 0 if offset < 0
569 571 end
570 572 offset ||= 0
571 573
572 574 [offset, limit]
573 575 end
574 576
575 577 # qvalues http header parser
576 578 # code taken from webrick
577 579 def parse_qvalues(value)
578 580 tmp = []
579 581 if value
580 582 parts = value.split(/,\s*/)
581 583 parts.each {|part|
582 584 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
583 585 val = m[1]
584 586 q = (m[2] or 1).to_f
585 587 tmp.push([val, q])
586 588 end
587 589 }
588 590 tmp = tmp.sort_by{|val, q| -q}
589 591 tmp.collect!{|val, q| val}
590 592 end
591 593 return tmp
592 594 rescue
593 595 nil
594 596 end
595 597
596 598 # Returns a string that can be used as filename value in Content-Disposition header
597 599 def filename_for_content_disposition(name)
598 600 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident)} ? ERB::Util.url_encode(name) : name
599 601 end
600 602
601 603 def api_request?
602 604 %w(xml json).include? params[:format]
603 605 end
604 606
605 607 # Returns the API key present in the request
606 608 def api_key_from_request
607 609 if params[:key].present?
608 610 params[:key].to_s
609 611 elsif request.headers["X-Redmine-API-Key"].present?
610 612 request.headers["X-Redmine-API-Key"].to_s
611 613 end
612 614 end
613 615
614 616 # Returns the API 'switch user' value if present
615 617 def api_switch_user_from_request
616 618 request.headers["X-Redmine-Switch-User"].to_s.presence
617 619 end
618 620
619 621 # Renders a warning flash if obj has unsaved attachments
620 622 def render_attachment_warning_if_needed(obj)
621 623 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
622 624 end
623 625
624 626 # Rescues an invalid query statement. Just in case...
625 627 def query_statement_invalid(exception)
626 628 logger.error "Query::StatementInvalid: #{exception.message}" if logger
627 629 session.delete(:query)
628 630 sort_clear if respond_to?(:sort_clear)
629 631 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
630 632 end
631 633
632 634 # Renders a 200 response for successfull updates or deletions via the API
633 635 def render_api_ok
634 636 render_api_head :ok
635 637 end
636 638
637 639 # Renders a head API response
638 640 def render_api_head(status)
639 641 # #head would return a response body with one space
640 642 render :text => '', :status => status, :layout => nil
641 643 end
642 644
643 645 # Renders API response on validation failure
644 646 # for an object or an array of objects
645 647 def render_validation_errors(objects)
646 648 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
647 649 render_api_errors(messages)
648 650 end
649 651
650 652 def render_api_errors(*messages)
651 653 @error_messages = messages.flatten
652 654 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
653 655 end
654 656
655 657 # Overrides #_include_layout? so that #render with no arguments
656 658 # doesn't use the layout for api requests
657 659 def _include_layout?(*args)
658 660 api_request? ? false : super
659 661 end
660 662 end
@@ -1,96 +1,97
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AuthSourcesController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :ldap_authentication
21 21
22 22 before_filter :require_admin
23 23 before_filter :find_auth_source, :only => [:edit, :update, :test_connection, :destroy]
24 require_sudo_mode :update, :destroy
24 25
25 26 def index
26 27 @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25
27 28 end
28 29
29 30 def new
30 31 klass_name = params[:type] || 'AuthSourceLdap'
31 32 @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source])
32 33 render_404 unless @auth_source
33 34 end
34 35
35 36 def create
36 37 @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source])
37 38 if @auth_source.save
38 39 flash[:notice] = l(:notice_successful_create)
39 40 redirect_to auth_sources_path
40 41 else
41 42 render :action => 'new'
42 43 end
43 44 end
44 45
45 46 def edit
46 47 end
47 48
48 49 def update
49 50 if @auth_source.update_attributes(params[:auth_source])
50 51 flash[:notice] = l(:notice_successful_update)
51 52 redirect_to auth_sources_path
52 53 else
53 54 render :action => 'edit'
54 55 end
55 56 end
56 57
57 58 def test_connection
58 59 begin
59 60 @auth_source.test_connection
60 61 flash[:notice] = l(:notice_successful_connection)
61 62 rescue Exception => e
62 63 flash[:error] = l(:error_unable_to_connect, e.message)
63 64 end
64 65 redirect_to auth_sources_path
65 66 end
66 67
67 68 def destroy
68 69 unless @auth_source.users.exists?
69 70 @auth_source.destroy
70 71 flash[:notice] = l(:notice_successful_delete)
71 72 end
72 73 redirect_to auth_sources_path
73 74 end
74 75
75 76 def autocomplete_for_new_user
76 77 results = AuthSource.search(params[:term])
77 78
78 79 render :json => results.map {|result| {
79 80 'value' => result[:login],
80 81 'label' => "#{result[:login]} (#{result[:firstname]} #{result[:lastname]})",
81 82 'login' => result[:login].to_s,
82 83 'firstname' => result[:firstname].to_s,
83 84 'lastname' => result[:lastname].to_s,
84 85 'mail' => result[:mail].to_s,
85 86 'auth_source_id' => result[:auth_source_id].to_s
86 87 }}
87 88 end
88 89
89 90 private
90 91
91 92 def find_auth_source
92 93 @auth_source = AuthSource.find(params[:id])
93 94 rescue ActiveRecord::RecordNotFound
94 95 render_404
95 96 end
96 97 end
@@ -1,105 +1,106
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class EmailAddressesController < ApplicationController
19 19 before_filter :find_user, :require_admin_or_current_user
20 20 before_filter :find_email_address, :only => [:update, :destroy]
21 require_sudo_mode :create, :update, :destroy
21 22
22 23 def index
23 24 @addresses = @user.email_addresses.order(:id).where(:is_default => false).to_a
24 25 @address ||= EmailAddress.new
25 26 end
26 27
27 28 def create
28 29 saved = false
29 30 if @user.email_addresses.count <= Setting.max_additional_emails.to_i
30 31 @address = EmailAddress.new(:user => @user, :is_default => false)
31 32 attrs = params[:email_address]
32 33 if attrs.is_a?(Hash)
33 34 @address.address = attrs[:address].to_s
34 35 end
35 36 saved = @address.save
36 37 end
37 38
38 39 respond_to do |format|
39 40 format.html {
40 41 if saved
41 42 redirect_to user_email_addresses_path(@user)
42 43 else
43 44 index
44 45 render :action => 'index'
45 46 end
46 47 }
47 48 format.js {
48 49 @address = nil if saved
49 50 index
50 51 render :action => 'index'
51 52 }
52 53 end
53 54 end
54 55
55 56 def update
56 57 if params[:notify].present?
57 58 @address.notify = params[:notify].to_s
58 59 end
59 60 @address.save
60 61
61 62 respond_to do |format|
62 63 format.html {
63 64 redirect_to user_email_addresses_path(@user)
64 65 }
65 66 format.js {
66 67 @address = nil
67 68 index
68 69 render :action => 'index'
69 70 }
70 71 end
71 72 end
72 73
73 74 def destroy
74 75 @address.destroy
75 76
76 77 respond_to do |format|
77 78 format.html {
78 79 redirect_to user_email_addresses_path(@user)
79 80 }
80 81 format.js {
81 82 @address = nil
82 83 index
83 84 render :action => 'index'
84 85 }
85 86 end
86 87 end
87 88
88 89 private
89 90
90 91 def find_user
91 92 @user = User.find(params[:user_id])
92 93 end
93 94
94 95 def find_email_address
95 96 @address = @user.email_addresses.where(:is_default => false).find(params[:id])
96 97 rescue ActiveRecord::RecordNotFound
97 98 render_404
98 99 end
99 100
100 101 def require_admin_or_current_user
101 102 unless @user == User.current
102 103 require_admin
103 104 end
104 105 end
105 106 end
@@ -1,147 +1,149
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class GroupsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :find_group, :except => [:index, :new, :create]
23 23 accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
24 24
25 require_sudo_mode :add_users, :remove_user, :create, :update, :destroy, :edit_membership, :destroy_membership
26
25 27 helper :custom_fields
26 28 helper :principal_memberships
27 29
28 30 def index
29 31 respond_to do |format|
30 32 format.html {
31 33 @groups = Group.sorted.to_a
32 34 @user_count_by_group_id = user_count_by_group_id
33 35 }
34 36 format.api {
35 37 scope = Group.sorted
36 38 scope = scope.givable unless params[:builtin] == '1'
37 39 @groups = scope.to_a
38 40 }
39 41 end
40 42 end
41 43
42 44 def show
43 45 respond_to do |format|
44 46 format.html
45 47 format.api
46 48 end
47 49 end
48 50
49 51 def new
50 52 @group = Group.new
51 53 end
52 54
53 55 def create
54 56 @group = Group.new
55 57 @group.safe_attributes = params[:group]
56 58
57 59 respond_to do |format|
58 60 if @group.save
59 61 format.html {
60 62 flash[:notice] = l(:notice_successful_create)
61 63 redirect_to(params[:continue] ? new_group_path : groups_path)
62 64 }
63 65 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
64 66 else
65 67 format.html { render :action => "new" }
66 68 format.api { render_validation_errors(@group) }
67 69 end
68 70 end
69 71 end
70 72
71 73 def edit
72 74 end
73 75
74 76 def update
75 77 @group.safe_attributes = params[:group]
76 78
77 79 respond_to do |format|
78 80 if @group.save
79 81 flash[:notice] = l(:notice_successful_update)
80 82 format.html { redirect_to(groups_path) }
81 83 format.api { render_api_ok }
82 84 else
83 85 format.html { render :action => "edit" }
84 86 format.api { render_validation_errors(@group) }
85 87 end
86 88 end
87 89 end
88 90
89 91 def destroy
90 92 @group.destroy
91 93
92 94 respond_to do |format|
93 95 format.html { redirect_to(groups_path) }
94 96 format.api { render_api_ok }
95 97 end
96 98 end
97 99
98 100 def new_users
99 101 end
100 102
101 103 def add_users
102 104 @users = User.not_in_group(@group).where(:id => (params[:user_id] || params[:user_ids])).to_a
103 105 @group.users << @users
104 106 respond_to do |format|
105 107 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
106 108 format.js
107 109 format.api {
108 110 if @users.any?
109 111 render_api_ok
110 112 else
111 113 render_api_errors "#{l(:label_user)} #{l('activerecord.errors.messages.invalid')}"
112 114 end
113 115 }
114 116 end
115 117 end
116 118
117 119 def remove_user
118 120 @group.users.delete(User.find(params[:user_id])) if request.delete?
119 121 respond_to do |format|
120 122 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
121 123 format.js
122 124 format.api { render_api_ok }
123 125 end
124 126 end
125 127
126 128 def autocomplete_for_user
127 129 respond_to do |format|
128 130 format.js
129 131 end
130 132 end
131 133
132 134 private
133 135
134 136 def find_group
135 137 @group = Group.find(params[:id])
136 138 rescue ActiveRecord::RecordNotFound
137 139 render_404
138 140 end
139 141
140 142 def user_count_by_group_id
141 143 h = User.joins(:groups).group('group_id').count
142 144 h.keys.each do |key|
143 145 h[key.to_i] = h.delete(key)
144 146 end
145 147 h
146 148 end
147 149 end
@@ -1,127 +1,129
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MembersController < ApplicationController
19 19 model_object Member
20 20 before_filter :find_model_object, :except => [:index, :new, :create, :autocomplete]
21 21 before_filter :find_project_from_association, :except => [:index, :new, :create, :autocomplete]
22 22 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :autocomplete]
23 23 before_filter :authorize
24 24 accept_api_auth :index, :show, :create, :update, :destroy
25 25
26 require_sudo_mode :create, :update, :destroy
27
26 28 def index
27 29 scope = @project.memberships.active
28 30 @offset, @limit = api_offset_and_limit
29 31 @member_count = scope.count
30 32 @member_pages = Paginator.new @member_count, @limit, params['page']
31 33 @offset ||= @member_pages.offset
32 34 @members = scope.order(:id).limit(@limit).offset(@offset).to_a
33 35
34 36 respond_to do |format|
35 37 format.html { head 406 }
36 38 format.api
37 39 end
38 40 end
39 41
40 42 def show
41 43 respond_to do |format|
42 44 format.html { head 406 }
43 45 format.api
44 46 end
45 47 end
46 48
47 49 def new
48 50 @member = Member.new
49 51 end
50 52
51 53 def create
52 54 members = []
53 55 if params[:membership]
54 56 user_ids = Array.wrap(params[:membership][:user_id] || params[:membership][:user_ids])
55 57 user_ids << nil if user_ids.empty?
56 58 user_ids.each do |user_id|
57 59 member = Member.new(:project => @project, :user_id => user_id)
58 60 member.set_editable_role_ids(params[:membership][:role_ids])
59 61 members << member
60 62 end
61 63 @project.members << members
62 64 end
63 65
64 66 respond_to do |format|
65 67 format.html { redirect_to_settings_in_projects }
66 68 format.js {
67 69 @members = members
68 70 @member = Member.new
69 71 }
70 72 format.api {
71 73 @member = members.first
72 74 if @member.valid?
73 75 render :action => 'show', :status => :created, :location => membership_url(@member)
74 76 else
75 77 render_validation_errors(@member)
76 78 end
77 79 }
78 80 end
79 81 end
80 82
81 83 def update
82 84 if params[:membership]
83 85 @member.set_editable_role_ids(params[:membership][:role_ids])
84 86 end
85 87 saved = @member.save
86 88 respond_to do |format|
87 89 format.html { redirect_to_settings_in_projects }
88 90 format.js
89 91 format.api {
90 92 if saved
91 93 render_api_ok
92 94 else
93 95 render_validation_errors(@member)
94 96 end
95 97 }
96 98 end
97 99 end
98 100
99 101 def destroy
100 102 if @member.deletable?
101 103 @member.destroy
102 104 end
103 105 respond_to do |format|
104 106 format.html { redirect_to_settings_in_projects }
105 107 format.js
106 108 format.api {
107 109 if @member.destroyed?
108 110 render_api_ok
109 111 else
110 112 head :unprocessable_entity
111 113 end
112 114 }
113 115 end
114 116 end
115 117
116 118 def autocomplete
117 119 respond_to do |format|
118 120 format.js
119 121 end
120 122 end
121 123
122 124 private
123 125
124 126 def redirect_to_settings_in_projects
125 127 redirect_to settings_project_path(@project, :tab => 'members')
126 128 end
127 129 end
@@ -1,204 +1,211
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MyController < ApplicationController
19 19 before_filter :require_login
20 20 # let user change user's password when user has to
21 21 skip_before_filter :check_password_change, :only => :password
22 22
23 require_sudo_mode :account, only: :post
24 require_sudo_mode :reset_rss_key, :reset_api_key, :show_api_key, :destroy
25
23 26 helper :issues
24 27 helper :users
25 28 helper :custom_fields
26 29
27 30 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
28 31 'issuesreportedbyme' => :label_reported_issues,
29 32 'issueswatched' => :label_watched_issues,
30 33 'news' => :label_news_latest,
31 34 'calendar' => :label_calendar,
32 35 'documents' => :label_document_plural,
33 36 'timelog' => :label_spent_time
34 37 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
35 38
36 39 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
37 40 'right' => ['issuesreportedbyme']
38 41 }.freeze
39 42
40 43 def index
41 44 page
42 45 render :action => 'page'
43 46 end
44 47
45 48 # Show user's page
46 49 def page
47 50 @user = User.current
48 51 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
49 52 end
50 53
51 54 # Edit user's account
52 55 def account
53 56 @user = User.current
54 57 @pref = @user.pref
55 58 if request.post?
56 59 @user.safe_attributes = params[:user] if params[:user]
57 60 @user.pref.attributes = params[:pref] if params[:pref]
58 61 if @user.save
59 62 @user.pref.save
60 63 set_language_if_valid @user.language
61 64 flash[:notice] = l(:notice_account_updated)
62 65 redirect_to my_account_path
63 66 return
64 67 end
65 68 end
66 69 end
67 70
68 71 # Destroys user's account
69 72 def destroy
70 73 @user = User.current
71 74 unless @user.own_account_deletable?
72 75 redirect_to my_account_path
73 76 return
74 77 end
75 78
76 79 if request.post? && params[:confirm]
77 80 @user.destroy
78 81 if @user.destroyed?
79 82 logout_user
80 83 flash[:notice] = l(:notice_account_deleted)
81 84 end
82 85 redirect_to home_path
83 86 end
84 87 end
85 88
86 89 # Manage user's password
87 90 def password
88 91 @user = User.current
89 92 unless @user.change_password_allowed?
90 93 flash[:error] = l(:notice_can_t_change_password)
91 94 redirect_to my_account_path
92 95 return
93 96 end
94 97 if request.post?
95 98 if !@user.check_password?(params[:password])
96 99 flash.now[:error] = l(:notice_account_wrong_password)
97 100 elsif params[:password] == params[:new_password]
98 101 flash.now[:error] = l(:notice_new_password_must_be_different)
99 102 else
100 103 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
101 104 @user.must_change_passwd = false
102 105 if @user.save
103 106 # Reset the session creation time to not log out this session on next
104 107 # request due to ApplicationController#force_logout_if_password_changed
105 108 session[:ctime] = User.current.passwd_changed_on.utc.to_i
106 109 flash[:notice] = l(:notice_account_password_updated)
107 110 redirect_to my_account_path
108 111 end
109 112 end
110 113 end
111 114 end
112 115
113 116 # Create a new feeds key
114 117 def reset_rss_key
115 118 if request.post?
116 119 if User.current.rss_token
117 120 User.current.rss_token.destroy
118 121 User.current.reload
119 122 end
120 123 User.current.rss_key
121 124 flash[:notice] = l(:notice_feeds_access_key_reseted)
122 125 end
123 126 redirect_to my_account_path
124 127 end
125 128
129 def show_api_key
130 @user = User.current
131 end
132
126 133 # Create a new API key
127 134 def reset_api_key
128 135 if request.post?
129 136 if User.current.api_token
130 137 User.current.api_token.destroy
131 138 User.current.reload
132 139 end
133 140 User.current.api_key
134 141 flash[:notice] = l(:notice_api_access_key_reseted)
135 142 end
136 143 redirect_to my_account_path
137 144 end
138 145
139 146 # User's page layout configuration
140 147 def page_layout
141 148 @user = User.current
142 149 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
143 150 @block_options = []
144 151 BLOCKS.each do |k, v|
145 152 unless @blocks.values.flatten.include?(k)
146 153 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
147 154 end
148 155 end
149 156 end
150 157
151 158 # Add a block to user's page
152 159 # The block is added on top of the page
153 160 # params[:block] : id of the block to add
154 161 def add_block
155 162 block = params[:block].to_s.underscore
156 163 if block.present? && BLOCKS.key?(block)
157 164 @user = User.current
158 165 layout = @user.pref[:my_page_layout] || {}
159 166 # remove if already present in a group
160 167 %w(top left right).each {|f| (layout[f] ||= []).delete block }
161 168 # add it on top
162 169 layout['top'].unshift block
163 170 @user.pref[:my_page_layout] = layout
164 171 @user.pref.save
165 172 end
166 173 redirect_to my_page_layout_path
167 174 end
168 175
169 176 # Remove a block to user's page
170 177 # params[:block] : id of the block to remove
171 178 def remove_block
172 179 block = params[:block].to_s.underscore
173 180 @user = User.current
174 181 # remove block in all groups
175 182 layout = @user.pref[:my_page_layout] || {}
176 183 %w(top left right).each {|f| (layout[f] ||= []).delete block }
177 184 @user.pref[:my_page_layout] = layout
178 185 @user.pref.save
179 186 redirect_to my_page_layout_path
180 187 end
181 188
182 189 # Change blocks order on user's page
183 190 # params[:group] : group to order (top, left or right)
184 191 # params[:list-(top|left|right)] : array of block ids of the group
185 192 def order_blocks
186 193 group = params[:group]
187 194 @user = User.current
188 195 if group.is_a?(String)
189 196 group_items = (params["blocks"] || []).collect(&:underscore)
190 197 group_items.each {|s| s.sub!(/^block_/, '')}
191 198 if group_items and group_items.is_a? Array
192 199 layout = @user.pref[:my_page_layout] || {}
193 200 # remove group blocks if they are presents in other groups
194 201 %w(top left right).each {|f|
195 202 layout[f] = (layout[f] || []) - group_items
196 203 }
197 204 layout[group] = group_items
198 205 @user.pref[:my_page_layout] = layout
199 206 @user.pref.save
200 207 end
201 208 end
202 209 render :nothing => true
203 210 end
204 211 end
@@ -1,233 +1,234
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :settings, :only => :settings
21 21
22 22 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 23 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 24 before_filter :authorize_global, :only => [:new, :create]
25 25 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 26 accept_rss_auth :index
27 27 accept_api_auth :index, :show, :create, :update, :destroy
28 require_sudo_mode :destroy
28 29
29 30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 31 if controller.request.post?
31 32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
32 33 end
33 34 end
34 35
35 36 helper :custom_fields
36 37 helper :issues
37 38 helper :queries
38 39 helper :repositories
39 40 helper :members
40 41
41 42 # Lists visible projects
42 43 def index
43 44 scope = Project.visible.sorted
44 45
45 46 respond_to do |format|
46 47 format.html {
47 48 unless params[:closed]
48 49 scope = scope.active
49 50 end
50 51 @projects = scope.to_a
51 52 }
52 53 format.api {
53 54 @offset, @limit = api_offset_and_limit
54 55 @project_count = scope.count
55 56 @projects = scope.offset(@offset).limit(@limit).to_a
56 57 }
57 58 format.atom {
58 59 projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
59 60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 61 }
61 62 end
62 63 end
63 64
64 65 def new
65 66 @issue_custom_fields = IssueCustomField.sorted.to_a
66 67 @trackers = Tracker.sorted.to_a
67 68 @project = Project.new
68 69 @project.safe_attributes = params[:project]
69 70 end
70 71
71 72 def create
72 73 @issue_custom_fields = IssueCustomField.sorted.to_a
73 74 @trackers = Tracker.sorted.to_a
74 75 @project = Project.new
75 76 @project.safe_attributes = params[:project]
76 77
77 78 if @project.save
78 79 unless User.current.admin?
79 80 @project.add_default_member(User.current)
80 81 end
81 82 respond_to do |format|
82 83 format.html {
83 84 flash[:notice] = l(:notice_successful_create)
84 85 if params[:continue]
85 86 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
86 87 redirect_to new_project_path(attrs)
87 88 else
88 89 redirect_to settings_project_path(@project)
89 90 end
90 91 }
91 92 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 93 end
93 94 else
94 95 respond_to do |format|
95 96 format.html { render :action => 'new' }
96 97 format.api { render_validation_errors(@project) }
97 98 end
98 99 end
99 100 end
100 101
101 102 def copy
102 103 @issue_custom_fields = IssueCustomField.sorted.to_a
103 104 @trackers = Tracker.sorted.to_a
104 105 @source_project = Project.find(params[:id])
105 106 if request.get?
106 107 @project = Project.copy_from(@source_project)
107 108 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
108 109 else
109 110 Mailer.with_deliveries(params[:notifications] == '1') do
110 111 @project = Project.new
111 112 @project.safe_attributes = params[:project]
112 113 if @project.copy(@source_project, :only => params[:only])
113 114 flash[:notice] = l(:notice_successful_create)
114 115 redirect_to settings_project_path(@project)
115 116 elsif !@project.new_record?
116 117 # Project was created
117 118 # But some objects were not copied due to validation failures
118 119 # (eg. issues from disabled trackers)
119 120 # TODO: inform about that
120 121 redirect_to settings_project_path(@project)
121 122 end
122 123 end
123 124 end
124 125 rescue ActiveRecord::RecordNotFound
125 126 # source_project not found
126 127 render_404
127 128 end
128 129
129 130 # Show @project
130 131 def show
131 132 # try to redirect to the requested menu item
132 133 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
133 134 return
134 135 end
135 136
136 137 @users_by_role = @project.users_by_role
137 138 @subprojects = @project.children.visible.to_a
138 139 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
139 140 @trackers = @project.rolled_up_trackers
140 141
141 142 cond = @project.project_condition(Setting.display_subprojects_issues?)
142 143
143 144 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
144 145 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
145 146
146 147 if User.current.allowed_to_view_all_time_entries?(@project)
147 148 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
148 149 end
149 150
150 151 @key = User.current.rss_key
151 152
152 153 respond_to do |format|
153 154 format.html
154 155 format.api
155 156 end
156 157 end
157 158
158 159 def settings
159 160 @issue_custom_fields = IssueCustomField.sorted.to_a
160 161 @issue_category ||= IssueCategory.new
161 162 @member ||= @project.members.new
162 163 @trackers = Tracker.sorted.to_a
163 164 @wiki ||= @project.wiki || Wiki.new(:project => @project)
164 165 end
165 166
166 167 def edit
167 168 end
168 169
169 170 def update
170 171 @project.safe_attributes = params[:project]
171 172 if @project.save
172 173 respond_to do |format|
173 174 format.html {
174 175 flash[:notice] = l(:notice_successful_update)
175 176 redirect_to settings_project_path(@project)
176 177 }
177 178 format.api { render_api_ok }
178 179 end
179 180 else
180 181 respond_to do |format|
181 182 format.html {
182 183 settings
183 184 render :action => 'settings'
184 185 }
185 186 format.api { render_validation_errors(@project) }
186 187 end
187 188 end
188 189 end
189 190
190 191 def modules
191 192 @project.enabled_module_names = params[:enabled_module_names]
192 193 flash[:notice] = l(:notice_successful_update)
193 194 redirect_to settings_project_path(@project, :tab => 'modules')
194 195 end
195 196
196 197 def archive
197 198 unless @project.archive
198 199 flash[:error] = l(:error_can_not_archive_project)
199 200 end
200 201 redirect_to admin_projects_path(:status => params[:status])
201 202 end
202 203
203 204 def unarchive
204 205 unless @project.active?
205 206 @project.unarchive
206 207 end
207 208 redirect_to admin_projects_path(:status => params[:status])
208 209 end
209 210
210 211 def close
211 212 @project.close
212 213 redirect_to project_path(@project)
213 214 end
214 215
215 216 def reopen
216 217 @project.reopen
217 218 redirect_to project_path(@project)
218 219 end
219 220
220 221 # Delete @project
221 222 def destroy
222 223 @project_to_destroy = @project
223 224 if api_request? || params[:confirm]
224 225 @project_to_destroy.destroy
225 226 respond_to do |format|
226 227 format.html { redirect_to admin_projects_path }
227 228 format.api { render_api_ok }
228 229 end
229 230 end
230 231 # hide project in layout
231 232 @project = nil
232 233 end
233 234 end
@@ -1,108 +1,110
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class RolesController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => [:index, :show]
22 22 before_filter :require_admin_or_api_request, :only => [:index, :show]
23 23 before_filter :find_role, :only => [:show, :edit, :update, :destroy]
24 24 accept_api_auth :index, :show
25 25
26 require_sudo_mode :create, :update, :destroy
27
26 28 def index
27 29 respond_to do |format|
28 30 format.html {
29 31 @role_pages, @roles = paginate Role.sorted, :per_page => 25
30 32 render :action => "index", :layout => false if request.xhr?
31 33 }
32 34 format.api {
33 35 @roles = Role.givable.to_a
34 36 }
35 37 end
36 38 end
37 39
38 40 def show
39 41 respond_to do |format|
40 42 format.api
41 43 end
42 44 end
43 45
44 46 def new
45 47 # Prefills the form with 'Non member' role permissions by default
46 48 @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
47 49 if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
48 50 @role.copy_from(@copy_from)
49 51 end
50 52 @roles = Role.sorted.to_a
51 53 end
52 54
53 55 def create
54 56 @role = Role.new(params[:role])
55 57 if request.post? && @role.save
56 58 # workflow copy
57 59 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
58 60 @role.workflow_rules.copy(copy_from)
59 61 end
60 62 flash[:notice] = l(:notice_successful_create)
61 63 redirect_to roles_path
62 64 else
63 65 @roles = Role.sorted.to_a
64 66 render :action => 'new'
65 67 end
66 68 end
67 69
68 70 def edit
69 71 end
70 72
71 73 def update
72 74 if @role.update_attributes(params[:role])
73 75 flash[:notice] = l(:notice_successful_update)
74 76 redirect_to roles_path(:page => params[:page])
75 77 else
76 78 render :action => 'edit'
77 79 end
78 80 end
79 81
80 82 def destroy
81 83 @role.destroy
82 84 redirect_to roles_path
83 85 rescue
84 86 flash[:error] = l(:error_can_not_remove_role)
85 87 redirect_to roles_path
86 88 end
87 89
88 90 def permissions
89 91 @roles = Role.sorted.to_a
90 92 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
91 93 if request.post?
92 94 @roles.each do |role|
93 95 role.permissions = params[:permissions][role.id.to_s]
94 96 role.save
95 97 end
96 98 flash[:notice] = l(:notice_successful_update)
97 99 redirect_to roles_path
98 100 end
99 101 end
100 102
101 103 private
102 104
103 105 def find_role
104 106 @role = Role.find(params[:id])
105 107 rescue ActiveRecord::RecordNotFound
106 108 render_404
107 109 end
108 110 end
@@ -1,74 +1,76
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class SettingsController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :plugins, :only => :plugin
21 21
22 22 helper :queries
23 23
24 24 before_filter :require_admin
25 25
26 require_sudo_mode :index, :edit, :plugin
27
26 28 def index
27 29 edit
28 30 render :action => 'edit'
29 31 end
30 32
31 33 def edit
32 34 @notifiables = Redmine::Notifiable.all
33 35 if request.post? && params[:settings] && params[:settings].is_a?(Hash)
34 36 settings = (params[:settings] || {}).dup.symbolize_keys
35 37 settings.each do |name, value|
36 38 Setting.set_from_params name, value
37 39 end
38 40 flash[:notice] = l(:notice_successful_update)
39 41 redirect_to settings_path(:tab => params[:tab])
40 42 else
41 43 @options = {}
42 44 user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}
43 45 @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]}
44 46 @deliveries = ActionMailer::Base.perform_deliveries
45 47
46 48 @guessed_host_and_path = request.host_with_port.dup
47 49 @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
48 50
49 51 @commit_update_keywords = Setting.commit_update_keywords.dup
50 52 @commit_update_keywords = [{}] unless @commit_update_keywords.is_a?(Array) && @commit_update_keywords.any?
51 53
52 54 Redmine::Themes.rescan
53 55 end
54 56 end
55 57
56 58 def plugin
57 59 @plugin = Redmine::Plugin.find(params[:id])
58 60 unless @plugin.configurable?
59 61 render_404
60 62 return
61 63 end
62 64
63 65 if request.post?
64 66 Setting.send "plugin_#{@plugin.id}=", params[:settings]
65 67 flash[:notice] = l(:notice_successful_update)
66 68 redirect_to plugin_settings_path(@plugin)
67 69 else
68 70 @partial = @plugin.settings[:partial]
69 71 @settings = Setting.send "plugin_#{@plugin.id}"
70 72 end
71 73 rescue Redmine::PluginNotFound
72 74 render_404
73 75 end
74 76 end
@@ -1,188 +1,190
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class UsersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :show
22 22 before_filter :find_user, :only => [:show, :edit, :update, :destroy]
23 23 accept_api_auth :index, :show, :create, :update, :destroy
24 24
25 25 helper :sort
26 26 include SortHelper
27 27 helper :custom_fields
28 28 include CustomFieldsHelper
29 29 helper :principal_memberships
30 30
31 require_sudo_mode :create, :update, :destroy
32
31 33 def index
32 34 sort_init 'login', 'asc'
33 35 sort_update %w(login firstname lastname admin created_on last_login_on)
34 36
35 37 case params[:format]
36 38 when 'xml', 'json'
37 39 @offset, @limit = api_offset_and_limit
38 40 else
39 41 @limit = per_page_option
40 42 end
41 43
42 44 @status = params[:status] || 1
43 45
44 46 scope = User.logged.status(@status).preload(:email_address)
45 47 scope = scope.like(params[:name]) if params[:name].present?
46 48 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
47 49
48 50 @user_count = scope.count
49 51 @user_pages = Paginator.new @user_count, @limit, params['page']
50 52 @offset ||= @user_pages.offset
51 53 @users = scope.order(sort_clause).limit(@limit).offset(@offset).to_a
52 54
53 55 respond_to do |format|
54 56 format.html {
55 57 @groups = Group.all.sort
56 58 render :layout => !request.xhr?
57 59 }
58 60 format.api
59 61 end
60 62 end
61 63
62 64 def show
63 65 unless @user.visible?
64 66 render_404
65 67 return
66 68 end
67 69
68 70 # show projects based on current user visibility
69 71 @memberships = @user.memberships.where(Project.visible_condition(User.current)).to_a
70 72
71 73 respond_to do |format|
72 74 format.html {
73 75 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
74 76 @events_by_day = events.group_by(&:event_date)
75 77 render :layout => 'base'
76 78 }
77 79 format.api
78 80 end
79 81 end
80 82
81 83 def new
82 84 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
83 85 @user.safe_attributes = params[:user]
84 86 @auth_sources = AuthSource.all
85 87 end
86 88
87 89 def create
88 90 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
89 91 @user.safe_attributes = params[:user]
90 92 @user.admin = params[:user][:admin] || false
91 93 @user.login = params[:user][:login]
92 94 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
93 95 @user.pref.attributes = params[:pref] if params[:pref]
94 96
95 97 if @user.save
96 98 Mailer.account_information(@user, @user.password).deliver if params[:send_information]
97 99
98 100 respond_to do |format|
99 101 format.html {
100 102 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
101 103 if params[:continue]
102 104 attrs = params[:user].slice(:generate_password)
103 105 redirect_to new_user_path(:user => attrs)
104 106 else
105 107 redirect_to edit_user_path(@user)
106 108 end
107 109 }
108 110 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
109 111 end
110 112 else
111 113 @auth_sources = AuthSource.all
112 114 # Clear password input
113 115 @user.password = @user.password_confirmation = nil
114 116
115 117 respond_to do |format|
116 118 format.html { render :action => 'new' }
117 119 format.api { render_validation_errors(@user) }
118 120 end
119 121 end
120 122 end
121 123
122 124 def edit
123 125 @auth_sources = AuthSource.all
124 126 @membership ||= Member.new
125 127 end
126 128
127 129 def update
128 130 @user.admin = params[:user][:admin] if params[:user][:admin]
129 131 @user.login = params[:user][:login] if params[:user][:login]
130 132 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
131 133 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
132 134 end
133 135 @user.safe_attributes = params[:user]
134 136 # Was the account actived ? (do it before User#save clears the change)
135 137 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
136 138 # TODO: Similar to My#account
137 139 @user.pref.attributes = params[:pref] if params[:pref]
138 140
139 141 if @user.save
140 142 @user.pref.save
141 143
142 144 if was_activated
143 145 Mailer.account_activated(@user).deliver
144 146 elsif @user.active? && params[:send_information] && @user.password.present? && @user.auth_source_id.nil?
145 147 Mailer.account_information(@user, @user.password).deliver
146 148 end
147 149
148 150 respond_to do |format|
149 151 format.html {
150 152 flash[:notice] = l(:notice_successful_update)
151 153 redirect_to_referer_or edit_user_path(@user)
152 154 }
153 155 format.api { render_api_ok }
154 156 end
155 157 else
156 158 @auth_sources = AuthSource.all
157 159 @membership ||= Member.new
158 160 # Clear password input
159 161 @user.password = @user.password_confirmation = nil
160 162
161 163 respond_to do |format|
162 164 format.html { render :action => :edit }
163 165 format.api { render_validation_errors(@user) }
164 166 end
165 167 end
166 168 end
167 169
168 170 def destroy
169 171 @user.destroy
170 172 respond_to do |format|
171 173 format.html { redirect_back_or_default(users_path) }
172 174 format.api { render_api_ok }
173 175 end
174 176 end
175 177
176 178 private
177 179
178 180 def find_user
179 181 if params[:id] == 'current'
180 182 require_login || return
181 183 @user = User.current
182 184 else
183 185 @user = User.find(params[:id])
184 186 end
185 187 rescue ActiveRecord::RecordNotFound
186 188 render_404
187 189 end
188 190 end
@@ -1,1326 +1,1327
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 29
29 30 extend Forwardable
30 31 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 32
32 33 # Return true if user is authorized for controller/action, otherwise false
33 34 def authorize_for(controller, action)
34 35 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 36 end
36 37
37 38 # Display a link if user is authorized
38 39 #
39 40 # @param [String] name Anchor text (passed to link_to)
40 41 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 42 # @param [optional, Hash] html_options Options passed to link_to
42 43 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 44 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 45 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 46 end
46 47
47 48 # Displays a link to user's account page if active
48 49 def link_to_user(user, options={})
49 50 if user.is_a?(User)
50 51 name = h(user.name(options[:format]))
51 52 if user.active? || (User.current.admin? && user.logged?)
52 53 link_to name, user_path(user), :class => user.css_classes
53 54 else
54 55 name
55 56 end
56 57 else
57 58 h(user.to_s)
58 59 end
59 60 end
60 61
61 62 # Displays a link to +issue+ with its subject.
62 63 # Examples:
63 64 #
64 65 # link_to_issue(issue) # => Defect #6: This is the subject
65 66 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 67 # link_to_issue(issue, :subject => false) # => Defect #6
67 68 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 69 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 70 #
70 71 def link_to_issue(issue, options={})
71 72 title = nil
72 73 subject = nil
73 74 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 75 if options[:subject] == false
75 76 title = issue.subject.truncate(60)
76 77 else
77 78 subject = issue.subject
78 79 if truncate_length = options[:truncate]
79 80 subject = subject.truncate(truncate_length)
80 81 end
81 82 end
82 83 only_path = options[:only_path].nil? ? true : options[:only_path]
83 84 s = link_to(text, issue_url(issue, :only_path => only_path),
84 85 :class => issue.css_classes, :title => title)
85 86 s << h(": #{subject}") if subject
86 87 s = h("#{issue.project} - ") + s if options[:project]
87 88 s
88 89 end
89 90
90 91 # Generates a link to an attachment.
91 92 # Options:
92 93 # * :text - Link text (default to attachment filename)
93 94 # * :download - Force download (default: false)
94 95 def link_to_attachment(attachment, options={})
95 96 text = options.delete(:text) || attachment.filename
96 97 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
97 98 html_options = options.slice!(:only_path)
98 99 options[:only_path] = true unless options.key?(:only_path)
99 100 url = send(route_method, attachment, attachment.filename, options)
100 101 link_to text, url, html_options
101 102 end
102 103
103 104 # Generates a link to a SCM revision
104 105 # Options:
105 106 # * :text - Link text (default to the formatted revision)
106 107 def link_to_revision(revision, repository, options={})
107 108 if repository.is_a?(Project)
108 109 repository = repository.repository
109 110 end
110 111 text = options.delete(:text) || format_revision(revision)
111 112 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 113 link_to(
113 114 h(text),
114 115 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 116 :title => l(:label_revision_id, format_revision(revision)),
116 117 :accesskey => options[:accesskey]
117 118 )
118 119 end
119 120
120 121 # Generates a link to a message
121 122 def link_to_message(message, options={}, html_options = nil)
122 123 link_to(
123 124 message.subject.truncate(60),
124 125 board_message_url(message.board_id, message.parent_id || message.id, {
125 126 :r => (message.parent_id && message.id),
126 127 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
127 128 :only_path => true
128 129 }.merge(options)),
129 130 html_options
130 131 )
131 132 end
132 133
133 134 # Generates a link to a project if active
134 135 # Examples:
135 136 #
136 137 # link_to_project(project) # => link to the specified project overview
137 138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 140 #
140 141 def link_to_project(project, options={}, html_options = nil)
141 142 if project.archived?
142 143 h(project.name)
143 144 else
144 145 link_to project.name,
145 146 project_url(project, {:only_path => true}.merge(options)),
146 147 html_options
147 148 end
148 149 end
149 150
150 151 # Generates a link to a project settings if active
151 152 def link_to_project_settings(project, options={}, html_options=nil)
152 153 if project.active?
153 154 link_to project.name, settings_project_path(project, options), html_options
154 155 elsif project.archived?
155 156 h(project.name)
156 157 else
157 158 link_to project.name, project_path(project, options), html_options
158 159 end
159 160 end
160 161
161 162 # Generates a link to a version
162 163 def link_to_version(version, options = {})
163 164 return '' unless version && version.is_a?(Version)
164 165 options = {:title => format_date(version.effective_date)}.merge(options)
165 166 link_to_if version.visible?, format_version_name(version), version_path(version), options
166 167 end
167 168
168 169 # Helper that formats object for html or text rendering
169 170 def format_object(object, html=true, &block)
170 171 if block_given?
171 172 object = yield object
172 173 end
173 174 case object.class.name
174 175 when 'Array'
175 176 object.map {|o| format_object(o, html)}.join(', ').html_safe
176 177 when 'Time'
177 178 format_time(object)
178 179 when 'Date'
179 180 format_date(object)
180 181 when 'Fixnum'
181 182 object.to_s
182 183 when 'Float'
183 184 sprintf "%.2f", object
184 185 when 'User'
185 186 html ? link_to_user(object) : object.to_s
186 187 when 'Project'
187 188 html ? link_to_project(object) : object.to_s
188 189 when 'Version'
189 190 html ? link_to_version(object) : object.to_s
190 191 when 'TrueClass'
191 192 l(:general_text_Yes)
192 193 when 'FalseClass'
193 194 l(:general_text_No)
194 195 when 'Issue'
195 196 object.visible? && html ? link_to_issue(object) : "##{object.id}"
196 197 when 'CustomValue', 'CustomFieldValue'
197 198 if object.custom_field
198 199 f = object.custom_field.format.formatted_custom_value(self, object, html)
199 200 if f.nil? || f.is_a?(String)
200 201 f
201 202 else
202 203 format_object(f, html, &block)
203 204 end
204 205 else
205 206 object.value.to_s
206 207 end
207 208 else
208 209 html ? h(object) : object.to_s
209 210 end
210 211 end
211 212
212 213 def wiki_page_path(page, options={})
213 214 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
214 215 end
215 216
216 217 def thumbnail_tag(attachment)
217 218 link_to image_tag(thumbnail_path(attachment)),
218 219 named_attachment_path(attachment, attachment.filename),
219 220 :title => attachment.filename
220 221 end
221 222
222 223 def toggle_link(name, id, options={})
223 224 onclick = "$('##{id}').toggle(); "
224 225 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
225 226 onclick << "return false;"
226 227 link_to(name, "#", :onclick => onclick)
227 228 end
228 229
229 230 def format_activity_title(text)
230 231 h(truncate_single_line_raw(text, 100))
231 232 end
232 233
233 234 def format_activity_day(date)
234 235 date == User.current.today ? l(:label_today).titleize : format_date(date)
235 236 end
236 237
237 238 def format_activity_description(text)
238 239 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
239 240 ).gsub(/[\r\n]+/, "<br />").html_safe
240 241 end
241 242
242 243 def format_version_name(version)
243 244 if version.project == @project
244 245 h(version)
245 246 else
246 247 h("#{version.project} - #{version}")
247 248 end
248 249 end
249 250
250 251 def due_date_distance_in_words(date)
251 252 if date
252 253 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
253 254 end
254 255 end
255 256
256 257 # Renders a tree of projects as a nested set of unordered lists
257 258 # The given collection may be a subset of the whole project tree
258 259 # (eg. some intermediate nodes are private and can not be seen)
259 260 def render_project_nested_lists(projects, &block)
260 261 s = ''
261 262 if projects.any?
262 263 ancestors = []
263 264 original_project = @project
264 265 projects.sort_by(&:lft).each do |project|
265 266 # set the project environment to please macros.
266 267 @project = project
267 268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 269 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
269 270 else
270 271 ancestors.pop
271 272 s << "</li>"
272 273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 274 ancestors.pop
274 275 s << "</ul></li>\n"
275 276 end
276 277 end
277 278 classes = (ancestors.empty? ? 'root' : 'child')
278 279 s << "<li class='#{classes}'><div class='#{classes}'>"
279 280 s << h(block_given? ? capture(project, &block) : project.name)
280 281 s << "</div>\n"
281 282 ancestors << project
282 283 end
283 284 s << ("</li></ul>\n" * ancestors.size)
284 285 @project = original_project
285 286 end
286 287 s.html_safe
287 288 end
288 289
289 290 def render_page_hierarchy(pages, node=nil, options={})
290 291 content = ''
291 292 if pages[node]
292 293 content << "<ul class=\"pages-hierarchy\">\n"
293 294 pages[node].each do |page|
294 295 content << "<li>"
295 296 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
296 297 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
297 298 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
298 299 content << "</li>\n"
299 300 end
300 301 content << "</ul>\n"
301 302 end
302 303 content.html_safe
303 304 end
304 305
305 306 # Renders flash messages
306 307 def render_flash_messages
307 308 s = ''
308 309 flash.each do |k,v|
309 310 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
310 311 end
311 312 s.html_safe
312 313 end
313 314
314 315 # Renders tabs and their content
315 316 def render_tabs(tabs, selected=params[:tab])
316 317 if tabs.any?
317 318 unless tabs.detect {|tab| tab[:name] == selected}
318 319 selected = nil
319 320 end
320 321 selected ||= tabs.first[:name]
321 322 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
322 323 else
323 324 content_tag 'p', l(:label_no_data), :class => "nodata"
324 325 end
325 326 end
326 327
327 328 # Renders the project quick-jump box
328 329 def render_project_jump_box
329 330 return unless User.current.logged?
330 331 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
331 332 if projects.any?
332 333 options =
333 334 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
334 335 '<option value="" disabled="disabled">---</option>').html_safe
335 336
336 337 options << project_tree_options_for_select(projects, :selected => @project) do |p|
337 338 { :value => project_path(:id => p, :jump => current_menu_item) }
338 339 end
339 340
340 341 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
341 342 end
342 343 end
343 344
344 345 def project_tree_options_for_select(projects, options = {})
345 346 s = ''.html_safe
346 347 if blank_text = options[:include_blank]
347 348 if blank_text == true
348 349 blank_text = '&nbsp;'.html_safe
349 350 end
350 351 s << content_tag('option', blank_text, :value => '')
351 352 end
352 353 project_tree(projects) do |project, level|
353 354 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
354 355 tag_options = {:value => project.id}
355 356 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
356 357 tag_options[:selected] = 'selected'
357 358 else
358 359 tag_options[:selected] = nil
359 360 end
360 361 tag_options.merge!(yield(project)) if block_given?
361 362 s << content_tag('option', name_prefix + h(project), tag_options)
362 363 end
363 364 s.html_safe
364 365 end
365 366
366 367 # Yields the given block for each project with its level in the tree
367 368 #
368 369 # Wrapper for Project#project_tree
369 370 def project_tree(projects, &block)
370 371 Project.project_tree(projects, &block)
371 372 end
372 373
373 374 def principals_check_box_tags(name, principals)
374 375 s = ''
375 376 principals.each do |principal|
376 377 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
377 378 end
378 379 s.html_safe
379 380 end
380 381
381 382 # Returns a string for users/groups option tags
382 383 def principals_options_for_select(collection, selected=nil)
383 384 s = ''
384 385 if collection.include?(User.current)
385 386 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
386 387 end
387 388 groups = ''
388 389 collection.sort.each do |element|
389 390 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
390 391 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
391 392 end
392 393 unless groups.empty?
393 394 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
394 395 end
395 396 s.html_safe
396 397 end
397 398
398 399 def option_tag(name, text, value, selected=nil, options={})
399 400 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
400 401 end
401 402
402 403 def truncate_single_line_raw(string, length)
403 404 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
404 405 end
405 406
406 407 # Truncates at line break after 250 characters or options[:length]
407 408 def truncate_lines(string, options={})
408 409 length = options[:length] || 250
409 410 if string.to_s =~ /\A(.{#{length}}.*?)$/m
410 411 "#{$1}..."
411 412 else
412 413 string
413 414 end
414 415 end
415 416
416 417 def anchor(text)
417 418 text.to_s.gsub(' ', '_')
418 419 end
419 420
420 421 def html_hours(text)
421 422 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
422 423 end
423 424
424 425 def authoring(created, author, options={})
425 426 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
426 427 end
427 428
428 429 def time_tag(time)
429 430 text = distance_of_time_in_words(Time.now, time)
430 431 if @project
431 432 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
432 433 else
433 434 content_tag('abbr', text, :title => format_time(time))
434 435 end
435 436 end
436 437
437 438 def syntax_highlight_lines(name, content)
438 439 lines = []
439 440 syntax_highlight(name, content).each_line { |line| lines << line }
440 441 lines
441 442 end
442 443
443 444 def syntax_highlight(name, content)
444 445 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
445 446 end
446 447
447 448 def to_path_param(path)
448 449 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
449 450 str.blank? ? nil : str
450 451 end
451 452
452 453 def reorder_links(name, url, method = :post)
453 454 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
454 455 url.merge({"#{name}[move_to]" => 'highest'}),
455 456 :method => method, :title => l(:label_sort_highest)) +
456 457 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
457 458 url.merge({"#{name}[move_to]" => 'higher'}),
458 459 :method => method, :title => l(:label_sort_higher)) +
459 460 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
460 461 url.merge({"#{name}[move_to]" => 'lower'}),
461 462 :method => method, :title => l(:label_sort_lower)) +
462 463 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
463 464 url.merge({"#{name}[move_to]" => 'lowest'}),
464 465 :method => method, :title => l(:label_sort_lowest))
465 466 end
466 467
467 468 def breadcrumb(*args)
468 469 elements = args.flatten
469 470 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
470 471 end
471 472
472 473 def other_formats_links(&block)
473 474 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
474 475 yield Redmine::Views::OtherFormatsBuilder.new(self)
475 476 concat('</p>'.html_safe)
476 477 end
477 478
478 479 def page_header_title
479 480 if @project.nil? || @project.new_record?
480 481 h(Setting.app_title)
481 482 else
482 483 b = []
483 484 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
484 485 if ancestors.any?
485 486 root = ancestors.shift
486 487 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
487 488 if ancestors.size > 2
488 489 b << "\xe2\x80\xa6"
489 490 ancestors = ancestors[-2, 2]
490 491 end
491 492 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
492 493 end
493 494 b << h(@project)
494 495 b.join(" \xc2\xbb ").html_safe
495 496 end
496 497 end
497 498
498 499 # Returns a h2 tag and sets the html title with the given arguments
499 500 def title(*args)
500 501 strings = args.map do |arg|
501 502 if arg.is_a?(Array) && arg.size >= 2
502 503 link_to(*arg)
503 504 else
504 505 h(arg.to_s)
505 506 end
506 507 end
507 508 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
508 509 content_tag('h2', strings.join(' &#187; ').html_safe)
509 510 end
510 511
511 512 # Sets the html title
512 513 # Returns the html title when called without arguments
513 514 # Current project name and app_title and automatically appended
514 515 # Exemples:
515 516 # html_title 'Foo', 'Bar'
516 517 # html_title # => 'Foo - Bar - My Project - Redmine'
517 518 def html_title(*args)
518 519 if args.empty?
519 520 title = @html_title || []
520 521 title << @project.name if @project
521 522 title << Setting.app_title unless Setting.app_title == title.last
522 523 title.reject(&:blank?).join(' - ')
523 524 else
524 525 @html_title ||= []
525 526 @html_title += args
526 527 end
527 528 end
528 529
529 530 # Returns the theme, controller name, and action as css classes for the
530 531 # HTML body.
531 532 def body_css_classes
532 533 css = []
533 534 if theme = Redmine::Themes.theme(Setting.ui_theme)
534 535 css << 'theme-' + theme.name
535 536 end
536 537
537 538 css << 'project-' + @project.identifier if @project && @project.identifier.present?
538 539 css << 'controller-' + controller_name
539 540 css << 'action-' + action_name
540 541 css.join(' ')
541 542 end
542 543
543 544 def accesskey(s)
544 545 @used_accesskeys ||= []
545 546 key = Redmine::AccessKeys.key_for(s)
546 547 return nil if @used_accesskeys.include?(key)
547 548 @used_accesskeys << key
548 549 key
549 550 end
550 551
551 552 # Formats text according to system settings.
552 553 # 2 ways to call this method:
553 554 # * with a String: textilizable(text, options)
554 555 # * with an object and one of its attribute: textilizable(issue, :description, options)
555 556 def textilizable(*args)
556 557 options = args.last.is_a?(Hash) ? args.pop : {}
557 558 case args.size
558 559 when 1
559 560 obj = options[:object]
560 561 text = args.shift
561 562 when 2
562 563 obj = args.shift
563 564 attr = args.shift
564 565 text = obj.send(attr).to_s
565 566 else
566 567 raise ArgumentError, 'invalid arguments to textilizable'
567 568 end
568 569 return '' if text.blank?
569 570 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
570 571 @only_path = only_path = options.delete(:only_path) == false ? false : true
571 572
572 573 text = text.dup
573 574 macros = catch_macros(text)
574 575 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
575 576
576 577 @parsed_headings = []
577 578 @heading_anchors = {}
578 579 @current_section = 0 if options[:edit_section_links]
579 580
580 581 parse_sections(text, project, obj, attr, only_path, options)
581 582 text = parse_non_pre_blocks(text, obj, macros) do |text|
582 583 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
583 584 send method_name, text, project, obj, attr, only_path, options
584 585 end
585 586 end
586 587 parse_headings(text, project, obj, attr, only_path, options)
587 588
588 589 if @parsed_headings.any?
589 590 replace_toc(text, @parsed_headings)
590 591 end
591 592
592 593 text.html_safe
593 594 end
594 595
595 596 def parse_non_pre_blocks(text, obj, macros)
596 597 s = StringScanner.new(text)
597 598 tags = []
598 599 parsed = ''
599 600 while !s.eos?
600 601 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
601 602 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
602 603 if tags.empty?
603 604 yield text
604 605 inject_macros(text, obj, macros) if macros.any?
605 606 else
606 607 inject_macros(text, obj, macros, false) if macros.any?
607 608 end
608 609 parsed << text
609 610 if tag
610 611 if closing
611 612 if tags.last == tag.downcase
612 613 tags.pop
613 614 end
614 615 else
615 616 tags << tag.downcase
616 617 end
617 618 parsed << full_tag
618 619 end
619 620 end
620 621 # Close any non closing tags
621 622 while tag = tags.pop
622 623 parsed << "</#{tag}>"
623 624 end
624 625 parsed
625 626 end
626 627
627 628 def parse_inline_attachments(text, project, obj, attr, only_path, options)
628 629 return if options[:inline_attachments] == false
629 630
630 631 # when using an image link, try to use an attachment, if possible
631 632 attachments = options[:attachments] || []
632 633 attachments += obj.attachments if obj.respond_to?(:attachments)
633 634 if attachments.present?
634 635 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
635 636 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
636 637 # search for the picture in attachments
637 638 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
638 639 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
639 640 desc = found.description.to_s.gsub('"', '')
640 641 if !desc.blank? && alttext.blank?
641 642 alt = " title=\"#{desc}\" alt=\"#{desc}\""
642 643 end
643 644 "src=\"#{image_url}\"#{alt}"
644 645 else
645 646 m
646 647 end
647 648 end
648 649 end
649 650 end
650 651
651 652 # Wiki links
652 653 #
653 654 # Examples:
654 655 # [[mypage]]
655 656 # [[mypage|mytext]]
656 657 # wiki links can refer other project wikis, using project name or identifier:
657 658 # [[project:]] -> wiki starting page
658 659 # [[project:|mytext]]
659 660 # [[project:mypage]]
660 661 # [[project:mypage|mytext]]
661 662 def parse_wiki_links(text, project, obj, attr, only_path, options)
662 663 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
663 664 link_project = project
664 665 esc, all, page, title = $1, $2, $3, $5
665 666 if esc.nil?
666 667 if page =~ /^([^\:]+)\:(.*)$/
667 668 identifier, page = $1, $2
668 669 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
669 670 title ||= identifier if page.blank?
670 671 end
671 672
672 673 if link_project && link_project.wiki
673 674 # extract anchor
674 675 anchor = nil
675 676 if page =~ /^(.+?)\#(.+)$/
676 677 page, anchor = $1, $2
677 678 end
678 679 anchor = sanitize_anchor_name(anchor) if anchor.present?
679 680 # check if page exists
680 681 wiki_page = link_project.wiki.find_page(page)
681 682 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
682 683 "##{anchor}"
683 684 else
684 685 case options[:wiki_links]
685 686 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
686 687 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
687 688 else
688 689 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
689 690 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
690 691 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
691 692 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
692 693 end
693 694 end
694 695 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
695 696 else
696 697 # project or wiki doesn't exist
697 698 all
698 699 end
699 700 else
700 701 all
701 702 end
702 703 end
703 704 end
704 705
705 706 # Redmine links
706 707 #
707 708 # Examples:
708 709 # Issues:
709 710 # #52 -> Link to issue #52
710 711 # Changesets:
711 712 # r52 -> Link to revision 52
712 713 # commit:a85130f -> Link to scmid starting with a85130f
713 714 # Documents:
714 715 # document#17 -> Link to document with id 17
715 716 # document:Greetings -> Link to the document with title "Greetings"
716 717 # document:"Some document" -> Link to the document with title "Some document"
717 718 # Versions:
718 719 # version#3 -> Link to version with id 3
719 720 # version:1.0.0 -> Link to version named "1.0.0"
720 721 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
721 722 # Attachments:
722 723 # attachment:file.zip -> Link to the attachment of the current object named file.zip
723 724 # Source files:
724 725 # source:some/file -> Link to the file located at /some/file in the project's repository
725 726 # source:some/file@52 -> Link to the file's revision 52
726 727 # source:some/file#L120 -> Link to line 120 of the file
727 728 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
728 729 # export:some/file -> Force the download of the file
729 730 # Forum messages:
730 731 # message#1218 -> Link to message with id 1218
731 732 # Projects:
732 733 # project:someproject -> Link to project named "someproject"
733 734 # project#3 -> Link to project with id 3
734 735 #
735 736 # Links can refer other objects from other projects, using project identifier:
736 737 # identifier:r52
737 738 # identifier:document:"Some document"
738 739 # identifier:version:1.0.0
739 740 # identifier:source:some/file
740 741 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
741 742 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
742 743 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
743 744 if tag_content
744 745 $&
745 746 else
746 747 link = nil
747 748 project = default_project
748 749 if project_identifier
749 750 project = Project.visible.find_by_identifier(project_identifier)
750 751 end
751 752 if esc.nil?
752 753 if prefix.nil? && sep == 'r'
753 754 if project
754 755 repository = nil
755 756 if repo_identifier
756 757 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
757 758 else
758 759 repository = project.repository
759 760 end
760 761 # project.changesets.visible raises an SQL error because of a double join on repositories
761 762 if repository &&
762 763 (changeset = Changeset.visible.
763 764 find_by_repository_id_and_revision(repository.id, identifier))
764 765 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
765 766 {:only_path => only_path, :controller => 'repositories',
766 767 :action => 'revision', :id => project,
767 768 :repository_id => repository.identifier_param,
768 769 :rev => changeset.revision},
769 770 :class => 'changeset',
770 771 :title => truncate_single_line_raw(changeset.comments, 100))
771 772 end
772 773 end
773 774 elsif sep == '#'
774 775 oid = identifier.to_i
775 776 case prefix
776 777 when nil
777 778 if oid.to_s == identifier &&
778 779 issue = Issue.visible.find_by_id(oid)
779 780 anchor = comment_id ? "note-#{comment_id}" : nil
780 781 link = link_to("##{oid}#{comment_suffix}",
781 782 issue_url(issue, :only_path => only_path, :anchor => anchor),
782 783 :class => issue.css_classes,
783 784 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
784 785 end
785 786 when 'document'
786 787 if document = Document.visible.find_by_id(oid)
787 788 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
788 789 end
789 790 when 'version'
790 791 if version = Version.visible.find_by_id(oid)
791 792 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
792 793 end
793 794 when 'message'
794 795 if message = Message.visible.find_by_id(oid)
795 796 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
796 797 end
797 798 when 'forum'
798 799 if board = Board.visible.find_by_id(oid)
799 800 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
800 801 end
801 802 when 'news'
802 803 if news = News.visible.find_by_id(oid)
803 804 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
804 805 end
805 806 when 'project'
806 807 if p = Project.visible.find_by_id(oid)
807 808 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
808 809 end
809 810 end
810 811 elsif sep == ':'
811 812 # removes the double quotes if any
812 813 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
813 814 name = CGI.unescapeHTML(name)
814 815 case prefix
815 816 when 'document'
816 817 if project && document = project.documents.visible.find_by_title(name)
817 818 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
818 819 end
819 820 when 'version'
820 821 if project && version = project.versions.visible.find_by_name(name)
821 822 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
822 823 end
823 824 when 'forum'
824 825 if project && board = project.boards.visible.find_by_name(name)
825 826 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
826 827 end
827 828 when 'news'
828 829 if project && news = project.news.visible.find_by_title(name)
829 830 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
830 831 end
831 832 when 'commit', 'source', 'export'
832 833 if project
833 834 repository = nil
834 835 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
835 836 repo_prefix, repo_identifier, name = $1, $2, $3
836 837 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
837 838 else
838 839 repository = project.repository
839 840 end
840 841 if prefix == 'commit'
841 842 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
842 843 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
843 844 :class => 'changeset',
844 845 :title => truncate_single_line_raw(changeset.comments, 100)
845 846 end
846 847 else
847 848 if repository && User.current.allowed_to?(:browse_repository, project)
848 849 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
849 850 path, rev, anchor = $1, $3, $5
850 851 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
851 852 :path => to_path_param(path),
852 853 :rev => rev,
853 854 :anchor => anchor},
854 855 :class => (prefix == 'export' ? 'source download' : 'source')
855 856 end
856 857 end
857 858 repo_prefix = nil
858 859 end
859 860 when 'attachment'
860 861 attachments = options[:attachments] || []
861 862 attachments += obj.attachments if obj.respond_to?(:attachments)
862 863 if attachments && attachment = Attachment.latest_attach(attachments, name)
863 864 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
864 865 end
865 866 when 'project'
866 867 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
867 868 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
868 869 end
869 870 end
870 871 end
871 872 end
872 873 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
873 874 end
874 875 end
875 876 end
876 877
877 878 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
878 879
879 880 def parse_sections(text, project, obj, attr, only_path, options)
880 881 return unless options[:edit_section_links]
881 882 text.gsub!(HEADING_RE) do
882 883 heading = $1
883 884 @current_section += 1
884 885 if @current_section > 1
885 886 content_tag('div',
886 887 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
887 888 :class => 'contextual',
888 889 :title => l(:button_edit_section),
889 890 :id => "section-#{@current_section}") + heading.html_safe
890 891 else
891 892 heading
892 893 end
893 894 end
894 895 end
895 896
896 897 # Headings and TOC
897 898 # Adds ids and links to headings unless options[:headings] is set to false
898 899 def parse_headings(text, project, obj, attr, only_path, options)
899 900 return if options[:headings] == false
900 901
901 902 text.gsub!(HEADING_RE) do
902 903 level, attrs, content = $2.to_i, $3, $4
903 904 item = strip_tags(content).strip
904 905 anchor = sanitize_anchor_name(item)
905 906 # used for single-file wiki export
906 907 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
907 908 @heading_anchors[anchor] ||= 0
908 909 idx = (@heading_anchors[anchor] += 1)
909 910 if idx > 1
910 911 anchor = "#{anchor}-#{idx}"
911 912 end
912 913 @parsed_headings << [level, anchor, item]
913 914 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
914 915 end
915 916 end
916 917
917 918 MACROS_RE = /(
918 919 (!)? # escaping
919 920 (
920 921 \{\{ # opening tag
921 922 ([\w]+) # macro name
922 923 (\(([^\n\r]*?)\))? # optional arguments
923 924 ([\n\r].*?[\n\r])? # optional block of text
924 925 \}\} # closing tag
925 926 )
926 927 )/mx unless const_defined?(:MACROS_RE)
927 928
928 929 MACRO_SUB_RE = /(
929 930 \{\{
930 931 macro\((\d+)\)
931 932 \}\}
932 933 )/x unless const_defined?(:MACRO_SUB_RE)
933 934
934 935 # Extracts macros from text
935 936 def catch_macros(text)
936 937 macros = {}
937 938 text.gsub!(MACROS_RE) do
938 939 all, macro = $1, $4.downcase
939 940 if macro_exists?(macro) || all =~ MACRO_SUB_RE
940 941 index = macros.size
941 942 macros[index] = all
942 943 "{{macro(#{index})}}"
943 944 else
944 945 all
945 946 end
946 947 end
947 948 macros
948 949 end
949 950
950 951 # Executes and replaces macros in text
951 952 def inject_macros(text, obj, macros, execute=true)
952 953 text.gsub!(MACRO_SUB_RE) do
953 954 all, index = $1, $2.to_i
954 955 orig = macros.delete(index)
955 956 if execute && orig && orig =~ MACROS_RE
956 957 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
957 958 if esc.nil?
958 959 h(exec_macro(macro, obj, args, block) || all)
959 960 else
960 961 h(all)
961 962 end
962 963 elsif orig
963 964 h(orig)
964 965 else
965 966 h(all)
966 967 end
967 968 end
968 969 end
969 970
970 971 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
971 972
972 973 # Renders the TOC with given headings
973 974 def replace_toc(text, headings)
974 975 text.gsub!(TOC_RE) do
975 976 left_align, right_align = $2, $3
976 977 # Keep only the 4 first levels
977 978 headings = headings.select{|level, anchor, item| level <= 4}
978 979 if headings.empty?
979 980 ''
980 981 else
981 982 div_class = 'toc'
982 983 div_class << ' right' if right_align
983 984 div_class << ' left' if left_align
984 985 out = "<ul class=\"#{div_class}\"><li>"
985 986 root = headings.map(&:first).min
986 987 current = root
987 988 started = false
988 989 headings.each do |level, anchor, item|
989 990 if level > current
990 991 out << '<ul><li>' * (level - current)
991 992 elsif level < current
992 993 out << "</li></ul>\n" * (current - level) + "</li><li>"
993 994 elsif started
994 995 out << '</li><li>'
995 996 end
996 997 out << "<a href=\"##{anchor}\">#{item}</a>"
997 998 current = level
998 999 started = true
999 1000 end
1000 1001 out << '</li></ul>' * (current - root)
1001 1002 out << '</li></ul>'
1002 1003 end
1003 1004 end
1004 1005 end
1005 1006
1006 1007 # Same as Rails' simple_format helper without using paragraphs
1007 1008 def simple_format_without_paragraph(text)
1008 1009 text.to_s.
1009 1010 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1010 1011 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1011 1012 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1012 1013 html_safe
1013 1014 end
1014 1015
1015 1016 def lang_options_for_select(blank=true)
1016 1017 (blank ? [["(auto)", ""]] : []) + languages_options
1017 1018 end
1018 1019
1019 1020 def labelled_form_for(*args, &proc)
1020 1021 args << {} unless args.last.is_a?(Hash)
1021 1022 options = args.last
1022 1023 if args.first.is_a?(Symbol)
1023 1024 options.merge!(:as => args.shift)
1024 1025 end
1025 1026 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1026 1027 form_for(*args, &proc)
1027 1028 end
1028 1029
1029 1030 def labelled_fields_for(*args, &proc)
1030 1031 args << {} unless args.last.is_a?(Hash)
1031 1032 options = args.last
1032 1033 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1033 1034 fields_for(*args, &proc)
1034 1035 end
1035 1036
1036 1037 def error_messages_for(*objects)
1037 1038 html = ""
1038 1039 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1039 1040 errors = objects.map {|o| o.errors.full_messages}.flatten
1040 1041 if errors.any?
1041 1042 html << "<div id='errorExplanation'><ul>\n"
1042 1043 errors.each do |error|
1043 1044 html << "<li>#{h error}</li>\n"
1044 1045 end
1045 1046 html << "</ul></div>\n"
1046 1047 end
1047 1048 html.html_safe
1048 1049 end
1049 1050
1050 1051 def delete_link(url, options={})
1051 1052 options = {
1052 1053 :method => :delete,
1053 1054 :data => {:confirm => l(:text_are_you_sure)},
1054 1055 :class => 'icon icon-del'
1055 1056 }.merge(options)
1056 1057
1057 1058 link_to l(:button_delete), url, options
1058 1059 end
1059 1060
1060 1061 def preview_link(url, form, target='preview', options={})
1061 1062 content_tag 'a', l(:label_preview), {
1062 1063 :href => "#",
1063 1064 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1064 1065 :accesskey => accesskey(:preview)
1065 1066 }.merge(options)
1066 1067 end
1067 1068
1068 1069 def link_to_function(name, function, html_options={})
1069 1070 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1070 1071 end
1071 1072
1072 1073 # Helper to render JSON in views
1073 1074 def raw_json(arg)
1074 1075 arg.to_json.to_s.gsub('/', '\/').html_safe
1075 1076 end
1076 1077
1077 1078 def back_url
1078 1079 url = params[:back_url]
1079 1080 if url.nil? && referer = request.env['HTTP_REFERER']
1080 1081 url = CGI.unescape(referer.to_s)
1081 1082 end
1082 1083 url
1083 1084 end
1084 1085
1085 1086 def back_url_hidden_field_tag
1086 1087 url = back_url
1087 1088 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1088 1089 end
1089 1090
1090 1091 def check_all_links(form_name)
1091 1092 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1092 1093 " | ".html_safe +
1093 1094 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1094 1095 end
1095 1096
1096 1097 def toggle_checkboxes_link(selector)
1097 1098 link_to_function image_tag('toggle_check.png'),
1098 1099 "toggleCheckboxesBySelector('#{selector}')",
1099 1100 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1100 1101 end
1101 1102
1102 1103 def progress_bar(pcts, options={})
1103 1104 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1104 1105 pcts = pcts.collect(&:round)
1105 1106 pcts[1] = pcts[1] - pcts[0]
1106 1107 pcts << (100 - pcts[1] - pcts[0])
1107 1108 width = options[:width] || '100px;'
1108 1109 legend = options[:legend] || ''
1109 1110 content_tag('table',
1110 1111 content_tag('tr',
1111 1112 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1112 1113 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1113 1114 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1114 1115 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1115 1116 content_tag('p', legend, :class => 'percent').html_safe
1116 1117 end
1117 1118
1118 1119 def checked_image(checked=true)
1119 1120 if checked
1120 1121 @checked_image_tag ||= image_tag('toggle_check.png')
1121 1122 end
1122 1123 end
1123 1124
1124 1125 def context_menu(url)
1125 1126 unless @context_menu_included
1126 1127 content_for :header_tags do
1127 1128 javascript_include_tag('context_menu') +
1128 1129 stylesheet_link_tag('context_menu')
1129 1130 end
1130 1131 if l(:direction) == 'rtl'
1131 1132 content_for :header_tags do
1132 1133 stylesheet_link_tag('context_menu_rtl')
1133 1134 end
1134 1135 end
1135 1136 @context_menu_included = true
1136 1137 end
1137 1138 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1138 1139 end
1139 1140
1140 1141 def calendar_for(field_id)
1141 1142 include_calendar_headers_tags
1142 1143 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1143 1144 end
1144 1145
1145 1146 def include_calendar_headers_tags
1146 1147 unless @calendar_headers_tags_included
1147 1148 tags = ''.html_safe
1148 1149 @calendar_headers_tags_included = true
1149 1150 content_for :header_tags do
1150 1151 start_of_week = Setting.start_of_week
1151 1152 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1152 1153 # Redmine uses 1..7 (monday..sunday) in settings and locales
1153 1154 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1154 1155 start_of_week = start_of_week.to_i % 7
1155 1156 tags << javascript_tag(
1156 1157 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1157 1158 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1158 1159 path_to_image('/images/calendar.png') +
1159 1160 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1160 1161 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1161 1162 "beforeShow: beforeShowDatePicker};")
1162 1163 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1163 1164 unless jquery_locale == 'en'
1164 1165 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1165 1166 end
1166 1167 tags
1167 1168 end
1168 1169 end
1169 1170 end
1170 1171
1171 1172 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1172 1173 # Examples:
1173 1174 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1174 1175 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1175 1176 #
1176 1177 def stylesheet_link_tag(*sources)
1177 1178 options = sources.last.is_a?(Hash) ? sources.pop : {}
1178 1179 plugin = options.delete(:plugin)
1179 1180 sources = sources.map do |source|
1180 1181 if plugin
1181 1182 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1182 1183 elsif current_theme && current_theme.stylesheets.include?(source)
1183 1184 current_theme.stylesheet_path(source)
1184 1185 else
1185 1186 source
1186 1187 end
1187 1188 end
1188 1189 super *sources, options
1189 1190 end
1190 1191
1191 1192 # Overrides Rails' image_tag with themes and plugins support.
1192 1193 # Examples:
1193 1194 # image_tag('image.png') # => picks image.png from the current theme or defaults
1194 1195 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1195 1196 #
1196 1197 def image_tag(source, options={})
1197 1198 if plugin = options.delete(:plugin)
1198 1199 source = "/plugin_assets/#{plugin}/images/#{source}"
1199 1200 elsif current_theme && current_theme.images.include?(source)
1200 1201 source = current_theme.image_path(source)
1201 1202 end
1202 1203 super source, options
1203 1204 end
1204 1205
1205 1206 # Overrides Rails' javascript_include_tag with plugins support
1206 1207 # Examples:
1207 1208 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1208 1209 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1209 1210 #
1210 1211 def javascript_include_tag(*sources)
1211 1212 options = sources.last.is_a?(Hash) ? sources.pop : {}
1212 1213 if plugin = options.delete(:plugin)
1213 1214 sources = sources.map do |source|
1214 1215 if plugin
1215 1216 "/plugin_assets/#{plugin}/javascripts/#{source}"
1216 1217 else
1217 1218 source
1218 1219 end
1219 1220 end
1220 1221 end
1221 1222 super *sources, options
1222 1223 end
1223 1224
1224 1225 def sidebar_content?
1225 1226 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1226 1227 end
1227 1228
1228 1229 def view_layouts_base_sidebar_hook_response
1229 1230 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1230 1231 end
1231 1232
1232 1233 def email_delivery_enabled?
1233 1234 !!ActionMailer::Base.perform_deliveries
1234 1235 end
1235 1236
1236 1237 # Returns the avatar image tag for the given +user+ if avatars are enabled
1237 1238 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1238 1239 def avatar(user, options = { })
1239 1240 if Setting.gravatar_enabled?
1240 1241 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1241 1242 email = nil
1242 1243 if user.respond_to?(:mail)
1243 1244 email = user.mail
1244 1245 elsif user.to_s =~ %r{<(.+?)>}
1245 1246 email = $1
1246 1247 end
1247 1248 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1248 1249 else
1249 1250 ''
1250 1251 end
1251 1252 end
1252 1253
1253 1254 def sanitize_anchor_name(anchor)
1254 1255 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1255 1256 end
1256 1257
1257 1258 # Returns the javascript tags that are included in the html layout head
1258 1259 def javascript_heads
1259 1260 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1260 1261 unless User.current.pref.warn_on_leaving_unsaved == '0'
1261 1262 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1262 1263 end
1263 1264 tags
1264 1265 end
1265 1266
1266 1267 def favicon
1267 1268 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1268 1269 end
1269 1270
1270 1271 # Returns the path to the favicon
1271 1272 def favicon_path
1272 1273 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1273 1274 image_path(icon)
1274 1275 end
1275 1276
1276 1277 # Returns the full URL to the favicon
1277 1278 def favicon_url
1278 1279 # TODO: use #image_url introduced in Rails4
1279 1280 path = favicon_path
1280 1281 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1281 1282 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1282 1283 end
1283 1284
1284 1285 def robot_exclusion_tag
1285 1286 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1286 1287 end
1287 1288
1288 1289 # Returns true if arg is expected in the API response
1289 1290 def include_in_api_response?(arg)
1290 1291 unless @included_in_api_response
1291 1292 param = params[:include]
1292 1293 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1293 1294 @included_in_api_response.collect!(&:strip)
1294 1295 end
1295 1296 @included_in_api_response.include?(arg.to_s)
1296 1297 end
1297 1298
1298 1299 # Returns options or nil if nometa param or X-Redmine-Nometa header
1299 1300 # was set in the request
1300 1301 def api_meta(options)
1301 1302 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1302 1303 # compatibility mode for activeresource clients that raise
1303 1304 # an error when deserializing an array with attributes
1304 1305 nil
1305 1306 else
1306 1307 options
1307 1308 end
1308 1309 end
1309 1310
1310 1311 def generate_csv(&block)
1311 1312 decimal_separator = l(:general_csv_decimal_separator)
1312 1313 encoding = l(:general_csv_encoding)
1313 1314 end
1314 1315
1315 1316 private
1316 1317
1317 1318 def wiki_helper
1318 1319 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1319 1320 extend helper
1320 1321 return self
1321 1322 end
1322 1323
1323 1324 def link_to_content_update(text, url_params = {}, html_options = {})
1324 1325 link_to(text, url_params, html_options)
1325 1326 end
1326 1327 end
@@ -1,36 +1,36
1 1 <h3><%=l(:label_my_account)%></h3>
2 2
3 3 <p><%=l(:field_login)%>: <strong><%= link_to_user(@user, :format => :username) %></strong><br />
4 4 <%=l(:field_created_on)%>: <%= format_time(@user.created_on) %></p>
5 5
6 6 <% if @user.own_account_deletable? %>
7 7 <p><%= link_to(l(:button_delete_my_account), {:action => 'destroy'}, :class => 'icon icon-del') %></p>
8 8 <% end %>
9 9
10 10 <h4><%= l(:label_feeds_access_key) %></h4>
11 11
12 12 <p>
13 13 <% if @user.rss_token %>
14 14 <%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %>
15 15 <% else %>
16 16 <%= l(:label_missing_feeds_access_key) %>
17 17 <% end %>
18 18 (<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)
19 19 </p>
20 20
21 21 <% if Setting.rest_api_enabled? %>
22 22 <h4><%= l(:label_api_access_key) %></h4>
23 23 <div>
24 <%= link_to_function(l(:button_show), "$('#api-access-key').toggle();")%>
25 <pre id='api-access-key' class='autoscroll'><%= @user.api_key %></pre>
24 <%= link_to l(:button_show), {:action => 'show_api_key'}, :remote => true %>
25 <pre id='api-access-key' class='autoscroll'></pre>
26 26 </div>
27 27 <%= javascript_tag("$('#api-access-key').hide();") %>
28 28 <p>
29 29 <% if @user.api_token %>
30 30 <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
31 31 <% else %>
32 32 <%= l(:label_missing_api_access_key) %>
33 33 <% end %>
34 34 (<%= link_to l(:button_reset), {:action => 'reset_api_key'}, :method => :post %>)
35 35 </p>
36 36 <% end %>
@@ -1,1164 +1,1166
1 1 # German translations for Ruby on Rails
2 2 # by Clemens Kofler (clemens@railway.at)
3 3 # additions for Redmine 1.2 by Jens Martsch (jmartsch@gmail.com)
4 4
5 5 de:
6 6 direction: ltr
7 7 date:
8 8 formats:
9 9 # Use the strftime parameters for formats.
10 10 # When no format has been given, it uses default.
11 11 # You can provide other formats here if you like!
12 12 default: "%d.%m.%Y"
13 13 short: "%e. %b"
14 14 long: "%e. %B %Y"
15 15
16 16 day_names: [Sonntag, Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag]
17 17 abbr_day_names: [So, Mo, Di, Mi, Do, Fr, Sa]
18 18
19 19 # Don't forget the nil at the beginning; there's no such thing as a 0th month
20 20 month_names: [~, Januar, Februar, März, April, Mai, Juni, Juli, August, September, Oktober, November, Dezember]
21 21 abbr_month_names: [~, Jan, Feb, Mär, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez]
22 22 # Used in date_select and datime_select.
23 23 order:
24 24 - :day
25 25 - :month
26 26 - :year
27 27
28 28 time:
29 29 formats:
30 30 default: "%d.%m.%Y %H:%M"
31 31 time: "%H:%M"
32 32 short: "%e. %b %H:%M"
33 33 long: "%A, %e. %B %Y, %H:%M Uhr"
34 34 am: "vormittags"
35 35 pm: "nachmittags"
36 36
37 37 datetime:
38 38 distance_in_words:
39 39 half_a_minute: 'eine halbe Minute'
40 40 less_than_x_seconds:
41 41 one: 'weniger als 1 Sekunde'
42 42 other: 'weniger als %{count} Sekunden'
43 43 x_seconds:
44 44 one: '1 Sekunde'
45 45 other: '%{count} Sekunden'
46 46 less_than_x_minutes:
47 47 one: 'weniger als 1 Minute'
48 48 other: 'weniger als %{count} Minuten'
49 49 x_minutes:
50 50 one: '1 Minute'
51 51 other: '%{count} Minuten'
52 52 about_x_hours:
53 53 one: 'etwa 1 Stunde'
54 54 other: 'etwa %{count} Stunden'
55 55 x_hours:
56 56 one: "1 Stunde"
57 57 other: "%{count} Stunden"
58 58 x_days:
59 59 one: '1 Tag'
60 60 other: '%{count} Tagen'
61 61 about_x_months:
62 62 one: 'etwa 1 Monat'
63 63 other: 'etwa %{count} Monaten'
64 64 x_months:
65 65 one: '1 Monat'
66 66 other: '%{count} Monaten'
67 67 about_x_years:
68 68 one: 'etwa 1 Jahr'
69 69 other: 'etwa %{count} Jahren'
70 70 over_x_years:
71 71 one: 'mehr als 1 Jahr'
72 72 other: 'mehr als %{count} Jahren'
73 73 almost_x_years:
74 74 one: "fast 1 Jahr"
75 75 other: "fast %{count} Jahren"
76 76
77 77 number:
78 78 # Default format for numbers
79 79 format:
80 80 separator: ','
81 81 delimiter: '.'
82 82 precision: 2
83 83 currency:
84 84 format:
85 85 unit: '€'
86 86 format: '%n %u'
87 87 delimiter: ''
88 88 percentage:
89 89 format:
90 90 delimiter: ""
91 91 precision:
92 92 format:
93 93 delimiter: ""
94 94 human:
95 95 format:
96 96 delimiter: ""
97 97 precision: 3
98 98 storage_units:
99 99 format: "%n %u"
100 100 units:
101 101 byte:
102 102 one: "Byte"
103 103 other: "Bytes"
104 104 kb: "KB"
105 105 mb: "MB"
106 106 gb: "GB"
107 107 tb: "TB"
108 108
109 109 # Used in array.to_sentence.
110 110 support:
111 111 array:
112 112 sentence_connector: "und"
113 113 skip_last_comma: true
114 114
115 115 activerecord:
116 116 errors:
117 117 template:
118 118 header:
119 119 one: "Dieses %{model}-Objekt konnte nicht gespeichert werden: %{count} Fehler."
120 120 other: "Dieses %{model}-Objekt konnte nicht gespeichert werden: %{count} Fehler."
121 121 body: "Bitte überprüfen Sie die folgenden Felder:"
122 122
123 123 messages:
124 124 inclusion: "ist kein gültiger Wert"
125 125 exclusion: "ist nicht verfügbar"
126 126 invalid: "ist nicht gültig"
127 127 confirmation: "stimmt nicht mit der Bestätigung überein"
128 128 accepted: "muss akzeptiert werden"
129 129 empty: "muss ausgefüllt werden"
130 130 blank: "muss ausgefüllt werden"
131 131 too_long: "ist zu lang (nicht mehr als %{count} Zeichen)"
132 132 too_short: "ist zu kurz (nicht weniger als %{count} Zeichen)"
133 133 wrong_length: "hat die falsche Länge (muss genau %{count} Zeichen haben)"
134 134 taken: "ist bereits vergeben"
135 135 not_a_number: "ist keine Zahl"
136 136 not_a_date: "ist kein gültiges Datum"
137 137 greater_than: "muss größer als %{count} sein"
138 138 greater_than_or_equal_to: "muss größer oder gleich %{count} sein"
139 139 equal_to: "muss genau %{count} sein"
140 140 less_than: "muss kleiner als %{count} sein"
141 141 less_than_or_equal_to: "muss kleiner oder gleich %{count} sein"
142 142 odd: "muss ungerade sein"
143 143 even: "muss gerade sein"
144 144 greater_than_start_date: "muss größer als Anfangsdatum sein"
145 145 not_same_project: "gehört nicht zum selben Projekt"
146 146 circular_dependency: "Diese Beziehung würde eine zyklische Abhängigkeit erzeugen"
147 147 cant_link_an_issue_with_a_descendant: "Ein Ticket kann nicht mit einer Ihrer Unteraufgaben verlinkt werden"
148 148 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
149 149
150 150 actionview_instancetag_blank_option: Bitte auswählen
151 151
152 152 button_activate: Aktivieren
153 153 button_add: Hinzufügen
154 154 button_annotate: Annotieren
155 155 button_apply: Anwenden
156 156 button_archive: Archivieren
157 157 button_back: Zurück
158 158 button_cancel: Abbrechen
159 159 button_change: Wechseln
160 160 button_change_password: Kennwort ändern
161 161 button_check_all: Alles auswählen
162 162 button_clear: Zurücksetzen
163 163 button_close: Schließen
164 164 button_collapse_all: Alle einklappen
165 165 button_configure: Konfigurieren
166 button_confirm_password: Kennwort bestätigen
166 167 button_copy: Kopieren
167 168 button_copy_and_follow: Kopieren und Ticket anzeigen
168 169 button_create: Anlegen
169 170 button_create_and_continue: Anlegen und weiter
170 171 button_delete: Löschen
171 172 button_delete_my_account: Mein Benutzerkonto löschen
172 173 button_download: Download
173 174 button_duplicate: Duplizieren
174 175 button_edit: Bearbeiten
175 176 button_edit_associated_wikipage: "Zugehörige Wikiseite bearbeiten: %{page_title}"
176 177 button_edit_section: Diesen Bereich bearbeiten
177 178 button_expand_all: Alle ausklappen
178 179 button_export: Exportieren
179 180 button_hide: Verstecken
180 181 button_list: Liste
181 182 button_lock: Sperren
182 183 button_log_time: Aufwand buchen
183 184 button_login: Anmelden
184 185 button_move: Verschieben
185 186 button_move_and_follow: Verschieben und Ticket anzeigen
186 187 button_quote: Zitieren
187 188 button_rename: Umbenennen
188 189 button_reopen: Öffnen
189 190 button_reply: Antworten
190 191 button_reset: Zurücksetzen
191 192 button_rollback: Auf diese Version zurücksetzen
192 193 button_save: Speichern
193 194 button_show: Anzeigen
194 195 button_sort: Sortieren
195 196 button_submit: OK
196 197 button_test: Testen
197 198 button_unarchive: Entarchivieren
198 199 button_uncheck_all: Alles abwählen
199 200 button_unlock: Entsperren
200 201 button_unwatch: Nicht beobachten
201 202 button_update: Aktualisieren
202 203 button_view: Anzeigen
203 204 button_watch: Beobachten
204 205
205 206 default_activity_design: Design
206 207 default_activity_development: Entwicklung
207 208 default_doc_category_tech: Technische Dokumentation
208 209 default_doc_category_user: Benutzerdokumentation
209 210 default_issue_status_closed: Erledigt
210 211 default_issue_status_feedback: Feedback
211 212 default_issue_status_in_progress: In Bearbeitung
212 213 default_issue_status_new: Neu
213 214 default_issue_status_rejected: Abgewiesen
214 215 default_issue_status_resolved: Gelöst
215 216 default_priority_high: Hoch
216 217 default_priority_immediate: Sofort
217 218 default_priority_low: Niedrig
218 219 default_priority_normal: Normal
219 220 default_priority_urgent: Dringend
220 221 default_role_developer: Entwickler
221 222 default_role_manager: Manager
222 223 default_role_reporter: Reporter
223 224 default_tracker_bug: Fehler
224 225 default_tracker_feature: Feature
225 226 default_tracker_support: Unterstützung
226 227
227 228 description_all_columns: Alle Spalten
228 229 description_available_columns: Verfügbare Spalten
229 230 description_choose_project: Projekte
230 231 description_date_from: Startdatum eintragen
231 232 description_date_range_interval: Zeitraum durch Start- und Enddatum festlegen
232 233 description_date_range_list: Zeitraum aus einer Liste wählen
233 234 description_date_to: Enddatum eintragen
234 235 description_filter: Filter
235 236 description_issue_category_reassign: Neue Kategorie wählen
236 237 description_message_content: Nachrichteninhalt
237 238 description_notes: Kommentare
238 239 description_project_scope: Suchbereich
239 240 description_query_sort_criteria_attribute: Sortierattribut
240 241 description_query_sort_criteria_direction: Sortierrichtung
241 242 description_search: Suchfeld
242 243 description_selected_columns: Ausgewählte Spalten
243 244
244 245 description_user_mail_notification: Mailbenachrichtigungseinstellung
245 246 description_wiki_subpages_reassign: Neue Elternseite wählen
246 247
247 248 enumeration_activities: Aktivitäten (Zeiterfassung)
248 249 enumeration_doc_categories: Dokumentenkategorien
249 250 enumeration_issue_priorities: Ticket-Prioritäten
250 251 enumeration_system_activity: System-Aktivität
251 252
252 253 error_attachment_too_big: Diese Datei kann nicht hochgeladen werden, da sie die maximale Dateigröße von (%{max_size}) überschreitet.
253 254 error_can_not_archive_project: Dieses Projekt kann nicht archiviert werden.
254 255 error_can_not_delete_custom_field: Kann das benutzerdefinierte Feld nicht löschen.
255 256 error_can_not_delete_tracker: Dieser Tracker enthält Tickets und kann nicht gelöscht werden.
256 257 error_can_not_remove_role: Diese Rolle wird verwendet und kann nicht gelöscht werden.
257 258 error_can_not_reopen_issue_on_closed_version: Das Ticket ist einer abgeschlossenen Version zugeordnet und kann daher nicht wieder geöffnet werden.
258 259 error_can_t_load_default_data: "Die Standard-Konfiguration konnte nicht geladen werden: %{value}"
259 260 error_issue_done_ratios_not_updated: Der Ticket-Fortschritt wurde nicht aktualisiert.
260 261 error_issue_not_found_in_project: 'Das Ticket wurde nicht gefunden oder gehört nicht zu diesem Projekt.'
261 262 error_no_default_issue_status: Es ist kein Status als Standard definiert. Bitte überprüfen Sie Ihre Konfiguration (unter "Administration -> Ticket-Status").
262 263 error_no_tracker_in_project: Diesem Projekt ist kein Tracker zugeordnet. Bitte überprüfen Sie die Projekteinstellungen.
263 264 error_scm_annotate: "Der Eintrag existiert nicht oder kann nicht annotiert werden."
264 265 error_scm_annotate_big_text_file: Der Eintrag kann nicht umgesetzt werden, da er die maximale Textlänge überschreitet.
265 266 error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %{value}"
266 267 error_scm_not_found: Eintrag und/oder Revision existiert nicht im Projektarchiv.
267 268 error_session_expired: Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.
268 269 error_unable_delete_issue_status: "Der Ticket-Status konnte nicht gelöscht werden."
269 270 error_unable_to_connect: Fehler beim Verbinden (%{value})
270 271 error_workflow_copy_source: Bitte wählen Sie einen Quell-Tracker und eine Quell-Rolle.
271 272 error_workflow_copy_target: Bitte wählen Sie die Ziel-Tracker und -Rollen.
272 273
273 274 field_account: Konto
274 275 field_active: Aktiv
275 276 field_activity: Aktivität
276 277 field_admin: Administrator
277 278 field_assignable: Tickets können dieser Rolle zugewiesen werden
278 279 field_assigned_to: Zugewiesen an
279 280 field_assigned_to_role: Zuständigkeitsrolle
280 281 field_attr_firstname: Vorname-Attribut
281 282 field_attr_lastname: Name-Attribut
282 283 field_attr_login: Mitgliedsname-Attribut
283 284 field_attr_mail: E-Mail-Attribut
284 285 field_auth_source: Authentifizierungs-Modus
285 286 field_auth_source_ldap_filter: LDAP-Filter
286 287 field_author: Autor
287 288 field_base_dn: Base DN
288 289 field_board_parent: Übergeordnetes Forum
289 290 field_category: Kategorie
290 291 field_column_names: Spalten
291 292 field_closed_on: Geschlossen am
292 293 field_comments: Kommentar
293 294 field_comments_sorting: Kommentare anzeigen
294 295 field_commit_logs_encoding: Kodierung der Commit-Log-Meldungen
295 296 field_content: Inhalt
296 297 field_core_fields: Standardwerte
297 298 field_created_on: Angelegt
298 299 field_cvs_module: Modul
299 300 field_cvsroot: CVSROOT
300 301 field_default_value: Standardwert
301 302 field_default_status: Standardstatus
302 303 field_delay: Pufferzeit
303 304 field_description: Beschreibung
304 305 field_done_ratio: "% erledigt"
305 306 field_downloads: Downloads
306 307 field_due_date: Abgabedatum
307 308 field_editable: Bearbeitbar
308 309 field_effective_date: Datum
309 310 field_estimated_hours: Geschätzter Aufwand
310 311 field_field_format: Format
311 312 field_filename: Datei
312 313 field_filesize: Größe
313 314 field_firstname: Vorname
314 315 field_fixed_version: Zielversion
315 316 field_generate_password: Passwort generieren
316 317 field_group_by: Gruppiere Ergebnisse nach
317 318 field_hide_mail: E-Mail-Adresse nicht anzeigen
318 319 field_homepage: Projekt-Homepage
319 320 field_host: Host
320 321 field_hours: Stunden
321 322 field_identifier: Kennung
322 323 field_identity_url: OpenID-URL
323 324 field_inherit_members: Benutzer erben
324 325 field_is_closed: Ticket geschlossen
325 326 field_is_default: Standardeinstellung
326 327 field_is_filter: Als Filter benutzen
327 328 field_is_for_all: Für alle Projekte
328 329 field_is_in_roadmap: In der Roadmap anzeigen
329 330 field_is_private: Privat
330 331 field_is_public: Öffentlich
331 332 field_is_required: Erforderlich
332 333 field_issue: Ticket
333 334 field_issue_to: Zugehöriges Ticket
334 335 field_issues_visibility: Ticket Sichtbarkeit
335 336 field_language: Sprache
336 337 field_last_login_on: Letzte Anmeldung
337 338 field_lastname: Nachname
338 339 field_login: Mitgliedsname
339 340 field_mail: E-Mail
340 341 field_mail_notification: Mailbenachrichtigung
341 342 field_max_length: Maximale Länge
342 343 field_member_of_group: Zuständigkeitsgruppe
343 344 field_min_length: Minimale Länge
344 345 field_multiple: Mehrere Werte
345 346 field_must_change_passwd: Passwort beim nächsten Login ändern
346 347 field_name: Name
347 348 field_new_password: Neues Kennwort
348 349 field_notes: Kommentare
349 350 field_onthefly: On-the-fly-Benutzererstellung
350 351 field_parent: Unterprojekt von
351 352 field_parent_issue: Übergeordnete Aufgabe
352 353 field_parent_title: Übergeordnete Seite
353 354 field_password: Kennwort
354 355 field_password_confirmation: Bestätigung
355 356 field_path_to_repository: Pfad zum Repository
356 357 field_port: Port
357 358 field_possible_values: Mögliche Werte
358 359 field_principal: Auftraggeber
359 360 field_priority: Priorität
360 361 field_private_notes: Privater Kommentar
361 362 field_project: Projekt
362 363 field_redirect_existing_links: Existierende Links umleiten
363 364 field_regexp: Regulärer Ausdruck
364 365 field_repository_is_default: Haupt-Repository
365 366 field_role: Rolle
366 367 field_root_directory: Wurzelverzeichnis
367 368 field_scm_path_encoding: Pfad-Kodierung
368 369 field_searchable: Durchsuchbar
369 370 field_sharing: Gemeinsame Verwendung
370 371 field_spent_on: Datum
371 372 field_start_date: Beginn
372 373 field_start_page: Hauptseite
373 374 field_status: Status
374 375 field_subject: Thema
375 376 field_subproject: Unterprojekt von
376 377 field_summary: Zusammenfassung
377 378 field_text: Textfeld
378 379 field_time_entries: Logzeit
379 380 field_time_zone: Zeitzone
380 381 field_timeout: Auszeit (in Sekunden)
381 382 field_title: Titel
382 383 field_tracker: Tracker
383 384 field_type: Typ
384 385 field_updated_on: Aktualisiert
385 386 field_url: URL
386 387 field_user: Benutzer
387 388 field_users_visibility: Benutzer Sichtbarkeit
388 389 field_value: Wert
389 390 field_version: Version
390 391 field_visible: Sichtbar
391 392 field_warn_on_leaving_unsaved: Vor dem Verlassen einer Seite mit ungesichertem Text im Editor warnen
392 393 field_watcher: Beobachter
393 394
394 395 general_csv_decimal_separator: ','
395 396 general_csv_encoding: ISO-8859-1
396 397 general_csv_separator: ';'
397 398 general_pdf_fontname: freesans
398 399 general_first_day_of_week: '1'
399 400 general_lang_name: 'German (Deutsch)'
400 401 general_text_No: 'Nein'
401 402 general_text_Yes: 'Ja'
402 403 general_text_no: 'nein'
403 404 general_text_yes: 'ja'
404 405
405 406 label_activity: Aktivität
406 407 label_add_another_file: Eine weitere Datei hinzufügen
407 408 label_add_note: Kommentar hinzufügen
408 409 label_add_projects: Projekt hinzufügen
409 410 label_added: hinzugefügt
410 411 label_added_time_by: "Von %{author} vor %{age} hinzugefügt"
411 412 label_additional_workflow_transitions_for_assignee: Zusätzliche Berechtigungen wenn der Benutzer der Zugewiesene ist
412 413 label_additional_workflow_transitions_for_author: Zusätzliche Berechtigungen wenn der Benutzer der Autor ist
413 414 label_administration: Administration
414 415 label_age: Geändert vor
415 416 label_ago: vor
416 417 label_all: alle
417 418 label_all_time: gesamter Zeitraum
418 419 label_all_words: Alle Wörter
419 420 label_and_its_subprojects: "%{value} und dessen Unterprojekte"
420 421 label_any: alle
421 422 label_any_issues_in_project: irgendein Ticket im Projekt
422 423 label_any_issues_not_in_project: irgendein Ticket nicht im Projekt
423 424 label_api_access_key: API-Zugriffsschlüssel
424 425 label_api_access_key_created_on: Der API-Zugriffsschlüssel wurde vor %{value} erstellt
425 426 label_applied_status: Zugewiesener Status
426 427 label_ascending: Aufsteigend
427 428 label_ask: Nachfragen
428 429 label_assigned_to_me_issues: Mir zugewiesene Tickets
429 430 label_associated_revisions: Zugehörige Revisionen
430 431 label_attachment: Datei
431 432 label_attachment_delete: Anhang löschen
432 433 label_attachment_new: Neue Datei
433 434 label_attachment_plural: Dateien
434 435 label_attribute: Attribut
435 436 label_attribute_of_assigned_to: "%{name} des Bearbeiters"
436 437 label_attribute_of_author: "%{name} des Autors"
437 438 label_attribute_of_fixed_version: "%{name} der Zielversion"
438 439 label_attribute_of_issue: "%{name} des Tickets"
439 440 label_attribute_of_project: "%{name} des Projekts"
440 441 label_attribute_of_user: "%{name} des Benutzers"
441 442 label_attribute_plural: Attribute
442 443 label_auth_source: Authentifizierungs-Modus
443 444 label_auth_source_new: Neuer Authentifizierungs-Modus
444 445 label_auth_source_plural: Authentifizierungs-Arten
445 446 label_authentication: Authentifizierung
446 447 label_between: zwischen
447 448 label_blocked_by: Blockiert durch
448 449 label_blocks: Blockiert
449 450 label_board: Forum
450 451 label_board_locked: Gesperrt
451 452 label_board_new: Neues Forum
452 453 label_board_plural: Foren
453 454 label_board_sticky: Wichtig (immer oben)
454 455 label_boolean: Boolean
455 456 label_branch: Zweig
456 457 label_browse: Codebrowser
457 458 label_bulk_edit_selected_issues: Alle ausgewählten Tickets bearbeiten
458 459 label_bulk_edit_selected_time_entries: Ausgewählte Zeitaufwände bearbeiten
459 460 label_calendar: Kalender
460 461 label_change_plural: Änderungen
461 462 label_change_properties: Eigenschaften ändern
462 463 label_change_status: Statuswechsel
463 464 label_change_view_all: Alle Änderungen anzeigen
464 465 label_changes_details: Details aller Änderungen
465 466 label_changeset_plural: Changesets
466 467 label_checkboxes: Checkboxen
467 468 label_check_for_updates: Auf Updates prüfen
468 469 label_child_revision: Nachfolger
469 470 label_chronological_order: in zeitlicher Reihenfolge
470 471 label_close_versions: Vollständige Versionen schließen
471 472 label_closed_issues: geschlossen
472 473 label_closed_issues_plural: geschlossen
473 474 label_comment: Kommentar
474 475 label_comment_add: Kommentar hinzufügen
475 476 label_comment_added: Kommentar hinzugefügt
476 477 label_comment_delete: Kommentar löschen
477 478 label_comment_plural: Kommentare
478 479 label_commits_per_author: Übertragungen pro Autor
479 480 label_commits_per_month: Übertragungen pro Monat
480 481 label_completed_versions: Abgeschlossene Versionen
481 482 label_confirmation: Bestätigung
482 483 label_contains: enthält
483 484 label_copied: kopiert
484 485 label_copied_from: Kopiert von
485 486 label_copied_to: Kopiert nach
486 487 label_copy_attachments: Anhänge kopieren
487 488 label_copy_same_as_target: So wie das Ziel
488 489 label_copy_source: Quelle
489 490 label_copy_subtasks: Unteraufgaben kopieren
490 491 label_copy_target: Ziel
491 492 label_copy_workflow_from: Workflow kopieren von
492 493 label_cross_project_descendants: Mit Unterprojekten
493 494 label_cross_project_hierarchy: Mit Projekthierarchie
494 495 label_cross_project_system: Mit allen Projekten
495 496 label_cross_project_tree: Mit Projektbaum
496 497 label_current_status: Gegenwärtiger Status
497 498 label_current_version: Gegenwärtige Version
498 499 label_custom_field: Benutzerdefiniertes Feld
499 500 label_custom_field_new: Neues Feld
500 501 label_custom_field_plural: Benutzerdefinierte Felder
501 502 label_custom_field_select_type: Bitte wählen Sie den Objekttyp, zu dem das benutzerdefinierte Feld hinzugefügt werden soll
502 503 label_date: Datum
503 504 label_date_from: Von
504 505 label_date_from_to: von %{start} bis %{end}
505 506 label_date_range: Zeitraum
506 507 label_date_to: Bis
507 508 label_day_plural: Tage
508 509 label_default: Standard
509 510 label_default_columns: Standard-Spalten
510 511 label_deleted: gelöscht
511 512 label_descending: Absteigend
512 513 label_details: Details
513 514 label_diff: diff
514 515 label_diff_inline: einspaltig
515 516 label_diff_side_by_side: nebeneinander
516 517 label_disabled: gesperrt
517 518 label_display: Anzeige
518 519 label_display_per_page: "Pro Seite: %{value}"
519 520 label_display_used_statuses_only: Zeige nur Status an, die von diesem Tracker verwendet werden
520 521 label_document: Dokument
521 522 label_document_added: Dokument hinzugefügt
522 523 label_document_new: Neues Dokument
523 524 label_document_plural: Dokumente
524 525 label_downloads_abbr: D/L
525 526 label_drop_down_list: Dropdown-Liste
526 527 label_duplicated_by: Dupliziert durch
527 528 label_duplicates: Duplikat von
528 529 label_edit_attachments: Angehängte Dateien bearbeiten
529 530 label_end_to_end: Ende - Ende
530 531 label_end_to_start: Ende - Anfang
531 532 label_enumeration_new: Neuer Wert
532 533 label_enumerations: Aufzählungen
533 534 label_environment: Umgebung
534 535 label_equals: ist
535 536 label_example: Beispiel
536 537 label_export_options: "%{export_format} Export-Eigenschaften"
537 538 label_export_to: "Auch abrufbar als:"
538 539 label_f_hour: "%{value} Stunde"
539 540 label_f_hour_plural: "%{value} Stunden"
540 541 label_feed_plural: Feeds
541 542 label_feeds_access_key: Atom-Zugriffsschlüssel
542 543 label_feeds_access_key_created_on: "Atom-Zugriffsschlüssel vor %{value} erstellt"
543 544 label_fields_permissions: Feldberechtigungen
544 545 label_file_added: Datei hinzugefügt
545 546 label_file_plural: Dateien
546 547 label_filter_add: Filter hinzufügen
547 548 label_filter_plural: Filter
548 549 label_float: Fließkommazahl
549 550 label_follows: Nachfolger von
550 551 label_gantt: Gantt-Diagramm
551 552 label_gantt_progress_line: Fortschrittslinie
552 553 label_general: Allgemein
553 554 label_generate_key: Generieren
554 555 label_git_report_last_commit: Bericht des letzten Commits für Dateien und Verzeichnisse
555 556 label_greater_or_equal: ">="
556 557 label_group: Gruppe
557 558 label_group_anonymous: Anonyme Benutzer
558 559 label_group_new: Neue Gruppe
559 560 label_group_non_member: Nichtmitglieder
560 561 label_group_plural: Gruppen
561 562 label_help: Hilfe
562 563 label_hidden: Versteckt
563 564 label_history: Historie
564 565 label_home: Hauptseite
565 566 label_in: in
566 567 label_in_less_than: in weniger als
567 568 label_in_more_than: in mehr als
568 569 label_in_the_next_days: in den nächsten
569 570 label_in_the_past_days: in den letzten
570 571 label_incoming_emails: Eingehende E-Mails
571 572 label_index_by_date: Seiten nach Datum sortiert
572 573 label_index_by_title: Seiten nach Titel sortiert
573 574 label_information: Information
574 575 label_information_plural: Informationen
575 576 label_integer: Zahl
576 577 label_internal: Intern
577 578 label_issue: Ticket
578 579 label_issue_added: Ticket hinzugefügt
579 580 label_issue_assigned_to_updated: Bearbeiter aktualisiert
580 581 label_issue_category: Ticket-Kategorie
581 582 label_issue_category_new: Neue Kategorie
582 583 label_issue_category_plural: Ticket-Kategorien
583 584 label_issue_new: Neues Ticket
584 585 label_issue_note_added: Notiz hinzugefügt
585 586 label_issue_plural: Tickets
586 587 label_issue_priority_updated: Priorität aktualisiert
587 588 label_issue_status: Ticket-Status
588 589 label_issue_status_new: Neuer Status
589 590 label_issue_status_plural: Ticket-Status
590 591 label_issue_status_updated: Status aktualisiert
591 592 label_issue_tracking: Tickets
592 593 label_issue_updated: Ticket aktualisiert
593 594 label_issue_view_all: Alle Tickets anzeigen
594 595 label_issue_watchers: Beobachter
595 596 label_issues_by: "Tickets pro %{value}"
596 597 label_issues_visibility_all: Alle Tickets
597 598 label_issues_visibility_own: Tickets die folgender Benutzer erstellt hat oder die ihm zugewiesen sind
598 599 label_issues_visibility_public: Alle öffentlichen Tickets
599 600 label_item_position: "%{position}/%{count}"
600 601 label_jump_to_a_project: Zu einem Projekt springen...
601 602 label_language_based: Sprachabhängig
602 603 label_last_changes: "%{count} letzte Änderungen"
603 604 label_last_login: Letzte Anmeldung
604 605 label_last_month: voriger Monat
605 606 label_last_n_days: "die letzten %{count} Tage"
606 607 label_last_n_weeks: letzte %{count} Wochen
607 608 label_last_week: vorige Woche
608 609 label_latest_compatible_version: Letzte kompatible Version
609 610 label_latest_revision: Aktuellste Revision
610 611 label_latest_revision_plural: Aktuellste Revisionen
611 612 label_ldap_authentication: LDAP-Authentifizierung
612 613 label_less_or_equal: "<="
613 614 label_less_than_ago: vor weniger als
614 615 label_link: Link
615 616 label_link_copied_issue: Kopierte Tickets verlinken
616 617 label_link_values_to: Werte mit URL verknüpfen
617 618 label_list: Liste
618 619 label_loading: Lade...
619 620 label_logged_as: Angemeldet als
620 621 label_login: Anmelden
621 622 label_login_with_open_id_option: oder mit OpenID anmelden
622 623 label_logout: Abmelden
623 624 label_only: nur
624 625 label_max_size: Maximale Größe
625 626 label_me: ich
626 627 label_member: Mitglied
627 628 label_member_new: Neues Mitglied
628 629 label_member_plural: Mitglieder
629 630 label_message_last: Letzter Forenbeitrag
630 631 label_message_new: Neues Thema
631 632 label_message_plural: Forenbeiträge
632 633 label_message_posted: Forenbeitrag hinzugefügt
633 634 label_min_max_length: Länge (Min. - Max.)
634 635 label_missing_api_access_key: Der API-Zugriffsschlüssel fehlt.
635 636 label_missing_feeds_access_key: Der Atom-Zugriffsschlüssel fehlt.
636 637 label_modified: geändert
637 638 label_module_plural: Module
638 639 label_month: Monat
639 640 label_months_from: Monate ab
640 641 label_more: Mehr
641 642 label_more_than_ago: vor mehr als
642 643 label_my_account: Mein Konto
643 644 label_my_page: Meine Seite
644 645 label_my_page_block: Verfügbare Widgets
645 646 label_my_projects: Meine Projekte
646 647 label_my_queries: Meine eigenen Abfragen
647 648 label_new: Neu
648 649 label_new_statuses_allowed: Neue Berechtigungen
649 650 label_news: News
650 651 label_news_added: News hinzugefügt
651 652 label_news_comment_added: Kommentar zu einer News hinzugefügt
652 653 label_news_latest: Letzte News
653 654 label_news_new: News hinzufügen
654 655 label_news_plural: News
655 656 label_news_view_all: Alle News anzeigen
656 657 label_next: Weiter
657 658 label_no_change_option: (Keine Änderung)
658 659 label_no_data: Nichts anzuzeigen
659 660 label_no_issues_in_project: keine Tickets im Projekt
660 661 label_nobody: Niemand
661 662 label_none: kein
662 663 label_not_contains: enthält nicht
663 664 label_not_equals: ist nicht
664 665 label_open_issues: offen
665 666 label_open_issues_plural: offen
666 667 label_optional_description: Beschreibung (optional)
667 668 label_options: Optionen
668 669 label_overall_activity: Aktivitäten aller Projekte anzeigen
669 670 label_overall_spent_time: Aufgewendete Zeit aller Projekte anzeigen
670 671 label_overview: Übersicht
671 672 label_parent_revision: Vorgänger
672 673 label_password_lost: Kennwort vergessen
674 label_password_required: Bitte geben Sie Ihr Kennwort ein
673 675 label_permissions: Berechtigungen
674 676 label_permissions_report: Berechtigungsübersicht
675 677 label_personalize_page: Diese Seite anpassen
676 678 label_planning: Terminplanung
677 679 label_please_login: Anmelden
678 680 label_plugins: Plugins
679 681 label_precedes: Vorgänger von
680 682 label_preferences: Präferenzen
681 683 label_preview: Vorschau
682 684 label_previous: Zurück
683 685 label_principal_search: "Nach Benutzer oder Gruppe suchen:"
684 686 label_profile: Profil
685 687 label_project: Projekt
686 688 label_project_all: Alle Projekte
687 689 label_project_copy_notifications: Sende Mailbenachrichtigungen beim Kopieren des Projekts.
688 690 label_project_latest: Neueste Projekte
689 691 label_project_new: Neues Projekt
690 692 label_project_plural: Projekte
691 693 label_public_projects: Öffentliche Projekte
692 694 label_query: Benutzerdefinierte Abfrage
693 695 label_query_new: Neue Abfrage
694 696 label_query_plural: Benutzerdefinierte Abfragen
695 697 label_radio_buttons: Radio-Buttons
696 698 label_read: Lesen...
697 699 label_readonly: Nur-Lese-Zugriff
698 700 label_register: Registrieren
699 701 label_registered_on: Angemeldet am
700 702 label_registration_activation_by_email: Kontoaktivierung durch E-Mail
701 703 label_registration_automatic_activation: Automatische Kontoaktivierung
702 704 label_registration_manual_activation: Manuelle Kontoaktivierung
703 705 label_related_issues: Zugehörige Tickets
704 706 label_relates_to: Beziehung mit
705 707 label_relation_delete: Beziehung löschen
706 708 label_relation_new: Neue Beziehung
707 709 label_renamed: umbenannt
708 710 label_reply_plural: Antworten
709 711 label_report: Bericht
710 712 label_report_plural: Berichte
711 713 label_reported_issues: Erstellte Tickets
712 714 label_repository: Projektarchiv
713 715 label_repository_new: Neues Repository
714 716 label_repository_plural: Projektarchive
715 717 label_required: Erforderlich
716 718 label_result_plural: Resultate
717 719 label_reverse_chronological_order: in umgekehrter zeitlicher Reihenfolge
718 720 label_revision: Revision
719 721 label_revision_id: Revision %{value}
720 722 label_revision_plural: Revisionen
721 723 label_roadmap: Roadmap
722 724 label_roadmap_due_in: "Fällig in %{value}"
723 725 label_roadmap_no_issues: Keine Tickets für diese Version
724 726 label_roadmap_overdue: "seit %{value} verspätet"
725 727 label_role: Rolle
726 728 label_role_and_permissions: Rollen und Rechte
727 729 label_role_anonymous: Anonymous
728 730 label_role_new: Neue Rolle
729 731 label_role_non_member: Nichtmitglied
730 732 label_role_plural: Rollen
731 733 label_scm: Versionskontrollsystem
732 734 label_search: Suche
733 735 label_search_for_watchers: Nach hinzufügbaren Beobachtern suchen
734 736 label_search_titles_only: Nur Titel durchsuchen
735 737 label_send_information: Sende Kontoinformationen an Benutzer
736 738 label_send_test_email: Test-E-Mail senden
737 739 label_session_expiration: Ende einer Sitzung
738 740 label_settings: Konfiguration
739 741 label_show_closed_projects: Geschlossene Projekte anzeigen
740 742 label_show_completed_versions: Abgeschlossene Versionen anzeigen
741 743 label_sort: Sortierung
742 744 label_sort_by: "Sortiert nach %{value}"
743 745 label_sort_higher: Eins höher
744 746 label_sort_highest: An den Anfang
745 747 label_sort_lower: Eins tiefer
746 748 label_sort_lowest: Ans Ende
747 749 label_spent_time: Aufgewendete Zeit
748 750 label_start_to_end: Anfang - Ende
749 751 label_start_to_start: Anfang - Anfang
750 752 label_statistics: Statistiken
751 753 label_status_transitions: Statusänderungen
752 754 label_stay_logged_in: Angemeldet bleiben
753 755 label_string: Text
754 756 label_subproject_new: Neues Unterprojekt
755 757 label_subproject_plural: Unterprojekte
756 758 label_subtask_plural: Unteraufgaben
757 759 label_tag: Markierung
758 760 label_text: Langer Text
759 761 label_theme: Stil
760 762 label_this_month: aktueller Monat
761 763 label_this_week: aktuelle Woche
762 764 label_this_year: aktuelles Jahr
763 765 label_time_entry_plural: Benötigte Zeit
764 766 label_time_tracking: Zeiterfassung
765 767 label_today: heute
766 768 label_topic_plural: Themen
767 769 label_total: Gesamtzahl
768 770 label_total_time: Gesamtzeit
769 771 label_tracker: Tracker
770 772 label_tracker_new: Neuer Tracker
771 773 label_tracker_plural: Tracker
772 774 label_unknown_plugin: Unbekanntes Plugin
773 775 label_update_issue_done_ratios: Ticket-Fortschritt aktualisieren
774 776 label_updated_time: "Vor %{value} aktualisiert"
775 777 label_updated_time_by: "Von %{author} vor %{age} aktualisiert"
776 778 label_used_by: Benutzt von
777 779 label_user: Benutzer
778 780 label_user_activity: "Aktivität von %{value}"
779 781 label_user_anonymous: Anonym
780 782 label_user_mail_no_self_notified: "Ich möchte nicht über Änderungen benachrichtigt werden, die ich selbst durchführe."
781 783 label_user_mail_option_all: "Für alle Ereignisse in all meinen Projekten"
782 784 label_user_mail_option_none: Keine Ereignisse
783 785 label_user_mail_option_only_assigned: Nur für Aufgaben für die ich zuständig bin
784 786 label_user_mail_option_only_my_events: Nur für Aufgaben die ich beobachte oder an welchen ich mitarbeite
785 787 label_user_mail_option_only_owner: Nur für Aufgaben die ich angelegt habe
786 788 label_user_mail_option_selected: "Für alle Ereignisse in den ausgewählten Projekten"
787 789 label_user_new: Neuer Benutzer
788 790 label_user_plural: Benutzer
789 791 label_user_search: "Nach Benutzer suchen:"
790 792 label_users_visibility_all: Alle aktiven Benutzer
791 793 label_users_visibility_members_of_visible_projects: Mitglieder von sichtbaren Projekten
792 794 label_version: Version
793 795 label_version_new: Neue Version
794 796 label_version_plural: Versionen
795 797 label_version_sharing_descendants: Mit Unterprojekten
796 798 label_version_sharing_hierarchy: Mit Projekthierarchie
797 799 label_version_sharing_none: Nicht gemeinsam verwenden
798 800 label_version_sharing_system: Mit allen Projekten
799 801 label_version_sharing_tree: Mit Projektbaum
800 802 label_view_all_revisions: Alle Revisionen anzeigen
801 803 label_view_diff: Unterschiede anzeigen
802 804 label_view_revisions: Revisionen anzeigen
803 805 label_visibility_private: nur für mich
804 806 label_visibility_public: für jeden Benutzer
805 807 label_visibility_roles: nur für diese Rollen
806 808 label_watched_issues: Beobachtete Tickets
807 809 label_week: Woche
808 810 label_wiki: Wiki
809 811 label_wiki_content_added: Wiki-Seite hinzugefügt
810 812 label_wiki_content_updated: Wiki-Seite aktualisiert
811 813 label_wiki_edit: Wiki-Bearbeitung
812 814 label_wiki_edit_plural: Wiki-Bearbeitungen
813 815 label_wiki_page: Wiki-Seite
814 816 label_wiki_page_plural: Wiki-Seiten
815 817 label_workflow: Workflow
816 818 label_x_closed_issues_abbr:
817 819 zero: 0 geschlossen
818 820 one: 1 geschlossen
819 821 other: "%{count} geschlossen"
820 822 label_x_comments:
821 823 zero: keine Kommentare
822 824 one: 1 Kommentar
823 825 other: "%{count} Kommentare"
824 826 label_x_issues:
825 827 zero: 0 Tickets
826 828 one: 1 Ticket
827 829 other: "%{count} Tickets"
828 830 label_x_open_issues_abbr:
829 831 zero: 0 offen
830 832 one: 1 offen
831 833 other: "%{count} offen"
832 834 label_x_open_issues_abbr_on_total:
833 835 zero: 0 offen / %{total}
834 836 one: 1 offen / %{total}
835 837 other: "%{count} offen / %{total}"
836 838 label_x_projects:
837 839 zero: keine Projekte
838 840 one: 1 Projekt
839 841 other: "%{count} Projekte"
840 842 label_year: Jahr
841 843 label_yesterday: gestern
842 844
843 845 mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:"
844 846 mail_body_account_information: Ihre Konto-Informationen
845 847 mail_body_account_information_external: "Sie können sich mit Ihrem Konto %{value} anmelden."
846 848 mail_body_lost_password: 'Benutzen Sie den folgenden Link, um Ihr Kennwort zu ändern:'
847 849 mail_body_register: 'Um Ihr Konto zu aktivieren, benutzen Sie folgenden Link:'
848 850 mail_body_reminder: "%{count} Tickets, die Ihnen zugewiesen sind, müssen in den nächsten %{days} Tagen abgegeben werden:"
849 851 mail_body_wiki_content_added: "Die Wiki-Seite '%{id}' wurde von %{author} hinzugefügt."
850 852 mail_body_wiki_content_updated: "Die Wiki-Seite '%{id}' wurde von %{author} aktualisiert."
851 853 mail_subject_account_activation_request: "Antrag auf %{value} Kontoaktivierung"
852 854 mail_subject_lost_password: "Ihr %{value} Kennwort"
853 855 mail_subject_register: "%{value} Kontoaktivierung"
854 856 mail_subject_reminder: "%{count} Tickets müssen in den nächsten %{days} Tagen abgegeben werden"
855 857 mail_subject_wiki_content_added: "Wiki-Seite '%{id}' hinzugefügt"
856 858 mail_subject_wiki_content_updated: "Wiki-Seite '%{id}' erfolgreich aktualisiert"
857 859
858 860 notice_account_activated: Ihr Konto ist aktiviert. Sie können sich jetzt anmelden.
859 861 notice_account_deleted: Ihr Benutzerkonto wurde unwiderruflich gelöscht.
860 862 notice_account_invalid_creditentials: Benutzer oder Kennwort ist ungültig.
861 863 notice_account_lost_email_sent: Eine E-Mail mit Anweisungen, ein neues Kennwort zu wählen, wurde Ihnen geschickt.
862 864 notice_account_locked: Ihr Konto ist gesperrt.
863 865 notice_account_not_activated_yet: Sie haben Ihr Konto noch nicht aktiviert. Wenn Sie die Aktivierungsmail erneut erhalten wollen, <a href="%{url}">klicken Sie bitte hier</a>.
864 866 notice_account_password_updated: Kennwort wurde erfolgreich aktualisiert.
865 867 notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators."
866 868 notice_account_register_done: Konto wurde erfolgreich angelegt. Eine E-Mail mit weiteren Instruktionen zur Kontoaktivierung wurde an %{email} gesendet.
867 869 notice_account_unknown_email: Unbekannter Benutzer.
868 870 notice_account_updated: Konto wurde erfolgreich aktualisiert.
869 871 notice_account_wrong_password: Falsches Kennwort.
870 872 notice_api_access_key_reseted: Ihr API-Zugriffsschlüssel wurde zurückgesetzt.
871 873 notice_can_t_change_password: Dieses Konto verwendet eine externe Authentifizierungs-Quelle. Unmöglich, das Kennwort zu ändern.
872 874 notice_default_data_loaded: Die Standard-Konfiguration wurde erfolgreich geladen.
873 875 notice_email_error: "Beim Senden einer E-Mail ist ein Fehler aufgetreten (%{value})."
874 876 notice_email_sent: "Eine E-Mail wurde an %{value} gesendet."
875 877 notice_failed_to_save_issues: "%{count} von %{total} ausgewählten Tickets konnte(n) nicht gespeichert werden: %{ids}."
876 878 notice_failed_to_save_members: "Benutzer konnte nicht gespeichert werden: %{errors}."
877 879 notice_failed_to_save_time_entries: "Gescheitert %{count} Zeiteinträge für %{total} von ausgewählten: %{ids} zu speichern."
878 880 notice_feeds_access_key_reseted: Ihr Atom-Zugriffsschlüssel wurde zurückgesetzt.
879 881 notice_file_not_found: Anhang existiert nicht oder ist gelöscht worden.
880 882 notice_gantt_chart_truncated: Die Grafik ist unvollständig, da das Maximum der anzeigbaren Aufgaben überschritten wurde (%{max})
881 883 notice_issue_done_ratios_updated: Der Ticket-Fortschritt wurde aktualisiert.
882 884 notice_issue_successful_create: Ticket %{id} erstellt.
883 885 notice_issue_update_conflict: Das Ticket wurde während Ihrer Bearbeitung von einem anderen Nutzer überarbeitet.
884 886 notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert.
885 887 notice_new_password_must_be_different: Das neue Passwort muss sich vom dem aktuellen
886 888 unterscheiden
887 889 notice_no_issue_selected: "Kein Ticket ausgewählt! Bitte wählen Sie die Tickets, die Sie bearbeiten möchten."
888 890 notice_not_authorized: Sie sind nicht berechtigt, auf diese Seite zuzugreifen.
889 891 notice_not_authorized_archived_project: Das Projekt wurde archiviert und ist daher nicht nicht verfügbar.
890 892 notice_successful_connection: Verbindung erfolgreich.
891 893 notice_successful_create: Erfolgreich angelegt
892 894 notice_successful_delete: Erfolgreich gelöscht.
893 895 notice_successful_update: Erfolgreich aktualisiert.
894 896 notice_unable_delete_time_entry: Der Zeiterfassungseintrag konnte nicht gelöscht werden.
895 897 notice_unable_delete_version: Die Version konnte nicht gelöscht werden.
896 898 notice_user_successful_create: Benutzer %{id} angelegt.
897 899
898 900 permission_add_issue_notes: Kommentare hinzufügen
899 901 permission_add_issue_watchers: Beobachter hinzufügen
900 902 permission_add_issues: Tickets hinzufügen
901 903 permission_add_messages: Forenbeiträge hinzufügen
902 904 permission_add_project: Projekt erstellen
903 905 permission_add_subprojects: Unterprojekte erstellen
904 906 permission_add_documents: Dokumente hinzufügen
905 907 permission_browse_repository: Projektarchiv ansehen
906 908 permission_close_project: Schließen / erneutes Öffnen eines Projekts
907 909 permission_comment_news: News kommentieren
908 910 permission_commit_access: Commit-Zugriff
909 911 permission_delete_issue_watchers: Beobachter löschen
910 912 permission_delete_issues: Tickets löschen
911 913 permission_delete_messages: Forenbeiträge löschen
912 914 permission_delete_own_messages: Eigene Forenbeiträge löschen
913 915 permission_delete_wiki_pages: Wiki-Seiten löschen
914 916 permission_delete_wiki_pages_attachments: Anhänge löschen
915 917 permission_delete_documents: Dokumente löschen
916 918 permission_edit_issue_notes: Kommentare bearbeiten
917 919 permission_edit_issues: Tickets bearbeiten
918 920 permission_edit_messages: Forenbeiträge bearbeiten
919 921 permission_edit_own_issue_notes: Eigene Kommentare bearbeiten
920 922 permission_edit_own_messages: Eigene Forenbeiträge bearbeiten
921 923 permission_edit_own_time_entries: Selbst gebuchte Aufwände bearbeiten
922 924 permission_edit_project: Projekt bearbeiten
923 925 permission_edit_time_entries: Gebuchte Aufwände bearbeiten
924 926 permission_edit_wiki_pages: Wiki-Seiten bearbeiten
925 927 permission_edit_documents: Dokumente bearbeiten
926 928 permission_export_wiki_pages: Wiki-Seiten exportieren
927 929 permission_log_time: Aufwände buchen
928 930 permission_manage_boards: Foren verwalten
929 931 permission_manage_categories: Ticket-Kategorien verwalten
930 932 permission_manage_files: Dateien verwalten
931 933 permission_manage_issue_relations: Ticket-Beziehungen verwalten
932 934 permission_manage_members: Mitglieder verwalten
933 935 permission_manage_news: News verwalten
934 936 permission_manage_project_activities: Aktivitäten (Zeiterfassung) verwalten
935 937 permission_manage_public_queries: Öffentliche Filter verwalten
936 938 permission_manage_related_issues: Zugehörige Tickets verwalten
937 939 permission_manage_repository: Projektarchiv verwalten
938 940 permission_manage_subtasks: Unteraufgaben verwalten
939 941 permission_manage_versions: Versionen verwalten
940 942 permission_manage_wiki: Wiki verwalten
941 943 permission_move_issues: Tickets verschieben
942 944 permission_protect_wiki_pages: Wiki-Seiten schützen
943 945 permission_rename_wiki_pages: Wiki-Seiten umbenennen
944 946 permission_save_queries: Filter speichern
945 947 permission_select_project_modules: Projektmodule auswählen
946 948 permission_set_issues_private: Tickets privat oder öffentlich markieren
947 949 permission_set_notes_private: Kommentar als privat markieren
948 950 permission_set_own_issues_private: Eigene Tickets privat oder öffentlich markieren
949 951 permission_view_calendar: Kalender ansehen
950 952 permission_view_changesets: Changesets ansehen
951 953 permission_view_documents: Dokumente ansehen
952 954 permission_view_files: Dateien ansehen
953 955 permission_view_gantt: Gantt-Diagramm ansehen
954 956 permission_view_issue_watchers: Liste der Beobachter ansehen
955 957 permission_view_issues: Tickets anzeigen
956 958 permission_view_messages: Forenbeiträge ansehen
957 959 permission_view_private_notes: Private Kommentare sehen
958 960 permission_view_time_entries: Gebuchte Aufwände ansehen
959 961 permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen
960 962 permission_view_wiki_pages: Wiki ansehen
961 963
962 964 project_module_boards: Foren
963 965 project_module_calendar: Kalender
964 966 project_module_documents: Dokumente
965 967 project_module_files: Dateien
966 968 project_module_gantt: Gantt
967 969 project_module_issue_tracking: Ticket-Verfolgung
968 970 project_module_news: News
969 971 project_module_repository: Projektarchiv
970 972 project_module_time_tracking: Zeiterfassung
971 973 project_module_wiki: Wiki
972 974 project_status_active: aktiv
973 975 project_status_archived: archiviert
974 976 project_status_closed: geschlossen
975 977
976 978 setting_activity_days_default: Anzahl Tage pro Seite der Projekt-Aktivität
977 979 setting_app_subtitle: Applikations-Untertitel
978 980 setting_app_title: Applikations-Titel
979 981 setting_attachment_max_size: Max. Dateigröße
980 982 setting_autofetch_changesets: Changesets automatisch abrufen
981 983 setting_autologin: Automatische Anmeldung läuft ab nach
982 984 setting_bcc_recipients: E-Mails als Blindkopie (BCC) senden
983 985 setting_cache_formatted_text: Formatierten Text im Cache speichern
984 986 setting_commit_cross_project_ref: Erlauben auf Tickets aller anderen Projekte zu referenzieren
985 987 setting_commit_fix_keywords: Schlüsselwörter (Status)
986 988 setting_commit_logtime_activity_id: Aktivität für die Zeiterfassung
987 989 setting_commit_logtime_enabled: Aktiviere Zeitlogging
988 990 setting_commit_ref_keywords: Schlüsselwörter (Beziehungen)
989 991 setting_cross_project_issue_relations: Ticket-Beziehungen zwischen Projekten erlauben
990 992 setting_cross_project_subtasks: Projektübergreifende Unteraufgaben erlauben
991 993 setting_date_format: Datumsformat
992 994 setting_default_issue_start_date_to_creation_date: Aktuelles Datum als Beginn für neue Tickets verwenden
993 995 setting_default_language: Standardsprache
994 996 setting_default_notification_option: Standard Benachrichtigungsoptionen
995 997 setting_default_projects_modules: Standardmäßig aktivierte Module für neue Projekte
996 998 setting_default_projects_public: Neue Projekte sind standardmäßig öffentlich
997 999 setting_default_projects_tracker_ids: Standardmäßig aktivierte Tracker für neue Projekte
998 1000 setting_diff_max_lines_displayed: Maximale Anzahl anzuzeigender Diff-Zeilen
999 1001 setting_display_subprojects_issues: Tickets von Unterprojekten im Hauptprojekt anzeigen
1000 1002 setting_emails_footer: E-Mail-Fußzeile
1001 1003 setting_emails_header: E-Mail-Kopfzeile
1002 1004 setting_enabled_scm: Aktivierte Versionskontrollsysteme
1003 1005 setting_feeds_limit: Max. Anzahl Einträge pro Atom-Feed
1004 1006 setting_file_max_size_displayed: Maximale Größe inline angezeigter Textdateien
1005 1007 setting_force_default_language_for_anonymous: Standardsprache für anonyme Benutzer erzwingen
1006 1008 setting_force_default_language_for_loggedin: Standardsprache für angemeldete Benutzer erzwingen
1007 1009 setting_gantt_items_limit: Maximale Anzahl von Aufgaben die im Gantt-Chart angezeigt werden
1008 1010 setting_gravatar_default: Standard-Gravatar-Bild
1009 1011 setting_gravatar_enabled: Gravatar-Benutzerbilder benutzen
1010 1012 setting_host_name: Hostname
1011 1013 setting_issue_done_ratio: Berechne den Ticket-Fortschritt mittels
1012 1014 setting_issue_done_ratio_issue_field: Ticket-Feld %-erledigt
1013 1015 setting_issue_done_ratio_issue_status: Ticket-Status
1014 1016 setting_issue_group_assignment: Ticketzuweisung an Gruppen erlauben
1015 1017 setting_issue_list_default_columns: Standard-Spalten in der Ticket-Auflistung
1016 1018 setting_issues_export_limit: Max. Anzahl Tickets bei CSV/PDF-Export
1017 1019 setting_jsonp_enabled: JSONP Unterstützung aktivieren
1018 1020 setting_link_copied_issue: Tickets beim kopieren verlinken
1019 1021 setting_login_required: Authentifizierung erforderlich
1020 1022 setting_mail_from: E-Mail-Absender
1021 1023 setting_mail_handler_api_enabled: Abruf eingehender E-Mails aktivieren
1022 1024 setting_mail_handler_api_key: API-Schlüssel
1023 1025 setting_mail_handler_body_delimiters: "Schneide E-Mails nach einer dieser Zeilen ab"
1024 1026 setting_mail_handler_excluded_filenames: Anhänge nach Namen ausschließen
1025 1027 setting_new_project_user_role_id: Rolle, die einem Nicht-Administrator zugeordnet wird, der ein Projekt erstellt
1026 1028 setting_non_working_week_days: Arbeitsfreie Tage
1027 1029 setting_openid: Erlaube OpenID-Anmeldung und -Registrierung
1028 1030 setting_password_min_length: Mindestlänge des Kennworts
1029 1031 setting_password_max_age: Erzwinge Passwortwechsel nach
1030 1032 setting_per_page_options: Objekte pro Seite
1031 1033 setting_plain_text_mail: Nur reinen Text (kein HTML) senden
1032 1034 setting_protocol: Protokoll
1033 1035 setting_repositories_encodings: Enkodierung von Anhängen und Repositories
1034 1036 setting_repository_log_display_limit: Maximale Anzahl anzuzeigender Revisionen in der Historie einer Datei
1035 1037 setting_rest_api_enabled: REST-Schnittstelle aktivieren
1036 1038 setting_self_registration: Registrierung ermöglichen
1037 1039 setting_sequential_project_identifiers: Fortlaufende Projektkennungen generieren
1038 1040 setting_session_lifetime: Längste Dauer einer Sitzung
1039 1041 setting_session_timeout: Zeitüberschreitung bei Inaktivität
1040 1042 setting_start_of_week: Wochenanfang
1041 1043 setting_sys_api_enabled: Webservice zur Verwaltung der Projektarchive benutzen
1042 1044 setting_text_formatting: Textformatierung
1043 1045 setting_thumbnails_enabled: Vorschaubilder von Dateianhängen anzeigen
1044 1046 setting_thumbnails_size: Größe der Vorschaubilder (in Pixel)
1045 1047 setting_time_format: Zeitformat
1046 1048 setting_unsubscribe: Erlaubt Benutzern das eigene Benutzerkonto zu löschen
1047 1049 setting_user_format: Benutzer-Anzeigeformat
1048 1050 setting_welcome_text: Willkommenstext
1049 1051 setting_wiki_compression: Wiki-Historie komprimieren
1050 1052
1051 1053 status_active: aktiv
1052 1054 status_locked: gesperrt
1053 1055 status_registered: nicht aktivierte
1054 1056
1055 1057 text_account_destroy_confirmation: "Möchten Sie wirklich fortfahren?\nIhr Benutzerkonto wird für immer gelöscht und kann nicht wiederhergestellt werden."
1056 1058 text_are_you_sure: Sind Sie sicher?
1057 1059 text_assign_time_entries_to_project: Gebuchte Aufwände dem Projekt zuweisen
1058 1060 text_caracters_maximum: "Max. %{count} Zeichen."
1059 1061 text_caracters_minimum: "Muss mindestens %{count} Zeichen lang sein."
1060 1062 text_comma_separated: Mehrere Werte erlaubt (durch Komma getrennt).
1061 1063 text_convert_available: ImageMagick Konvertierung verfügbar (optional)
1062 1064 text_custom_field_possible_values_info: 'Eine Zeile pro Wert'
1063 1065 text_default_administrator_account_changed: Administrator-Kennwort geändert
1064 1066 text_destroy_time_entries: Gebuchte Aufwände löschen
1065 1067 text_destroy_time_entries_question: Es wurden bereits %{hours} Stunden auf dieses Ticket gebucht. Was soll mit den Aufwänden geschehen?
1066 1068 text_diff_truncated: '... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.'
1067 1069 text_email_delivery_not_configured: "Der SMTP-Server ist nicht konfiguriert und Mailbenachrichtigungen sind ausgeschaltet.\nNehmen Sie die Einstellungen für Ihren SMTP-Server in config/configuration.yml vor und starten Sie die Applikation neu."
1068 1070 text_enumeration_category_reassign_to: 'Die Objekte stattdessen diesem Wert zuordnen:'
1069 1071 text_enumeration_destroy_question: "%{count} Objekt(e) sind diesem Wert zugeordnet."
1070 1072 text_file_repository_writable: Verzeichnis für Dateien beschreibbar
1071 1073 text_git_repository_note: Repository steht für sich alleine (bare) und liegt lokal (z.B. /gitrepo, c:\gitrepo)
1072 1074 text_issue_added: "Ticket %{id} wurde erstellt von %{author}."
1073 1075 text_issue_category_destroy_assignments: Kategorie-Zuordnung entfernen
1074 1076 text_issue_category_destroy_question: "Einige Tickets (%{count}) sind dieser Kategorie zugeodnet. Was möchten Sie tun?"
1075 1077 text_issue_category_reassign_to: Tickets dieser Kategorie zuordnen
1076 1078 text_issue_conflict_resolution_add_notes: Meine Änderungen übernehmen und alle anderen Änderungen verwerfen
1077 1079 text_issue_conflict_resolution_cancel: Meine Änderungen verwerfen und %{link} neu anzeigen
1078 1080 text_issue_conflict_resolution_overwrite: Meine Änderungen trotzdem übernehmen (vorherige Notizen bleiben erhalten aber manche können überschrieben werden)
1079 1081 text_issue_updated: "Ticket %{id} wurde aktualisiert von %{author}."
1080 1082 text_issues_destroy_confirmation: 'Sind Sie sicher, dass Sie die ausgewählten Tickets löschen möchten?'
1081 1083 text_issues_destroy_descendants_confirmation: Dies wird auch %{count} Unteraufgabe/n löschen.
1082 1084 text_issues_ref_in_commit_messages: Ticket-Beziehungen und -Status in Commit-Log-Meldungen
1083 1085 text_journal_added: "%{label} %{value} wurde hinzugefügt"
1084 1086 text_journal_changed: "%{label} wurde von %{old} zu %{new} geändert"
1085 1087 text_journal_changed_no_detail: "%{label} aktualisiert"
1086 1088 text_journal_deleted: "%{label} %{old} wurde gelöscht"
1087 1089 text_journal_set_to: "%{label} wurde auf %{value} gesetzt"
1088 1090 text_length_between: "Länge zwischen %{min} und %{max} Zeichen."
1089 1091 text_line_separated: Mehrere Werte sind erlaubt (eine Zeile pro Wert).
1090 1092 text_load_default_configuration: Standard-Konfiguration laden
1091 1093 text_mercurial_repository_note: Lokales repository (e.g. /hgrepo, c:\hgrepo)
1092 1094 text_min_max_length_info: 0 heißt keine Beschränkung
1093 1095 text_no_configuration_data: "Rollen, Tracker, Ticket-Status und Workflows wurden noch nicht konfiguriert.\nEs ist sehr zu empfehlen, die Standard-Konfiguration zu laden. Sobald sie geladen ist, können Sie diese abändern."
1094 1096 text_own_membership_delete_confirmation: "Sie sind dabei, einige oder alle Ihre Berechtigungen zu entfernen. Es ist möglich, dass Sie danach das Projekt nicht mehr ansehen oder bearbeiten dürfen.\nSind Sie sicher, dass Sie dies tun möchten?"
1095 1097 text_plugin_assets_writable: Verzeichnis für Plugin-Assets beschreibbar
1096 1098 text_project_closed: Dieses Projekt ist geschlossen und kann nicht bearbeitet werden.
1097 1099 text_project_destroy_confirmation: Sind Sie sicher, dass Sie das Projekt löschen wollen?
1098 1100 text_project_identifier_info: 'Kleinbuchstaben (a-z), Ziffern, Binde- und Unterstriche erlaubt, muss mit einem Kleinbuchstaben beginnen.<br />Einmal gespeichert, kann die Kennung nicht mehr geändert werden.'
1099 1101 text_reassign_time_entries: 'Gebuchte Aufwände diesem Ticket zuweisen:'
1100 1102 text_regexp_info: z. B. ^[A-Z0-9]+$
1101 1103 text_repository_identifier_info: 'Kleinbuchstaben (a-z), Ziffern, Binde- und Unterstriche erlaubt.<br />Einmal gespeichert, kann die Kennung nicht mehr geändert werden.'
1102 1104 text_repository_usernames_mapping: "Bitte legen Sie die Zuordnung der Redmine-Benutzer zu den Benutzernamen der Commit-Log-Meldungen des Projektarchivs fest.\nBenutzer mit identischen Redmine- und Projektarchiv-Benutzernamen oder -E-Mail-Adressen werden automatisch zugeordnet."
1103 1105 text_rmagick_available: RMagick verfügbar (optional)
1104 1106 text_scm_command: Kommando
1105 1107 text_scm_command_not_available: SCM-Kommando ist nicht verfügbar. Bitte prüfen Sie die Einstellungen im Administrationspanel.
1106 1108 text_scm_command_version: Version
1107 1109 text_scm_config: Die SCM-Kommandos können in der in config/configuration.yml konfiguriert werden. Redmine muss anschließend neu gestartet werden.
1108 1110 text_scm_path_encoding_note: "Standard: UTF-8"
1109 1111 text_select_mail_notifications: Bitte wählen Sie die Aktionen aus, für die eine Mailbenachrichtigung gesendet werden soll.
1110 1112 text_select_project_modules: 'Bitte wählen Sie die Module aus, die in diesem Projekt aktiviert sein sollen:'
1111 1113 text_session_expiration_settings: "Achtung: Änderungen können aktuelle Sitzungen beenden, Ihre eingeschlossen!"
1112 1114 text_status_changed_by_changeset: "Status geändert durch Changeset %{value}."
1113 1115 text_subprojects_destroy_warning: "Dessen Unterprojekte (%{value}) werden ebenfalls gelöscht."
1114 1116 text_subversion_repository_note: 'Beispiele: file:///, http://, https://, svn://, svn+[tunnelscheme]://'
1115 1117 text_time_entries_destroy_confirmation: Sind Sie sicher, dass Sie die ausgewählten Zeitaufwände löschen möchten?
1116 1118 text_time_logged_by_changeset: Angewendet in Changeset %{value}.
1117 1119 text_tip_issue_begin_day: Aufgabe, die an diesem Tag beginnt
1118 1120 text_tip_issue_begin_end_day: Aufgabe, die an diesem Tag beginnt und endet
1119 1121 text_tip_issue_end_day: Aufgabe, die an diesem Tag endet
1120 1122 text_tracker_no_workflow: Kein Workflow für diesen Tracker definiert.
1121 1123 text_turning_multiple_off: Wenn Sie die Mehrfachauswahl deaktivieren, werden Felder mit Mehrfachauswahl bereinigt.
1122 1124 Dadurch wird sichergestellt, dass lediglich ein Wert pro Feld ausgewählt ist.
1123 1125 text_unallowed_characters: Nicht erlaubte Zeichen
1124 1126 text_user_mail_option: "Für nicht ausgewählte Projekte werden Sie nur Benachrichtigungen für Dinge erhalten, die Sie beobachten oder an denen Sie beteiligt sind (z. B. Tickets, deren Autor Sie sind oder die Ihnen zugewiesen sind)."
1125 1127 text_user_wrote: "%{value} schrieb:"
1126 1128 text_warn_on_leaving_unsaved: Die aktuellen Änderungen gehen verloren, wenn Sie diese Seite verlassen.
1127 1129 text_wiki_destroy_confirmation: Sind Sie sicher, dass Sie dieses Wiki mit sämtlichem Inhalt löschen möchten?
1128 1130 text_wiki_page_destroy_children: Lösche alle Unterseiten
1129 1131 text_wiki_page_destroy_question: "Diese Seite hat %{descendants} Unterseite(n). Was möchten Sie tun?"
1130 1132 text_wiki_page_nullify_children: Verschiebe die Unterseiten auf die oberste Ebene
1131 1133 text_wiki_page_reassign_children: Ordne die Unterseiten dieser Seite zu
1132 1134 text_workflow_edit: Workflow zum Bearbeiten auswählen
1133 1135 text_zoom_in: Ansicht vergrößern
1134 1136 text_zoom_out: Ansicht verkleinern
1135 1137
1136 1138 version_status_closed: abgeschlossen
1137 1139 version_status_locked: gesperrt
1138 1140 version_status_open: offen
1139 1141
1140 1142 warning_attachments_not_saved: "%{count} Datei(en) konnten nicht gespeichert werden."
1141 1143 label_search_attachments_yes: Namen und Beschreibungen von Anhängen durchsuchen
1142 1144 label_search_attachments_no: Keine Anhänge suchen
1143 1145 label_search_attachments_only: Nur Anhänge suchen
1144 1146 label_search_open_issues_only: Nur offene Tickets
1145 1147 field_address: E-Mail
1146 1148 setting_max_additional_emails: Maximale Anzahl zusätzlicher E-Mailadressen
1147 1149 label_email_address_plural: E-Mails
1148 1150 label_email_address_add: E-Mailadresse hinzufügen
1149 1151 label_enable_notifications: Benachrichtigungen aktivieren
1150 1152 label_disable_notifications: Benachrichtigungen deaktivieren
1151 1153 setting_search_results_per_page: Suchergebnisse pro Seite
1152 1154 label_blank_value: leer
1153 1155 permission_copy_issues: Tickets kopieren
1154 1156 error_password_expired: Your password has expired or the administrator requires you
1155 1157 to change it.
1156 1158 field_time_entries_visibility: Time logs visibility
1157 1159 label_parent_task_attributes: Parent tasks attributes
1158 1160 label_parent_task_attributes_derived: Calculated from subtasks
1159 1161 label_parent_task_attributes_independent: Independent of subtasks
1160 1162 label_time_entries_visibility_all: All time entries
1161 1163 label_time_entries_visibility_own: Time entries created by the user
1162 1164 label_member_management: Member management
1163 1165 label_member_management_all_roles: All roles
1164 1166 label_member_management_selected_roles_only: Only these roles
@@ -1,1144 +1,1146
1 1 en:
2 2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 3 direction: ltr
4 4 date:
5 5 formats:
6 6 # Use the strftime parameters for formats.
7 7 # When no format has been given, it uses default.
8 8 # You can provide other formats here if you like!
9 9 default: "%m/%d/%Y"
10 10 short: "%b %d"
11 11 long: "%B %d, %Y"
12 12
13 13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15 15
16 16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 19 # Used in date_select and datime_select.
20 20 order:
21 21 - :year
22 22 - :month
23 23 - :day
24 24
25 25 time:
26 26 formats:
27 27 default: "%m/%d/%Y %I:%M %p"
28 28 time: "%I:%M %p"
29 29 short: "%d %b %H:%M"
30 30 long: "%B %d, %Y %H:%M"
31 31 am: "am"
32 32 pm: "pm"
33 33
34 34 datetime:
35 35 distance_in_words:
36 36 half_a_minute: "half a minute"
37 37 less_than_x_seconds:
38 38 one: "less than 1 second"
39 39 other: "less than %{count} seconds"
40 40 x_seconds:
41 41 one: "1 second"
42 42 other: "%{count} seconds"
43 43 less_than_x_minutes:
44 44 one: "less than a minute"
45 45 other: "less than %{count} minutes"
46 46 x_minutes:
47 47 one: "1 minute"
48 48 other: "%{count} minutes"
49 49 about_x_hours:
50 50 one: "about 1 hour"
51 51 other: "about %{count} hours"
52 52 x_hours:
53 53 one: "1 hour"
54 54 other: "%{count} hours"
55 55 x_days:
56 56 one: "1 day"
57 57 other: "%{count} days"
58 58 about_x_months:
59 59 one: "about 1 month"
60 60 other: "about %{count} months"
61 61 x_months:
62 62 one: "1 month"
63 63 other: "%{count} months"
64 64 about_x_years:
65 65 one: "about 1 year"
66 66 other: "about %{count} years"
67 67 over_x_years:
68 68 one: "over 1 year"
69 69 other: "over %{count} years"
70 70 almost_x_years:
71 71 one: "almost 1 year"
72 72 other: "almost %{count} years"
73 73
74 74 number:
75 75 format:
76 76 separator: "."
77 77 delimiter: ""
78 78 precision: 3
79 79
80 80 human:
81 81 format:
82 82 delimiter: ""
83 83 precision: 3
84 84 storage_units:
85 85 format: "%n %u"
86 86 units:
87 87 byte:
88 88 one: "Byte"
89 89 other: "Bytes"
90 90 kb: "KB"
91 91 mb: "MB"
92 92 gb: "GB"
93 93 tb: "TB"
94 94
95 95 # Used in array.to_sentence.
96 96 support:
97 97 array:
98 98 sentence_connector: "and"
99 99 skip_last_comma: false
100 100
101 101 activerecord:
102 102 errors:
103 103 template:
104 104 header:
105 105 one: "1 error prohibited this %{model} from being saved"
106 106 other: "%{count} errors prohibited this %{model} from being saved"
107 107 messages:
108 108 inclusion: "is not included in the list"
109 109 exclusion: "is reserved"
110 110 invalid: "is invalid"
111 111 confirmation: "doesn't match confirmation"
112 112 accepted: "must be accepted"
113 113 empty: "cannot be empty"
114 114 blank: "cannot be blank"
115 115 too_long: "is too long (maximum is %{count} characters)"
116 116 too_short: "is too short (minimum is %{count} characters)"
117 117 wrong_length: "is the wrong length (should be %{count} characters)"
118 118 taken: "has already been taken"
119 119 not_a_number: "is not a number"
120 120 not_a_date: "is not a valid date"
121 121 greater_than: "must be greater than %{count}"
122 122 greater_than_or_equal_to: "must be greater than or equal to %{count}"
123 123 equal_to: "must be equal to %{count}"
124 124 less_than: "must be less than %{count}"
125 125 less_than_or_equal_to: "must be less than or equal to %{count}"
126 126 odd: "must be odd"
127 127 even: "must be even"
128 128 greater_than_start_date: "must be greater than start date"
129 129 not_same_project: "doesn't belong to the same project"
130 130 circular_dependency: "This relation would create a circular dependency"
131 131 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
132 132 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
133 133
134 134 actionview_instancetag_blank_option: Please select
135 135
136 136 general_text_No: 'No'
137 137 general_text_Yes: 'Yes'
138 138 general_text_no: 'no'
139 139 general_text_yes: 'yes'
140 140 general_lang_name: 'English'
141 141 general_csv_separator: ','
142 142 general_csv_decimal_separator: '.'
143 143 general_csv_encoding: ISO-8859-1
144 144 general_pdf_fontname: freesans
145 145 general_first_day_of_week: '7'
146 146
147 147 notice_account_updated: Account was successfully updated.
148 148 notice_account_invalid_creditentials: Invalid user or password
149 149 notice_account_password_updated: Password was successfully updated.
150 150 notice_account_wrong_password: Wrong password
151 151 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
152 152 notice_account_unknown_email: Unknown user.
153 153 notice_account_not_activated_yet: You haven't activated your account yet. If you want to receive a new activation email, please <a href="%{url}">click this link</a>.
154 154 notice_account_locked: Your account is locked.
155 155 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
156 156 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
157 157 notice_account_activated: Your account has been activated. You can now log in.
158 158 notice_successful_create: Successful creation.
159 159 notice_successful_update: Successful update.
160 160 notice_successful_delete: Successful deletion.
161 161 notice_successful_connection: Successful connection.
162 162 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
163 163 notice_locking_conflict: Data has been updated by another user.
164 164 notice_not_authorized: You are not authorized to access this page.
165 165 notice_not_authorized_archived_project: The project you're trying to access has been archived.
166 166 notice_email_sent: "An email was sent to %{value}"
167 167 notice_email_error: "An error occurred while sending mail (%{value})"
168 168 notice_feeds_access_key_reseted: Your Atom access key was reset.
169 169 notice_api_access_key_reseted: Your API access key was reset.
170 170 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
171 171 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
172 172 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
173 173 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
174 174 notice_account_pending: "Your account was created and is now pending administrator approval."
175 175 notice_default_data_loaded: Default configuration successfully loaded.
176 176 notice_unable_delete_version: Unable to delete version.
177 177 notice_unable_delete_time_entry: Unable to delete time log entry.
178 178 notice_issue_done_ratios_updated: Issue done ratios updated.
179 179 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
180 180 notice_issue_successful_create: "Issue %{id} created."
181 181 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
182 182 notice_account_deleted: "Your account has been permanently deleted."
183 183 notice_user_successful_create: "User %{id} created."
184 184 notice_new_password_must_be_different: The new password must be different from the current password
185 185
186 186 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
187 187 error_scm_not_found: "The entry or revision was not found in the repository."
188 188 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
189 189 error_scm_annotate: "The entry does not exist or cannot be annotated."
190 190 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
191 191 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
192 192 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
193 193 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
194 194 error_can_not_delete_custom_field: Unable to delete custom field
195 195 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
196 196 error_can_not_remove_role: "This role is in use and cannot be deleted."
197 197 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
198 198 error_can_not_archive_project: This project cannot be archived
199 199 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
200 200 error_workflow_copy_source: 'Please select a source tracker or role'
201 201 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
202 202 error_unable_delete_issue_status: 'Unable to delete issue status'
203 203 error_unable_to_connect: "Unable to connect (%{value})"
204 204 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
205 205 error_session_expired: "Your session has expired. Please login again."
206 206 warning_attachments_not_saved: "%{count} file(s) could not be saved."
207 207 error_password_expired: "Your password has expired or the administrator requires you to change it."
208 208
209 209 mail_subject_lost_password: "Your %{value} password"
210 210 mail_body_lost_password: 'To change your password, click on the following link:'
211 211 mail_subject_register: "Your %{value} account activation"
212 212 mail_body_register: 'To activate your account, click on the following link:'
213 213 mail_body_account_information_external: "You can use your %{value} account to log in."
214 214 mail_body_account_information: Your account information
215 215 mail_subject_account_activation_request: "%{value} account activation request"
216 216 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
217 217 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
218 218 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
219 219 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
220 220 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
221 221 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
222 222 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
223 223
224 224 field_name: Name
225 225 field_description: Description
226 226 field_summary: Summary
227 227 field_is_required: Required
228 228 field_firstname: First name
229 229 field_lastname: Last name
230 230 field_mail: Email
231 231 field_address: Email
232 232 field_filename: File
233 233 field_filesize: Size
234 234 field_downloads: Downloads
235 235 field_author: Author
236 236 field_created_on: Created
237 237 field_updated_on: Updated
238 238 field_closed_on: Closed
239 239 field_field_format: Format
240 240 field_is_for_all: For all projects
241 241 field_possible_values: Possible values
242 242 field_regexp: Regular expression
243 243 field_min_length: Minimum length
244 244 field_max_length: Maximum length
245 245 field_value: Value
246 246 field_category: Category
247 247 field_title: Title
248 248 field_project: Project
249 249 field_issue: Issue
250 250 field_status: Status
251 251 field_notes: Notes
252 252 field_is_closed: Issue closed
253 253 field_is_default: Default value
254 254 field_tracker: Tracker
255 255 field_subject: Subject
256 256 field_due_date: Due date
257 257 field_assigned_to: Assignee
258 258 field_priority: Priority
259 259 field_fixed_version: Target version
260 260 field_user: User
261 261 field_principal: Principal
262 262 field_role: Role
263 263 field_homepage: Homepage
264 264 field_is_public: Public
265 265 field_parent: Subproject of
266 266 field_is_in_roadmap: Issues displayed in roadmap
267 267 field_login: Login
268 268 field_mail_notification: Email notifications
269 269 field_admin: Administrator
270 270 field_last_login_on: Last connection
271 271 field_language: Language
272 272 field_effective_date: Date
273 273 field_password: Password
274 274 field_new_password: New password
275 275 field_password_confirmation: Confirmation
276 276 field_version: Version
277 277 field_type: Type
278 278 field_host: Host
279 279 field_port: Port
280 280 field_account: Account
281 281 field_base_dn: Base DN
282 282 field_attr_login: Login attribute
283 283 field_attr_firstname: Firstname attribute
284 284 field_attr_lastname: Lastname attribute
285 285 field_attr_mail: Email attribute
286 286 field_onthefly: On-the-fly user creation
287 287 field_start_date: Start date
288 288 field_done_ratio: "% Done"
289 289 field_auth_source: Authentication mode
290 290 field_hide_mail: Hide my email address
291 291 field_comments: Comment
292 292 field_url: URL
293 293 field_start_page: Start page
294 294 field_subproject: Subproject
295 295 field_hours: Hours
296 296 field_activity: Activity
297 297 field_spent_on: Date
298 298 field_identifier: Identifier
299 299 field_is_filter: Used as a filter
300 300 field_issue_to: Related issue
301 301 field_delay: Delay
302 302 field_assignable: Issues can be assigned to this role
303 303 field_redirect_existing_links: Redirect existing links
304 304 field_estimated_hours: Estimated time
305 305 field_column_names: Columns
306 306 field_time_entries: Log time
307 307 field_time_zone: Time zone
308 308 field_searchable: Searchable
309 309 field_default_value: Default value
310 310 field_comments_sorting: Display comments
311 311 field_parent_title: Parent page
312 312 field_editable: Editable
313 313 field_watcher: Watcher
314 314 field_identity_url: OpenID URL
315 315 field_content: Content
316 316 field_group_by: Group results by
317 317 field_sharing: Sharing
318 318 field_parent_issue: Parent task
319 319 field_member_of_group: "Assignee's group"
320 320 field_assigned_to_role: "Assignee's role"
321 321 field_text: Text field
322 322 field_visible: Visible
323 323 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
324 324 field_issues_visibility: Issues visibility
325 325 field_is_private: Private
326 326 field_commit_logs_encoding: Commit messages encoding
327 327 field_scm_path_encoding: Path encoding
328 328 field_path_to_repository: Path to repository
329 329 field_root_directory: Root directory
330 330 field_cvsroot: CVSROOT
331 331 field_cvs_module: Module
332 332 field_repository_is_default: Main repository
333 333 field_multiple: Multiple values
334 334 field_auth_source_ldap_filter: LDAP filter
335 335 field_core_fields: Standard fields
336 336 field_timeout: "Timeout (in seconds)"
337 337 field_board_parent: Parent forum
338 338 field_private_notes: Private notes
339 339 field_inherit_members: Inherit members
340 340 field_generate_password: Generate password
341 341 field_must_change_passwd: Must change password at next logon
342 342 field_default_status: Default status
343 343 field_users_visibility: Users visibility
344 344 field_time_entries_visibility: Time logs visibility
345 345
346 346 setting_app_title: Application title
347 347 setting_app_subtitle: Application subtitle
348 348 setting_welcome_text: Welcome text
349 349 setting_default_language: Default language
350 350 setting_login_required: Authentication required
351 351 setting_self_registration: Self-registration
352 352 setting_attachment_max_size: Maximum attachment size
353 353 setting_issues_export_limit: Issues export limit
354 354 setting_mail_from: Emission email address
355 355 setting_bcc_recipients: Blind carbon copy recipients (bcc)
356 356 setting_plain_text_mail: Plain text mail (no HTML)
357 357 setting_host_name: Host name and path
358 358 setting_text_formatting: Text formatting
359 359 setting_wiki_compression: Wiki history compression
360 360 setting_feeds_limit: Maximum number of items in Atom feeds
361 361 setting_default_projects_public: New projects are public by default
362 362 setting_autofetch_changesets: Fetch commits automatically
363 363 setting_sys_api_enabled: Enable WS for repository management
364 364 setting_commit_ref_keywords: Referencing keywords
365 365 setting_commit_fix_keywords: Fixing keywords
366 366 setting_autologin: Autologin
367 367 setting_date_format: Date format
368 368 setting_time_format: Time format
369 369 setting_cross_project_issue_relations: Allow cross-project issue relations
370 370 setting_cross_project_subtasks: Allow cross-project subtasks
371 371 setting_issue_list_default_columns: Default columns displayed on the issue list
372 372 setting_repositories_encodings: Attachments and repositories encodings
373 373 setting_emails_header: Email header
374 374 setting_emails_footer: Email footer
375 375 setting_protocol: Protocol
376 376 setting_per_page_options: Objects per page options
377 377 setting_user_format: Users display format
378 378 setting_activity_days_default: Days displayed on project activity
379 379 setting_display_subprojects_issues: Display subprojects issues on main projects by default
380 380 setting_enabled_scm: Enabled SCM
381 381 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
382 382 setting_mail_handler_api_enabled: Enable WS for incoming emails
383 383 setting_mail_handler_api_key: API key
384 384 setting_sequential_project_identifiers: Generate sequential project identifiers
385 385 setting_gravatar_enabled: Use Gravatar user icons
386 386 setting_gravatar_default: Default Gravatar image
387 387 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
388 388 setting_file_max_size_displayed: Maximum size of text files displayed inline
389 389 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
390 390 setting_openid: Allow OpenID login and registration
391 391 setting_password_max_age: Require password change after
392 392 setting_password_min_length: Minimum password length
393 393 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
394 394 setting_default_projects_modules: Default enabled modules for new projects
395 395 setting_issue_done_ratio: Calculate the issue done ratio with
396 396 setting_issue_done_ratio_issue_field: Use the issue field
397 397 setting_issue_done_ratio_issue_status: Use the issue status
398 398 setting_start_of_week: Start calendars on
399 399 setting_rest_api_enabled: Enable REST web service
400 400 setting_cache_formatted_text: Cache formatted text
401 401 setting_default_notification_option: Default notification option
402 402 setting_commit_logtime_enabled: Enable time logging
403 403 setting_commit_logtime_activity_id: Activity for logged time
404 404 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
405 405 setting_issue_group_assignment: Allow issue assignment to groups
406 406 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
407 407 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
408 408 setting_unsubscribe: Allow users to delete their own account
409 409 setting_session_lifetime: Session maximum lifetime
410 410 setting_session_timeout: Session inactivity timeout
411 411 setting_thumbnails_enabled: Display attachment thumbnails
412 412 setting_thumbnails_size: Thumbnails size (in pixels)
413 413 setting_non_working_week_days: Non-working days
414 414 setting_jsonp_enabled: Enable JSONP support
415 415 setting_default_projects_tracker_ids: Default trackers for new projects
416 416 setting_mail_handler_excluded_filenames: Exclude attachments by name
417 417 setting_force_default_language_for_anonymous: Force default language for anonymous users
418 418 setting_force_default_language_for_loggedin: Force default language for logged-in users
419 419 setting_link_copied_issue: Link issues on copy
420 420 setting_max_additional_emails: Maximum number of additional email addresses
421 421 setting_search_results_per_page: Search results per page
422 422
423 423 permission_add_project: Create project
424 424 permission_add_subprojects: Create subprojects
425 425 permission_edit_project: Edit project
426 426 permission_close_project: Close / reopen the project
427 427 permission_select_project_modules: Select project modules
428 428 permission_manage_members: Manage members
429 429 permission_manage_project_activities: Manage project activities
430 430 permission_manage_versions: Manage versions
431 431 permission_manage_categories: Manage issue categories
432 432 permission_view_issues: View Issues
433 433 permission_add_issues: Add issues
434 434 permission_edit_issues: Edit issues
435 435 permission_copy_issues: Copy issues
436 436 permission_manage_issue_relations: Manage issue relations
437 437 permission_set_issues_private: Set issues public or private
438 438 permission_set_own_issues_private: Set own issues public or private
439 439 permission_add_issue_notes: Add notes
440 440 permission_edit_issue_notes: Edit notes
441 441 permission_edit_own_issue_notes: Edit own notes
442 442 permission_view_private_notes: View private notes
443 443 permission_set_notes_private: Set notes as private
444 444 permission_move_issues: Move issues
445 445 permission_delete_issues: Delete issues
446 446 permission_manage_public_queries: Manage public queries
447 447 permission_save_queries: Save queries
448 448 permission_view_gantt: View gantt chart
449 449 permission_view_calendar: View calendar
450 450 permission_view_issue_watchers: View watchers list
451 451 permission_add_issue_watchers: Add watchers
452 452 permission_delete_issue_watchers: Delete watchers
453 453 permission_log_time: Log spent time
454 454 permission_view_time_entries: View spent time
455 455 permission_edit_time_entries: Edit time logs
456 456 permission_edit_own_time_entries: Edit own time logs
457 457 permission_manage_news: Manage news
458 458 permission_comment_news: Comment news
459 459 permission_view_documents: View documents
460 460 permission_add_documents: Add documents
461 461 permission_edit_documents: Edit documents
462 462 permission_delete_documents: Delete documents
463 463 permission_manage_files: Manage files
464 464 permission_view_files: View files
465 465 permission_manage_wiki: Manage wiki
466 466 permission_rename_wiki_pages: Rename wiki pages
467 467 permission_delete_wiki_pages: Delete wiki pages
468 468 permission_view_wiki_pages: View wiki
469 469 permission_view_wiki_edits: View wiki history
470 470 permission_edit_wiki_pages: Edit wiki pages
471 471 permission_delete_wiki_pages_attachments: Delete attachments
472 472 permission_protect_wiki_pages: Protect wiki pages
473 473 permission_manage_repository: Manage repository
474 474 permission_browse_repository: Browse repository
475 475 permission_view_changesets: View changesets
476 476 permission_commit_access: Commit access
477 477 permission_manage_boards: Manage forums
478 478 permission_view_messages: View messages
479 479 permission_add_messages: Post messages
480 480 permission_edit_messages: Edit messages
481 481 permission_edit_own_messages: Edit own messages
482 482 permission_delete_messages: Delete messages
483 483 permission_delete_own_messages: Delete own messages
484 484 permission_export_wiki_pages: Export wiki pages
485 485 permission_manage_subtasks: Manage subtasks
486 486 permission_manage_related_issues: Manage related issues
487 487
488 488 project_module_issue_tracking: Issue tracking
489 489 project_module_time_tracking: Time tracking
490 490 project_module_news: News
491 491 project_module_documents: Documents
492 492 project_module_files: Files
493 493 project_module_wiki: Wiki
494 494 project_module_repository: Repository
495 495 project_module_boards: Forums
496 496 project_module_calendar: Calendar
497 497 project_module_gantt: Gantt
498 498
499 499 label_user: User
500 500 label_user_plural: Users
501 501 label_user_new: New user
502 502 label_user_anonymous: Anonymous
503 503 label_project: Project
504 504 label_project_new: New project
505 505 label_project_plural: Projects
506 506 label_x_projects:
507 507 zero: no projects
508 508 one: 1 project
509 509 other: "%{count} projects"
510 510 label_project_all: All Projects
511 511 label_project_latest: Latest projects
512 512 label_issue: Issue
513 513 label_issue_new: New issue
514 514 label_issue_plural: Issues
515 515 label_issue_view_all: View all issues
516 516 label_issues_by: "Issues by %{value}"
517 517 label_issue_added: Issue added
518 518 label_issue_updated: Issue updated
519 519 label_issue_note_added: Note added
520 520 label_issue_status_updated: Status updated
521 521 label_issue_assigned_to_updated: Assignee updated
522 522 label_issue_priority_updated: Priority updated
523 523 label_document: Document
524 524 label_document_new: New document
525 525 label_document_plural: Documents
526 526 label_document_added: Document added
527 527 label_role: Role
528 528 label_role_plural: Roles
529 529 label_role_new: New role
530 530 label_role_and_permissions: Roles and permissions
531 531 label_role_anonymous: Anonymous
532 532 label_role_non_member: Non member
533 533 label_member: Member
534 534 label_member_new: New member
535 535 label_member_plural: Members
536 536 label_tracker: Tracker
537 537 label_tracker_plural: Trackers
538 538 label_tracker_new: New tracker
539 539 label_workflow: Workflow
540 540 label_issue_status: Issue status
541 541 label_issue_status_plural: Issue statuses
542 542 label_issue_status_new: New status
543 543 label_issue_category: Issue category
544 544 label_issue_category_plural: Issue categories
545 545 label_issue_category_new: New category
546 546 label_custom_field: Custom field
547 547 label_custom_field_plural: Custom fields
548 548 label_custom_field_new: New custom field
549 549 label_enumerations: Enumerations
550 550 label_enumeration_new: New value
551 551 label_information: Information
552 552 label_information_plural: Information
553 553 label_please_login: Please log in
554 554 label_register: Register
555 555 label_login_with_open_id_option: or login with OpenID
556 556 label_password_lost: Lost password
557 label_password_required: Confirm your password to continue
557 558 label_home: Home
558 559 label_my_page: My page
559 560 label_my_account: My account
560 561 label_my_projects: My projects
561 562 label_my_page_block: My page block
562 563 label_administration: Administration
563 564 label_login: Sign in
564 565 label_logout: Sign out
565 566 label_help: Help
566 567 label_reported_issues: Reported issues
567 568 label_assigned_to_me_issues: Issues assigned to me
568 569 label_last_login: Last connection
569 570 label_registered_on: Registered on
570 571 label_activity: Activity
571 572 label_overall_activity: Overall activity
572 573 label_user_activity: "%{value}'s activity"
573 574 label_new: New
574 575 label_logged_as: Logged in as
575 576 label_environment: Environment
576 577 label_authentication: Authentication
577 578 label_auth_source: Authentication mode
578 579 label_auth_source_new: New authentication mode
579 580 label_auth_source_plural: Authentication modes
580 581 label_subproject_plural: Subprojects
581 582 label_subproject_new: New subproject
582 583 label_and_its_subprojects: "%{value} and its subprojects"
583 584 label_min_max_length: Min - Max length
584 585 label_list: List
585 586 label_date: Date
586 587 label_integer: Integer
587 588 label_float: Float
588 589 label_boolean: Boolean
589 590 label_string: Text
590 591 label_text: Long text
591 592 label_attribute: Attribute
592 593 label_attribute_plural: Attributes
593 594 label_no_data: No data to display
594 595 label_change_status: Change status
595 596 label_history: History
596 597 label_attachment: File
597 598 label_attachment_new: New file
598 599 label_attachment_delete: Delete file
599 600 label_attachment_plural: Files
600 601 label_file_added: File added
601 602 label_report: Report
602 603 label_report_plural: Reports
603 604 label_news: News
604 605 label_news_new: Add news
605 606 label_news_plural: News
606 607 label_news_latest: Latest news
607 608 label_news_view_all: View all news
608 609 label_news_added: News added
609 610 label_news_comment_added: Comment added to a news
610 611 label_settings: Settings
611 612 label_overview: Overview
612 613 label_version: Version
613 614 label_version_new: New version
614 615 label_version_plural: Versions
615 616 label_close_versions: Close completed versions
616 617 label_confirmation: Confirmation
617 618 label_export_to: 'Also available in:'
618 619 label_read: Read...
619 620 label_public_projects: Public projects
620 621 label_open_issues: open
621 622 label_open_issues_plural: open
622 623 label_closed_issues: closed
623 624 label_closed_issues_plural: closed
624 625 label_x_open_issues_abbr_on_total:
625 626 zero: 0 open / %{total}
626 627 one: 1 open / %{total}
627 628 other: "%{count} open / %{total}"
628 629 label_x_open_issues_abbr:
629 630 zero: 0 open
630 631 one: 1 open
631 632 other: "%{count} open"
632 633 label_x_closed_issues_abbr:
633 634 zero: 0 closed
634 635 one: 1 closed
635 636 other: "%{count} closed"
636 637 label_x_issues:
637 638 zero: 0 issues
638 639 one: 1 issue
639 640 other: "%{count} issues"
640 641 label_total: Total
641 642 label_total_time: Total time
642 643 label_permissions: Permissions
643 644 label_current_status: Current status
644 645 label_new_statuses_allowed: New statuses allowed
645 646 label_all: all
646 647 label_any: any
647 648 label_none: none
648 649 label_nobody: nobody
649 650 label_next: Next
650 651 label_previous: Previous
651 652 label_used_by: Used by
652 653 label_details: Details
653 654 label_add_note: Add a note
654 655 label_calendar: Calendar
655 656 label_months_from: months from
656 657 label_gantt: Gantt
657 658 label_internal: Internal
658 659 label_last_changes: "last %{count} changes"
659 660 label_change_view_all: View all changes
660 661 label_personalize_page: Personalize this page
661 662 label_comment: Comment
662 663 label_comment_plural: Comments
663 664 label_x_comments:
664 665 zero: no comments
665 666 one: 1 comment
666 667 other: "%{count} comments"
667 668 label_comment_add: Add a comment
668 669 label_comment_added: Comment added
669 670 label_comment_delete: Delete comments
670 671 label_query: Custom query
671 672 label_query_plural: Custom queries
672 673 label_query_new: New query
673 674 label_my_queries: My custom queries
674 675 label_filter_add: Add filter
675 676 label_filter_plural: Filters
676 677 label_equals: is
677 678 label_not_equals: is not
678 679 label_in_less_than: in less than
679 680 label_in_more_than: in more than
680 681 label_in_the_next_days: in the next
681 682 label_in_the_past_days: in the past
682 683 label_greater_or_equal: '>='
683 684 label_less_or_equal: '<='
684 685 label_between: between
685 686 label_in: in
686 687 label_today: today
687 688 label_all_time: all time
688 689 label_yesterday: yesterday
689 690 label_this_week: this week
690 691 label_last_week: last week
691 692 label_last_n_weeks: "last %{count} weeks"
692 693 label_last_n_days: "last %{count} days"
693 694 label_this_month: this month
694 695 label_last_month: last month
695 696 label_this_year: this year
696 697 label_date_range: Date range
697 698 label_less_than_ago: less than days ago
698 699 label_more_than_ago: more than days ago
699 700 label_ago: days ago
700 701 label_contains: contains
701 702 label_not_contains: doesn't contain
702 703 label_any_issues_in_project: any issues in project
703 704 label_any_issues_not_in_project: any issues not in project
704 705 label_no_issues_in_project: no issues in project
705 706 label_day_plural: days
706 707 label_repository: Repository
707 708 label_repository_new: New repository
708 709 label_repository_plural: Repositories
709 710 label_browse: Browse
710 711 label_branch: Branch
711 712 label_tag: Tag
712 713 label_revision: Revision
713 714 label_revision_plural: Revisions
714 715 label_revision_id: "Revision %{value}"
715 716 label_associated_revisions: Associated revisions
716 717 label_added: added
717 718 label_modified: modified
718 719 label_copied: copied
719 720 label_renamed: renamed
720 721 label_deleted: deleted
721 722 label_latest_revision: Latest revision
722 723 label_latest_revision_plural: Latest revisions
723 724 label_view_revisions: View revisions
724 725 label_view_all_revisions: View all revisions
725 726 label_max_size: Maximum size
726 727 label_sort_highest: Move to top
727 728 label_sort_higher: Move up
728 729 label_sort_lower: Move down
729 730 label_sort_lowest: Move to bottom
730 731 label_roadmap: Roadmap
731 732 label_roadmap_due_in: "Due in %{value}"
732 733 label_roadmap_overdue: "%{value} late"
733 734 label_roadmap_no_issues: No issues for this version
734 735 label_search: Search
735 736 label_result_plural: Results
736 737 label_all_words: All words
737 738 label_wiki: Wiki
738 739 label_wiki_edit: Wiki edit
739 740 label_wiki_edit_plural: Wiki edits
740 741 label_wiki_page: Wiki page
741 742 label_wiki_page_plural: Wiki pages
742 743 label_index_by_title: Index by title
743 744 label_index_by_date: Index by date
744 745 label_current_version: Current version
745 746 label_preview: Preview
746 747 label_feed_plural: Feeds
747 748 label_changes_details: Details of all changes
748 749 label_issue_tracking: Issue tracking
749 750 label_spent_time: Spent time
750 751 label_overall_spent_time: Overall spent time
751 752 label_f_hour: "%{value} hour"
752 753 label_f_hour_plural: "%{value} hours"
753 754 label_time_tracking: Time tracking
754 755 label_change_plural: Changes
755 756 label_statistics: Statistics
756 757 label_commits_per_month: Commits per month
757 758 label_commits_per_author: Commits per author
758 759 label_diff: diff
759 760 label_view_diff: View differences
760 761 label_diff_inline: inline
761 762 label_diff_side_by_side: side by side
762 763 label_options: Options
763 764 label_copy_workflow_from: Copy workflow from
764 765 label_permissions_report: Permissions report
765 766 label_watched_issues: Watched issues
766 767 label_related_issues: Related issues
767 768 label_applied_status: Applied status
768 769 label_loading: Loading...
769 770 label_relation_new: New relation
770 771 label_relation_delete: Delete relation
771 772 label_relates_to: Related to
772 773 label_duplicates: Duplicates
773 774 label_duplicated_by: Duplicated by
774 775 label_blocks: Blocks
775 776 label_blocked_by: Blocked by
776 777 label_precedes: Precedes
777 778 label_follows: Follows
778 779 label_copied_to: Copied to
779 780 label_copied_from: Copied from
780 781 label_end_to_start: end to start
781 782 label_end_to_end: end to end
782 783 label_start_to_start: start to start
783 784 label_start_to_end: start to end
784 785 label_stay_logged_in: Stay logged in
785 786 label_disabled: disabled
786 787 label_show_completed_versions: Show completed versions
787 788 label_me: me
788 789 label_board: Forum
789 790 label_board_new: New forum
790 791 label_board_plural: Forums
791 792 label_board_locked: Locked
792 793 label_board_sticky: Sticky
793 794 label_topic_plural: Topics
794 795 label_message_plural: Messages
795 796 label_message_last: Last message
796 797 label_message_new: New message
797 798 label_message_posted: Message added
798 799 label_reply_plural: Replies
799 800 label_send_information: Send account information to the user
800 801 label_year: Year
801 802 label_month: Month
802 803 label_week: Week
803 804 label_date_from: From
804 805 label_date_to: To
805 806 label_language_based: Based on user's language
806 807 label_sort_by: "Sort by %{value}"
807 808 label_send_test_email: Send a test email
808 809 label_feeds_access_key: Atom access key
809 810 label_missing_feeds_access_key: Missing a Atom access key
810 811 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
811 812 label_module_plural: Modules
812 813 label_added_time_by: "Added by %{author} %{age} ago"
813 814 label_updated_time_by: "Updated by %{author} %{age} ago"
814 815 label_updated_time: "Updated %{value} ago"
815 816 label_jump_to_a_project: Jump to a project...
816 817 label_file_plural: Files
817 818 label_changeset_plural: Changesets
818 819 label_default_columns: Default columns
819 820 label_no_change_option: (No change)
820 821 label_bulk_edit_selected_issues: Bulk edit selected issues
821 822 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
822 823 label_theme: Theme
823 824 label_default: Default
824 825 label_search_titles_only: Search titles only
825 826 label_user_mail_option_all: "For any event on all my projects"
826 827 label_user_mail_option_selected: "For any event on the selected projects only..."
827 828 label_user_mail_option_none: "No events"
828 829 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
829 830 label_user_mail_option_only_assigned: "Only for things I am assigned to"
830 831 label_user_mail_option_only_owner: "Only for things I am the owner of"
831 832 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
832 833 label_registration_activation_by_email: account activation by email
833 834 label_registration_manual_activation: manual account activation
834 835 label_registration_automatic_activation: automatic account activation
835 836 label_display_per_page: "Per page: %{value}"
836 837 label_age: Age
837 838 label_change_properties: Change properties
838 839 label_general: General
839 840 label_more: More
840 841 label_scm: SCM
841 842 label_plugins: Plugins
842 843 label_ldap_authentication: LDAP authentication
843 844 label_downloads_abbr: D/L
844 845 label_optional_description: Optional description
845 846 label_add_another_file: Add another file
846 847 label_preferences: Preferences
847 848 label_chronological_order: In chronological order
848 849 label_reverse_chronological_order: In reverse chronological order
849 850 label_planning: Planning
850 851 label_incoming_emails: Incoming emails
851 852 label_generate_key: Generate a key
852 853 label_issue_watchers: Watchers
853 854 label_example: Example
854 855 label_display: Display
855 856 label_sort: Sort
856 857 label_ascending: Ascending
857 858 label_descending: Descending
858 859 label_date_from_to: From %{start} to %{end}
859 860 label_wiki_content_added: Wiki page added
860 861 label_wiki_content_updated: Wiki page updated
861 862 label_group: Group
862 863 label_group_plural: Groups
863 864 label_group_new: New group
864 865 label_group_anonymous: Anonymous users
865 866 label_group_non_member: Non member users
866 867 label_time_entry_plural: Spent time
867 868 label_version_sharing_none: Not shared
868 869 label_version_sharing_descendants: With subprojects
869 870 label_version_sharing_hierarchy: With project hierarchy
870 871 label_version_sharing_tree: With project tree
871 872 label_version_sharing_system: With all projects
872 873 label_update_issue_done_ratios: Update issue done ratios
873 874 label_copy_source: Source
874 875 label_copy_target: Target
875 876 label_copy_same_as_target: Same as target
876 877 label_display_used_statuses_only: Only display statuses that are used by this tracker
877 878 label_api_access_key: API access key
878 879 label_missing_api_access_key: Missing an API access key
879 880 label_api_access_key_created_on: "API access key created %{value} ago"
880 881 label_profile: Profile
881 882 label_subtask_plural: Subtasks
882 883 label_project_copy_notifications: Send email notifications during the project copy
883 884 label_principal_search: "Search for user or group:"
884 885 label_user_search: "Search for user:"
885 886 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
886 887 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
887 888 label_issues_visibility_all: All issues
888 889 label_issues_visibility_public: All non private issues
889 890 label_issues_visibility_own: Issues created by or assigned to the user
890 891 label_git_report_last_commit: Report last commit for files and directories
891 892 label_parent_revision: Parent
892 893 label_child_revision: Child
893 894 label_export_options: "%{export_format} export options"
894 895 label_copy_attachments: Copy attachments
895 896 label_copy_subtasks: Copy subtasks
896 897 label_item_position: "%{position} of %{count}"
897 898 label_completed_versions: Completed versions
898 899 label_search_for_watchers: Search for watchers to add
899 900 label_session_expiration: Session expiration
900 901 label_show_closed_projects: View closed projects
901 902 label_status_transitions: Status transitions
902 903 label_fields_permissions: Fields permissions
903 904 label_readonly: Read-only
904 905 label_required: Required
905 906 label_hidden: Hidden
906 907 label_attribute_of_project: "Project's %{name}"
907 908 label_attribute_of_issue: "Issue's %{name}"
908 909 label_attribute_of_author: "Author's %{name}"
909 910 label_attribute_of_assigned_to: "Assignee's %{name}"
910 911 label_attribute_of_user: "User's %{name}"
911 912 label_attribute_of_fixed_version: "Target version's %{name}"
912 913 label_cross_project_descendants: With subprojects
913 914 label_cross_project_tree: With project tree
914 915 label_cross_project_hierarchy: With project hierarchy
915 916 label_cross_project_system: With all projects
916 917 label_gantt_progress_line: Progress line
917 918 label_visibility_private: to me only
918 919 label_visibility_roles: to these roles only
919 920 label_visibility_public: to any users
920 921 label_link: Link
921 922 label_only: only
922 923 label_drop_down_list: drop-down list
923 924 label_checkboxes: checkboxes
924 925 label_radio_buttons: radio buttons
925 926 label_link_values_to: Link values to URL
926 927 label_custom_field_select_type: Select the type of object to which the custom field is to be attached
927 928 label_check_for_updates: Check for updates
928 929 label_latest_compatible_version: Latest compatible version
929 930 label_unknown_plugin: Unknown plugin
930 931 label_add_projects: Add projects
931 932 label_users_visibility_all: All active users
932 933 label_users_visibility_members_of_visible_projects: Members of visible projects
933 934 label_edit_attachments: Edit attached files
934 935 label_link_copied_issue: Link copied issue
935 936 label_ask: Ask
936 937 label_search_attachments_yes: Search attachment filenames and descriptions
937 938 label_search_attachments_no: Do not search attachments
938 939 label_search_attachments_only: Search attachments only
939 940 label_search_open_issues_only: Open issues only
940 941 label_email_address_plural: Emails
941 942 label_email_address_add: Add email address
942 943 label_enable_notifications: Enable notifications
943 944 label_disable_notifications: Disable notifications
944 945 label_blank_value: blank
945 946 label_parent_task_attributes: Parent tasks attributes
946 947 label_parent_task_attributes_derived: Calculated from subtasks
947 948 label_parent_task_attributes_independent: Independent of subtasks
948 949 label_time_entries_visibility_all: All time entries
949 950 label_time_entries_visibility_own: Time entries created by the user
950 951 label_member_management: Member management
951 952 label_member_management_all_roles: All roles
952 953 label_member_management_selected_roles_only: Only these roles
953 954
954 955 button_login: Login
955 956 button_submit: Submit
956 957 button_save: Save
957 958 button_check_all: Check all
958 959 button_uncheck_all: Uncheck all
959 960 button_collapse_all: Collapse all
960 961 button_expand_all: Expand all
961 962 button_delete: Delete
962 963 button_create: Create
963 964 button_create_and_continue: Create and continue
964 965 button_test: Test
965 966 button_edit: Edit
966 967 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
967 968 button_add: Add
968 969 button_change: Change
969 970 button_apply: Apply
970 971 button_clear: Clear
971 972 button_lock: Lock
972 973 button_unlock: Unlock
973 974 button_download: Download
974 975 button_list: List
975 976 button_view: View
976 977 button_move: Move
977 978 button_move_and_follow: Move and follow
978 979 button_back: Back
979 980 button_cancel: Cancel
980 981 button_activate: Activate
981 982 button_sort: Sort
982 983 button_log_time: Log time
983 984 button_rollback: Rollback to this version
984 985 button_watch: Watch
985 986 button_unwatch: Unwatch
986 987 button_reply: Reply
987 988 button_archive: Archive
988 989 button_unarchive: Unarchive
989 990 button_reset: Reset
990 991 button_rename: Rename
991 992 button_change_password: Change password
993 button_confirm_password: Confirm password
992 994 button_copy: Copy
993 995 button_copy_and_follow: Copy and follow
994 996 button_annotate: Annotate
995 997 button_update: Update
996 998 button_configure: Configure
997 999 button_quote: Quote
998 1000 button_duplicate: Duplicate
999 1001 button_show: Show
1000 1002 button_hide: Hide
1001 1003 button_edit_section: Edit this section
1002 1004 button_export: Export
1003 1005 button_delete_my_account: Delete my account
1004 1006 button_close: Close
1005 1007 button_reopen: Reopen
1006 1008
1007 1009 status_active: active
1008 1010 status_registered: registered
1009 1011 status_locked: locked
1010 1012
1011 1013 project_status_active: active
1012 1014 project_status_closed: closed
1013 1015 project_status_archived: archived
1014 1016
1015 1017 version_status_open: open
1016 1018 version_status_locked: locked
1017 1019 version_status_closed: closed
1018 1020
1019 1021 field_active: Active
1020 1022
1021 1023 text_select_mail_notifications: Select actions for which email notifications should be sent.
1022 1024 text_regexp_info: eg. ^[A-Z0-9]+$
1023 1025 text_min_max_length_info: 0 means no restriction
1024 1026 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
1025 1027 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
1026 1028 text_workflow_edit: Select a role and a tracker to edit the workflow
1027 1029 text_are_you_sure: Are you sure?
1028 1030 text_journal_changed: "%{label} changed from %{old} to %{new}"
1029 1031 text_journal_changed_no_detail: "%{label} updated"
1030 1032 text_journal_set_to: "%{label} set to %{value}"
1031 1033 text_journal_deleted: "%{label} deleted (%{old})"
1032 1034 text_journal_added: "%{label} %{value} added"
1033 1035 text_tip_issue_begin_day: issue beginning this day
1034 1036 text_tip_issue_end_day: issue ending this day
1035 1037 text_tip_issue_begin_end_day: issue beginning and ending this day
1036 1038 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed.'
1037 1039 text_caracters_maximum: "%{count} characters maximum."
1038 1040 text_caracters_minimum: "Must be at least %{count} characters long."
1039 1041 text_length_between: "Length between %{min} and %{max} characters."
1040 1042 text_tracker_no_workflow: No workflow defined for this tracker
1041 1043 text_unallowed_characters: Unallowed characters
1042 1044 text_comma_separated: Multiple values allowed (comma separated).
1043 1045 text_line_separated: Multiple values allowed (one line for each value).
1044 1046 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
1045 1047 text_issue_added: "Issue %{id} has been reported by %{author}."
1046 1048 text_issue_updated: "Issue %{id} has been updated by %{author}."
1047 1049 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
1048 1050 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
1049 1051 text_issue_category_destroy_assignments: Remove category assignments
1050 1052 text_issue_category_reassign_to: Reassign issues to this category
1051 1053 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)."
1052 1054 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."
1053 1055 text_load_default_configuration: Load the default configuration
1054 1056 text_status_changed_by_changeset: "Applied in changeset %{value}."
1055 1057 text_time_logged_by_changeset: "Applied in changeset %{value}."
1056 1058 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1057 1059 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1058 1060 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1059 1061 text_select_project_modules: 'Select modules to enable for this project:'
1060 1062 text_default_administrator_account_changed: Default administrator account changed
1061 1063 text_file_repository_writable: Attachments directory writable
1062 1064 text_plugin_assets_writable: Plugin assets directory writable
1063 1065 text_rmagick_available: RMagick available (optional)
1064 1066 text_convert_available: ImageMagick convert available (optional)
1065 1067 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
1066 1068 text_destroy_time_entries: Delete reported hours
1067 1069 text_assign_time_entries_to_project: Assign reported hours to the project
1068 1070 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1069 1071 text_user_wrote: "%{value} wrote:"
1070 1072 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1071 1073 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1072 1074 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."
1073 1075 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."
1074 1076 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1075 1077 text_custom_field_possible_values_info: 'One line for each value'
1076 1078 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1077 1079 text_wiki_page_nullify_children: "Keep child pages as root pages"
1078 1080 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1079 1081 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1080 1082 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?"
1081 1083 text_zoom_in: Zoom in
1082 1084 text_zoom_out: Zoom out
1083 1085 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1084 1086 text_scm_path_encoding_note: "Default: UTF-8"
1085 1087 text_subversion_repository_note: "Examples: file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1086 1088 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1087 1089 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1088 1090 text_scm_command: Command
1089 1091 text_scm_command_version: Version
1090 1092 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1091 1093 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1092 1094 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1093 1095 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1094 1096 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1095 1097 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1096 1098 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1097 1099 text_project_closed: This project is closed and read-only.
1098 1100 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
1099 1101
1100 1102 default_role_manager: Manager
1101 1103 default_role_developer: Developer
1102 1104 default_role_reporter: Reporter
1103 1105 default_tracker_bug: Bug
1104 1106 default_tracker_feature: Feature
1105 1107 default_tracker_support: Support
1106 1108 default_issue_status_new: New
1107 1109 default_issue_status_in_progress: In Progress
1108 1110 default_issue_status_resolved: Resolved
1109 1111 default_issue_status_feedback: Feedback
1110 1112 default_issue_status_closed: Closed
1111 1113 default_issue_status_rejected: Rejected
1112 1114 default_doc_category_user: User documentation
1113 1115 default_doc_category_tech: Technical documentation
1114 1116 default_priority_low: Low
1115 1117 default_priority_normal: Normal
1116 1118 default_priority_high: High
1117 1119 default_priority_urgent: Urgent
1118 1120 default_priority_immediate: Immediate
1119 1121 default_activity_design: Design
1120 1122 default_activity_development: Development
1121 1123
1122 1124 enumeration_issue_priorities: Issue priorities
1123 1125 enumeration_doc_categories: Document categories
1124 1126 enumeration_activities: Activities (time tracking)
1125 1127 enumeration_system_activity: System Activity
1126 1128 description_filter: Filter
1127 1129 description_search: Searchfield
1128 1130 description_choose_project: Projects
1129 1131 description_project_scope: Search scope
1130 1132 description_notes: Notes
1131 1133 description_message_content: Message content
1132 1134 description_query_sort_criteria_attribute: Sort attribute
1133 1135 description_query_sort_criteria_direction: Sort direction
1134 1136 description_user_mail_notification: Mail notification settings
1135 1137 description_available_columns: Available Columns
1136 1138 description_selected_columns: Selected Columns
1137 1139 description_all_columns: All Columns
1138 1140 description_issue_category_reassign: Choose issue category
1139 1141 description_wiki_subpages_reassign: Choose new parent page
1140 1142 description_date_range_list: Choose range from list
1141 1143 description_date_range_interval: Choose range by selecting start and end date
1142 1144 description_date_from: Enter start date
1143 1145 description_date_to: Enter end date
1144 1146 text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
@@ -1,365 +1,366
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 Rails.application.routes.draw do
19 19 root :to => 'welcome#index', :as => 'home'
20 20
21 21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
22 22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
23 23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 25 match 'account/activate', :to => 'account#activate', :via => :get
26 26 get 'account/activation_email', :to => 'account#activation_email', :as => 'activation_email'
27 27
28 28 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put, :patch]
29 29 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put, :patch]
30 30 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put, :patch]
31 31 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put, :patch]
32 32
33 33 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
34 34 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
35 35
36 36 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
37 37 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
38 38 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
39 39 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
40 40
41 41 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
42 42 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
43 43 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
44 44 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
45 45
46 46 # Misc issue routes. TODO: move into resources
47 47 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
48 48 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
49 49 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
50 50 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
51 51
52 52 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
53 53 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
54 54
55 55 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
56 56 get '/issues/gantt', :to => 'gantts#show'
57 57
58 58 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
59 59 get '/issues/calendar', :to => 'calendars#show'
60 60
61 61 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
62 62 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
63 63
64 64 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
65 65 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
66 66 match 'my/page', :controller => 'my', :action => 'page', :via => :get
67 67 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
68 68 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
69 69 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
70 match 'my/show_api_key', :controller => 'my', :action => 'show_api_key', :via => :get
70 71 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
71 72 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
72 73 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
73 74 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
74 75 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
75 76
76 77 resources :users do
77 78 resources :memberships, :controller => 'principal_memberships'
78 79 resources :email_addresses, :only => [:index, :create, :update, :destroy]
79 80 end
80 81
81 82 post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
82 83 delete 'watchers/watch', :to => 'watchers#unwatch'
83 84 get 'watchers/new', :to => 'watchers#new'
84 85 post 'watchers', :to => 'watchers#create'
85 86 post 'watchers/append', :to => 'watchers#append'
86 87 delete 'watchers', :to => 'watchers#destroy'
87 88 get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
88 89 # Specific routes for issue watchers API
89 90 post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
90 91 delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue'
91 92
92 93 resources :projects do
93 94 member do
94 95 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
95 96 post 'modules'
96 97 post 'archive'
97 98 post 'unarchive'
98 99 post 'close'
99 100 post 'reopen'
100 101 match 'copy', :via => [:get, :post]
101 102 end
102 103
103 104 shallow do
104 105 resources :memberships, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
105 106 collection do
106 107 get 'autocomplete'
107 108 end
108 109 end
109 110 end
110 111
111 112 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
112 113
113 114 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
114 115 resources :issues, :only => [:index, :new, :create]
115 116 # Used when updating the form of a new issue
116 117 post 'issues/new', :to => 'issues#new'
117 118
118 119 resources :files, :only => [:index, :new, :create]
119 120
120 121 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
121 122 collection do
122 123 put 'close_completed'
123 124 end
124 125 end
125 126 get 'versions.:format', :to => 'versions#index'
126 127 get 'roadmap', :to => 'versions#index', :format => false
127 128 get 'versions', :to => 'versions#index'
128 129
129 130 resources :news, :except => [:show, :edit, :update, :destroy]
130 131 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
131 132 get 'report', :on => :collection
132 133 end
133 134 resources :queries, :only => [:new, :create]
134 135 shallow do
135 136 resources :issue_categories
136 137 end
137 138 resources :documents, :except => [:show, :edit, :update, :destroy]
138 139 resources :boards
139 140 shallow do
140 141 resources :repositories, :except => [:index, :show] do
141 142 member do
142 143 match 'committers', :via => [:get, :post]
143 144 end
144 145 end
145 146 end
146 147
147 148 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
148 149 resources :wiki, :except => [:index, :new, :create], :as => 'wiki_page' do
149 150 member do
150 151 get 'rename'
151 152 post 'rename'
152 153 get 'history'
153 154 get 'diff'
154 155 match 'preview', :via => [:post, :put, :patch]
155 156 post 'protect'
156 157 post 'add_attachment'
157 158 end
158 159 collection do
159 160 get 'export'
160 161 get 'date_index'
161 162 end
162 163 end
163 164 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
164 165 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
165 166 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
166 167 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
167 168 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
168 169 end
169 170
170 171 resources :issues do
171 172 member do
172 173 # Used when updating the form of an existing issue
173 174 patch 'edit', :to => 'issues#edit'
174 175 end
175 176 collection do
176 177 match 'bulk_edit', :via => [:get, :post]
177 178 post 'bulk_update'
178 179 end
179 180 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
180 181 collection do
181 182 get 'report'
182 183 end
183 184 end
184 185 shallow do
185 186 resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
186 187 end
187 188 end
188 189 # Used when updating the form of a new issue outside a project
189 190 post '/issues/new', :to => 'issues#new'
190 191 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
191 192
192 193 resources :queries, :except => [:show]
193 194
194 195 resources :news, :only => [:index, :show, :edit, :update, :destroy]
195 196 match '/news/:id/comments', :to => 'comments#create', :via => :post
196 197 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
197 198
198 199 resources :versions, :only => [:show, :edit, :update, :destroy] do
199 200 post 'status_by', :on => :member
200 201 end
201 202
202 203 resources :documents, :only => [:show, :edit, :update, :destroy] do
203 204 post 'add_attachment', :on => :member
204 205 end
205 206
206 207 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
207 208
208 209 resources :time_entries, :controller => 'timelog', :except => :destroy do
209 210 collection do
210 211 get 'report'
211 212 get 'bulk_edit'
212 213 post 'bulk_update'
213 214 end
214 215 end
215 216 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
216 217 # TODO: delete /time_entries for bulk deletion
217 218 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
218 219 # Used to update the new time entry form
219 220 post '/time_entries/new', :to => 'timelog#new'
220 221
221 222 get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity
222 223 get 'activity', :to => 'activities#index'
223 224
224 225 # repositories routes
225 226 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
226 227 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
227 228
228 229 get 'projects/:id/repository/:repository_id/changes(/*path)',
229 230 :to => 'repositories#changes',
230 231 :format => false
231 232
232 233 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
233 234 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
234 235 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
235 236 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
236 237 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
237 238 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path)',
238 239 :controller => 'repositories',
239 240 :format => false,
240 241 :constraints => {
241 242 :action => /(browse|show|entry|raw|annotate|diff)/,
242 243 :rev => /[a-z0-9\.\-_]+/
243 244 }
244 245
245 246 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
246 247 get 'projects/:id/repository/graph', :to => 'repositories#graph'
247 248
248 249 get 'projects/:id/repository/changes(/*path)',
249 250 :to => 'repositories#changes',
250 251 :format => false
251 252
252 253 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
253 254 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
254 255 get 'projects/:id/repository/revision', :to => 'repositories#revision'
255 256 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
256 257 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
257 258 get 'projects/:id/repository/revisions/:rev/:action(/*path)',
258 259 :controller => 'repositories',
259 260 :format => false,
260 261 :constraints => {
261 262 :action => /(browse|show|entry|raw|annotate|diff)/,
262 263 :rev => /[a-z0-9\.\-_]+/
263 264 }
264 265 get 'projects/:id/repository/:repository_id/:action(/*path)',
265 266 :controller => 'repositories',
266 267 :action => /(browse|show|entry|raw|changes|annotate|diff)/,
267 268 :format => false
268 269 get 'projects/:id/repository/:action(/*path)',
269 270 :controller => 'repositories',
270 271 :action => /(browse|show|entry|raw|changes|annotate|diff)/,
271 272 :format => false
272 273
273 274 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
274 275 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
275 276
276 277 # additional routes for having the file name at the end of url
277 278 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
278 279 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
279 280 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
280 281 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
281 282 resources :attachments, :only => [:show, :destroy]
282 283 get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit', :as => :object_attachments_edit
283 284 patch 'attachments/:object_type/:object_id', :to => 'attachments#update', :as => :object_attachments
284 285
285 286 resources :groups do
286 287 resources :memberships, :controller => 'principal_memberships'
287 288 member do
288 289 get 'autocomplete_for_user'
289 290 end
290 291 end
291 292
292 293 get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users'
293 294 post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users'
294 295 delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
295 296
296 297 resources :trackers, :except => :show do
297 298 collection do
298 299 match 'fields', :via => [:get, :post]
299 300 end
300 301 end
301 302 resources :issue_statuses, :except => :show do
302 303 collection do
303 304 post 'update_issue_done_ratio'
304 305 end
305 306 end
306 307 resources :custom_fields, :except => :show
307 308 resources :roles do
308 309 collection do
309 310 match 'permissions', :via => [:get, :post]
310 311 end
311 312 end
312 313 resources :enumerations, :except => :show
313 314 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
314 315
315 316 get 'projects/:id/search', :controller => 'search', :action => 'index'
316 317 get 'search', :controller => 'search', :action => 'index'
317 318
318 319
319 320 get 'mail_handler', :to => 'mail_handler#new'
320 321 post 'mail_handler', :to => 'mail_handler#index'
321 322
322 323 match 'admin', :controller => 'admin', :action => 'index', :via => :get
323 324 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
324 325 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
325 326 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
326 327 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
327 328 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
328 329
329 330 resources :auth_sources do
330 331 member do
331 332 get 'test_connection', :as => 'try_connection'
332 333 end
333 334 collection do
334 335 get 'autocomplete_for_new_user'
335 336 end
336 337 end
337 338
338 339 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
339 340 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
340 341 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
341 342 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
342 343 match 'settings', :controller => 'settings', :action => 'index', :via => :get
343 344 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
344 345 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
345 346
346 347 match 'sys/projects', :to => 'sys#projects', :via => :get
347 348 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
348 349 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post]
349 350
350 351 match 'uploads', :to => 'attachments#upload', :via => :post
351 352
352 353 get 'robots.txt', :to => 'welcome#robots'
353 354
354 355 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
355 356 file = File.join(plugin_dir, "config/routes.rb")
356 357 if File.exists?(file)
357 358 begin
358 359 instance_eval File.read(file)
359 360 rescue Exception => e
360 361 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
361 362 exit 1
362 363 end
363 364 end
364 365 end
365 366 end
@@ -1,173 +1,174
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class AuthSourcesControllerTest < ActionController::TestCase
21 21 fixtures :users, :auth_sources
22 22
23 23 def setup
24 24 @request.session[:user_id] = 1
25 Redmine::SudoMode.disable!
25 26 end
26 27
27 28 def test_index
28 29 get :index
29 30
30 31 assert_response :success
31 32 assert_template 'index'
32 33 assert_not_nil assigns(:auth_sources)
33 34 end
34 35
35 36 def test_new
36 37 get :new
37 38
38 39 assert_response :success
39 40 assert_template 'new'
40 41
41 42 source = assigns(:auth_source)
42 43 assert_equal AuthSourceLdap, source.class
43 44 assert source.new_record?
44 45
45 46 assert_select 'form#auth_source_form' do
46 47 assert_select 'input[name=type][value=AuthSourceLdap]'
47 48 assert_select 'input[name=?]', 'auth_source[host]'
48 49 end
49 50 end
50 51
51 52 def test_new_with_invalid_type_should_respond_with_404
52 53 get :new, :type => 'foo'
53 54 assert_response 404
54 55 end
55 56
56 57 def test_create
57 58 assert_difference 'AuthSourceLdap.count' do
58 59 post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '127.0.0.1', :port => '389', :attr_login => 'cn'}
59 60 assert_redirected_to '/auth_sources'
60 61 end
61 62
62 63 source = AuthSourceLdap.order('id DESC').first
63 64 assert_equal 'Test', source.name
64 65 assert_equal '127.0.0.1', source.host
65 66 assert_equal 389, source.port
66 67 assert_equal 'cn', source.attr_login
67 68 end
68 69
69 70 def test_create_with_failure
70 71 assert_no_difference 'AuthSourceLdap.count' do
71 72 post :create, :type => 'AuthSourceLdap',
72 73 :auth_source => {:name => 'Test', :host => '',
73 74 :port => '389', :attr_login => 'cn'}
74 75 assert_response :success
75 76 assert_template 'new'
76 77 end
77 78 assert_select_error /host cannot be blank/i
78 79 end
79 80
80 81 def test_edit
81 82 get :edit, :id => 1
82 83
83 84 assert_response :success
84 85 assert_template 'edit'
85 86
86 87 assert_select 'form#auth_source_form' do
87 88 assert_select 'input[name=?]', 'auth_source[host]'
88 89 end
89 90 end
90 91
91 92 def test_edit_should_not_contain_password
92 93 AuthSource.find(1).update_column :account_password, 'secret'
93 94
94 95 get :edit, :id => 1
95 96 assert_response :success
96 97 assert_select 'input[value=secret]', 0
97 98 assert_select 'input[name=dummy_password][value^=xxxxxx]'
98 99 end
99 100
100 101 def test_edit_invalid_should_respond_with_404
101 102 get :edit, :id => 99
102 103 assert_response 404
103 104 end
104 105
105 106 def test_update
106 107 put :update, :id => 1,
107 108 :auth_source => {:name => 'Renamed', :host => '192.168.0.10',
108 109 :port => '389', :attr_login => 'uid'}
109 110 assert_redirected_to '/auth_sources'
110 111 source = AuthSourceLdap.find(1)
111 112 assert_equal 'Renamed', source.name
112 113 assert_equal '192.168.0.10', source.host
113 114 end
114 115
115 116 def test_update_with_failure
116 117 put :update, :id => 1,
117 118 :auth_source => {:name => 'Renamed', :host => '',
118 119 :port => '389', :attr_login => 'uid'}
119 120 assert_response :success
120 121 assert_template 'edit'
121 122 assert_select_error /host cannot be blank/i
122 123 end
123 124
124 125 def test_destroy
125 126 assert_difference 'AuthSourceLdap.count', -1 do
126 127 delete :destroy, :id => 1
127 128 assert_redirected_to '/auth_sources'
128 129 end
129 130 end
130 131
131 132 def test_destroy_auth_source_in_use
132 133 User.find(2).update_attribute :auth_source_id, 1
133 134
134 135 assert_no_difference 'AuthSourceLdap.count' do
135 136 delete :destroy, :id => 1
136 137 assert_redirected_to '/auth_sources'
137 138 end
138 139 end
139 140
140 141 def test_test_connection
141 142 AuthSourceLdap.any_instance.stubs(:test_connection).returns(true)
142 143
143 144 get :test_connection, :id => 1
144 145 assert_redirected_to '/auth_sources'
145 146 assert_not_nil flash[:notice]
146 147 assert_match /successful/i, flash[:notice]
147 148 end
148 149
149 150 def test_test_connection_with_failure
150 151 AuthSourceLdap.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError.new("Something went wrong"))
151 152
152 153 get :test_connection, :id => 1
153 154 assert_redirected_to '/auth_sources'
154 155 assert_not_nil flash[:error]
155 156 assert_include 'Something went wrong', flash[:error]
156 157 end
157 158
158 159 def test_autocomplete_for_new_user
159 160 AuthSource.expects(:search).with('foo').returns([
160 161 {:login => 'foo1', :firstname => 'John', :lastname => 'Smith', :mail => 'foo1@example.net', :auth_source_id => 1},
161 162 {:login => 'Smith', :firstname => 'John', :lastname => 'Doe', :mail => 'foo2@example.net', :auth_source_id => 1}
162 163 ])
163 164
164 165 get :autocomplete_for_new_user, :term => 'foo'
165 166 assert_response :success
166 167 assert_equal 'application/json', response.content_type
167 168 json = ActiveSupport::JSON.decode(response.body)
168 169 assert_kind_of Array, json
169 170 assert_equal 2, json.size
170 171 assert_equal 'foo1', json.first['value']
171 172 assert_equal 'foo1 (John Smith)', json.first['label']
172 173 end
173 174 end
@@ -1,144 +1,145
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class EmailAddressesControllerTest < ActionController::TestCase
21 21 fixtures :users, :email_addresses
22 22
23 23 def setup
24 24 User.current = nil
25 Redmine::SudoMode.disable!
25 26 end
26 27
27 28 def test_index_with_no_additional_emails
28 29 @request.session[:user_id] = 2
29 30 get :index, :user_id => 2
30 31 assert_response :success
31 32 assert_template 'index'
32 33 end
33 34
34 35 def test_index_with_additional_emails
35 36 @request.session[:user_id] = 2
36 37 EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
37 38
38 39 get :index, :user_id => 2
39 40 assert_response :success
40 41 assert_template 'index'
41 42 assert_select '.email', :text => 'another@somenet.foo'
42 43 end
43 44
44 45 def test_index_with_additional_emails_as_js
45 46 @request.session[:user_id] = 2
46 47 EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
47 48
48 49 xhr :get, :index, :user_id => 2
49 50 assert_response :success
50 51 assert_template 'index'
51 52 assert_include 'another@somenet.foo', response.body
52 53 end
53 54
54 55 def test_index_by_admin_should_be_allowed
55 56 @request.session[:user_id] = 1
56 57 get :index, :user_id => 2
57 58 assert_response :success
58 59 assert_template 'index'
59 60 end
60 61
61 62 def test_index_by_another_user_should_be_denied
62 63 @request.session[:user_id] = 3
63 64 get :index, :user_id => 2
64 65 assert_response 403
65 66 end
66 67
67 68 def test_create
68 69 @request.session[:user_id] = 2
69 70 assert_difference 'EmailAddress.count' do
70 71 post :create, :user_id => 2, :email_address => {:address => 'another@somenet.foo'}
71 72 assert_response 302
72 73 assert_redirected_to '/users/2/email_addresses'
73 74 end
74 75 email = EmailAddress.order('id DESC').first
75 76 assert_equal 2, email.user_id
76 77 assert_equal 'another@somenet.foo', email.address
77 78 end
78 79
79 80 def test_create_as_js
80 81 @request.session[:user_id] = 2
81 82 assert_difference 'EmailAddress.count' do
82 83 xhr :post, :create, :user_id => 2, :email_address => {:address => 'another@somenet.foo'}
83 84 assert_response 200
84 85 end
85 86 end
86 87
87 88 def test_create_with_failure
88 89 @request.session[:user_id] = 2
89 90 assert_no_difference 'EmailAddress.count' do
90 91 post :create, :user_id => 2, :email_address => {:address => 'invalid'}
91 92 assert_response 200
92 93 end
93 94 end
94 95
95 96 def test_update
96 97 @request.session[:user_id] = 2
97 98 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
98 99
99 100 put :update, :user_id => 2, :id => email.id, :notify => '0'
100 101 assert_response 302
101 102
102 103 assert_equal false, email.reload.notify
103 104 end
104 105
105 106 def test_update_as_js
106 107 @request.session[:user_id] = 2
107 108 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
108 109
109 110 xhr :put, :update, :user_id => 2, :id => email.id, :notify => '0'
110 111 assert_response 200
111 112
112 113 assert_equal false, email.reload.notify
113 114 end
114 115
115 116 def test_destroy
116 117 @request.session[:user_id] = 2
117 118 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
118 119
119 120 assert_difference 'EmailAddress.count', -1 do
120 121 delete :destroy, :user_id => 2, :id => email.id
121 122 assert_response 302
122 123 assert_redirected_to '/users/2/email_addresses'
123 124 end
124 125 end
125 126
126 127 def test_destroy_as_js
127 128 @request.session[:user_id] = 2
128 129 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
129 130
130 131 assert_difference 'EmailAddress.count', -1 do
131 132 xhr :delete, :destroy, :user_id => 2, :id => email.id
132 133 assert_response 200
133 134 end
134 135 end
135 136
136 137 def test_should_not_destroy_default
137 138 @request.session[:user_id] = 2
138 139
139 140 assert_no_difference 'EmailAddress.count' do
140 141 delete :destroy, :user_id => 2, :id => User.find(2).email_address.id
141 142 assert_response 404
142 143 end
143 144 end
144 145 end
@@ -1,164 +1,165
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class GroupsControllerTest < ActionController::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles, :groups_users
22 22
23 23 def setup
24 24 @request.session[:user_id] = 1
25 Redmine::SudoMode.disable!
25 26 end
26 27
27 28 def test_index
28 29 get :index
29 30 assert_response :success
30 31 assert_template 'index'
31 32 end
32 33
33 34 def test_index_should_show_user_count
34 35 get :index
35 36 assert_response :success
36 37 assert_select 'tr#group-11 td.user_count', :text => '1'
37 38 end
38 39
39 40 def test_show
40 41 get :show, :id => 10
41 42 assert_response :success
42 43 assert_template 'show'
43 44 end
44 45
45 46 def test_show_invalid_should_return_404
46 47 get :show, :id => 99
47 48 assert_response 404
48 49 end
49 50
50 51 def test_new
51 52 get :new
52 53 assert_response :success
53 54 assert_template 'new'
54 55 assert_select 'input[name=?]', 'group[name]'
55 56 end
56 57
57 58 def test_create
58 59 assert_difference 'Group.count' do
59 60 post :create, :group => {:name => 'New group'}
60 61 end
61 62 assert_redirected_to '/groups'
62 63 group = Group.order('id DESC').first
63 64 assert_equal 'New group', group.name
64 65 assert_equal [], group.users
65 66 end
66 67
67 68 def test_create_and_continue
68 69 assert_difference 'Group.count' do
69 70 post :create, :group => {:name => 'New group'}, :continue => 'Create and continue'
70 71 end
71 72 assert_redirected_to '/groups/new'
72 73 group = Group.order('id DESC').first
73 74 assert_equal 'New group', group.name
74 75 end
75 76
76 77 def test_create_with_failure
77 78 assert_no_difference 'Group.count' do
78 79 post :create, :group => {:name => ''}
79 80 end
80 81 assert_response :success
81 82 assert_template 'new'
82 83 end
83 84
84 85 def test_edit
85 86 get :edit, :id => 10
86 87 assert_response :success
87 88 assert_template 'edit'
88 89
89 90 assert_select 'div#tab-content-users'
90 91 assert_select 'div#tab-content-memberships' do
91 92 assert_select 'a', :text => 'Private child of eCookbook'
92 93 end
93 94 end
94 95
95 96 def test_update
96 97 new_name = 'New name'
97 98 put :update, :id => 10, :group => {:name => new_name}
98 99 assert_redirected_to '/groups'
99 100 group = Group.find(10)
100 101 assert_equal new_name, group.name
101 102 end
102 103
103 104 def test_update_with_failure
104 105 put :update, :id => 10, :group => {:name => ''}
105 106 assert_response :success
106 107 assert_template 'edit'
107 108 end
108 109
109 110 def test_destroy
110 111 assert_difference 'Group.count', -1 do
111 112 post :destroy, :id => 10
112 113 end
113 114 assert_redirected_to '/groups'
114 115 end
115 116
116 117 def test_new_users
117 118 get :new_users, :id => 10
118 119 assert_response :success
119 120 assert_template 'new_users'
120 121 end
121 122
122 123 def test_xhr_new_users
123 124 xhr :get, :new_users, :id => 10
124 125 assert_response :success
125 126 assert_equal 'text/javascript', response.content_type
126 127 end
127 128
128 129 def test_add_users
129 130 assert_difference 'Group.find(10).users.count', 2 do
130 131 post :add_users, :id => 10, :user_ids => ['2', '3']
131 132 end
132 133 end
133 134
134 135 def test_xhr_add_users
135 136 assert_difference 'Group.find(10).users.count', 2 do
136 137 xhr :post, :add_users, :id => 10, :user_ids => ['2', '3']
137 138 assert_response :success
138 139 assert_template 'add_users'
139 140 assert_equal 'text/javascript', response.content_type
140 141 end
141 142 assert_match /John Smith/, response.body
142 143 end
143 144
144 145 def test_remove_user
145 146 assert_difference 'Group.find(10).users.count', -1 do
146 147 delete :remove_user, :id => 10, :user_id => '8'
147 148 end
148 149 end
149 150
150 151 def test_xhr_remove_user
151 152 assert_difference 'Group.find(10).users.count', -1 do
152 153 xhr :delete, :remove_user, :id => 10, :user_id => '8'
153 154 assert_response :success
154 155 assert_template 'remove_user'
155 156 assert_equal 'text/javascript', response.content_type
156 157 end
157 158 end
158 159
159 160 def test_autocomplete_for_user
160 161 xhr :get, :autocomplete_for_user, :id => 10, :q => 'smi', :format => 'js'
161 162 assert_response :success
162 163 assert_include 'John Smith', response.body
163 164 end
164 165 end
@@ -1,201 +1,202
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class MembersControllerTest < ActionController::TestCase
21 21 fixtures :projects, :members, :member_roles, :roles, :users
22 22
23 23 def setup
24 24 User.current = nil
25 25 @request.session[:user_id] = 2
26 Redmine::SudoMode.disable!
26 27 end
27 28
28 29 def test_new
29 30 get :new, :project_id => 1
30 31 assert_response :success
31 32 end
32 33
33 34 def test_new_should_propose_managed_roles_only
34 35 role = Role.find(1)
35 36 role.update! :all_roles_managed => false
36 37 role.managed_roles = Role.where(:id => [2, 3]).to_a
37 38
38 39 get :new, :project_id => 1
39 40 assert_response :success
40 41 assert_select 'div.roles-selection' do
41 42 assert_select 'label', :text => 'Manager', :count => 0
42 43 assert_select 'label', :text => 'Developer'
43 44 assert_select 'label', :text => 'Reporter'
44 45 end
45 46 end
46 47
47 48 def test_xhr_new
48 49 xhr :get, :new, :project_id => 1
49 50 assert_response :success
50 51 assert_equal 'text/javascript', response.content_type
51 52 end
52 53
53 54 def test_create
54 55 assert_difference 'Member.count' do
55 56 post :create, :project_id => 1, :membership => {:role_ids => [1], :user_id => 7}
56 57 end
57 58 assert_redirected_to '/projects/ecookbook/settings/members'
58 59 assert User.find(7).member_of?(Project.find(1))
59 60 end
60 61
61 62 def test_create_multiple
62 63 assert_difference 'Member.count', 3 do
63 64 post :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]}
64 65 end
65 66 assert_redirected_to '/projects/ecookbook/settings/members'
66 67 assert User.find(7).member_of?(Project.find(1))
67 68 end
68 69
69 70 def test_create_should_ignore_unmanaged_roles
70 71 role = Role.find(1)
71 72 role.update! :all_roles_managed => false
72 73 role.managed_roles = Role.where(:id => [2, 3]).to_a
73 74
74 75 assert_difference 'Member.count' do
75 76 post :create, :project_id => 1, :membership => {:role_ids => [1, 2], :user_id => 7}
76 77 end
77 78 member = Member.order(:id => :desc).first
78 79 assert_equal [2], member.role_ids
79 80 end
80 81
81 82 def test_create_should_be_allowed_for_admin_without_role
82 83 User.find(1).members.delete_all
83 84 @request.session[:user_id] = 1
84 85
85 86 assert_difference 'Member.count' do
86 87 post :create, :project_id => 1, :membership => {:role_ids => [1, 2], :user_id => 7}
87 88 end
88 89 member = Member.order(:id => :desc).first
89 90 assert_equal [1, 2], member.role_ids
90 91 end
91 92
92 93 def test_xhr_create
93 94 assert_difference 'Member.count', 3 do
94 95 xhr :post, :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]}
95 96 assert_response :success
96 97 assert_template 'create'
97 98 assert_equal 'text/javascript', response.content_type
98 99 end
99 100 assert User.find(7).member_of?(Project.find(1))
100 101 assert User.find(8).member_of?(Project.find(1))
101 102 assert User.find(9).member_of?(Project.find(1))
102 103 assert_include 'tab-content-members', response.body
103 104 end
104 105
105 106 def test_xhr_create_with_failure
106 107 assert_no_difference 'Member.count' do
107 108 xhr :post, :create, :project_id => 1, :membership => {:role_ids => [], :user_ids => [7, 8, 9]}
108 109 assert_response :success
109 110 assert_template 'create'
110 111 assert_equal 'text/javascript', response.content_type
111 112 end
112 113 assert_match /alert/, response.body, "Alert message not sent"
113 114 end
114 115
115 116 def test_update
116 117 assert_no_difference 'Member.count' do
117 118 put :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3}
118 119 end
119 120 assert_redirected_to '/projects/ecookbook/settings/members'
120 121 end
121 122
122 123 def test_update_should_not_add_unmanaged_roles
123 124 role = Role.find(1)
124 125 role.update! :all_roles_managed => false
125 126 role.managed_roles = Role.where(:id => [2, 3]).to_a
126 127 member = Member.create!(:user => User.find(9), :role_ids => [3], :project_id => 1)
127 128
128 129 put :update, :id => member.id, :membership => {:role_ids => [1, 2, 3]}
129 130 assert_equal [2, 3], member.reload.role_ids.sort
130 131 end
131 132
132 133 def test_update_should_not_remove_unmanaged_roles
133 134 role = Role.find(1)
134 135 role.update! :all_roles_managed => false
135 136 role.managed_roles = Role.where(:id => [2, 3]).to_a
136 137 member = Member.create!(:user => User.find(9), :role_ids => [1, 3], :project_id => 1)
137 138
138 139 put :update, :id => member.id, :membership => {:role_ids => [2]}
139 140 assert_equal [1, 2], member.reload.role_ids.sort
140 141 end
141 142
142 143 def test_xhr_update
143 144 assert_no_difference 'Member.count' do
144 145 xhr :put, :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3}
145 146 assert_response :success
146 147 assert_template 'update'
147 148 assert_equal 'text/javascript', response.content_type
148 149 end
149 150 member = Member.find(2)
150 151 assert_equal [1], member.role_ids
151 152 assert_equal 3, member.user_id
152 153 assert_include 'tab-content-members', response.body
153 154 end
154 155
155 156 def test_destroy
156 157 assert_difference 'Member.count', -1 do
157 158 delete :destroy, :id => 2
158 159 end
159 160 assert_redirected_to '/projects/ecookbook/settings/members'
160 161 assert !User.find(3).member_of?(Project.find(1))
161 162 end
162 163
163 164 def test_destroy_should_fail_with_unmanaged_roles
164 165 role = Role.find(1)
165 166 role.update! :all_roles_managed => false
166 167 role.managed_roles = Role.where(:id => [2, 3]).to_a
167 168 member = Member.create!(:user => User.find(9), :role_ids => [1, 3], :project_id => 1)
168 169
169 170 assert_no_difference 'Member.count' do
170 171 delete :destroy, :id => member.id
171 172 end
172 173 end
173 174
174 175 def test_destroy_should_succeed_with_managed_roles_only
175 176 role = Role.find(1)
176 177 role.update! :all_roles_managed => false
177 178 role.managed_roles = Role.where(:id => [2, 3]).to_a
178 179 member = Member.create!(:user => User.find(9), :role_ids => [3], :project_id => 1)
179 180
180 181 assert_difference 'Member.count', -1 do
181 182 delete :destroy, :id => member.id
182 183 end
183 184 end
184 185
185 186 def test_xhr_destroy
186 187 assert_difference 'Member.count', -1 do
187 188 xhr :delete, :destroy, :id => 2
188 189 assert_response :success
189 190 assert_template 'destroy'
190 191 assert_equal 'text/javascript', response.content_type
191 192 end
192 193 assert_nil Member.find_by_id(2)
193 194 assert_include 'tab-content-members', response.body
194 195 end
195 196
196 197 def test_autocomplete
197 198 xhr :get, :autocomplete, :project_id => 1, :q => 'mis', :format => 'js'
198 199 assert_response :success
199 200 assert_include 'User Misc', response.body
200 201 end
201 202 end
@@ -1,274 +1,281
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class MyControllerTest < ActionController::TestCase
21 21 fixtures :users, :email_addresses, :user_preferences, :roles, :projects, :members, :member_roles,
22 22 :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources
23 23
24 24 def setup
25 25 @request.session[:user_id] = 2
26 Redmine::SudoMode.disable!
26 27 end
27 28
28 29 def test_index
29 30 get :index
30 31 assert_response :success
31 32 assert_template 'page'
32 33 end
33 34
34 35 def test_page
35 36 get :page
36 37 assert_response :success
37 38 assert_template 'page'
38 39 end
39 40
40 41 def test_page_with_timelog_block
41 42 preferences = User.find(2).pref
42 43 preferences[:my_page_layout] = {'top' => ['timelog']}
43 44 preferences.save!
44 45 TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10)
45 46
46 47 get :page
47 48 assert_response :success
48 49 assert_select 'tr.time-entry' do
49 50 assert_select 'td.subject a[href="/issues/1"]'
50 51 assert_select 'td.hours', :text => '2.50'
51 52 end
52 53 end
53 54
54 55 def test_page_with_all_blocks
55 56 blocks = MyController::BLOCKS.keys
56 57 preferences = User.find(2).pref
57 58 preferences[:my_page_layout] = {'top' => blocks}
58 59 preferences.save!
59 60
60 61 get :page
61 62 assert_response :success
62 63 assert_select 'div.mypage-box', blocks.size
63 64 end
64 65
65 66 def test_my_account_should_show_editable_custom_fields
66 67 get :account
67 68 assert_response :success
68 69 assert_template 'account'
69 70 assert_equal User.find(2), assigns(:user)
70 71
71 72 assert_select 'input[name=?]', 'user[custom_field_values][4]'
72 73 end
73 74
74 75 def test_my_account_should_not_show_non_editable_custom_fields
75 76 UserCustomField.find(4).update_attribute :editable, false
76 77
77 78 get :account
78 79 assert_response :success
79 80 assert_template 'account'
80 81 assert_equal User.find(2), assigns(:user)
81 82
82 83 assert_select 'input[name=?]', 'user[custom_field_values][4]', 0
83 84 end
84 85
85 86 def test_my_account_should_show_language_select
86 87 get :account
87 88 assert_response :success
88 89 assert_select 'select[name=?]', 'user[language]'
89 90 end
90 91
91 92 def test_my_account_should_not_show_language_select_with_force_default_language_for_loggedin
92 93 with_settings :force_default_language_for_loggedin => '1' do
93 94 get :account
94 95 assert_response :success
95 96 assert_select 'select[name=?]', 'user[language]', 0
96 97 end
97 98 end
98 99
99 100 def test_update_account
100 101 post :account,
101 102 :user => {
102 103 :firstname => "Joe",
103 104 :login => "root",
104 105 :admin => 1,
105 106 :group_ids => ['10'],
106 107 :custom_field_values => {"4" => "0100562500"}
107 108 }
108 109
109 110 assert_redirected_to '/my/account'
110 111 user = User.find(2)
111 112 assert_equal user, assigns(:user)
112 113 assert_equal "Joe", user.firstname
113 114 assert_equal "jsmith", user.login
114 115 assert_equal "0100562500", user.custom_value_for(4).value
115 116 # ignored
116 117 assert !user.admin?
117 118 assert user.groups.empty?
118 119 end
119 120
120 121 def test_my_account_should_show_destroy_link
121 122 get :account
122 123 assert_select 'a[href="/my/account/destroy"]'
123 124 end
124 125
125 126 def test_get_destroy_should_display_the_destroy_confirmation
126 127 get :destroy
127 128 assert_response :success
128 129 assert_template 'destroy'
129 130 assert_select 'form[action="/my/account/destroy"]' do
130 131 assert_select 'input[name=confirm]'
131 132 end
132 133 end
133 134
134 135 def test_post_destroy_without_confirmation_should_not_destroy_account
135 136 assert_no_difference 'User.count' do
136 137 post :destroy
137 138 end
138 139 assert_response :success
139 140 assert_template 'destroy'
140 141 end
141 142
142 143 def test_post_destroy_without_confirmation_should_destroy_account
143 144 assert_difference 'User.count', -1 do
144 145 post :destroy, :confirm => '1'
145 146 end
146 147 assert_redirected_to '/'
147 148 assert_match /deleted/i, flash[:notice]
148 149 end
149 150
150 151 def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account
151 152 User.any_instance.stubs(:own_account_deletable?).returns(false)
152 153
153 154 assert_no_difference 'User.count' do
154 155 post :destroy, :confirm => '1'
155 156 end
156 157 assert_redirected_to '/my/account'
157 158 end
158 159
159 160 def test_change_password
160 161 get :password
161 162 assert_response :success
162 163 assert_template 'password'
163 164
164 165 # non matching password confirmation
165 166 post :password, :password => 'jsmith',
166 167 :new_password => 'secret123',
167 168 :new_password_confirmation => 'secret1234'
168 169 assert_response :success
169 170 assert_template 'password'
170 171 assert_select_error /Password doesn.*t match confirmation/
171 172
172 173 # wrong password
173 174 post :password, :password => 'wrongpassword',
174 175 :new_password => 'secret123',
175 176 :new_password_confirmation => 'secret123'
176 177 assert_response :success
177 178 assert_template 'password'
178 179 assert_equal 'Wrong password', flash[:error]
179 180
180 181 # good password
181 182 post :password, :password => 'jsmith',
182 183 :new_password => 'secret123',
183 184 :new_password_confirmation => 'secret123'
184 185 assert_redirected_to '/my/account'
185 186 assert User.try_to_login('jsmith', 'secret123')
186 187 end
187 188
188 189 def test_change_password_kills_other_sessions
189 190 @request.session[:ctime] = (Time.now - 30.minutes).utc.to_i
190 191
191 192 jsmith = User.find(2)
192 193 jsmith.passwd_changed_on = Time.now
193 194 jsmith.save!
194 195
195 196 get 'account'
196 197 assert_response 302
197 198 assert flash[:error].match(/Your session has expired/)
198 199 end
199 200
200 201 def test_change_password_should_redirect_if_user_cannot_change_its_password
201 202 User.find(2).update_attribute(:auth_source_id, 1)
202 203
203 204 get :password
204 205 assert_not_nil flash[:error]
205 206 assert_redirected_to '/my/account'
206 207 end
207 208
208 209 def test_page_layout
209 210 get :page_layout
210 211 assert_response :success
211 212 assert_template 'page_layout'
212 213 end
213 214
214 215 def test_add_block
215 216 post :add_block, :block => 'issuesreportedbyme'
216 217 assert_redirected_to '/my/page_layout'
217 218 assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme')
218 219 end
219 220
220 221 def test_add_invalid_block_should_redirect
221 222 post :add_block, :block => 'invalid'
222 223 assert_redirected_to '/my/page_layout'
223 224 end
224 225
225 226 def test_remove_block
226 227 post :remove_block, :block => 'issuesassignedtome'
227 228 assert_redirected_to '/my/page_layout'
228 229 assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome')
229 230 end
230 231
231 232 def test_order_blocks
232 233 xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews']
233 234 assert_response :success
234 235 assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left']
235 236 end
236 237
237 238 def test_reset_rss_key_with_existing_key
238 239 @previous_token_value = User.find(2).rss_key # Will generate one if it's missing
239 240 post :reset_rss_key
240 241
241 242 assert_not_equal @previous_token_value, User.find(2).rss_key
242 243 assert User.find(2).rss_token
243 244 assert_match /reset/, flash[:notice]
244 245 assert_redirected_to '/my/account'
245 246 end
246 247
247 248 def test_reset_rss_key_without_existing_key
248 249 assert_nil User.find(2).rss_token
249 250 post :reset_rss_key
250 251
251 252 assert User.find(2).rss_token
252 253 assert_match /reset/, flash[:notice]
253 254 assert_redirected_to '/my/account'
254 255 end
255 256
257 def test_show_api_key
258 get :show_api_key
259 assert_response :success
260 assert_select 'pre', User.find(2).api_key
261 end
262
256 263 def test_reset_api_key_with_existing_key
257 264 @previous_token_value = User.find(2).api_key # Will generate one if it's missing
258 265 post :reset_api_key
259 266
260 267 assert_not_equal @previous_token_value, User.find(2).api_key
261 268 assert User.find(2).api_token
262 269 assert_match /reset/, flash[:notice]
263 270 assert_redirected_to '/my/account'
264 271 end
265 272
266 273 def test_reset_api_key_without_existing_key
267 274 assert_nil User.find(2).api_token
268 275 post :reset_api_key
269 276
270 277 assert User.find(2).api_token
271 278 assert_match /reset/, flash[:notice]
272 279 assert_redirected_to '/my/account'
273 280 end
274 281 end
@@ -1,684 +1,685
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class ProjectsControllerTest < ActionController::TestCase
21 21 fixtures :projects, :versions, :users, :email_addresses, :roles, :members,
22 22 :member_roles, :issues, :journals, :journal_details,
23 23 :trackers, :projects_trackers, :issue_statuses,
24 24 :enabled_modules, :enumerations, :boards, :messages,
25 25 :attachments, :custom_fields, :custom_values, :time_entries,
26 26 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
27 27
28 28 def setup
29 29 @request.session[:user_id] = nil
30 30 Setting.default_language = 'en'
31 Redmine::SudoMode.disable!
31 32 end
32 33
33 34 def test_index_by_anonymous_should_not_show_private_projects
34 35 get :index
35 36 assert_response :success
36 37 assert_template 'index'
37 38 projects = assigns(:projects)
38 39 assert_not_nil projects
39 40 assert projects.all?(&:is_public?)
40 41
41 42 assert_select 'ul' do
42 43 assert_select 'li' do
43 44 assert_select 'a', :text => 'eCookbook'
44 45 assert_select 'ul' do
45 46 assert_select 'a', :text => 'Child of private child'
46 47 end
47 48 end
48 49 end
49 50 assert_select 'a', :text => /Private child of eCookbook/, :count => 0
50 51 end
51 52
52 53 def test_index_atom
53 54 get :index, :format => 'atom'
54 55 assert_response :success
55 56 assert_template 'common/feed'
56 57 assert_select 'feed>title', :text => 'Redmine: Latest projects'
57 58 assert_select 'feed>entry', :count => Project.visible(User.current).count
58 59 end
59 60
60 61 test "#index by non-admin user with view_time_entries permission should show overall spent time link" do
61 62 @request.session[:user_id] = 3
62 63 get :index
63 64 assert_template 'index'
64 65 assert_select 'a[href=?]', '/time_entries'
65 66 end
66 67
67 68 test "#index by non-admin user without view_time_entries permission should not show overall spent time link" do
68 69 Role.find(2).remove_permission! :view_time_entries
69 70 Role.non_member.remove_permission! :view_time_entries
70 71 Role.anonymous.remove_permission! :view_time_entries
71 72 @request.session[:user_id] = 3
72 73
73 74 get :index
74 75 assert_template 'index'
75 76 assert_select 'a[href=?]', '/time_entries', 0
76 77 end
77 78
78 79 test "#index by non-admin user with permission should show add project link" do
79 80 Role.find(1).add_permission! :add_project
80 81 @request.session[:user_id] = 2
81 82 get :index
82 83 assert_template 'index'
83 84 assert_select 'a[href=?]', '/projects/new'
84 85 end
85 86
86 87 test "#new by admin user should accept get" do
87 88 @request.session[:user_id] = 1
88 89
89 90 get :new
90 91 assert_response :success
91 92 assert_template 'new'
92 93 end
93 94
94 95 test "#new by non-admin user with add_project permission should accept get" do
95 96 Role.non_member.add_permission! :add_project
96 97 @request.session[:user_id] = 9
97 98
98 99 get :new
99 100 assert_response :success
100 101 assert_template 'new'
101 102 assert_select 'select[name=?]', 'project[parent_id]', 0
102 103 end
103 104
104 105 test "#new by non-admin user with add_subprojects permission should accept get" do
105 106 Role.find(1).remove_permission! :add_project
106 107 Role.find(1).add_permission! :add_subprojects
107 108 @request.session[:user_id] = 2
108 109
109 110 get :new, :parent_id => 'ecookbook'
110 111 assert_response :success
111 112 assert_template 'new'
112 113
113 114 assert_select 'select[name=?]', 'project[parent_id]' do
114 115 # parent project selected
115 116 assert_select 'option[value="1"][selected=selected]'
116 117 # no empty value
117 118 assert_select 'option[value=""]', 0
118 119 end
119 120 end
120 121
121 122 test "#create by admin user should create a new project" do
122 123 @request.session[:user_id] = 1
123 124
124 125 post :create,
125 126 :project => {
126 127 :name => "blog",
127 128 :description => "weblog",
128 129 :homepage => 'http://weblog',
129 130 :identifier => "blog",
130 131 :is_public => 1,
131 132 :custom_field_values => { '3' => 'Beta' },
132 133 :tracker_ids => ['1', '3'],
133 134 # an issue custom field that is not for all project
134 135 :issue_custom_field_ids => ['9'],
135 136 :enabled_module_names => ['issue_tracking', 'news', 'repository']
136 137 }
137 138 assert_redirected_to '/projects/blog/settings'
138 139
139 140 project = Project.find_by_name('blog')
140 141 assert_kind_of Project, project
141 142 assert project.active?
142 143 assert_equal 'weblog', project.description
143 144 assert_equal 'http://weblog', project.homepage
144 145 assert_equal true, project.is_public?
145 146 assert_nil project.parent
146 147 assert_equal 'Beta', project.custom_value_for(3).value
147 148 assert_equal [1, 3], project.trackers.map(&:id).sort
148 149 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
149 150 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
150 151 end
151 152
152 153 test "#create by admin user should create a new subproject" do
153 154 @request.session[:user_id] = 1
154 155
155 156 assert_difference 'Project.count' do
156 157 post :create, :project => { :name => "blog",
157 158 :description => "weblog",
158 159 :identifier => "blog",
159 160 :is_public => 1,
160 161 :custom_field_values => { '3' => 'Beta' },
161 162 :parent_id => 1
162 163 }
163 164 assert_redirected_to '/projects/blog/settings'
164 165 end
165 166
166 167 project = Project.find_by_name('blog')
167 168 assert_kind_of Project, project
168 169 assert_equal Project.find(1), project.parent
169 170 end
170 171
171 172 test "#create by admin user should continue" do
172 173 @request.session[:user_id] = 1
173 174
174 175 assert_difference 'Project.count' do
175 176 post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue'
176 177 end
177 178 assert_redirected_to '/projects/new'
178 179 end
179 180
180 181 test "#create by non-admin user with add_project permission should create a new project" do
181 182 Role.non_member.add_permission! :add_project
182 183 @request.session[:user_id] = 9
183 184
184 185 post :create, :project => { :name => "blog",
185 186 :description => "weblog",
186 187 :identifier => "blog",
187 188 :is_public => 1,
188 189 :custom_field_values => { '3' => 'Beta' },
189 190 :tracker_ids => ['1', '3'],
190 191 :enabled_module_names => ['issue_tracking', 'news', 'repository']
191 192 }
192 193
193 194 assert_redirected_to '/projects/blog/settings'
194 195
195 196 project = Project.find_by_name('blog')
196 197 assert_kind_of Project, project
197 198 assert_equal 'weblog', project.description
198 199 assert_equal true, project.is_public?
199 200 assert_equal [1, 3], project.trackers.map(&:id).sort
200 201 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
201 202
202 203 # User should be added as a project member
203 204 assert User.find(9).member_of?(project)
204 205 assert_equal 1, project.members.size
205 206 end
206 207
207 208 test "#create by non-admin user with add_project permission should fail with parent_id" do
208 209 Role.non_member.add_permission! :add_project
209 210 @request.session[:user_id] = 9
210 211
211 212 assert_no_difference 'Project.count' do
212 213 post :create, :project => { :name => "blog",
213 214 :description => "weblog",
214 215 :identifier => "blog",
215 216 :is_public => 1,
216 217 :custom_field_values => { '3' => 'Beta' },
217 218 :parent_id => 1
218 219 }
219 220 end
220 221 assert_response :success
221 222 project = assigns(:project)
222 223 assert_kind_of Project, project
223 224 assert_not_equal [], project.errors[:parent_id]
224 225 end
225 226
226 227 test "#create by non-admin user with add_subprojects permission should create a project with a parent_id" do
227 228 Role.find(1).remove_permission! :add_project
228 229 Role.find(1).add_permission! :add_subprojects
229 230 @request.session[:user_id] = 2
230 231
231 232 post :create, :project => { :name => "blog",
232 233 :description => "weblog",
233 234 :identifier => "blog",
234 235 :is_public => 1,
235 236 :custom_field_values => { '3' => 'Beta' },
236 237 :parent_id => 1
237 238 }
238 239 assert_redirected_to '/projects/blog/settings'
239 240 project = Project.find_by_name('blog')
240 241 end
241 242
242 243 test "#create by non-admin user with add_subprojects permission should fail without parent_id" do
243 244 Role.find(1).remove_permission! :add_project
244 245 Role.find(1).add_permission! :add_subprojects
245 246 @request.session[:user_id] = 2
246 247
247 248 assert_no_difference 'Project.count' do
248 249 post :create, :project => { :name => "blog",
249 250 :description => "weblog",
250 251 :identifier => "blog",
251 252 :is_public => 1,
252 253 :custom_field_values => { '3' => 'Beta' }
253 254 }
254 255 end
255 256 assert_response :success
256 257 project = assigns(:project)
257 258 assert_kind_of Project, project
258 259 assert_not_equal [], project.errors[:parent_id]
259 260 end
260 261
261 262 test "#create by non-admin user with add_subprojects permission should fail with unauthorized parent_id" do
262 263 Role.find(1).remove_permission! :add_project
263 264 Role.find(1).add_permission! :add_subprojects
264 265 @request.session[:user_id] = 2
265 266
266 267 assert !User.find(2).member_of?(Project.find(6))
267 268 assert_no_difference 'Project.count' do
268 269 post :create, :project => { :name => "blog",
269 270 :description => "weblog",
270 271 :identifier => "blog",
271 272 :is_public => 1,
272 273 :custom_field_values => { '3' => 'Beta' },
273 274 :parent_id => 6
274 275 }
275 276 end
276 277 assert_response :success
277 278 project = assigns(:project)
278 279 assert_kind_of Project, project
279 280 assert_not_equal [], project.errors[:parent_id]
280 281 end
281 282
282 283 def test_create_subproject_with_inherit_members_should_inherit_members
283 284 Role.find_by_name('Manager').add_permission! :add_subprojects
284 285 parent = Project.find(1)
285 286 @request.session[:user_id] = 2
286 287
287 288 assert_difference 'Project.count' do
288 289 post :create, :project => {
289 290 :name => 'inherited', :identifier => 'inherited', :parent_id => parent.id, :inherit_members => '1'
290 291 }
291 292 assert_response 302
292 293 end
293 294
294 295 project = Project.order('id desc').first
295 296 assert_equal 'inherited', project.name
296 297 assert_equal parent, project.parent
297 298 assert project.memberships.count > 0
298 299 assert_equal parent.memberships.count, project.memberships.count
299 300 end
300 301
301 302 def test_create_should_preserve_modules_on_validation_failure
302 303 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
303 304 @request.session[:user_id] = 1
304 305 assert_no_difference 'Project.count' do
305 306 post :create, :project => {
306 307 :name => "blog",
307 308 :identifier => "",
308 309 :enabled_module_names => %w(issue_tracking news)
309 310 }
310 311 end
311 312 assert_response :success
312 313 project = assigns(:project)
313 314 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
314 315 end
315 316 end
316 317
317 318 def test_show_by_id
318 319 get :show, :id => 1
319 320 assert_response :success
320 321 assert_template 'show'
321 322 assert_not_nil assigns(:project)
322 323 end
323 324
324 325 def test_show_by_identifier
325 326 get :show, :id => 'ecookbook'
326 327 assert_response :success
327 328 assert_template 'show'
328 329 assert_not_nil assigns(:project)
329 330 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
330 331
331 332 assert_select 'li', :text => /Development status/
332 333 end
333 334
334 335 def test_show_should_not_display_empty_sidebar
335 336 p = Project.find(1)
336 337 p.enabled_module_names = []
337 338 p.save!
338 339
339 340 get :show, :id => 'ecookbook'
340 341 assert_response :success
341 342 assert_select '#main.nosidebar'
342 343 end
343 344
344 345 def test_show_should_not_display_hidden_custom_fields
345 346 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
346 347 get :show, :id => 'ecookbook'
347 348 assert_response :success
348 349 assert_template 'show'
349 350 assert_not_nil assigns(:project)
350 351
351 352 assert_select 'li', :text => /Development status/, :count => 0
352 353 end
353 354
354 355 def test_show_should_not_display_blank_custom_fields_with_multiple_values
355 356 f1 = ProjectCustomField.generate! :field_format => 'list', :possible_values => %w(Foo Bar), :multiple => true
356 357 f2 = ProjectCustomField.generate! :field_format => 'list', :possible_values => %w(Baz Qux), :multiple => true
357 358 project = Project.generate!(:custom_field_values => {f2.id.to_s => %w(Qux)})
358 359
359 360 get :show, :id => project.id
360 361 assert_response :success
361 362
362 363 assert_select 'li', :text => /#{f1.name}/, :count => 0
363 364 assert_select 'li', :text => /#{f2.name}/
364 365 end
365 366
366 367 def test_show_should_not_display_blank_text_custom_fields
367 368 f1 = ProjectCustomField.generate! :field_format => 'text'
368 369
369 370 get :show, :id => 1
370 371 assert_response :success
371 372
372 373 assert_select 'li', :text => /#{f1.name}/, :count => 0
373 374 end
374 375
375 376 def test_show_should_not_fail_when_custom_values_are_nil
376 377 project = Project.find_by_identifier('ecookbook')
377 378 project.custom_values.first.update_attribute(:value, nil)
378 379 get :show, :id => 'ecookbook'
379 380 assert_response :success
380 381 assert_template 'show'
381 382 assert_not_nil assigns(:project)
382 383 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
383 384 end
384 385
385 386 def show_archived_project_should_be_denied
386 387 project = Project.find_by_identifier('ecookbook')
387 388 project.archive!
388 389
389 390 get :show, :id => 'ecookbook'
390 391 assert_response 403
391 392 assert_nil assigns(:project)
392 393 assert_select 'p', :text => /archived/
393 394 end
394 395
395 396 def test_show_should_not_show_private_subprojects_that_are_not_visible
396 397 get :show, :id => 'ecookbook'
397 398 assert_response :success
398 399 assert_template 'show'
399 400 assert_select 'a', :text => /Private child/, :count => 0
400 401 end
401 402
402 403 def test_show_should_show_private_subprojects_that_are_visible
403 404 @request.session[:user_id] = 2 # manager who is a member of the private subproject
404 405 get :show, :id => 'ecookbook'
405 406 assert_response :success
406 407 assert_template 'show'
407 408 assert_select 'a', :text => /Private child/
408 409 end
409 410
410 411 def test_settings
411 412 @request.session[:user_id] = 2 # manager
412 413 get :settings, :id => 1
413 414 assert_response :success
414 415 assert_template 'settings'
415 416 end
416 417
417 418 def test_settings_of_subproject
418 419 @request.session[:user_id] = 2
419 420 get :settings, :id => 'private-child'
420 421 assert_response :success
421 422 assert_template 'settings'
422 423
423 424 assert_select 'input[type=checkbox][name=?]', 'project[inherit_members]'
424 425 end
425 426
426 427 def test_settings_should_be_denied_for_member_on_closed_project
427 428 Project.find(1).close
428 429 @request.session[:user_id] = 2 # manager
429 430
430 431 get :settings, :id => 1
431 432 assert_response 403
432 433 end
433 434
434 435 def test_settings_should_be_denied_for_anonymous_on_closed_project
435 436 Project.find(1).close
436 437
437 438 get :settings, :id => 1
438 439 assert_response 302
439 440 end
440 441
441 442 def test_setting_with_wiki_module_and_no_wiki
442 443 Project.find(1).wiki.destroy
443 444 Role.find(1).add_permission! :manage_wiki
444 445 @request.session[:user_id] = 2
445 446
446 447 get :settings, :id => 1
447 448 assert_response :success
448 449 assert_template 'settings'
449 450
450 451 assert_select 'form[action=?]', '/projects/ecookbook/wiki' do
451 452 assert_select 'input[name=?]', 'wiki[start_page]'
452 453 end
453 454 end
454 455
455 456 def test_update
456 457 @request.session[:user_id] = 2 # manager
457 458 post :update, :id => 1, :project => {:name => 'Test changed name',
458 459 :issue_custom_field_ids => ['']}
459 460 assert_redirected_to '/projects/ecookbook/settings'
460 461 project = Project.find(1)
461 462 assert_equal 'Test changed name', project.name
462 463 end
463 464
464 465 def test_update_with_failure
465 466 @request.session[:user_id] = 2 # manager
466 467 post :update, :id => 1, :project => {:name => ''}
467 468 assert_response :success
468 469 assert_template 'settings'
469 470 assert_select_error /name cannot be blank/i
470 471 end
471 472
472 473 def test_update_should_be_denied_for_member_on_closed_project
473 474 Project.find(1).close
474 475 @request.session[:user_id] = 2 # manager
475 476
476 477 post :update, :id => 1, :project => {:name => 'Closed'}
477 478 assert_response 403
478 479 assert_equal 'eCookbook', Project.find(1).name
479 480 end
480 481
481 482 def test_update_should_be_denied_for_anonymous_on_closed_project
482 483 Project.find(1).close
483 484
484 485 post :update, :id => 1, :project => {:name => 'Closed'}
485 486 assert_response 302
486 487 assert_equal 'eCookbook', Project.find(1).name
487 488 end
488 489
489 490 def test_modules
490 491 @request.session[:user_id] = 2
491 492 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
492 493
493 494 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
494 495 assert_redirected_to '/projects/ecookbook/settings/modules'
495 496 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
496 497 end
497 498
498 499 def test_destroy_leaf_project_without_confirmation_should_show_confirmation
499 500 @request.session[:user_id] = 1 # admin
500 501
501 502 assert_no_difference 'Project.count' do
502 503 delete :destroy, :id => 2
503 504 assert_response :success
504 505 assert_template 'destroy'
505 506 end
506 507 end
507 508
508 509 def test_destroy_without_confirmation_should_show_confirmation_with_subprojects
509 510 @request.session[:user_id] = 1 # admin
510 511
511 512 assert_no_difference 'Project.count' do
512 513 delete :destroy, :id => 1
513 514 assert_response :success
514 515 assert_template 'destroy'
515 516 end
516 517 assert_select 'strong',
517 518 :text => ['Private child of eCookbook',
518 519 'Child of private child, eCookbook Subproject 1',
519 520 'eCookbook Subproject 2'].join(', ')
520 521 end
521 522
522 523 def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects
523 524 @request.session[:user_id] = 1 # admin
524 525
525 526 assert_difference 'Project.count', -5 do
526 527 delete :destroy, :id => 1, :confirm => 1
527 528 assert_redirected_to '/admin/projects'
528 529 end
529 530 assert_nil Project.find_by_id(1)
530 531 end
531 532
532 533 def test_archive
533 534 @request.session[:user_id] = 1 # admin
534 535 post :archive, :id => 1
535 536 assert_redirected_to '/admin/projects'
536 537 assert !Project.find(1).active?
537 538 end
538 539
539 540 def test_archive_with_failure
540 541 @request.session[:user_id] = 1
541 542 Project.any_instance.stubs(:archive).returns(false)
542 543 post :archive, :id => 1
543 544 assert_redirected_to '/admin/projects'
544 545 assert_match /project cannot be archived/i, flash[:error]
545 546 end
546 547
547 548 def test_unarchive
548 549 @request.session[:user_id] = 1 # admin
549 550 Project.find(1).archive
550 551 post :unarchive, :id => 1
551 552 assert_redirected_to '/admin/projects'
552 553 assert Project.find(1).active?
553 554 end
554 555
555 556 def test_close
556 557 @request.session[:user_id] = 2
557 558 post :close, :id => 1
558 559 assert_redirected_to '/projects/ecookbook'
559 560 assert_equal Project::STATUS_CLOSED, Project.find(1).status
560 561 end
561 562
562 563 def test_reopen
563 564 Project.find(1).close
564 565 @request.session[:user_id] = 2
565 566 post :reopen, :id => 1
566 567 assert_redirected_to '/projects/ecookbook'
567 568 assert Project.find(1).active?
568 569 end
569 570
570 571 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
571 572 CustomField.delete_all
572 573 parent = nil
573 574 6.times do |i|
574 575 p = Project.generate_with_parent!(parent)
575 576 get :show, :id => p
576 577 assert_select '#header h1' do
577 578 assert_select 'a', :count => [i, 3].min
578 579 end
579 580
580 581 parent = p
581 582 end
582 583 end
583 584
584 585 def test_get_copy
585 586 @request.session[:user_id] = 1 # admin
586 587 get :copy, :id => 1
587 588 assert_response :success
588 589 assert_template 'copy'
589 590 assert assigns(:project)
590 591 assert_equal Project.find(1).description, assigns(:project).description
591 592 assert_nil assigns(:project).id
592 593
593 594 assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1
594 595 end
595 596
596 597 def test_get_copy_with_invalid_source_should_respond_with_404
597 598 @request.session[:user_id] = 1
598 599 get :copy, :id => 99
599 600 assert_response 404
600 601 end
601 602
602 603 def test_post_copy_should_copy_requested_items
603 604 @request.session[:user_id] = 1 # admin
604 605 CustomField.delete_all
605 606
606 607 assert_difference 'Project.count' do
607 608 post :copy, :id => 1,
608 609 :project => {
609 610 :name => 'Copy',
610 611 :identifier => 'unique-copy',
611 612 :tracker_ids => ['1', '2', '3', ''],
612 613 :enabled_module_names => %w(issue_tracking time_tracking)
613 614 },
614 615 :only => %w(issues versions)
615 616 end
616 617 project = Project.find('unique-copy')
617 618 source = Project.find(1)
618 619 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
619 620
620 621 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
621 622 assert_equal source.issues.count, project.issues.count, "All issues were not copied"
622 623 assert_equal 0, project.members.count
623 624 end
624 625
625 626 def test_post_copy_should_redirect_to_settings_when_successful
626 627 @request.session[:user_id] = 1 # admin
627 628 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
628 629 assert_response :redirect
629 630 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
630 631 end
631 632
632 633 def test_post_copy_with_failure
633 634 @request.session[:user_id] = 1
634 635 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => ''}
635 636 assert_response :success
636 637 assert_template 'copy'
637 638 end
638 639
639 640 def test_jump_should_redirect_to_active_tab
640 641 get :show, :id => 1, :jump => 'issues'
641 642 assert_redirected_to '/projects/ecookbook/issues'
642 643 end
643 644
644 645 def test_jump_should_not_redirect_to_inactive_tab
645 646 get :show, :id => 3, :jump => 'documents'
646 647 assert_response :success
647 648 assert_template 'show'
648 649 end
649 650
650 651 def test_jump_should_not_redirect_to_unknown_tab
651 652 get :show, :id => 3, :jump => 'foobar'
652 653 assert_response :success
653 654 assert_template 'show'
654 655 end
655 656
656 657 def test_body_should_have_project_css_class
657 658 get :show, :id => 1
658 659 assert_select 'body.project-ecookbook'
659 660 end
660 661
661 662 def test_project_menu_should_include_new_issue_link
662 663 @request.session[:user_id] = 2
663 664 get :show, :id => 1
664 665 assert_select '#main-menu a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue'
665 666 end
666 667
667 668 def test_project_menu_should_not_include_new_issue_link_for_project_without_trackers
668 669 Project.find(1).trackers.clear
669 670
670 671 @request.session[:user_id] = 2
671 672 get :show, :id => 1
672 673 assert_select '#main-menu a.new-issue', 0
673 674 end
674 675
675 676 def test_project_menu_should_not_include_new_issue_link_for_users_with_copy_issues_permission_only
676 677 role = Role.find(1)
677 678 role.remove_permission! :add_issues
678 679 role.add_permission! :copy_issues
679 680
680 681 @request.session[:user_id] = 2
681 682 get :show, :id => 1
682 683 assert_select '#main-menu a.new-issue', 0
683 684 end
684 685 end
@@ -1,208 +1,209
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RolesControllerTest < ActionController::TestCase
21 21 fixtures :roles, :users, :members, :member_roles, :workflows, :trackers
22 22
23 23 def setup
24 24 User.current = nil
25 25 @request.session[:user_id] = 1 # admin
26 Redmine::SudoMode.disable!
26 27 end
27 28
28 29 def test_index
29 30 get :index
30 31 assert_response :success
31 32 assert_template 'index'
32 33
33 34 assert_not_nil assigns(:roles)
34 35 assert_equal Role.order('builtin, position').to_a, assigns(:roles)
35 36
36 37 assert_select 'a[href="/roles/1/edit"]', :text => 'Manager'
37 38 end
38 39
39 40 def test_new
40 41 get :new
41 42 assert_response :success
42 43 assert_template 'new'
43 44 end
44 45
45 46 def test_new_with_copy
46 47 copy_from = Role.find(2)
47 48
48 49 get :new, :copy => copy_from.id.to_s
49 50 assert_response :success
50 51 assert_template 'new'
51 52
52 53 role = assigns(:role)
53 54 assert_equal copy_from.permissions, role.permissions
54 55
55 56 assert_select 'form' do
56 57 # blank name
57 58 assert_select 'input[name=?][value=""]', 'role[name]'
58 59 # edit_project permission checked
59 60 assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]'
60 61 # add_project permission not checked
61 62 assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]'
62 63 assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0
63 64 # workflow copy selected
64 65 assert_select 'select[name=?]', 'copy_workflow_from' do
65 66 assert_select 'option[value="2"][selected=selected]'
66 67 end
67 68 end
68 69 end
69 70
70 71 def test_create_with_validaton_failure
71 72 post :create, :role => {:name => '',
72 73 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
73 74 :assignable => '0'}
74 75
75 76 assert_response :success
76 77 assert_template 'new'
77 78 assert_select 'div#errorExplanation'
78 79 end
79 80
80 81 def test_create_without_workflow_copy
81 82 post :create, :role => {:name => 'RoleWithoutWorkflowCopy',
82 83 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
83 84 :assignable => '0'}
84 85
85 86 assert_redirected_to '/roles'
86 87 role = Role.find_by_name('RoleWithoutWorkflowCopy')
87 88 assert_not_nil role
88 89 assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
89 90 assert !role.assignable?
90 91 end
91 92
92 93 def test_create_with_workflow_copy
93 94 post :create, :role => {:name => 'RoleWithWorkflowCopy',
94 95 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
95 96 :assignable => '0'},
96 97 :copy_workflow_from => '1'
97 98
98 99 assert_redirected_to '/roles'
99 100 role = Role.find_by_name('RoleWithWorkflowCopy')
100 101 assert_not_nil role
101 102 assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size
102 103 end
103 104
104 105 def test_edit
105 106 get :edit, :id => 1
106 107 assert_response :success
107 108 assert_template 'edit'
108 109 assert_equal Role.find(1), assigns(:role)
109 110 assert_select 'select[name=?]', 'role[issues_visibility]'
110 111 end
111 112
112 113 def test_edit_anonymous
113 114 get :edit, :id => Role.anonymous.id
114 115 assert_response :success
115 116 assert_template 'edit'
116 117 assert_select 'select[name=?]', 'role[issues_visibility]', 0
117 118 end
118 119
119 120 def test_edit_invalid_should_respond_with_404
120 121 get :edit, :id => 999
121 122 assert_response 404
122 123 end
123 124
124 125 def test_update
125 126 put :update, :id => 1,
126 127 :role => {:name => 'Manager',
127 128 :permissions => ['edit_project', ''],
128 129 :assignable => '0'}
129 130
130 131 assert_redirected_to '/roles'
131 132 role = Role.find(1)
132 133 assert_equal [:edit_project], role.permissions
133 134 end
134 135
135 136 def test_update_with_failure
136 137 put :update, :id => 1, :role => {:name => ''}
137 138 assert_response :success
138 139 assert_template 'edit'
139 140 end
140 141
141 142 def test_destroy
142 143 r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
143 144
144 145 delete :destroy, :id => r
145 146 assert_redirected_to '/roles'
146 147 assert_nil Role.find_by_id(r.id)
147 148 end
148 149
149 150 def test_destroy_role_in_use
150 151 delete :destroy, :id => 1
151 152 assert_redirected_to '/roles'
152 153 assert_equal 'This role is in use and cannot be deleted.', flash[:error]
153 154 assert_not_nil Role.find_by_id(1)
154 155 end
155 156
156 157 def test_get_permissions
157 158 get :permissions
158 159 assert_response :success
159 160 assert_template 'permissions'
160 161
161 162 assert_not_nil assigns(:roles)
162 163 assert_equal Role.order('builtin, position').to_a, assigns(:roles)
163 164
164 165 assert_select 'input[name=?][type=checkbox][value=add_issues][checked=checked]', 'permissions[3][]'
165 166 assert_select 'input[name=?][type=checkbox][value=delete_issues]:not([checked])', 'permissions[3][]'
166 167 end
167 168
168 169 def test_post_permissions
169 170 post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
170 171 assert_redirected_to '/roles'
171 172
172 173 assert_equal [:edit_issues], Role.find(1).permissions
173 174 assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
174 175 assert Role.find(2).permissions.empty?
175 176 end
176 177
177 178 def test_clear_all_permissions
178 179 post :permissions, :permissions => { '0' => '' }
179 180 assert_redirected_to '/roles'
180 181 assert Role.find(1).permissions.empty?
181 182 end
182 183
183 184 def test_move_highest
184 185 put :update, :id => 3, :role => {:move_to => 'highest'}
185 186 assert_redirected_to '/roles'
186 187 assert_equal 1, Role.find(3).position
187 188 end
188 189
189 190 def test_move_higher
190 191 position = Role.find(3).position
191 192 put :update, :id => 3, :role => {:move_to => 'higher'}
192 193 assert_redirected_to '/roles'
193 194 assert_equal position - 1, Role.find(3).position
194 195 end
195 196
196 197 def test_move_lower
197 198 position = Role.find(2).position
198 199 put :update, :id => 2, :role => {:move_to => 'lower'}
199 200 assert_redirected_to '/roles'
200 201 assert_equal position + 1, Role.find(2).position
201 202 end
202 203
203 204 def test_move_lowest
204 205 put :update, :id => 2, :role => {:move_to => 'lowest'}
205 206 assert_redirected_to '/roles'
206 207 assert_equal Role.count, Role.find(2).position
207 208 end
208 209 end
@@ -1,192 +1,193
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class SettingsControllerTest < ActionController::TestCase
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :users
23 23
24 24 def setup
25 25 User.current = nil
26 26 @request.session[:user_id] = 1 # admin
27 Redmine::SudoMode.disable!
27 28 end
28 29
29 30 def test_index
30 31 get :index
31 32 assert_response :success
32 33 assert_template 'edit'
33 34 end
34 35
35 36 def test_get_edit
36 37 get :edit
37 38 assert_response :success
38 39 assert_template 'edit'
39 40
40 41 assert_select 'input[name=?][value=""]', 'settings[enabled_scm][]'
41 42 end
42 43
43 44 def test_get_edit_should_preselect_default_issue_list_columns
44 45 with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
45 46 get :edit
46 47 assert_response :success
47 48 end
48 49
49 50 assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
50 51 assert_select 'option', 4
51 52 assert_select 'option[value=tracker]', :text => 'Tracker'
52 53 assert_select 'option[value=subject]', :text => 'Subject'
53 54 assert_select 'option[value=status]', :text => 'Status'
54 55 assert_select 'option[value=updated_on]', :text => 'Updated'
55 56 end
56 57
57 58 assert_select 'select[id=available_columns]' do
58 59 assert_select 'option[value=tracker]', 0
59 60 assert_select 'option[value=priority]', :text => 'Priority'
60 61 end
61 62 end
62 63
63 64 def test_get_edit_without_trackers_should_succeed
64 65 Tracker.delete_all
65 66
66 67 get :edit
67 68 assert_response :success
68 69 end
69 70
70 71 def test_post_edit_notifications
71 72 post :edit, :settings => {:mail_from => 'functional@test.foo',
72 73 :bcc_recipients => '0',
73 74 :notified_events => %w(issue_added issue_updated news_added),
74 75 :emails_footer => 'Test footer'
75 76 }
76 77 assert_redirected_to '/settings'
77 78 assert_equal 'functional@test.foo', Setting.mail_from
78 79 assert !Setting.bcc_recipients?
79 80 assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
80 81 assert_equal 'Test footer', Setting.emails_footer
81 82 Setting.clear_cache
82 83 end
83 84
84 85 def test_edit_commit_update_keywords
85 86 with_settings :commit_update_keywords => [
86 87 {"keywords" => "fixes, resolves", "status_id" => "3"},
87 88 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
88 89 ] do
89 90 get :edit
90 91 end
91 92 assert_response :success
92 93 assert_select 'tr.commit-keywords', 2
93 94 assert_select 'tr.commit-keywords:nth-child(1)' do
94 95 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
95 96 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
96 97 assert_select 'option[value="3"][selected=selected]'
97 98 end
98 99 end
99 100 assert_select 'tr.commit-keywords:nth-child(2)' do
100 101 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
101 102 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
102 103 assert_select 'option[value="5"][selected=selected]', :text => 'Closed'
103 104 end
104 105 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
105 106 assert_select 'option[value="100"][selected=selected]', :text => '100 %'
106 107 end
107 108 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
108 109 assert_select 'option[value="2"][selected=selected]', :text => 'Feature request'
109 110 end
110 111 end
111 112 end
112 113
113 114 def test_edit_without_commit_update_keywords_should_show_blank_line
114 115 with_settings :commit_update_keywords => [] do
115 116 get :edit
116 117 end
117 118 assert_response :success
118 119 assert_select 'tr.commit-keywords', 1 do
119 120 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
120 121 end
121 122 end
122 123
123 124 def test_post_edit_commit_update_keywords
124 125 post :edit, :settings => {
125 126 :commit_update_keywords => {
126 127 :keywords => ["resolves", "closes"],
127 128 :status_id => ["3", "5"],
128 129 :done_ratio => ["", "100"],
129 130 :if_tracker_id => ["", "2"]
130 131 }
131 132 }
132 133 assert_redirected_to '/settings'
133 134 assert_equal([
134 135 {"keywords" => "resolves", "status_id" => "3"},
135 136 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
136 137 ], Setting.commit_update_keywords)
137 138 end
138 139
139 140 def test_get_plugin_settings
140 141 ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins"))
141 142 Redmine::Plugin.register :foo do
142 143 settings :partial => "foo_plugin/foo_plugin_settings"
143 144 end
144 145 Setting.plugin_foo = {'sample_setting' => 'Plugin setting value'}
145 146
146 147 get :plugin, :id => 'foo'
147 148 assert_response :success
148 149 assert_template 'plugin'
149 150 assert_select 'form[action="/settings/plugin/foo"]' do
150 151 assert_select 'input[name=?][value=?]', 'settings[sample_setting]', 'Plugin setting value'
151 152 end
152 153 ensure
153 154 Redmine::Plugin.unregister(:foo)
154 155 end
155 156
156 157 def test_get_invalid_plugin_settings
157 158 get :plugin, :id => 'none'
158 159 assert_response 404
159 160 end
160 161
161 162 def test_get_non_configurable_plugin_settings
162 163 Redmine::Plugin.register(:foo) {}
163 164
164 165 get :plugin, :id => 'foo'
165 166 assert_response 404
166 167
167 168 ensure
168 169 Redmine::Plugin.unregister(:foo)
169 170 end
170 171
171 172 def test_post_plugin_settings
172 173 Redmine::Plugin.register(:foo) do
173 174 settings :partial => 'not blank', # so that configurable? is true
174 175 :default => {'sample_setting' => 'Plugin setting value'}
175 176 end
176 177
177 178 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
178 179 assert_redirected_to '/settings/plugin/foo'
179 180
180 181 assert_equal({'sample_setting' => 'Value'}, Setting.plugin_foo)
181 182 end
182 183
183 184 def test_post_non_configurable_plugin_settings
184 185 Redmine::Plugin.register(:foo) {}
185 186
186 187 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
187 188 assert_response 404
188 189
189 190 ensure
190 191 Redmine::Plugin.unregister(:foo)
191 192 end
192 193 end
@@ -1,452 +1,453
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class UsersControllerTest < ActionController::TestCase
21 21 include Redmine::I18n
22 22
23 23 fixtures :users, :email_addresses, :projects, :members, :member_roles, :roles,
24 24 :custom_fields, :custom_values, :groups_users,
25 25 :auth_sources,
26 26 :enabled_modules,
27 27 :issues, :issue_statuses,
28 28 :trackers
29 29
30 30 def setup
31 31 User.current = nil
32 32 @request.session[:user_id] = 1 # admin
33 Redmine::SudoMode.disable!
33 34 end
34 35
35 36 def test_index
36 37 get :index
37 38 assert_response :success
38 39 assert_template 'index'
39 40 assert_not_nil assigns(:users)
40 41 # active users only
41 42 assert_nil assigns(:users).detect {|u| !u.active?}
42 43 end
43 44
44 45 def test_index_with_status_filter
45 46 get :index, :status => 3
46 47 assert_response :success
47 48 assert_template 'index'
48 49 assert_not_nil assigns(:users)
49 50 assert_equal [3], assigns(:users).map(&:status).uniq
50 51 end
51 52
52 53 def test_index_with_name_filter
53 54 get :index, :name => 'john'
54 55 assert_response :success
55 56 assert_template 'index'
56 57 users = assigns(:users)
57 58 assert_not_nil users
58 59 assert_equal 1, users.size
59 60 assert_equal 'John', users.first.firstname
60 61 end
61 62
62 63 def test_index_with_group_filter
63 64 get :index, :group_id => '10'
64 65 assert_response :success
65 66 assert_template 'index'
66 67 users = assigns(:users)
67 68 assert users.any?
68 69 assert_equal([], (users - Group.find(10).users))
69 70 assert_select 'select[name=group_id]' do
70 71 assert_select 'option[value="10"][selected=selected]'
71 72 end
72 73 end
73 74
74 75 def test_show
75 76 @request.session[:user_id] = nil
76 77 get :show, :id => 2
77 78 assert_response :success
78 79 assert_template 'show'
79 80 assert_not_nil assigns(:user)
80 81
81 82 assert_select 'li', :text => /Phone number/
82 83 end
83 84
84 85 def test_show_should_not_display_hidden_custom_fields
85 86 @request.session[:user_id] = nil
86 87 UserCustomField.find_by_name('Phone number').update_attribute :visible, false
87 88 get :show, :id => 2
88 89 assert_response :success
89 90 assert_template 'show'
90 91 assert_not_nil assigns(:user)
91 92
92 93 assert_select 'li', :text => /Phone number/, :count => 0
93 94 end
94 95
95 96 def test_show_should_not_fail_when_custom_values_are_nil
96 97 user = User.find(2)
97 98
98 99 # Create a custom field to illustrate the issue
99 100 custom_field = CustomField.create!(:name => 'Testing', :field_format => 'text')
100 101 custom_value = user.custom_values.build(:custom_field => custom_field).save!
101 102
102 103 get :show, :id => 2
103 104 assert_response :success
104 105 end
105 106
106 107 def test_show_inactive
107 108 @request.session[:user_id] = nil
108 109 get :show, :id => 5
109 110 assert_response 404
110 111 end
111 112
112 113 def test_show_inactive_by_admin
113 114 @request.session[:user_id] = 1
114 115 get :show, :id => 5
115 116 assert_response 200
116 117 assert_not_nil assigns(:user)
117 118 end
118 119
119 120 def test_show_user_who_is_not_visible_should_return_404
120 121 Role.anonymous.update! :users_visibility => 'members_of_visible_projects'
121 122 user = User.generate!
122 123
123 124 @request.session[:user_id] = nil
124 125 get :show, :id => user.id
125 126 assert_response 404
126 127 end
127 128
128 129 def test_show_displays_memberships_based_on_project_visibility
129 130 @request.session[:user_id] = 1
130 131 get :show, :id => 2
131 132 assert_response :success
132 133 memberships = assigns(:memberships)
133 134 assert_not_nil memberships
134 135 project_ids = memberships.map(&:project_id)
135 136 assert project_ids.include?(2) #private project admin can see
136 137 end
137 138
138 139 def test_show_current_should_require_authentication
139 140 @request.session[:user_id] = nil
140 141 get :show, :id => 'current'
141 142 assert_response 302
142 143 end
143 144
144 145 def test_show_current
145 146 @request.session[:user_id] = 2
146 147 get :show, :id => 'current'
147 148 assert_response :success
148 149 assert_template 'show'
149 150 assert_equal User.find(2), assigns(:user)
150 151 end
151 152
152 153 def test_new
153 154 get :new
154 155 assert_response :success
155 156 assert_template :new
156 157 assert assigns(:user)
157 158 end
158 159
159 160 def test_create
160 161 Setting.bcc_recipients = '1'
161 162
162 163 assert_difference 'User.count' do
163 164 assert_difference 'ActionMailer::Base.deliveries.size' do
164 165 post :create,
165 166 :user => {
166 167 :firstname => 'John',
167 168 :lastname => 'Doe',
168 169 :login => 'jdoe',
169 170 :password => 'secret123',
170 171 :password_confirmation => 'secret123',
171 172 :mail => 'jdoe@gmail.com',
172 173 :mail_notification => 'none'
173 174 },
174 175 :send_information => '1'
175 176 end
176 177 end
177 178
178 179 user = User.order('id DESC').first
179 180 assert_redirected_to :controller => 'users', :action => 'edit', :id => user.id
180 181
181 182 assert_equal 'John', user.firstname
182 183 assert_equal 'Doe', user.lastname
183 184 assert_equal 'jdoe', user.login
184 185 assert_equal 'jdoe@gmail.com', user.mail
185 186 assert_equal 'none', user.mail_notification
186 187 assert user.check_password?('secret123')
187 188
188 189 mail = ActionMailer::Base.deliveries.last
189 190 assert_not_nil mail
190 191 assert_equal [user.mail], mail.bcc
191 192 assert_mail_body_match 'secret', mail
192 193 end
193 194
194 195 def test_create_with_preferences
195 196 assert_difference 'User.count' do
196 197 post :create,
197 198 :user => {
198 199 :firstname => 'John',
199 200 :lastname => 'Doe',
200 201 :login => 'jdoe',
201 202 :password => 'secret123',
202 203 :password_confirmation => 'secret123',
203 204 :mail => 'jdoe@gmail.com',
204 205 :mail_notification => 'none'
205 206 },
206 207 :pref => {
207 208 'hide_mail' => '1',
208 209 'time_zone' => 'Paris',
209 210 'comments_sorting' => 'desc',
210 211 'warn_on_leaving_unsaved' => '0'
211 212 }
212 213 end
213 214 user = User.order('id DESC').first
214 215 assert_equal 'jdoe', user.login
215 216 assert_equal true, user.pref.hide_mail
216 217 assert_equal 'Paris', user.pref.time_zone
217 218 assert_equal 'desc', user.pref[:comments_sorting]
218 219 assert_equal '0', user.pref[:warn_on_leaving_unsaved]
219 220 end
220 221
221 222 def test_create_with_generate_password_should_email_the_password
222 223 assert_difference 'User.count' do
223 224 post :create, :user => {
224 225 :login => 'randompass',
225 226 :firstname => 'Random',
226 227 :lastname => 'Pass',
227 228 :mail => 'randompass@example.net',
228 229 :language => 'en',
229 230 :generate_password => '1',
230 231 :password => '',
231 232 :password_confirmation => ''
232 233 }, :send_information => 1
233 234 end
234 235 user = User.order('id DESC').first
235 236 assert_equal 'randompass', user.login
236 237
237 238 mail = ActionMailer::Base.deliveries.last
238 239 assert_not_nil mail
239 240 m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/)
240 241 assert m
241 242 password = m[1]
242 243 assert user.check_password?(password)
243 244 end
244 245
245 246 def test_create_and_continue
246 247 post :create, :user => {
247 248 :login => 'randompass',
248 249 :firstname => 'Random',
249 250 :lastname => 'Pass',
250 251 :mail => 'randompass@example.net',
251 252 :generate_password => '1'
252 253 }, :continue => '1'
253 254 assert_redirected_to '/users/new?user%5Bgenerate_password%5D=1'
254 255 end
255 256
256 257 def test_create_with_failure
257 258 assert_no_difference 'User.count' do
258 259 post :create, :user => {}
259 260 end
260 261 assert_response :success
261 262 assert_template 'new'
262 263 end
263 264
264 265 def test_create_with_failure_sould_preserve_preference
265 266 assert_no_difference 'User.count' do
266 267 post :create,
267 268 :user => {},
268 269 :pref => {
269 270 'no_self_notified' => '1',
270 271 'hide_mail' => '1',
271 272 'time_zone' => 'Paris',
272 273 'comments_sorting' => 'desc',
273 274 'warn_on_leaving_unsaved' => '0'
274 275 }
275 276 end
276 277 assert_response :success
277 278 assert_template 'new'
278 279
279 280 assert_select 'select#pref_time_zone option[selected=selected]', :text => /Paris/
280 281 assert_select 'input#pref_no_self_notified[value="1"][checked=checked]'
281 282 end
282 283
283 284 def test_edit
284 285 get :edit, :id => 2
285 286 assert_response :success
286 287 assert_template 'edit'
287 288 assert_equal User.find(2), assigns(:user)
288 289 end
289 290
290 291 def test_edit_registered_user
291 292 assert User.find(2).register!
292 293
293 294 get :edit, :id => 2
294 295 assert_response :success
295 296 assert_select 'a', :text => 'Activate'
296 297 end
297 298
298 299 def test_update
299 300 ActionMailer::Base.deliveries.clear
300 301 put :update, :id => 2,
301 302 :user => {:firstname => 'Changed', :mail_notification => 'only_assigned'},
302 303 :pref => {:hide_mail => '1', :comments_sorting => 'desc'}
303 304 user = User.find(2)
304 305 assert_equal 'Changed', user.firstname
305 306 assert_equal 'only_assigned', user.mail_notification
306 307 assert_equal true, user.pref[:hide_mail]
307 308 assert_equal 'desc', user.pref[:comments_sorting]
308 309 assert ActionMailer::Base.deliveries.empty?
309 310 end
310 311
311 312 def test_update_with_failure
312 313 assert_no_difference 'User.count' do
313 314 put :update, :id => 2, :user => {:firstname => ''}
314 315 end
315 316 assert_response :success
316 317 assert_template 'edit'
317 318 end
318 319
319 320 def test_update_with_group_ids_should_assign_groups
320 321 put :update, :id => 2, :user => {:group_ids => ['10']}
321 322 user = User.find(2)
322 323 assert_equal [10], user.group_ids
323 324 end
324 325
325 326 def test_update_with_activation_should_send_a_notification
326 327 u = User.new(:firstname => 'Foo', :lastname => 'Bar', :mail => 'foo.bar@somenet.foo', :language => 'fr')
327 328 u.login = 'foo'
328 329 u.status = User::STATUS_REGISTERED
329 330 u.save!
330 331 ActionMailer::Base.deliveries.clear
331 332 Setting.bcc_recipients = '1'
332 333
333 334 put :update, :id => u.id, :user => {:status => User::STATUS_ACTIVE}
334 335 assert u.reload.active?
335 336 mail = ActionMailer::Base.deliveries.last
336 337 assert_not_nil mail
337 338 assert_equal ['foo.bar@somenet.foo'], mail.bcc
338 339 assert_mail_body_match ll('fr', :notice_account_activated), mail
339 340 end
340 341
341 342 def test_update_with_password_change_should_send_a_notification
342 343 ActionMailer::Base.deliveries.clear
343 344 Setting.bcc_recipients = '1'
344 345
345 346 put :update, :id => 2, :user => {:password => 'newpass123', :password_confirmation => 'newpass123'}, :send_information => '1'
346 347 u = User.find(2)
347 348 assert u.check_password?('newpass123')
348 349
349 350 mail = ActionMailer::Base.deliveries.last
350 351 assert_not_nil mail
351 352 assert_equal [u.mail], mail.bcc
352 353 assert_mail_body_match 'newpass123', mail
353 354 end
354 355
355 356 def test_update_with_generate_password_should_email_the_password
356 357 ActionMailer::Base.deliveries.clear
357 358 Setting.bcc_recipients = '1'
358 359
359 360 put :update, :id => 2, :user => {
360 361 :generate_password => '1',
361 362 :password => '',
362 363 :password_confirmation => ''
363 364 }, :send_information => '1'
364 365
365 366 mail = ActionMailer::Base.deliveries.last
366 367 assert_not_nil mail
367 368 m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/)
368 369 assert m
369 370 password = m[1]
370 371 assert User.find(2).check_password?(password)
371 372 end
372 373
373 374 def test_update_without_generate_password_should_not_change_password
374 375 put :update, :id => 2, :user => {
375 376 :firstname => 'changed',
376 377 :generate_password => '0',
377 378 :password => '',
378 379 :password_confirmation => ''
379 380 }, :send_information => '1'
380 381
381 382 user = User.find(2)
382 383 assert_equal 'changed', user.firstname
383 384 assert user.check_password?('jsmith')
384 385 end
385 386
386 387 def test_update_user_switchin_from_auth_source_to_password_authentication
387 388 # Configure as auth source
388 389 u = User.find(2)
389 390 u.auth_source = AuthSource.find(1)
390 391 u.save!
391 392
392 393 put :update, :id => u.id, :user => {:auth_source_id => '', :password => 'newpass123', :password_confirmation => 'newpass123'}
393 394
394 395 assert_equal nil, u.reload.auth_source
395 396 assert u.check_password?('newpass123')
396 397 end
397 398
398 399 def test_update_notified_project
399 400 get :edit, :id => 2
400 401 assert_response :success
401 402 assert_template 'edit'
402 403 u = User.find(2)
403 404 assert_equal [1, 2, 5], u.projects.collect{|p| p.id}.sort
404 405 assert_equal [1, 2, 5], u.notified_projects_ids.sort
405 406 assert_select 'input[name=?][value=?]', 'user[notified_project_ids][]', '1'
406 407 assert_equal 'all', u.mail_notification
407 408 put :update, :id => 2,
408 409 :user => {
409 410 :mail_notification => 'selected',
410 411 :notified_project_ids => [1, 2]
411 412 }
412 413 u = User.find(2)
413 414 assert_equal 'selected', u.mail_notification
414 415 assert_equal [1, 2], u.notified_projects_ids.sort
415 416 end
416 417
417 418 def test_update_status_should_not_update_attributes
418 419 user = User.find(2)
419 420 user.pref[:no_self_notified] = '1'
420 421 user.pref.save
421 422
422 423 put :update, :id => 2, :user => {:status => 3}
423 424 assert_response 302
424 425 user = User.find(2)
425 426 assert_equal 3, user.status
426 427 assert_equal '1', user.pref[:no_self_notified]
427 428 end
428 429
429 430 def test_destroy
430 431 assert_difference 'User.count', -1 do
431 432 delete :destroy, :id => 2
432 433 end
433 434 assert_redirected_to '/users'
434 435 assert_nil User.find_by_id(2)
435 436 end
436 437
437 438 def test_destroy_should_be_denied_for_non_admin_users
438 439 @request.session[:user_id] = 3
439 440
440 441 assert_no_difference 'User.count' do
441 442 get :destroy, :id => 2
442 443 end
443 444 assert_response 403
444 445 end
445 446
446 447 def test_destroy_should_redirect_to_back_url_param
447 448 assert_difference 'User.count', -1 do
448 449 delete :destroy, :id => 2, :back_url => '/users?name=foo'
449 450 end
450 451 assert_redirected_to '/users?name=foo'
451 452 end
452 453 end
@@ -1,61 +1,78
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class AdminTest < Redmine::IntegrationTest
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :enumerations, :users, :issue_categories,
23 23 :projects_trackers,
24 24 :roles,
25 25 :member_roles,
26 26 :members,
27 27 :enabled_modules
28 28
29 def setup
30 Redmine::SudoMode.enable!
31 end
32
33 def teardown
34 Redmine::SudoMode.disable!
35 end
36
29 37 def test_add_user
30 38 log_user("admin", "admin")
31 39 get "/users/new"
32 40 assert_response :success
33 41 assert_template "users/new"
34 42 post "/users",
35 43 :user => { :login => "psmith", :firstname => "Paul",
36 44 :lastname => "Smith", :mail => "psmith@somenet.foo",
37 45 :language => "en", :password => "psmith09",
38 46 :password_confirmation => "psmith09" }
47 assert_response :success
48 assert_nil User.find_by_login("psmith")
49
50 post "/users",
51 :user => { :login => "psmith", :firstname => "Paul",
52 :lastname => "Smith", :mail => "psmith@somenet.foo",
53 :language => "en", :password => "psmith09",
54 :password_confirmation => "psmith09" },
55 :sudo_password => 'admin'
39 56
40 57 user = User.find_by_login("psmith")
41 58 assert_kind_of User, user
42 59 assert_redirected_to "/users/#{ user.id }/edit"
43 60
44 61 logged_user = User.try_to_login("psmith", "psmith09")
45 62 assert_kind_of User, logged_user
46 63 assert_equal "Paul", logged_user.firstname
47 64
48 65 put "/users/#{user.id}", :id => user.id, :user => { :status => User::STATUS_LOCKED }
49 66 assert_redirected_to "/users/#{ user.id }/edit"
50 67 locked_user = User.try_to_login("psmith", "psmith09")
51 68 assert_equal nil, locked_user
52 69 end
53 70
54 71 test "Add a user as an anonymous user should fail" do
55 72 post '/users',
56 73 :user => { :login => 'psmith', :firstname => 'Paul'},
57 74 :password => "psmith09", :password_confirmation => "psmith09"
58 75 assert_response :redirect
59 76 assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fusers"
60 77 end
61 78 end
General Comments 0
You need to be logged in to leave comments. Login now