##// END OF EJS Templates
Security notifications when password or email adress is changed (#21421)....
Jean-Philippe Lang -
r14763:5d70fce6ce4c
parent child
Show More
@@ -0,0 +1,13
1 <p><%= @message %><br />
2 <% if @url && @title -%>
3 <%= link_to @title, @url -%>
4 <% elsif @url -%>
5 <%= link_to @url -%>
6 <% elsif @title -%>
7 <%= content_tag :h1, @title -%>
8 <% end %></p>
9
10 <p><%= l(:field_user) %>: <strong><%= User.current.login %></strong><br/>
11 <%= l(:field_remote_ip) %>: <strong><%= User.current.remote_ip %></strong><br/>
12 <%= l(:label_date) %>: <strong><%= format_time Time.now, true, @user %></strong></p>
13
@@ -0,0 +1,8
1 <%= @message %>
2
3 <%= @url || @title %>
4
5 <%= l(:field_user) %>: <%= User.current.login %>
6 <%= l(:field_remote_ip) %>: <%= User.current.remote_ip %>
7 <%= l(:label_date) %>: <%= format_time Time.now, true, @user %>
8
@@ -1,360 +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 class AccountController < ApplicationController
19 19 helper :custom_fields
20 20 include CustomFieldsHelper
21 21
22 22 # prevents login action to be filtered by check_if_login_required application scope filter
23 23 skip_before_filter :check_if_login_required, :check_password_change
24 24
25 25 # Overrides ApplicationController#verify_authenticity_token to disable
26 26 # token verification on openid callbacks
27 27 def verify_authenticity_token
28 28 unless using_open_id?
29 29 super
30 30 end
31 31 end
32 32
33 33 # Login request and validation
34 34 def login
35 35 if request.get?
36 36 if User.current.logged?
37 37 redirect_back_or_default home_url, :referer => true
38 38 end
39 39 else
40 40 authenticate_user
41 41 end
42 42 rescue AuthSourceException => e
43 43 logger.error "An error occured when authenticating #{params[:username]}: #{e.message}"
44 44 render_error :message => e.message
45 45 end
46 46
47 47 # Log out current user and redirect to welcome page
48 48 def logout
49 49 if User.current.anonymous?
50 50 redirect_to home_url
51 51 elsif request.post?
52 52 logout_user
53 53 redirect_to home_url
54 54 end
55 55 # display the logout form
56 56 end
57 57
58 58 # Lets user choose a new password
59 59 def lost_password
60 60 (redirect_to(home_url); return) unless Setting.lost_password?
61 61 if params[:token]
62 62 @token = Token.find_token("recovery", params[:token].to_s)
63 63 if @token.nil? || @token.expired?
64 64 redirect_to home_url
65 65 return
66 66 end
67 67 @user = @token.user
68 68 unless @user && @user.active?
69 69 redirect_to home_url
70 70 return
71 71 end
72 72 if request.post?
73 73 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
74 74 if @user.save
75 75 @token.destroy
76 Mailer.security_notification(@user,
77 message: :mail_body_security_notification_change,
78 field: :field_password,
79 title: :button_change_password,
80 url: {controller: 'my', action: 'password'}
81 ).deliver
76 82 flash[:notice] = l(:notice_account_password_updated)
77 83 redirect_to signin_path
78 84 return
79 85 end
80 86 end
81 87 render :template => "account/password_recovery"
82 88 return
83 89 else
84 90 if request.post?
85 91 email = params[:mail].to_s
86 92 user = User.find_by_mail(email)
87 93 # user not found
88 94 unless user
89 95 flash.now[:error] = l(:notice_account_unknown_email)
90 96 return
91 97 end
92 98 unless user.active?
93 99 handle_inactive_user(user, lost_password_path)
94 100 return
95 101 end
96 102 # user cannot change its password
97 103 unless user.change_password_allowed?
98 104 flash.now[:error] = l(:notice_can_t_change_password)
99 105 return
100 106 end
101 107 # create a new token for password recovery
102 108 token = Token.new(:user => user, :action => "recovery")
103 109 if token.save
104 110 # Don't use the param to send the email
105 111 recipent = user.mails.detect {|e| email.casecmp(e) == 0} || user.mail
106 112 Mailer.lost_password(token, recipent).deliver
107 113 flash[:notice] = l(:notice_account_lost_email_sent)
108 114 redirect_to signin_path
109 115 return
110 116 end
111 117 end
112 118 end
113 119 end
114 120
115 121 # User self-registration
116 122 def register
117 123 (redirect_to(home_url); return) unless Setting.self_registration? || session[:auth_source_registration]
118 124 if request.get?
119 125 session[:auth_source_registration] = nil
120 126 @user = User.new(:language => current_language.to_s)
121 127 else
122 128 user_params = params[:user] || {}
123 129 @user = User.new
124 130 @user.safe_attributes = user_params
125 131 @user.pref.attributes = params[:pref] if params[:pref]
126 132 @user.admin = false
127 133 @user.register
128 134 if session[:auth_source_registration]
129 135 @user.activate
130 136 @user.login = session[:auth_source_registration][:login]
131 137 @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
132 138 if @user.save
133 139 session[:auth_source_registration] = nil
134 140 self.logged_user = @user
135 141 flash[:notice] = l(:notice_account_activated)
136 142 redirect_to my_account_path
137 143 end
138 144 else
139 145 @user.login = params[:user][:login]
140 146 unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank?
141 147 @user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation]
142 148 end
143 149
144 150 case Setting.self_registration
145 151 when '1'
146 152 register_by_email_activation(@user)
147 153 when '3'
148 154 register_automatically(@user)
149 155 else
150 156 register_manually_by_administrator(@user)
151 157 end
152 158 end
153 159 end
154 160 end
155 161
156 162 # Token based account activation
157 163 def activate
158 164 (redirect_to(home_url); return) unless Setting.self_registration? && params[:token].present?
159 165 token = Token.find_token('register', params[:token].to_s)
160 166 (redirect_to(home_url); return) unless token and !token.expired?
161 167 user = token.user
162 168 (redirect_to(home_url); return) unless user.registered?
163 169 user.activate
164 170 if user.save
165 171 token.destroy
166 172 flash[:notice] = l(:notice_account_activated)
167 173 end
168 174 redirect_to signin_path
169 175 end
170 176
171 177 # Sends a new account activation email
172 178 def activation_email
173 179 if session[:registered_user_id] && Setting.self_registration == '1'
174 180 user_id = session.delete(:registered_user_id).to_i
175 181 user = User.find_by_id(user_id)
176 182 if user && user.registered?
177 183 register_by_email_activation(user)
178 184 return
179 185 end
180 186 end
181 187 redirect_to(home_url)
182 188 end
183 189
184 190 private
185 191
186 192 def authenticate_user
187 193 if Setting.openid? && using_open_id?
188 194 open_id_authenticate(params[:openid_url])
189 195 else
190 196 password_authentication
191 197 end
192 198 end
193 199
194 200 def password_authentication
195 201 user = User.try_to_login(params[:username], params[:password], false)
196 202
197 203 if user.nil?
198 204 invalid_credentials
199 205 elsif user.new_record?
200 206 onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
201 207 else
202 208 # Valid user
203 209 if user.active?
204 210 successful_authentication(user)
205 211 update_sudo_timestamp! # activate Sudo Mode
206 212 else
207 213 handle_inactive_user(user)
208 214 end
209 215 end
210 216 end
211 217
212 218 def open_id_authenticate(openid_url)
213 219 back_url = signin_url(:autologin => params[:autologin])
214 220 authenticate_with_open_id(
215 221 openid_url, :required => [:nickname, :fullname, :email],
216 222 :return_to => back_url, :method => :post
217 223 ) do |result, identity_url, registration|
218 224 if result.successful?
219 225 user = User.find_or_initialize_by_identity_url(identity_url)
220 226 if user.new_record?
221 227 # Self-registration off
222 228 (redirect_to(home_url); return) unless Setting.self_registration?
223 229 # Create on the fly
224 230 user.login = registration['nickname'] unless registration['nickname'].nil?
225 231 user.mail = registration['email'] unless registration['email'].nil?
226 232 user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
227 233 user.random_password
228 234 user.register
229 235 case Setting.self_registration
230 236 when '1'
231 237 register_by_email_activation(user) do
232 238 onthefly_creation_failed(user)
233 239 end
234 240 when '3'
235 241 register_automatically(user) do
236 242 onthefly_creation_failed(user)
237 243 end
238 244 else
239 245 register_manually_by_administrator(user) do
240 246 onthefly_creation_failed(user)
241 247 end
242 248 end
243 249 else
244 250 # Existing record
245 251 if user.active?
246 252 successful_authentication(user)
247 253 else
248 254 handle_inactive_user(user)
249 255 end
250 256 end
251 257 end
252 258 end
253 259 end
254 260
255 261 def successful_authentication(user)
256 262 logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
257 263 # Valid user
258 264 self.logged_user = user
259 265 # generate a key and set cookie if autologin
260 266 if params[:autologin] && Setting.autologin?
261 267 set_autologin_cookie(user)
262 268 end
263 269 call_hook(:controller_account_success_authentication_after, {:user => user })
264 270 redirect_back_or_default my_page_path
265 271 end
266 272
267 273 def set_autologin_cookie(user)
268 274 token = Token.create(:user => user, :action => 'autologin')
269 275 secure = Redmine::Configuration['autologin_cookie_secure']
270 276 if secure.nil?
271 277 secure = request.ssl?
272 278 end
273 279 cookie_options = {
274 280 :value => token.value,
275 281 :expires => 1.year.from_now,
276 282 :path => (Redmine::Configuration['autologin_cookie_path'] || RedmineApp::Application.config.relative_url_root || '/'),
277 283 :secure => secure,
278 284 :httponly => true
279 285 }
280 286 cookies[autologin_cookie_name] = cookie_options
281 287 end
282 288
283 289 # Onthefly creation failed, display the registration form to fill/fix attributes
284 290 def onthefly_creation_failed(user, auth_source_options = { })
285 291 @user = user
286 292 session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
287 293 render :action => 'register'
288 294 end
289 295
290 296 def invalid_credentials
291 297 logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
292 298 flash.now[:error] = l(:notice_account_invalid_credentials)
293 299 end
294 300
295 301 # Register a user for email activation.
296 302 #
297 303 # Pass a block for behavior when a user fails to save
298 304 def register_by_email_activation(user, &block)
299 305 token = Token.new(:user => user, :action => "register")
300 306 if user.save and token.save
301 307 Mailer.register(token).deliver
302 308 flash[:notice] = l(:notice_account_register_done, :email => ERB::Util.h(user.mail))
303 309 redirect_to signin_path
304 310 else
305 311 yield if block_given?
306 312 end
307 313 end
308 314
309 315 # Automatically register a user
310 316 #
311 317 # Pass a block for behavior when a user fails to save
312 318 def register_automatically(user, &block)
313 319 # Automatic activation
314 320 user.activate
315 321 user.last_login_on = Time.now
316 322 if user.save
317 323 self.logged_user = user
318 324 flash[:notice] = l(:notice_account_activated)
319 325 redirect_to my_account_path
320 326 else
321 327 yield if block_given?
322 328 end
323 329 end
324 330
325 331 # Manual activation by the administrator
326 332 #
327 333 # Pass a block for behavior when a user fails to save
328 334 def register_manually_by_administrator(user, &block)
329 335 if user.save
330 336 # Sends an email to the administrators
331 337 Mailer.account_activation_request(user).deliver
332 338 account_pending(user)
333 339 else
334 340 yield if block_given?
335 341 end
336 342 end
337 343
338 344 def handle_inactive_user(user, redirect_path=signin_path)
339 345 if user.registered?
340 346 account_pending(user, redirect_path)
341 347 else
342 348 account_locked(user, redirect_path)
343 349 end
344 350 end
345 351
346 352 def account_pending(user, redirect_path=signin_path)
347 353 if Setting.self_registration == '1'
348 354 flash[:error] = l(:notice_account_not_activated_yet, :url => activation_email_path)
349 355 session[:registered_user_id] = user.id
350 356 else
351 357 flash[:error] = l(:notice_account_pending)
352 358 end
353 359 redirect_to redirect_path
354 360 end
355 361
356 362 def account_locked(user, redirect_path=signin_path)
357 363 flash[:error] = l(:notice_account_locked)
358 364 redirect_to redirect_path
359 365 end
360 366 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 Redmine::Hook::Helper
27 27 include RoutesHelper
28 28 helper :routes
29 29
30 30 class_attribute :accept_api_auth_actions
31 31 class_attribute :accept_rss_auth_actions
32 32 class_attribute :model_object
33 33
34 34 layout 'base'
35 35
36 36 protect_from_forgery
37 37
38 38 def verify_authenticity_token
39 39 unless api_request?
40 40 super
41 41 end
42 42 end
43 43
44 44 def handle_unverified_request
45 45 unless api_request?
46 46 super
47 47 cookies.delete(autologin_cookie_name)
48 48 self.logged_user = nil
49 49 set_localization
50 50 render_error :status => 422, :message => "Invalid form authenticity token."
51 51 end
52 52 end
53 53
54 54 before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
55 55
56 56 rescue_from ::Unauthorized, :with => :deny_access
57 57 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
58 58
59 59 include Redmine::Search::Controller
60 60 include Redmine::MenuManager::MenuController
61 61 helper Redmine::MenuManager::MenuHelper
62 62
63 63 include Redmine::SudoMode::Controller
64 64
65 65 def session_expiration
66 66 if session[:user_id] && Rails.application.config.redmine_verify_sessions != false
67 67 if session_expired? && !try_to_autologin
68 68 set_localization(User.active.find_by_id(session[:user_id]))
69 69 self.logged_user = nil
70 70 flash[:error] = l(:error_session_expired)
71 71 require_login
72 72 end
73 73 end
74 74 end
75 75
76 76 def session_expired?
77 77 ! User.verify_session_token(session[:user_id], session[:tk])
78 78 end
79 79
80 80 def start_user_session(user)
81 81 session[:user_id] = user.id
82 82 session[:tk] = user.generate_session_token
83 83 if user.must_change_password?
84 84 session[:pwd] = '1'
85 85 end
86 86 end
87 87
88 88 def user_setup
89 89 # Check the settings cache for each request
90 90 Setting.check_cache
91 91 # Find the current user
92 92 User.current = find_current_user
93 93 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
94 94 end
95 95
96 96 # Returns the current user or nil if no user is logged in
97 97 # and starts a session if needed
98 98 def find_current_user
99 99 user = nil
100 100 unless api_request?
101 101 if session[:user_id]
102 102 # existing session
103 103 user = (User.active.find(session[:user_id]) rescue nil)
104 104 elsif autologin_user = try_to_autologin
105 105 user = autologin_user
106 106 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
107 107 # RSS key authentication does not start a session
108 108 user = User.find_by_rss_key(params[:key])
109 109 end
110 110 end
111 111 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
112 112 if (key = api_key_from_request)
113 113 # Use API key
114 114 user = User.find_by_api_key(key)
115 115 elsif request.authorization.to_s =~ /\ABasic /i
116 116 # HTTP Basic, either username/password or API key/random
117 117 authenticate_with_http_basic do |username, password|
118 118 user = User.try_to_login(username, password) || User.find_by_api_key(username)
119 119 end
120 120 if user && user.must_change_password?
121 121 render_error :message => 'You must change your password', :status => 403
122 122 return
123 123 end
124 124 end
125 125 # Switch user if requested by an admin user
126 126 if user && user.admin? && (username = api_switch_user_from_request)
127 127 su = User.find_by_login(username)
128 128 if su && su.active?
129 129 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
130 130 user = su
131 131 else
132 132 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
133 133 end
134 134 end
135 135 end
136 # store current ip address in user object ephemerally
137 user.remote_ip = request.remote_ip if user
136 138 user
137 139 end
138 140
139 141 def autologin_cookie_name
140 142 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
141 143 end
142 144
143 145 def try_to_autologin
144 146 if cookies[autologin_cookie_name] && Setting.autologin?
145 147 # auto-login feature starts a new session
146 148 user = User.try_to_autologin(cookies[autologin_cookie_name])
147 149 if user
148 150 reset_session
149 151 start_user_session(user)
150 152 end
151 153 user
152 154 end
153 155 end
154 156
155 157 # Sets the logged in user
156 158 def logged_user=(user)
157 159 reset_session
158 160 if user && user.is_a?(User)
159 161 User.current = user
160 162 start_user_session(user)
161 163 else
162 164 User.current = User.anonymous
163 165 end
164 166 end
165 167
166 168 # Logs out current user
167 169 def logout_user
168 170 if User.current.logged?
169 171 cookies.delete(autologin_cookie_name)
170 172 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
171 173 Token.delete_all(["user_id = ? AND action = ? AND value = ?", User.current.id, 'session', session[:tk]])
172 174 self.logged_user = nil
173 175 end
174 176 end
175 177
176 178 # check if login is globally required to access the application
177 179 def check_if_login_required
178 180 # no check needed if user is already logged in
179 181 return true if User.current.logged?
180 182 require_login if Setting.login_required?
181 183 end
182 184
183 185 def check_password_change
184 186 if session[:pwd]
185 187 if User.current.must_change_password?
186 188 flash[:error] = l(:error_password_expired)
187 189 redirect_to my_password_path
188 190 else
189 191 session.delete(:pwd)
190 192 end
191 193 end
192 194 end
193 195
194 196 def set_localization(user=User.current)
195 197 lang = nil
196 198 if user && user.logged?
197 199 lang = find_language(user.language)
198 200 end
199 201 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
200 202 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
201 203 if !accept_lang.blank?
202 204 accept_lang = accept_lang.downcase
203 205 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
204 206 end
205 207 end
206 208 lang ||= Setting.default_language
207 209 set_language_if_valid(lang)
208 210 end
209 211
210 212 def require_login
211 213 if !User.current.logged?
212 214 # Extract only the basic url parameters on non-GET requests
213 215 if request.get?
214 216 url = url_for(params)
215 217 else
216 218 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
217 219 end
218 220 respond_to do |format|
219 221 format.html {
220 222 if request.xhr?
221 223 head :unauthorized
222 224 else
223 225 redirect_to signin_path(:back_url => url)
224 226 end
225 227 }
226 228 format.any(:atom, :pdf, :csv) {
227 229 redirect_to signin_path(:back_url => url)
228 230 }
229 231 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
230 232 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
231 233 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
232 234 format.any { head :unauthorized }
233 235 end
234 236 return false
235 237 end
236 238 true
237 239 end
238 240
239 241 def require_admin
240 242 return unless require_login
241 243 if !User.current.admin?
242 244 render_403
243 245 return false
244 246 end
245 247 true
246 248 end
247 249
248 250 def deny_access
249 251 User.current.logged? ? render_403 : require_login
250 252 end
251 253
252 254 # Authorize the user for the requested action
253 255 def authorize(ctrl = params[:controller], action = params[:action], global = false)
254 256 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
255 257 if allowed
256 258 true
257 259 else
258 260 if @project && @project.archived?
259 261 render_403 :message => :notice_not_authorized_archived_project
260 262 else
261 263 deny_access
262 264 end
263 265 end
264 266 end
265 267
266 268 # Authorize the user for the requested action outside a project
267 269 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
268 270 authorize(ctrl, action, global)
269 271 end
270 272
271 273 # Find project of id params[:id]
272 274 def find_project
273 275 @project = Project.find(params[:id])
274 276 rescue ActiveRecord::RecordNotFound
275 277 render_404
276 278 end
277 279
278 280 # Find project of id params[:project_id]
279 281 def find_project_by_project_id
280 282 @project = Project.find(params[:project_id])
281 283 rescue ActiveRecord::RecordNotFound
282 284 render_404
283 285 end
284 286
285 287 # Find a project based on params[:project_id]
286 288 # TODO: some subclasses override this, see about merging their logic
287 289 def find_optional_project
288 290 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
289 291 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
290 292 allowed ? true : deny_access
291 293 rescue ActiveRecord::RecordNotFound
292 294 render_404
293 295 end
294 296
295 297 # Finds and sets @project based on @object.project
296 298 def find_project_from_association
297 299 render_404 unless @object.present?
298 300
299 301 @project = @object.project
300 302 end
301 303
302 304 def find_model_object
303 305 model = self.class.model_object
304 306 if model
305 307 @object = model.find(params[:id])
306 308 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
307 309 end
308 310 rescue ActiveRecord::RecordNotFound
309 311 render_404
310 312 end
311 313
312 314 def self.model_object(model)
313 315 self.model_object = model
314 316 end
315 317
316 318 # Find the issue whose id is the :id parameter
317 319 # Raises a Unauthorized exception if the issue is not visible
318 320 def find_issue
319 321 # Issue.visible.find(...) can not be used to redirect user to the login form
320 322 # if the issue actually exists but requires authentication
321 323 @issue = Issue.find(params[:id])
322 324 raise Unauthorized unless @issue.visible?
323 325 @project = @issue.project
324 326 rescue ActiveRecord::RecordNotFound
325 327 render_404
326 328 end
327 329
328 330 # Find issues with a single :id param or :ids array param
329 331 # Raises a Unauthorized exception if one of the issues is not visible
330 332 def find_issues
331 333 @issues = Issue.
332 334 where(:id => (params[:id] || params[:ids])).
333 335 preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to, {:custom_values => :custom_field}).
334 336 to_a
335 337 raise ActiveRecord::RecordNotFound if @issues.empty?
336 338 raise Unauthorized unless @issues.all?(&:visible?)
337 339 @projects = @issues.collect(&:project).compact.uniq
338 340 @project = @projects.first if @projects.size == 1
339 341 rescue ActiveRecord::RecordNotFound
340 342 render_404
341 343 end
342 344
343 345 def find_attachments
344 346 if (attachments = params[:attachments]).present?
345 347 att = attachments.values.collect do |attachment|
346 348 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
347 349 end
348 350 att.compact!
349 351 end
350 352 @attachments = att || []
351 353 end
352 354
353 355 # make sure that the user is a member of the project (or admin) if project is private
354 356 # used as a before_filter for actions that do not require any particular permission on the project
355 357 def check_project_privacy
356 358 if @project && !@project.archived?
357 359 if @project.visible?
358 360 true
359 361 else
360 362 deny_access
361 363 end
362 364 else
363 365 @project = nil
364 366 render_404
365 367 false
366 368 end
367 369 end
368 370
369 371 def back_url
370 372 url = params[:back_url]
371 373 if url.nil? && referer = request.env['HTTP_REFERER']
372 374 url = CGI.unescape(referer.to_s)
373 375 end
374 376 url
375 377 end
376 378
377 379 def redirect_back_or_default(default, options={})
378 380 back_url = params[:back_url].to_s
379 381 if back_url.present? && valid_url = validate_back_url(back_url)
380 382 redirect_to(valid_url)
381 383 return
382 384 elsif options[:referer]
383 385 redirect_to_referer_or default
384 386 return
385 387 end
386 388 redirect_to default
387 389 false
388 390 end
389 391
390 392 # Returns a validated URL string if back_url is a valid url for redirection,
391 393 # otherwise false
392 394 def validate_back_url(back_url)
393 395 if CGI.unescape(back_url).include?('..')
394 396 return false
395 397 end
396 398
397 399 begin
398 400 uri = URI.parse(back_url)
399 401 rescue URI::InvalidURIError
400 402 return false
401 403 end
402 404
403 405 [:scheme, :host, :port].each do |component|
404 406 if uri.send(component).present? && uri.send(component) != request.send(component)
405 407 return false
406 408 end
407 409 uri.send(:"#{component}=", nil)
408 410 end
409 411 # Always ignore basic user:password in the URL
410 412 uri.userinfo = nil
411 413
412 414 path = uri.to_s
413 415 # Ensure that the remaining URL starts with a slash, followed by a
414 416 # non-slash character or the end
415 417 if path !~ %r{\A/([^/]|\z)}
416 418 return false
417 419 end
418 420
419 421 if path.match(%r{/(login|account/register)})
420 422 return false
421 423 end
422 424
423 425 if relative_url_root.present? && !path.starts_with?(relative_url_root)
424 426 return false
425 427 end
426 428
427 429 return path
428 430 end
429 431 private :validate_back_url
430 432
431 433 def valid_back_url?(back_url)
432 434 !!validate_back_url(back_url)
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|Edge)} ? 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,210 +1,216
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 23 require_sudo_mode :account, only: :post
24 24 require_sudo_mode :reset_rss_key, :reset_api_key, :show_api_key, :destroy
25 25
26 26 helper :issues
27 27 helper :users
28 28 helper :custom_fields
29 29
30 30 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
31 31 'issuesreportedbyme' => :label_reported_issues,
32 32 'issueswatched' => :label_watched_issues,
33 33 'news' => :label_news_latest,
34 34 'calendar' => :label_calendar,
35 35 'documents' => :label_document_plural,
36 36 'timelog' => :label_spent_time
37 37 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
38 38
39 39 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
40 40 'right' => ['issuesreportedbyme']
41 41 }.freeze
42 42
43 43 def index
44 44 page
45 45 render :action => 'page'
46 46 end
47 47
48 48 # Show user's page
49 49 def page
50 50 @user = User.current
51 51 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
52 52 end
53 53
54 54 # Edit user's account
55 55 def account
56 56 @user = User.current
57 57 @pref = @user.pref
58 58 if request.post?
59 59 @user.safe_attributes = params[:user] if params[:user]
60 60 @user.pref.attributes = params[:pref] if params[:pref]
61 61 if @user.save
62 62 @user.pref.save
63 63 set_language_if_valid @user.language
64 64 flash[:notice] = l(:notice_account_updated)
65 65 redirect_to my_account_path
66 66 return
67 67 end
68 68 end
69 69 end
70 70
71 71 # Destroys user's account
72 72 def destroy
73 73 @user = User.current
74 74 unless @user.own_account_deletable?
75 75 redirect_to my_account_path
76 76 return
77 77 end
78 78
79 79 if request.post? && params[:confirm]
80 80 @user.destroy
81 81 if @user.destroyed?
82 82 logout_user
83 83 flash[:notice] = l(:notice_account_deleted)
84 84 end
85 85 redirect_to home_path
86 86 end
87 87 end
88 88
89 89 # Manage user's password
90 90 def password
91 91 @user = User.current
92 92 unless @user.change_password_allowed?
93 93 flash[:error] = l(:notice_can_t_change_password)
94 94 redirect_to my_account_path
95 95 return
96 96 end
97 97 if request.post?
98 98 if !@user.check_password?(params[:password])
99 99 flash.now[:error] = l(:notice_account_wrong_password)
100 100 elsif params[:password] == params[:new_password]
101 101 flash.now[:error] = l(:notice_new_password_must_be_different)
102 102 else
103 103 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
104 104 @user.must_change_passwd = false
105 105 if @user.save
106 106 # The session token was destroyed by the password change, generate a new one
107 107 session[:tk] = @user.generate_session_token
108 Mailer.security_notification(@user,
109 message: :mail_body_security_notification_change,
110 field: :field_password,
111 title: :button_change_password,
112 url: {controller: 'my', action: 'password'}
113 ).deliver
108 114 flash[:notice] = l(:notice_account_password_updated)
109 115 redirect_to my_account_path
110 116 end
111 117 end
112 118 end
113 119 end
114 120
115 121 # Create a new feeds key
116 122 def reset_rss_key
117 123 if request.post?
118 124 if User.current.rss_token
119 125 User.current.rss_token.destroy
120 126 User.current.reload
121 127 end
122 128 User.current.rss_key
123 129 flash[:notice] = l(:notice_feeds_access_key_reseted)
124 130 end
125 131 redirect_to my_account_path
126 132 end
127 133
128 134 def show_api_key
129 135 @user = User.current
130 136 end
131 137
132 138 # Create a new API key
133 139 def reset_api_key
134 140 if request.post?
135 141 if User.current.api_token
136 142 User.current.api_token.destroy
137 143 User.current.reload
138 144 end
139 145 User.current.api_key
140 146 flash[:notice] = l(:notice_api_access_key_reseted)
141 147 end
142 148 redirect_to my_account_path
143 149 end
144 150
145 151 # User's page layout configuration
146 152 def page_layout
147 153 @user = User.current
148 154 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
149 155 @block_options = []
150 156 BLOCKS.each do |k, v|
151 157 unless @blocks.values.flatten.include?(k)
152 158 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
153 159 end
154 160 end
155 161 end
156 162
157 163 # Add a block to user's page
158 164 # The block is added on top of the page
159 165 # params[:block] : id of the block to add
160 166 def add_block
161 167 block = params[:block].to_s.underscore
162 168 if block.present? && BLOCKS.key?(block)
163 169 @user = User.current
164 170 layout = @user.pref[:my_page_layout] || {}
165 171 # remove if already present in a group
166 172 %w(top left right).each {|f| (layout[f] ||= []).delete block }
167 173 # add it on top
168 174 layout['top'].unshift block
169 175 @user.pref[:my_page_layout] = layout
170 176 @user.pref.save
171 177 end
172 178 redirect_to my_page_layout_path
173 179 end
174 180
175 181 # Remove a block to user's page
176 182 # params[:block] : id of the block to remove
177 183 def remove_block
178 184 block = params[:block].to_s.underscore
179 185 @user = User.current
180 186 # remove block in all groups
181 187 layout = @user.pref[:my_page_layout] || {}
182 188 %w(top left right).each {|f| (layout[f] ||= []).delete block }
183 189 @user.pref[:my_page_layout] = layout
184 190 @user.pref.save
185 191 redirect_to my_page_layout_path
186 192 end
187 193
188 194 # Change blocks order on user's page
189 195 # params[:group] : group to order (top, left or right)
190 196 # params[:list-(top|left|right)] : array of block ids of the group
191 197 def order_blocks
192 198 group = params[:group]
193 199 @user = User.current
194 200 if group.is_a?(String)
195 201 group_items = (params["blocks"] || []).collect(&:underscore)
196 202 group_items.each {|s| s.sub!(/^block_/, '')}
197 203 if group_items and group_items.is_a? Array
198 204 layout = @user.pref[:my_page_layout] || {}
199 205 # remove group blocks if they are presents in other groups
200 206 %w(top left right).each {|f|
201 207 layout[f] = (layout[f] || []) - group_items
202 208 }
203 209 layout[group] = group_items
204 210 @user.pref[:my_page_layout] = layout
205 211 @user.pref.save
206 212 end
207 213 end
208 214 render :nothing => true
209 215 end
210 216 end
@@ -1,54 +1,107
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 EmailAddress < ActiveRecord::Base
19 19 belongs_to :user
20 20 attr_protected :id
21 21
22 after_update :destroy_tokens
23 after_destroy :destroy_tokens
22 after_create :deliver_security_notification_create
23 after_update :destroy_tokens, :deliver_security_notification_update
24 after_destroy :destroy_tokens, :deliver_security_notification_destroy
24 25
25 26 validates_presence_of :address
26 27 validates_format_of :address, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
27 28 validates_length_of :address, :maximum => User::MAIL_LENGTH_LIMIT, :allow_nil => true
28 29 validates_uniqueness_of :address, :case_sensitive => false,
29 30 :if => Proc.new {|email| email.address_changed? && email.address.present?}
30 31
31 32 def address=(arg)
32 33 write_attribute(:address, arg.to_s.strip)
33 34 end
34 35
35 36 def destroy
36 37 if is_default?
37 38 false
38 39 else
39 40 super
40 41 end
41 42 end
42 43
43 44 private
44 45
46 # send a security notification to user that a new email address was added
47 def deliver_security_notification_create
48 # only deliver if this isn't the only address.
49 # in that case, the user is just being created and
50 # should not receive this email.
51 if user.mails != [address]
52 deliver_security_notification(user,
53 message: :mail_body_security_notification_add,
54 field: :field_mail,
55 value: address
56 )
57 end
58 end
59
60 # send a security notification to user that an email has been changed (notified/not notified)
61 def deliver_security_notification_update
62 if address_changed?
63 recipients = [user, address_was]
64 options = {
65 message: :mail_body_security_notification_change_to,
66 field: :field_mail,
67 value: address
68 }
69 elsif notify_changed?
70 recipients = [user, address]
71 options = {
72 message: notify_was ? :mail_body_security_notification_notify_disabled : :mail_body_security_notification_notify_enabled,
73 value: address
74 }
75 end
76 deliver_security_notification(recipients, options)
77 end
78
79 # send a security notification to user that an email address was deleted
80 def deliver_security_notification_destroy
81 deliver_security_notification([user, address],
82 message: :mail_body_security_notification_remove,
83 field: :field_mail,
84 value: address
85 )
86 end
87
88 # generic method to send security notifications for email addresses
89 def deliver_security_notification(recipients, options={})
90 Mailer.security_notification(recipients,
91 options.merge(
92 title: :label_my_account,
93 url: {controller: 'my', action: 'account'}
94 )
95 ).deliver
96 end
97
45 98 # Delete all outstanding password reset tokens on email change.
46 99 # This helps to keep the account secure in case the associated email account
47 100 # was compromised.
48 101 def destroy_tokens
49 102 if address_changed? || destroyed?
50 103 tokens = ['recovery']
51 104 Token.where(:user_id => user_id, :action => tokens).delete_all
52 105 end
53 106 end
54 107 end
@@ -1,541 +1,555
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 'roadie'
19 19
20 20 class Mailer < ActionMailer::Base
21 21 layout 'mailer'
22 22 helper :application
23 23 helper :issues
24 24 helper :custom_fields
25 25
26 26 include Redmine::I18n
27 27 include Roadie::Rails::Automatic
28 28
29 29 def self.default_url_options
30 30 options = {:protocol => Setting.protocol}
31 31 if Setting.host_name.to_s =~ /\A(https?\:\/\/)?(.+?)(\:(\d+))?(\/.+)?\z/i
32 32 host, port, prefix = $2, $4, $5
33 33 options.merge!({
34 34 :host => host, :port => port, :script_name => prefix
35 35 })
36 36 else
37 37 options[:host] = Setting.host_name
38 38 end
39 39 options
40 40 end
41 41
42 42 # Builds a mail for notifying to_users and cc_users about a new issue
43 43 def issue_add(issue, to_users, cc_users)
44 44 redmine_headers 'Project' => issue.project.identifier,
45 45 'Issue-Id' => issue.id,
46 46 'Issue-Author' => issue.author.login
47 47 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
48 48 message_id issue
49 49 references issue
50 50 @author = issue.author
51 51 @issue = issue
52 52 @users = to_users + cc_users
53 53 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
54 54 mail :to => to_users,
55 55 :cc => cc_users,
56 56 :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
57 57 end
58 58
59 59 # Notifies users about a new issue
60 60 def self.deliver_issue_add(issue)
61 61 to = issue.notified_users
62 62 cc = issue.notified_watchers - to
63 63 issue.each_notification(to + cc) do |users|
64 64 Mailer.issue_add(issue, to & users, cc & users).deliver
65 65 end
66 66 end
67 67
68 68 # Builds a mail for notifying to_users and cc_users about an issue update
69 69 def issue_edit(journal, to_users, cc_users)
70 70 issue = journal.journalized
71 71 redmine_headers 'Project' => issue.project.identifier,
72 72 'Issue-Id' => issue.id,
73 73 'Issue-Author' => issue.author.login
74 74 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
75 75 message_id journal
76 76 references issue
77 77 @author = journal.user
78 78 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
79 79 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
80 80 s << issue.subject
81 81 @issue = issue
82 82 @users = to_users + cc_users
83 83 @journal = journal
84 84 @journal_details = journal.visible_details(@users.first)
85 85 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
86 86 mail :to => to_users,
87 87 :cc => cc_users,
88 88 :subject => s
89 89 end
90 90
91 91 # Notifies users about an issue update
92 92 def self.deliver_issue_edit(journal)
93 93 issue = journal.journalized.reload
94 94 to = journal.notified_users
95 95 cc = journal.notified_watchers - to
96 96 journal.each_notification(to + cc) do |users|
97 97 issue.each_notification(users) do |users2|
98 98 Mailer.issue_edit(journal, to & users2, cc & users2).deliver
99 99 end
100 100 end
101 101 end
102 102
103 103 def reminder(user, issues, days)
104 104 set_language_if_valid user.language
105 105 @issues = issues
106 106 @days = days
107 107 @issues_url = url_for(:controller => 'issues', :action => 'index',
108 108 :set_filter => 1, :assigned_to_id => user.id,
109 109 :sort => 'due_date:asc')
110 110 mail :to => user,
111 111 :subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
112 112 end
113 113
114 114 # Builds a Mail::Message object used to email users belonging to the added document's project.
115 115 #
116 116 # Example:
117 117 # document_added(document) => Mail::Message object
118 118 # Mailer.document_added(document).deliver => sends an email to the document's project recipients
119 119 def document_added(document)
120 120 redmine_headers 'Project' => document.project.identifier
121 121 @author = User.current
122 122 @document = document
123 123 @document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
124 124 mail :to => document.notified_users,
125 125 :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
126 126 end
127 127
128 128 # Builds a Mail::Message object used to email recipients of a project when an attachements are added.
129 129 #
130 130 # Example:
131 131 # attachments_added(attachments) => Mail::Message object
132 132 # Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients
133 133 def attachments_added(attachments)
134 134 container = attachments.first.container
135 135 added_to = ''
136 136 added_to_url = ''
137 137 @author = attachments.first.author
138 138 case container.class.name
139 139 when 'Project'
140 140 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
141 141 added_to = "#{l(:label_project)}: #{container}"
142 142 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
143 143 when 'Version'
144 144 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
145 145 added_to = "#{l(:label_version)}: #{container.name}"
146 146 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
147 147 when 'Document'
148 148 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
149 149 added_to = "#{l(:label_document)}: #{container.title}"
150 150 recipients = container.notified_users
151 151 end
152 152 redmine_headers 'Project' => container.project.identifier
153 153 @attachments = attachments
154 154 @added_to = added_to
155 155 @added_to_url = added_to_url
156 156 mail :to => recipients,
157 157 :subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
158 158 end
159 159
160 160 # Builds a Mail::Message object used to email recipients of a news' project when a news item is added.
161 161 #
162 162 # Example:
163 163 # news_added(news) => Mail::Message object
164 164 # Mailer.news_added(news).deliver => sends an email to the news' project recipients
165 165 def news_added(news)
166 166 redmine_headers 'Project' => news.project.identifier
167 167 @author = news.author
168 168 message_id news
169 169 references news
170 170 @news = news
171 171 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
172 172 mail :to => news.notified_users,
173 173 :cc => news.notified_watchers_for_added_news,
174 174 :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
175 175 end
176 176
177 177 # Builds a Mail::Message object used to email recipients of a news' project when a news comment is added.
178 178 #
179 179 # Example:
180 180 # news_comment_added(comment) => Mail::Message object
181 181 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
182 182 def news_comment_added(comment)
183 183 news = comment.commented
184 184 redmine_headers 'Project' => news.project.identifier
185 185 @author = comment.author
186 186 message_id comment
187 187 references news
188 188 @news = news
189 189 @comment = comment
190 190 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
191 191 mail :to => news.notified_users,
192 192 :cc => news.notified_watchers,
193 193 :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
194 194 end
195 195
196 196 # Builds a Mail::Message object used to email the recipients of the specified message that was posted.
197 197 #
198 198 # Example:
199 199 # message_posted(message) => Mail::Message object
200 200 # Mailer.message_posted(message).deliver => sends an email to the recipients
201 201 def message_posted(message)
202 202 redmine_headers 'Project' => message.project.identifier,
203 203 'Topic-Id' => (message.parent_id || message.id)
204 204 @author = message.author
205 205 message_id message
206 206 references message.root
207 207 recipients = message.notified_users
208 208 cc = ((message.root.notified_watchers + message.board.notified_watchers).uniq - recipients)
209 209 @message = message
210 210 @message_url = url_for(message.event_url)
211 211 mail :to => recipients,
212 212 :cc => cc,
213 213 :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
214 214 end
215 215
216 216 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added.
217 217 #
218 218 # Example:
219 219 # wiki_content_added(wiki_content) => Mail::Message object
220 220 # Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients
221 221 def wiki_content_added(wiki_content)
222 222 redmine_headers 'Project' => wiki_content.project.identifier,
223 223 'Wiki-Page-Id' => wiki_content.page.id
224 224 @author = wiki_content.author
225 225 message_id wiki_content
226 226 recipients = wiki_content.notified_users
227 227 cc = wiki_content.page.wiki.notified_watchers - recipients
228 228 @wiki_content = wiki_content
229 229 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
230 230 :project_id => wiki_content.project,
231 231 :id => wiki_content.page.title)
232 232 mail :to => recipients,
233 233 :cc => cc,
234 234 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
235 235 end
236 236
237 237 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated.
238 238 #
239 239 # Example:
240 240 # wiki_content_updated(wiki_content) => Mail::Message object
241 241 # Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients
242 242 def wiki_content_updated(wiki_content)
243 243 redmine_headers 'Project' => wiki_content.project.identifier,
244 244 'Wiki-Page-Id' => wiki_content.page.id
245 245 @author = wiki_content.author
246 246 message_id wiki_content
247 247 recipients = wiki_content.notified_users
248 248 cc = wiki_content.page.wiki.notified_watchers + wiki_content.page.notified_watchers - recipients
249 249 @wiki_content = wiki_content
250 250 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
251 251 :project_id => wiki_content.project,
252 252 :id => wiki_content.page.title)
253 253 @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
254 254 :project_id => wiki_content.project, :id => wiki_content.page.title,
255 255 :version => wiki_content.version)
256 256 mail :to => recipients,
257 257 :cc => cc,
258 258 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
259 259 end
260 260
261 261 # Builds a Mail::Message object used to email the specified user their account information.
262 262 #
263 263 # Example:
264 264 # account_information(user, password) => Mail::Message object
265 265 # Mailer.account_information(user, password).deliver => sends account information to the user
266 266 def account_information(user, password)
267 267 set_language_if_valid user.language
268 268 @user = user
269 269 @password = password
270 270 @login_url = url_for(:controller => 'account', :action => 'login')
271 271 mail :to => user.mail,
272 272 :subject => l(:mail_subject_register, Setting.app_title)
273 273 end
274 274
275 275 # Builds a Mail::Message object used to email all active administrators of an account activation request.
276 276 #
277 277 # Example:
278 278 # account_activation_request(user) => Mail::Message object
279 279 # Mailer.account_activation_request(user).deliver => sends an email to all active administrators
280 280 def account_activation_request(user)
281 281 # Send the email to all active administrators
282 282 recipients = User.active.where(:admin => true)
283 283 @user = user
284 284 @url = url_for(:controller => 'users', :action => 'index',
285 285 :status => User::STATUS_REGISTERED,
286 286 :sort_key => 'created_on', :sort_order => 'desc')
287 287 mail :to => recipients,
288 288 :subject => l(:mail_subject_account_activation_request, Setting.app_title)
289 289 end
290 290
291 291 # Builds a Mail::Message object used to email the specified user that their account was activated by an administrator.
292 292 #
293 293 # Example:
294 294 # account_activated(user) => Mail::Message object
295 295 # Mailer.account_activated(user).deliver => sends an email to the registered user
296 296 def account_activated(user)
297 297 set_language_if_valid user.language
298 298 @user = user
299 299 @login_url = url_for(:controller => 'account', :action => 'login')
300 300 mail :to => user.mail,
301 301 :subject => l(:mail_subject_register, Setting.app_title)
302 302 end
303 303
304 304 def lost_password(token, recipient=nil)
305 305 set_language_if_valid(token.user.language)
306 306 recipient ||= token.user.mail
307 307 @token = token
308 308 @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
309 309 mail :to => recipient,
310 310 :subject => l(:mail_subject_lost_password, Setting.app_title)
311 311 end
312 312
313 313 def register(token)
314 314 set_language_if_valid(token.user.language)
315 315 @token = token
316 316 @url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
317 317 mail :to => token.user.mail,
318 318 :subject => l(:mail_subject_register, Setting.app_title)
319 319 end
320 320
321 def security_notification(recipients, options={})
322 redmine_headers 'Sender' => User.current.login
323 @user = Array(recipients).detect{|r| r.is_a? User }
324 set_language_if_valid(@user.try :language)
325 @message = l(options[:message],
326 field: (options[:field] && l(options[:field])),
327 value: options[:value]
328 )
329 @title = options[:title] && l(options[:title])
330 @url = options[:url] && (options[:url].is_a?(Hash) ? url_for(options[:url]) : options[:url])
331 mail :to => recipients,
332 :subject => l(:mail_subject_security_notification)
333 end
334
321 335 def test_email(user)
322 336 set_language_if_valid(user.language)
323 337 @url = url_for(:controller => 'welcome')
324 338 mail :to => user.mail,
325 339 :subject => 'Redmine test'
326 340 end
327 341
328 342 # Sends reminders to issue assignees
329 343 # Available options:
330 344 # * :days => how many days in the future to remind about (defaults to 7)
331 345 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
332 346 # * :project => id or identifier of project to process (defaults to all projects)
333 347 # * :users => array of user/group ids who should be reminded
334 348 # * :version => name of target version for filtering issues (defaults to none)
335 349 def self.reminders(options={})
336 350 days = options[:days] || 7
337 351 project = options[:project] ? Project.find(options[:project]) : nil
338 352 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
339 353 target_version_id = options[:version] ? Version.named(options[:version]).pluck(:id) : nil
340 354 if options[:version] && target_version_id.blank?
341 355 raise ActiveRecord::RecordNotFound.new("Couldn't find Version with named #{options[:version]}")
342 356 end
343 357 user_ids = options[:users]
344 358
345 359 scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" +
346 360 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
347 361 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date
348 362 )
349 363 scope = scope.where(:assigned_to_id => user_ids) if user_ids.present?
350 364 scope = scope.where(:project_id => project.id) if project
351 365 scope = scope.where(:fixed_version_id => target_version_id) if target_version_id.present?
352 366 scope = scope.where(:tracker_id => tracker.id) if tracker
353 367 issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).
354 368 group_by(&:assigned_to)
355 369 issues_by_assignee.keys.each do |assignee|
356 370 if assignee.is_a?(Group)
357 371 assignee.users.each do |user|
358 372 issues_by_assignee[user] ||= []
359 373 issues_by_assignee[user] += issues_by_assignee[assignee]
360 374 end
361 375 end
362 376 end
363 377
364 378 issues_by_assignee.each do |assignee, issues|
365 379 reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
366 380 end
367 381 end
368 382
369 383 # Activates/desactivates email deliveries during +block+
370 384 def self.with_deliveries(enabled = true, &block)
371 385 was_enabled = ActionMailer::Base.perform_deliveries
372 386 ActionMailer::Base.perform_deliveries = !!enabled
373 387 yield
374 388 ensure
375 389 ActionMailer::Base.perform_deliveries = was_enabled
376 390 end
377 391
378 392 # Sends emails synchronously in the given block
379 393 def self.with_synched_deliveries(&block)
380 394 saved_method = ActionMailer::Base.delivery_method
381 395 if m = saved_method.to_s.match(%r{^async_(.+)$})
382 396 synched_method = m[1]
383 397 ActionMailer::Base.delivery_method = synched_method.to_sym
384 398 ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings")
385 399 end
386 400 yield
387 401 ensure
388 402 ActionMailer::Base.delivery_method = saved_method
389 403 end
390 404
391 405 def mail(headers={}, &block)
392 406 headers.reverse_merge! 'X-Mailer' => 'Redmine',
393 407 'X-Redmine-Host' => Setting.host_name,
394 408 'X-Redmine-Site' => Setting.app_title,
395 409 'X-Auto-Response-Suppress' => 'All',
396 410 'Auto-Submitted' => 'auto-generated',
397 411 'From' => Setting.mail_from,
398 412 'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
399 413
400 414 # Replaces users with their email addresses
401 415 [:to, :cc, :bcc].each do |key|
402 416 if headers[key].present?
403 417 headers[key] = self.class.email_addresses(headers[key])
404 418 end
405 419 end
406 420
407 421 # Removes the author from the recipients and cc
408 422 # if the author does not want to receive notifications
409 423 # about what the author do
410 424 if @author && @author.logged? && @author.pref.no_self_notified
411 425 addresses = @author.mails
412 426 headers[:to] -= addresses if headers[:to].is_a?(Array)
413 427 headers[:cc] -= addresses if headers[:cc].is_a?(Array)
414 428 end
415 429
416 430 if @author && @author.logged?
417 431 redmine_headers 'Sender' => @author.login
418 432 end
419 433
420 434 # Blind carbon copy recipients
421 435 if Setting.bcc_recipients?
422 436 headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
423 437 headers[:to] = nil
424 438 headers[:cc] = nil
425 439 end
426 440
427 441 if @message_id_object
428 442 headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
429 443 end
430 444 if @references_objects
431 445 headers[:references] = @references_objects.collect {|o| "<#{self.class.references_for(o)}>"}.join(' ')
432 446 end
433 447
434 448 m = if block_given?
435 449 super headers, &block
436 450 else
437 451 super headers do |format|
438 452 format.text
439 453 format.html unless Setting.plain_text_mail?
440 454 end
441 455 end
442 456 set_language_if_valid @initial_language
443 457
444 458 m
445 459 end
446 460
447 461 def initialize(*args)
448 462 @initial_language = current_language
449 463 set_language_if_valid Setting.default_language
450 464 super
451 465 end
452 466
453 467 def self.deliver_mail(mail)
454 468 return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
455 469 begin
456 470 # Log errors when raise_delivery_errors is set to false, Rails does not
457 471 mail.raise_delivery_errors = true
458 472 super
459 473 rescue Exception => e
460 474 if ActionMailer::Base.raise_delivery_errors
461 475 raise e
462 476 else
463 477 Rails.logger.error "Email delivery error: #{e.message}"
464 478 end
465 479 end
466 480 end
467 481
468 482 def self.method_missing(method, *args, &block)
469 483 if m = method.to_s.match(%r{^deliver_(.+)$})
470 484 ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead."
471 485 send(m[1], *args).deliver
472 486 else
473 487 super
474 488 end
475 489 end
476 490
477 491 # Returns an array of email addresses to notify by
478 492 # replacing users in arg with their notified email addresses
479 493 #
480 494 # Example:
481 495 # Mailer.email_addresses(users)
482 496 # => ["foo@example.net", "bar@example.net"]
483 497 def self.email_addresses(arg)
484 498 arr = Array.wrap(arg)
485 499 mails = arr.reject {|a| a.is_a? Principal}
486 500 users = arr - mails
487 501 if users.any?
488 502 mails += EmailAddress.
489 503 where(:user_id => users.map(&:id)).
490 504 where("is_default = ? OR notify = ?", true, true).
491 505 pluck(:address)
492 506 end
493 507 mails
494 508 end
495 509
496 510 private
497 511
498 512 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
499 513 def redmine_headers(h)
500 514 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
501 515 end
502 516
503 517 def self.token_for(object, rand=true)
504 518 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
505 519 hash = [
506 520 "redmine",
507 521 "#{object.class.name.demodulize.underscore}-#{object.id}",
508 522 timestamp.strftime("%Y%m%d%H%M%S")
509 523 ]
510 524 if rand
511 525 hash << Redmine::Utils.random_hex(8)
512 526 end
513 527 host = Setting.mail_from.to_s.strip.gsub(%r{^.*@|>}, '')
514 528 host = "#{::Socket.gethostname}.redmine" if host.empty?
515 529 "#{hash.join('.')}@#{host}"
516 530 end
517 531
518 532 # Returns a Message-Id for the given object
519 533 def self.message_id_for(object)
520 534 token_for(object, true)
521 535 end
522 536
523 537 # Returns a uniq token for a given object referenced by all notifications
524 538 # related to this object
525 539 def self.references_for(object)
526 540 token_for(object, false)
527 541 end
528 542
529 543 def message_id(object)
530 544 @message_id_object = object
531 545 end
532 546
533 547 def references(object)
534 548 @references_objects ||= []
535 549 @references_objects << object
536 550 end
537 551
538 552 def mylogger
539 553 Rails.logger
540 554 end
541 555 end
@@ -1,886 +1,888
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 "digest/sha1"
19 19
20 20 class User < Principal
21 21 include Redmine::SafeAttributes
22 22
23 23 # Different ways of displaying/sorting users
24 24 USER_FORMATS = {
25 25 :firstname_lastname => {
26 26 :string => '#{firstname} #{lastname}',
27 27 :order => %w(firstname lastname id),
28 28 :setting_order => 1
29 29 },
30 30 :firstname_lastinitial => {
31 31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 32 :order => %w(firstname lastname id),
33 33 :setting_order => 2
34 34 },
35 35 :firstinitial_lastname => {
36 36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 37 :order => %w(firstname lastname id),
38 38 :setting_order => 2
39 39 },
40 40 :firstname => {
41 41 :string => '#{firstname}',
42 42 :order => %w(firstname id),
43 43 :setting_order => 3
44 44 },
45 45 :lastname_firstname => {
46 46 :string => '#{lastname} #{firstname}',
47 47 :order => %w(lastname firstname id),
48 48 :setting_order => 4
49 49 },
50 50 :lastnamefirstname => {
51 51 :string => '#{lastname}#{firstname}',
52 52 :order => %w(lastname firstname id),
53 53 :setting_order => 5
54 54 },
55 55 :lastname_comma_firstname => {
56 56 :string => '#{lastname}, #{firstname}',
57 57 :order => %w(lastname firstname id),
58 58 :setting_order => 6
59 59 },
60 60 :lastname => {
61 61 :string => '#{lastname}',
62 62 :order => %w(lastname id),
63 63 :setting_order => 7
64 64 },
65 65 :username => {
66 66 :string => '#{login}',
67 67 :order => %w(login id),
68 68 :setting_order => 8
69 69 },
70 70 }
71 71
72 72 MAIL_NOTIFICATION_OPTIONS = [
73 73 ['all', :label_user_mail_option_all],
74 74 ['selected', :label_user_mail_option_selected],
75 75 ['only_my_events', :label_user_mail_option_only_my_events],
76 76 ['only_assigned', :label_user_mail_option_only_assigned],
77 77 ['only_owner', :label_user_mail_option_only_owner],
78 78 ['none', :label_user_mail_option_none]
79 79 ]
80 80
81 81 has_and_belongs_to_many :groups,
82 82 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
83 83 :after_add => Proc.new {|user, group| group.user_added(user)},
84 84 :after_remove => Proc.new {|user, group| group.user_removed(user)}
85 85 has_many :changesets, :dependent => :nullify
86 86 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
87 87 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
88 88 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
89 89 has_one :email_address, lambda {where :is_default => true}, :autosave => true
90 90 has_many :email_addresses, :dependent => :delete_all
91 91 belongs_to :auth_source
92 92
93 93 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
94 94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
95 95
96 96 acts_as_customizable
97 97
98 98 attr_accessor :password, :password_confirmation, :generate_password
99 99 attr_accessor :last_before_login_on
100 attr_accessor :remote_ip
101
100 102 # Prevents unauthorized assignments
101 103 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
102 104
103 105 LOGIN_LENGTH_LIMIT = 60
104 106 MAIL_LENGTH_LIMIT = 60
105 107
106 108 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
107 109 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
108 110 # Login must contain letters, numbers, underscores only
109 111 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
110 112 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
111 113 validates_length_of :firstname, :lastname, :maximum => 30
112 114 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
113 115 validate :validate_password_length
114 116 validate do
115 117 if password_confirmation && password != password_confirmation
116 118 errors.add(:password, :confirmation)
117 119 end
118 120 end
119 121
120 122 before_validation :instantiate_email_address
121 123 before_create :set_mail_notification
122 124 before_save :generate_password_if_needed, :update_hashed_password
123 125 before_destroy :remove_references_before_destroy
124 126 after_save :update_notified_project_ids, :destroy_tokens
125 127
126 128 scope :in_group, lambda {|group|
127 129 group_id = group.is_a?(Group) ? group.id : group.to_i
128 130 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
129 131 }
130 132 scope :not_in_group, lambda {|group|
131 133 group_id = group.is_a?(Group) ? group.id : group.to_i
132 134 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
133 135 }
134 136 scope :sorted, lambda { order(*User.fields_for_order_statement)}
135 137 scope :having_mail, lambda {|arg|
136 138 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
137 139 if addresses.any?
138 140 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
139 141 else
140 142 none
141 143 end
142 144 }
143 145
144 146 def set_mail_notification
145 147 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
146 148 true
147 149 end
148 150
149 151 def update_hashed_password
150 152 # update hashed_password if password was set
151 153 if self.password && self.auth_source_id.blank?
152 154 salt_password(password)
153 155 end
154 156 end
155 157
156 158 alias :base_reload :reload
157 159 def reload(*args)
158 160 @name = nil
159 161 @projects_by_role = nil
160 162 @membership_by_project_id = nil
161 163 @notified_projects_ids = nil
162 164 @notified_projects_ids_changed = false
163 165 @builtin_role = nil
164 166 @visible_project_ids = nil
165 167 @managed_roles = nil
166 168 base_reload(*args)
167 169 end
168 170
169 171 def mail
170 172 email_address.try(:address)
171 173 end
172 174
173 175 def mail=(arg)
174 176 email = email_address || build_email_address
175 177 email.address = arg
176 178 end
177 179
178 180 def mail_changed?
179 181 email_address.try(:address_changed?)
180 182 end
181 183
182 184 def mails
183 185 email_addresses.pluck(:address)
184 186 end
185 187
186 188 def self.find_or_initialize_by_identity_url(url)
187 189 user = where(:identity_url => url).first
188 190 unless user
189 191 user = User.new
190 192 user.identity_url = url
191 193 end
192 194 user
193 195 end
194 196
195 197 def identity_url=(url)
196 198 if url.blank?
197 199 write_attribute(:identity_url, '')
198 200 else
199 201 begin
200 202 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
201 203 rescue OpenIdAuthentication::InvalidOpenId
202 204 # Invalid url, don't save
203 205 end
204 206 end
205 207 self.read_attribute(:identity_url)
206 208 end
207 209
208 210 # Returns the user that matches provided login and password, or nil
209 211 def self.try_to_login(login, password, active_only=true)
210 212 login = login.to_s
211 213 password = password.to_s
212 214
213 215 # Make sure no one can sign in with an empty login or password
214 216 return nil if login.empty? || password.empty?
215 217 user = find_by_login(login)
216 218 if user
217 219 # user is already in local database
218 220 return nil unless user.check_password?(password)
219 221 return nil if !user.active? && active_only
220 222 else
221 223 # user is not yet registered, try to authenticate with available sources
222 224 attrs = AuthSource.authenticate(login, password)
223 225 if attrs
224 226 user = new(attrs)
225 227 user.login = login
226 228 user.language = Setting.default_language
227 229 if user.save
228 230 user.reload
229 231 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
230 232 end
231 233 end
232 234 end
233 235 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
234 236 user
235 237 rescue => text
236 238 raise text
237 239 end
238 240
239 241 # Returns the user who matches the given autologin +key+ or nil
240 242 def self.try_to_autologin(key)
241 243 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
242 244 if user
243 245 user.update_column(:last_login_on, Time.now)
244 246 user
245 247 end
246 248 end
247 249
248 250 def self.name_formatter(formatter = nil)
249 251 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
250 252 end
251 253
252 254 # Returns an array of fields names than can be used to make an order statement for users
253 255 # according to how user names are displayed
254 256 # Examples:
255 257 #
256 258 # User.fields_for_order_statement => ['users.login', 'users.id']
257 259 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
258 260 def self.fields_for_order_statement(table=nil)
259 261 table ||= table_name
260 262 name_formatter[:order].map {|field| "#{table}.#{field}"}
261 263 end
262 264
263 265 # Return user's full name for display
264 266 def name(formatter = nil)
265 267 f = self.class.name_formatter(formatter)
266 268 if formatter
267 269 eval('"' + f[:string] + '"')
268 270 else
269 271 @name ||= eval('"' + f[:string] + '"')
270 272 end
271 273 end
272 274
273 275 def active?
274 276 self.status == STATUS_ACTIVE
275 277 end
276 278
277 279 def registered?
278 280 self.status == STATUS_REGISTERED
279 281 end
280 282
281 283 def locked?
282 284 self.status == STATUS_LOCKED
283 285 end
284 286
285 287 def activate
286 288 self.status = STATUS_ACTIVE
287 289 end
288 290
289 291 def register
290 292 self.status = STATUS_REGISTERED
291 293 end
292 294
293 295 def lock
294 296 self.status = STATUS_LOCKED
295 297 end
296 298
297 299 def activate!
298 300 update_attribute(:status, STATUS_ACTIVE)
299 301 end
300 302
301 303 def register!
302 304 update_attribute(:status, STATUS_REGISTERED)
303 305 end
304 306
305 307 def lock!
306 308 update_attribute(:status, STATUS_LOCKED)
307 309 end
308 310
309 311 # Returns true if +clear_password+ is the correct user's password, otherwise false
310 312 def check_password?(clear_password)
311 313 if auth_source_id.present?
312 314 auth_source.authenticate(self.login, clear_password)
313 315 else
314 316 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
315 317 end
316 318 end
317 319
318 320 # Generates a random salt and computes hashed_password for +clear_password+
319 321 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
320 322 def salt_password(clear_password)
321 323 self.salt = User.generate_salt
322 324 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
323 325 self.passwd_changed_on = Time.now.change(:usec => 0)
324 326 end
325 327
326 328 # Does the backend storage allow this user to change their password?
327 329 def change_password_allowed?
328 330 return true if auth_source.nil?
329 331 return auth_source.allow_password_changes?
330 332 end
331 333
332 334 # Returns true if the user password has expired
333 335 def password_expired?
334 336 period = Setting.password_max_age.to_i
335 337 if period.zero?
336 338 false
337 339 else
338 340 changed_on = self.passwd_changed_on || Time.at(0)
339 341 changed_on < period.days.ago
340 342 end
341 343 end
342 344
343 345 def must_change_password?
344 346 (must_change_passwd? || password_expired?) && change_password_allowed?
345 347 end
346 348
347 349 def generate_password?
348 350 generate_password == '1' || generate_password == true
349 351 end
350 352
351 353 # Generate and set a random password on given length
352 354 def random_password(length=40)
353 355 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
354 356 chars -= %w(0 O 1 l)
355 357 password = ''
356 358 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
357 359 self.password = password
358 360 self.password_confirmation = password
359 361 self
360 362 end
361 363
362 364 def pref
363 365 self.preference ||= UserPreference.new(:user => self)
364 366 end
365 367
366 368 def time_zone
367 369 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
368 370 end
369 371
370 372 def force_default_language?
371 373 Setting.force_default_language_for_loggedin?
372 374 end
373 375
374 376 def language
375 377 if force_default_language?
376 378 Setting.default_language
377 379 else
378 380 super
379 381 end
380 382 end
381 383
382 384 def wants_comments_in_reverse_order?
383 385 self.pref[:comments_sorting] == 'desc'
384 386 end
385 387
386 388 # Return user's RSS key (a 40 chars long string), used to access feeds
387 389 def rss_key
388 390 if rss_token.nil?
389 391 create_rss_token(:action => 'feeds')
390 392 end
391 393 rss_token.value
392 394 end
393 395
394 396 # Return user's API key (a 40 chars long string), used to access the API
395 397 def api_key
396 398 if api_token.nil?
397 399 create_api_token(:action => 'api')
398 400 end
399 401 api_token.value
400 402 end
401 403
402 404 # Generates a new session token and returns its value
403 405 def generate_session_token
404 406 token = Token.create!(:user_id => id, :action => 'session')
405 407 token.value
406 408 end
407 409
408 410 # Returns true if token is a valid session token for the user whose id is user_id
409 411 def self.verify_session_token(user_id, token)
410 412 return false if user_id.blank? || token.blank?
411 413
412 414 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
413 415 if Setting.session_lifetime?
414 416 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
415 417 end
416 418 if Setting.session_timeout?
417 419 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
418 420 end
419 421 scope.update_all(:updated_on => Time.now) == 1
420 422 end
421 423
422 424 # Return an array of project ids for which the user has explicitly turned mail notifications on
423 425 def notified_projects_ids
424 426 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
425 427 end
426 428
427 429 def notified_project_ids=(ids)
428 430 @notified_projects_ids_changed = true
429 431 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
430 432 end
431 433
432 434 # Updates per project notifications (after_save callback)
433 435 def update_notified_project_ids
434 436 if @notified_projects_ids_changed
435 437 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
436 438 members.update_all(:mail_notification => false)
437 439 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
438 440 end
439 441 end
440 442 private :update_notified_project_ids
441 443
442 444 def valid_notification_options
443 445 self.class.valid_notification_options(self)
444 446 end
445 447
446 448 # Only users that belong to more than 1 project can select projects for which they are notified
447 449 def self.valid_notification_options(user=nil)
448 450 # Note that @user.membership.size would fail since AR ignores
449 451 # :include association option when doing a count
450 452 if user.nil? || user.memberships.length < 1
451 453 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
452 454 else
453 455 MAIL_NOTIFICATION_OPTIONS
454 456 end
455 457 end
456 458
457 459 # Find a user account by matching the exact login and then a case-insensitive
458 460 # version. Exact matches will be given priority.
459 461 def self.find_by_login(login)
460 462 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
461 463 if login.present?
462 464 # First look for an exact match
463 465 user = where(:login => login).detect {|u| u.login == login}
464 466 unless user
465 467 # Fail over to case-insensitive if none was found
466 468 user = where("LOWER(login) = ?", login.downcase).first
467 469 end
468 470 user
469 471 end
470 472 end
471 473
472 474 def self.find_by_rss_key(key)
473 475 Token.find_active_user('feeds', key)
474 476 end
475 477
476 478 def self.find_by_api_key(key)
477 479 Token.find_active_user('api', key)
478 480 end
479 481
480 482 # Makes find_by_mail case-insensitive
481 483 def self.find_by_mail(mail)
482 484 having_mail(mail).first
483 485 end
484 486
485 487 # Returns true if the default admin account can no longer be used
486 488 def self.default_admin_account_changed?
487 489 !User.active.find_by_login("admin").try(:check_password?, "admin")
488 490 end
489 491
490 492 def to_s
491 493 name
492 494 end
493 495
494 496 CSS_CLASS_BY_STATUS = {
495 497 STATUS_ANONYMOUS => 'anon',
496 498 STATUS_ACTIVE => 'active',
497 499 STATUS_REGISTERED => 'registered',
498 500 STATUS_LOCKED => 'locked'
499 501 }
500 502
501 503 def css_classes
502 504 "user #{CSS_CLASS_BY_STATUS[status]}"
503 505 end
504 506
505 507 # Returns the current day according to user's time zone
506 508 def today
507 509 if time_zone.nil?
508 510 Date.today
509 511 else
510 512 Time.now.in_time_zone(time_zone).to_date
511 513 end
512 514 end
513 515
514 516 # Returns the day of +time+ according to user's time zone
515 517 def time_to_date(time)
516 518 if time_zone.nil?
517 519 time.to_date
518 520 else
519 521 time.in_time_zone(time_zone).to_date
520 522 end
521 523 end
522 524
523 525 def logged?
524 526 true
525 527 end
526 528
527 529 def anonymous?
528 530 !logged?
529 531 end
530 532
531 533 # Returns user's membership for the given project
532 534 # or nil if the user is not a member of project
533 535 def membership(project)
534 536 project_id = project.is_a?(Project) ? project.id : project
535 537
536 538 @membership_by_project_id ||= Hash.new {|h, project_id|
537 539 h[project_id] = memberships.where(:project_id => project_id).first
538 540 }
539 541 @membership_by_project_id[project_id]
540 542 end
541 543
542 544 # Returns the user's bult-in role
543 545 def builtin_role
544 546 @builtin_role ||= Role.non_member
545 547 end
546 548
547 549 # Return user's roles for project
548 550 def roles_for_project(project)
549 551 # No role on archived projects
550 552 return [] if project.nil? || project.archived?
551 553 if membership = membership(project)
552 554 membership.roles.to_a
553 555 elsif project.is_public?
554 556 project.override_roles(builtin_role)
555 557 else
556 558 []
557 559 end
558 560 end
559 561
560 562 # Returns a hash of user's projects grouped by roles
561 563 def projects_by_role
562 564 return @projects_by_role if @projects_by_role
563 565
564 566 hash = Hash.new([])
565 567
566 568 group_class = anonymous? ? GroupAnonymous : GroupNonMember
567 569 members = Member.joins(:project, :principal).
568 570 where("#{Project.table_name}.status <> 9").
569 571 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
570 572 preload(:project, :roles).
571 573 to_a
572 574
573 575 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
574 576 members.each do |member|
575 577 if member.project
576 578 member.roles.each do |role|
577 579 hash[role] = [] unless hash.key?(role)
578 580 hash[role] << member.project
579 581 end
580 582 end
581 583 end
582 584
583 585 hash.each do |role, projects|
584 586 projects.uniq!
585 587 end
586 588
587 589 @projects_by_role = hash
588 590 end
589 591
590 592 # Returns the ids of visible projects
591 593 def visible_project_ids
592 594 @visible_project_ids ||= Project.visible(self).pluck(:id)
593 595 end
594 596
595 597 # Returns the roles that the user is allowed to manage for the given project
596 598 def managed_roles(project)
597 599 if admin?
598 600 @managed_roles ||= Role.givable.to_a
599 601 else
600 602 membership(project).try(:managed_roles) || []
601 603 end
602 604 end
603 605
604 606 # Returns true if user is arg or belongs to arg
605 607 def is_or_belongs_to?(arg)
606 608 if arg.is_a?(User)
607 609 self == arg
608 610 elsif arg.is_a?(Group)
609 611 arg.users.include?(self)
610 612 else
611 613 false
612 614 end
613 615 end
614 616
615 617 # Return true if the user is allowed to do the specified action on a specific context
616 618 # Action can be:
617 619 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
618 620 # * a permission Symbol (eg. :edit_project)
619 621 # Context can be:
620 622 # * a project : returns true if user is allowed to do the specified action on this project
621 623 # * an array of projects : returns true if user is allowed on every project
622 624 # * nil with options[:global] set : check if user has at least one role allowed for this action,
623 625 # or falls back to Non Member / Anonymous permissions depending if the user is logged
624 626 def allowed_to?(action, context, options={}, &block)
625 627 if context && context.is_a?(Project)
626 628 return false unless context.allows_to?(action)
627 629 # Admin users are authorized for anything else
628 630 return true if admin?
629 631
630 632 roles = roles_for_project(context)
631 633 return false unless roles
632 634 roles.any? {|role|
633 635 (context.is_public? || role.member?) &&
634 636 role.allowed_to?(action) &&
635 637 (block_given? ? yield(role, self) : true)
636 638 }
637 639 elsif context && context.is_a?(Array)
638 640 if context.empty?
639 641 false
640 642 else
641 643 # Authorize if user is authorized on every element of the array
642 644 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
643 645 end
644 646 elsif context
645 647 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
646 648 elsif options[:global]
647 649 # Admin users are always authorized
648 650 return true if admin?
649 651
650 652 # authorize if user has at least one role that has this permission
651 653 roles = memberships.collect {|m| m.roles}.flatten.uniq
652 654 roles << (self.logged? ? Role.non_member : Role.anonymous)
653 655 roles.any? {|role|
654 656 role.allowed_to?(action) &&
655 657 (block_given? ? yield(role, self) : true)
656 658 }
657 659 else
658 660 false
659 661 end
660 662 end
661 663
662 664 # Is the user allowed to do the specified action on any project?
663 665 # See allowed_to? for the actions and valid options.
664 666 #
665 667 # NB: this method is not used anywhere in the core codebase as of
666 668 # 2.5.2, but it's used by many plugins so if we ever want to remove
667 669 # it it has to be carefully deprecated for a version or two.
668 670 def allowed_to_globally?(action, options={}, &block)
669 671 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
670 672 end
671 673
672 674 def allowed_to_view_all_time_entries?(context)
673 675 allowed_to?(:view_time_entries, context) do |role, user|
674 676 role.time_entries_visibility == 'all'
675 677 end
676 678 end
677 679
678 680 # Returns true if the user is allowed to delete the user's own account
679 681 def own_account_deletable?
680 682 Setting.unsubscribe? &&
681 683 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
682 684 end
683 685
684 686 safe_attributes 'firstname',
685 687 'lastname',
686 688 'mail',
687 689 'mail_notification',
688 690 'notified_project_ids',
689 691 'language',
690 692 'custom_field_values',
691 693 'custom_fields',
692 694 'identity_url'
693 695
694 696 safe_attributes 'status',
695 697 'auth_source_id',
696 698 'generate_password',
697 699 'must_change_passwd',
698 700 :if => lambda {|user, current_user| current_user.admin?}
699 701
700 702 safe_attributes 'group_ids',
701 703 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
702 704
703 705 # Utility method to help check if a user should be notified about an
704 706 # event.
705 707 #
706 708 # TODO: only supports Issue events currently
707 709 def notify_about?(object)
708 710 if mail_notification == 'all'
709 711 true
710 712 elsif mail_notification.blank? || mail_notification == 'none'
711 713 false
712 714 else
713 715 case object
714 716 when Issue
715 717 case mail_notification
716 718 when 'selected', 'only_my_events'
717 719 # user receives notifications for created/assigned issues on unselected projects
718 720 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
719 721 when 'only_assigned'
720 722 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
721 723 when 'only_owner'
722 724 object.author == self
723 725 end
724 726 when News
725 727 # always send to project members except when mail_notification is set to 'none'
726 728 true
727 729 end
728 730 end
729 731 end
730 732
731 733 def self.current=(user)
732 734 RequestStore.store[:current_user] = user
733 735 end
734 736
735 737 def self.current
736 738 RequestStore.store[:current_user] ||= User.anonymous
737 739 end
738 740
739 741 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
740 742 # one anonymous user per database.
741 743 def self.anonymous
742 744 anonymous_user = AnonymousUser.first
743 745 if anonymous_user.nil?
744 746 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
745 747 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
746 748 end
747 749 anonymous_user
748 750 end
749 751
750 752 # Salts all existing unsalted passwords
751 753 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
752 754 # This method is used in the SaltPasswords migration and is to be kept as is
753 755 def self.salt_unsalted_passwords!
754 756 transaction do
755 757 User.where("salt IS NULL OR salt = ''").find_each do |user|
756 758 next if user.hashed_password.blank?
757 759 salt = User.generate_salt
758 760 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
759 761 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
760 762 end
761 763 end
762 764 end
763 765
764 766 protected
765 767
766 768 def validate_password_length
767 769 return if password.blank? && generate_password?
768 770 # Password length validation based on setting
769 771 if !password.nil? && password.size < Setting.password_min_length.to_i
770 772 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
771 773 end
772 774 end
773 775
774 776 def instantiate_email_address
775 777 email_address || build_email_address
776 778 end
777 779
778 780 private
779 781
780 782 def generate_password_if_needed
781 783 if generate_password? && auth_source.nil?
782 784 length = [Setting.password_min_length.to_i + 2, 10].max
783 785 random_password(length)
784 786 end
785 787 end
786 788
787 789 # Delete all outstanding password reset tokens on password change.
788 790 # Delete the autologin tokens on password change to prohibit session leakage.
789 791 # This helps to keep the account secure in case the associated email account
790 792 # was compromised.
791 793 def destroy_tokens
792 794 if hashed_password_changed? || (status_changed? && !active?)
793 795 tokens = ['recovery', 'autologin', 'session']
794 796 Token.where(:user_id => id, :action => tokens).delete_all
795 797 end
796 798 end
797 799
798 800 # Removes references that are not handled by associations
799 801 # Things that are not deleted are reassociated with the anonymous user
800 802 def remove_references_before_destroy
801 803 return if self.id.nil?
802 804
803 805 substitute = User.anonymous
804 806 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
805 807 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
806 808 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
807 809 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
808 810 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
809 811 JournalDetail.
810 812 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
811 813 update_all(['old_value = ?', substitute.id.to_s])
812 814 JournalDetail.
813 815 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
814 816 update_all(['value = ?', substitute.id.to_s])
815 817 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
816 818 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
817 819 # Remove private queries and keep public ones
818 820 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
819 821 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
820 822 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
821 823 Token.delete_all ['user_id = ?', id]
822 824 Watcher.delete_all ['user_id = ?', id]
823 825 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
824 826 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
825 827 end
826 828
827 829 # Return password digest
828 830 def self.hash_password(clear_password)
829 831 Digest::SHA1.hexdigest(clear_password || "")
830 832 end
831 833
832 834 # Returns a 128bits random salt as a hex string (32 chars long)
833 835 def self.generate_salt
834 836 Redmine::Utils.random_hex(16)
835 837 end
836 838
837 839 end
838 840
839 841 class AnonymousUser < User
840 842 validate :validate_anonymous_uniqueness, :on => :create
841 843
842 844 def validate_anonymous_uniqueness
843 845 # There should be only one AnonymousUser in the database
844 846 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
845 847 end
846 848
847 849 def available_custom_fields
848 850 []
849 851 end
850 852
851 853 # Overrides a few properties
852 854 def logged?; false end
853 855 def admin; false end
854 856 def name(*args); I18n.t(:label_user_anonymous) end
855 857 def mail=(*args); nil end
856 858 def mail; nil end
857 859 def time_zone; nil end
858 860 def rss_key; nil end
859 861
860 862 def pref
861 863 UserPreference.new(:user => self)
862 864 end
863 865
864 866 # Returns the user's bult-in role
865 867 def builtin_role
866 868 @builtin_role ||= Role.anonymous
867 869 end
868 870
869 871 def membership(*args)
870 872 nil
871 873 end
872 874
873 875 def member_of?(*args)
874 876 false
875 877 end
876 878
877 879 # Anonymous user can not be destroyed
878 880 def destroy
879 881 false
880 882 end
881 883
882 884 protected
883 885
884 886 def instantiate_email_address
885 887 end
886 888 end
@@ -1,1194 +1,1202
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 166 button_copy: Kopieren
167 167 button_copy_and_follow: Kopieren und Ticket anzeigen
168 168 button_create: Anlegen
169 169 button_create_and_continue: Anlegen und weiter
170 170 button_delete: LΓΆschen
171 171 button_delete_my_account: Mein Benutzerkonto lΓΆschen
172 172 button_download: Download
173 173 button_duplicate: Duplizieren
174 174 button_edit: Bearbeiten
175 175 button_edit_associated_wikipage: "ZugehΓΆrige Wikiseite bearbeiten: %{page_title}"
176 176 button_edit_section: Diesen Bereich bearbeiten
177 177 button_expand_all: Alle ausklappen
178 178 button_export: Exportieren
179 179 button_hide: Verstecken
180 180 button_list: Liste
181 181 button_lock: Sperren
182 182 button_log_time: Aufwand buchen
183 183 button_login: Anmelden
184 184 button_move: Verschieben
185 185 button_move_and_follow: Verschieben und Ticket anzeigen
186 186 button_quote: Zitieren
187 187 button_rename: Umbenennen
188 188 button_reopen: Γ–ffnen
189 189 button_reply: Antworten
190 190 button_reset: ZurΓΌcksetzen
191 191 button_rollback: Auf diese Version zurΓΌcksetzen
192 192 button_save: Speichern
193 193 button_show: Anzeigen
194 194 button_sort: Sortieren
195 195 button_submit: OK
196 196 button_test: Testen
197 197 button_unarchive: Entarchivieren
198 198 button_uncheck_all: Alles abwΓ€hlen
199 199 button_unlock: Entsperren
200 200 button_unwatch: Nicht beobachten
201 201 button_update: Aktualisieren
202 202 button_view: Anzeigen
203 203 button_watch: Beobachten
204 204
205 205 default_activity_design: Design
206 206 default_activity_development: Entwicklung
207 207 default_doc_category_tech: Technische Dokumentation
208 208 default_doc_category_user: Benutzerdokumentation
209 209 default_issue_status_closed: Erledigt
210 210 default_issue_status_feedback: Feedback
211 211 default_issue_status_in_progress: In Bearbeitung
212 212 default_issue_status_new: Neu
213 213 default_issue_status_rejected: Abgewiesen
214 214 default_issue_status_resolved: GelΓΆst
215 215 default_priority_high: Hoch
216 216 default_priority_immediate: Sofort
217 217 default_priority_low: Niedrig
218 218 default_priority_normal: Normal
219 219 default_priority_urgent: Dringend
220 220 default_role_developer: Entwickler
221 221 default_role_manager: Manager
222 222 default_role_reporter: Reporter
223 223 default_tracker_bug: Fehler
224 224 default_tracker_feature: Feature
225 225 default_tracker_support: UnterstΓΌtzung
226 226
227 227 description_all_columns: Alle Spalten
228 228 description_available_columns: VerfΓΌgbare Spalten
229 229 description_choose_project: Projekte
230 230 description_date_from: Startdatum eintragen
231 231 description_date_range_interval: Zeitraum durch Start- und Enddatum festlegen
232 232 description_date_range_list: Zeitraum aus einer Liste wΓ€hlen
233 233 description_date_to: Enddatum eintragen
234 234 description_filter: Filter
235 235 description_issue_category_reassign: Neue Kategorie wΓ€hlen
236 236 description_message_content: Nachrichteninhalt
237 237 description_notes: Kommentare
238 238 description_project_scope: Suchbereich
239 239 description_query_sort_criteria_attribute: Sortierattribut
240 240 description_query_sort_criteria_direction: Sortierrichtung
241 241 description_search: Suchfeld
242 242 description_selected_columns: AusgewΓ€hlte Spalten
243 243
244 244 description_user_mail_notification: Mailbenachrichtigungseinstellung
245 245 description_wiki_subpages_reassign: Neue Elternseite wΓ€hlen
246 246
247 247 enumeration_activities: AktivitΓ€ten (Zeiterfassung)
248 248 enumeration_doc_categories: Dokumentenkategorien
249 249 enumeration_issue_priorities: Ticket-PrioritΓ€ten
250 250 enumeration_system_activity: System-AktivitΓ€t
251 251
252 252 error_attachment_too_big: Diese Datei kann nicht hochgeladen werden, da sie die maximale Dateigrâße von (%{max_size}) überschreitet.
253 253 error_can_not_archive_project: Dieses Projekt kann nicht archiviert werden.
254 254 error_can_not_delete_custom_field: Kann das benutzerdefinierte Feld nicht lΓΆschen.
255 255 error_can_not_delete_tracker: Dieser Tracker enthΓ€lt Tickets und kann nicht gelΓΆscht werden.
256 256 error_can_not_remove_role: Diese Rolle wird verwendet und kann nicht gelΓΆscht werden.
257 257 error_can_not_reopen_issue_on_closed_version: Das Ticket ist einer abgeschlossenen Version zugeordnet und kann daher nicht wieder geΓΆffnet werden.
258 258 error_can_t_load_default_data: "Die Standard-Konfiguration konnte nicht geladen werden: %{value}"
259 259 error_issue_done_ratios_not_updated: Der Ticket-Fortschritt wurde nicht aktualisiert.
260 260 error_issue_not_found_in_project: 'Das Ticket wurde nicht gefunden oder gehΓΆrt nicht zu diesem Projekt.'
261 261 error_no_default_issue_status: Es ist kein Status als Standard definiert. Bitte ΓΌberprΓΌfen Sie Ihre Konfiguration (unter "Administration -> Ticket-Status").
262 262 error_no_tracker_in_project: Diesem Projekt ist kein Tracker zugeordnet. Bitte ΓΌberprΓΌfen Sie die Projekteinstellungen.
263 263 error_scm_annotate: "Der Eintrag existiert nicht oder kann nicht annotiert werden."
264 264 error_scm_annotate_big_text_file: Der Eintrag kann nicht umgesetzt werden, da er die maximale TextlΓ€nge ΓΌberschreitet.
265 265 error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %{value}"
266 266 error_scm_not_found: Eintrag und/oder Revision existiert nicht im Projektarchiv.
267 267 error_session_expired: Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.
268 268 error_unable_delete_issue_status: "Der Ticket-Status konnte nicht gelΓΆscht werden."
269 269 error_unable_to_connect: Fehler beim Verbinden (%{value})
270 270 error_workflow_copy_source: Bitte wΓ€hlen Sie einen Quell-Tracker und eine Quell-Rolle.
271 271 error_workflow_copy_target: Bitte wΓ€hlen Sie die Ziel-Tracker und -Rollen.
272 272
273 273 field_account: Konto
274 274 field_active: Aktiv
275 275 field_activity: AktivitΓ€t
276 276 field_admin: Administrator
277 277 field_assignable: Tickets kΓΆnnen dieser Rolle zugewiesen werden
278 278 field_assigned_to: Zugewiesen an
279 279 field_assigned_to_role: ZustΓ€ndigkeitsrolle
280 280 field_attr_firstname: Vorname-Attribut
281 281 field_attr_lastname: Name-Attribut
282 282 field_attr_login: Mitgliedsname-Attribut
283 283 field_attr_mail: E-Mail-Attribut
284 284 field_auth_source: Authentifizierungs-Modus
285 285 field_auth_source_ldap_filter: LDAP-Filter
286 286 field_author: Autor
287 287 field_base_dn: Base DN
288 288 field_board_parent: Übergeordnetes Forum
289 289 field_category: Kategorie
290 290 field_column_names: Spalten
291 291 field_closed_on: Geschlossen am
292 292 field_comments: Kommentar
293 293 field_comments_sorting: Kommentare anzeigen
294 294 field_commit_logs_encoding: Kodierung der Commit-Log-Meldungen
295 295 field_content: Inhalt
296 296 field_core_fields: Standardwerte
297 297 field_created_on: Angelegt
298 298 field_cvs_module: Modul
299 299 field_cvsroot: CVSROOT
300 300 field_default_value: Standardwert
301 301 field_default_status: Standardstatus
302 302 field_delay: Pufferzeit
303 303 field_description: Beschreibung
304 304 field_done_ratio: "% erledigt"
305 305 field_downloads: Downloads
306 306 field_due_date: Abgabedatum
307 307 field_editable: Bearbeitbar
308 308 field_effective_date: Datum
309 309 field_estimated_hours: GeschΓ€tzter Aufwand
310 310 field_field_format: Format
311 311 field_filename: Datei
312 312 field_filesize: Grâße
313 313 field_firstname: Vorname
314 314 field_fixed_version: Zielversion
315 315 field_generate_password: Passwort generieren
316 316 field_group_by: Gruppiere Ergebnisse nach
317 317 field_hide_mail: E-Mail-Adresse nicht anzeigen
318 318 field_homepage: Projekt-Homepage
319 319 field_host: Host
320 320 field_hours: Stunden
321 321 field_identifier: Kennung
322 322 field_identity_url: OpenID-URL
323 323 field_inherit_members: Benutzer erben
324 324 field_is_closed: Ticket geschlossen
325 325 field_is_default: Standardeinstellung
326 326 field_is_filter: Als Filter benutzen
327 327 field_is_for_all: FΓΌr alle Projekte
328 328 field_is_in_roadmap: In der Roadmap anzeigen
329 329 field_is_private: Privat
330 330 field_is_public: Γ–ffentlich
331 331 field_is_required: Erforderlich
332 332 field_issue: Ticket
333 333 field_issue_to: ZugehΓΆriges Ticket
334 334 field_issues_visibility: Ticket Sichtbarkeit
335 335 field_language: Sprache
336 336 field_last_login_on: Letzte Anmeldung
337 337 field_lastname: Nachname
338 338 field_login: Mitgliedsname
339 339 field_mail: E-Mail
340 340 field_mail_notification: Mailbenachrichtigung
341 341 field_max_length: Maximale LΓ€nge
342 342 field_member_of_group: ZustΓ€ndigkeitsgruppe
343 343 field_min_length: Minimale LΓ€nge
344 344 field_multiple: Mehrere Werte
345 345 field_must_change_passwd: Passwort beim nΓ€chsten Login Γ€ndern
346 346 field_name: Name
347 347 field_new_password: Neues Kennwort
348 348 field_notes: Kommentare
349 349 field_onthefly: On-the-fly-Benutzererstellung
350 350 field_parent: Unterprojekt von
351 351 field_parent_issue: Übergeordnete Aufgabe
352 352 field_parent_title: Übergeordnete Seite
353 353 field_password: Kennwort
354 354 field_password_confirmation: BestΓ€tigung
355 355 field_path_to_repository: Pfad zum Repository
356 356 field_port: Port
357 357 field_possible_values: MΓΆgliche Werte
358 358 field_principal: Auftraggeber
359 359 field_priority: PrioritΓ€t
360 360 field_private_notes: Privater Kommentar
361 361 field_project: Projekt
362 362 field_redirect_existing_links: Existierende Links umleiten
363 363 field_regexp: RegulΓ€rer Ausdruck
364 364 field_repository_is_default: Haupt-Repository
365 365 field_role: Rolle
366 366 field_root_directory: Wurzelverzeichnis
367 367 field_scm_path_encoding: Pfad-Kodierung
368 368 field_searchable: Durchsuchbar
369 369 field_sharing: Gemeinsame Verwendung
370 370 field_spent_on: Datum
371 371 field_start_date: Beginn
372 372 field_start_page: Hauptseite
373 373 field_status: Status
374 374 field_subject: Thema
375 375 field_subproject: Unterprojekt von
376 376 field_summary: Zusammenfassung
377 377 field_text: Textfeld
378 378 field_time_entries: Logzeit
379 379 field_time_zone: Zeitzone
380 380 field_timeout: Auszeit (in Sekunden)
381 381 field_title: Titel
382 382 field_tracker: Tracker
383 383 field_type: Typ
384 384 field_updated_on: Aktualisiert
385 385 field_url: URL
386 386 field_user: Benutzer
387 387 field_users_visibility: Benutzer Sichtbarkeit
388 388 field_value: Wert
389 389 field_version: Version
390 390 field_visible: Sichtbar
391 391 field_warn_on_leaving_unsaved: Vor dem Verlassen einer Seite mit ungesichertem Text im Editor warnen
392 392 field_watcher: Beobachter
393 393
394 394 general_csv_decimal_separator: ','
395 395 general_csv_encoding: ISO-8859-1
396 396 general_csv_separator: ';'
397 397 general_pdf_fontname: freesans
398 398 general_pdf_monospaced_fontname: freemono
399 399 general_first_day_of_week: '1'
400 400 general_lang_name: 'German (Deutsch)'
401 401 general_text_No: 'Nein'
402 402 general_text_Yes: 'Ja'
403 403 general_text_no: 'nein'
404 404 general_text_yes: 'ja'
405 405
406 406 label_activity: AktivitΓ€t
407 407 label_add_another_file: Eine weitere Datei hinzufΓΌgen
408 408 label_add_note: Kommentar hinzufΓΌgen
409 409 label_add_projects: Projekt hinzufΓΌgen
410 410 label_added: hinzugefΓΌgt
411 411 label_added_time_by: "Von %{author} vor %{age} hinzugefΓΌgt"
412 412 label_additional_workflow_transitions_for_assignee: ZusΓ€tzliche Berechtigungen wenn der Benutzer der Zugewiesene ist
413 413 label_additional_workflow_transitions_for_author: ZusΓ€tzliche Berechtigungen wenn der Benutzer der Autor ist
414 414 label_administration: Administration
415 415 label_age: GeΓ€ndert vor
416 416 label_ago: vor
417 417 label_all: alle
418 418 label_all_time: gesamter Zeitraum
419 419 label_all_words: Alle WΓΆrter
420 420 label_and_its_subprojects: "%{value} und dessen Unterprojekte"
421 421 label_any: alle
422 422 label_any_issues_in_project: irgendein Ticket im Projekt
423 423 label_any_issues_not_in_project: irgendein Ticket nicht im Projekt
424 424 label_api_access_key: API-ZugriffsschlΓΌssel
425 425 label_api_access_key_created_on: Der API-ZugriffsschlΓΌssel wurde vor %{value} erstellt
426 426 label_applied_status: Zugewiesener Status
427 427 label_ascending: Aufsteigend
428 428 label_ask: Nachfragen
429 429 label_assigned_to_me_issues: Mir zugewiesene Tickets
430 430 label_associated_revisions: ZugehΓΆrige Revisionen
431 431 label_attachment: Datei
432 432 label_attachment_delete: Anhang lΓΆschen
433 433 label_attachment_new: Neue Datei
434 434 label_attachment_plural: Dateien
435 435 label_attribute: Attribut
436 436 label_attribute_of_assigned_to: "%{name} des Bearbeiters"
437 437 label_attribute_of_author: "%{name} des Autors"
438 438 label_attribute_of_fixed_version: "%{name} der Zielversion"
439 439 label_attribute_of_issue: "%{name} des Tickets"
440 440 label_attribute_of_project: "%{name} des Projekts"
441 441 label_attribute_of_user: "%{name} des Benutzers"
442 442 label_attribute_plural: Attribute
443 443 label_auth_source: Authentifizierungs-Modus
444 444 label_auth_source_new: Neuer Authentifizierungs-Modus
445 445 label_auth_source_plural: Authentifizierungs-Arten
446 446 label_authentication: Authentifizierung
447 447 label_between: zwischen
448 448 label_blocked_by: Blockiert durch
449 449 label_blocks: Blockiert
450 450 label_board: Forum
451 451 label_board_locked: Gesperrt
452 452 label_board_new: Neues Forum
453 453 label_board_plural: Foren
454 454 label_board_sticky: Wichtig (immer oben)
455 455 label_boolean: Boolean
456 456 label_branch: Zweig
457 457 label_browse: Codebrowser
458 458 label_bulk_edit_selected_issues: Alle ausgewΓ€hlten Tickets bearbeiten
459 459 label_bulk_edit_selected_time_entries: AusgewΓ€hlte ZeitaufwΓ€nde bearbeiten
460 460 label_calendar: Kalender
461 461 label_change_plural: Γ„nderungen
462 462 label_change_properties: Eigenschaften Γ€ndern
463 463 label_change_status: Statuswechsel
464 464 label_change_view_all: Alle Γ„nderungen anzeigen
465 465 label_changes_details: Details aller Γ„nderungen
466 466 label_changeset_plural: Changesets
467 467 label_checkboxes: Checkboxen
468 468 label_check_for_updates: Auf Updates prΓΌfen
469 469 label_child_revision: Nachfolger
470 470 label_chronological_order: in zeitlicher Reihenfolge
471 471 label_close_versions: VollstÀndige Versionen schließen
472 472 label_closed_issues: geschlossen
473 473 label_closed_issues_plural: geschlossen
474 474 label_comment: Kommentar
475 475 label_comment_add: Kommentar hinzufΓΌgen
476 476 label_comment_added: Kommentar hinzugefΓΌgt
477 477 label_comment_delete: Kommentar lΓΆschen
478 478 label_comment_plural: Kommentare
479 479 label_commits_per_author: Übertragungen pro Autor
480 480 label_commits_per_month: Übertragungen pro Monat
481 481 label_completed_versions: Abgeschlossene Versionen
482 482 label_confirmation: BestΓ€tigung
483 483 label_contains: enthΓ€lt
484 484 label_copied: kopiert
485 485 label_copied_from: Kopiert von
486 486 label_copied_to: Kopiert nach
487 487 label_copy_attachments: AnhΓ€nge kopieren
488 488 label_copy_same_as_target: So wie das Ziel
489 489 label_copy_source: Quelle
490 490 label_copy_subtasks: Unteraufgaben kopieren
491 491 label_copy_target: Ziel
492 492 label_copy_workflow_from: Workflow kopieren von
493 493 label_cross_project_descendants: Mit Unterprojekten
494 494 label_cross_project_hierarchy: Mit Projekthierarchie
495 495 label_cross_project_system: Mit allen Projekten
496 496 label_cross_project_tree: Mit Projektbaum
497 497 label_current_status: GegenwΓ€rtiger Status
498 498 label_current_version: GegenwΓ€rtige Version
499 499 label_custom_field: Benutzerdefiniertes Feld
500 500 label_custom_field_new: Neues Feld
501 501 label_custom_field_plural: Benutzerdefinierte Felder
502 502 label_custom_field_select_type: Bitte wΓ€hlen Sie den Objekttyp, zu dem das benutzerdefinierte Feld hinzugefΓΌgt werden soll
503 503 label_date: Datum
504 504 label_date_from: Von
505 505 label_date_from_to: von %{start} bis %{end}
506 506 label_date_range: Zeitraum
507 507 label_date_to: Bis
508 508 label_day_plural: Tage
509 509 label_default: Standard
510 510 label_default_columns: Standard-Spalten
511 511 label_deleted: gelΓΆscht
512 512 label_descending: Absteigend
513 513 label_details: Details
514 514 label_diff: diff
515 515 label_diff_inline: einspaltig
516 516 label_diff_side_by_side: nebeneinander
517 517 label_disabled: gesperrt
518 518 label_display: Anzeige
519 519 label_display_per_page: "Pro Seite: %{value}"
520 520 label_display_used_statuses_only: Zeige nur Status an, die von diesem Tracker verwendet werden
521 521 label_document: Dokument
522 522 label_document_added: Dokument hinzugefΓΌgt
523 523 label_document_new: Neues Dokument
524 524 label_document_plural: Dokumente
525 525 label_downloads_abbr: D/L
526 526 label_drop_down_list: Dropdown-Liste
527 527 label_duplicated_by: Dupliziert durch
528 528 label_duplicates: Duplikat von
529 529 label_edit_attachments: AngehΓ€ngte Dateien bearbeiten
530 530 label_enumeration_new: Neuer Wert
531 531 label_enumerations: AufzΓ€hlungen
532 532 label_environment: Umgebung
533 533 label_equals: ist
534 534 label_example: Beispiel
535 535 label_export_options: "%{export_format} Export-Eigenschaften"
536 536 label_export_to: "Auch abrufbar als:"
537 537 label_f_hour: "%{value} Stunde"
538 538 label_f_hour_plural: "%{value} Stunden"
539 539 label_feed_plural: Feeds
540 540 label_feeds_access_key: Atom-ZugriffsschlΓΌssel
541 541 label_feeds_access_key_created_on: "Atom-ZugriffsschlΓΌssel vor %{value} erstellt"
542 542 label_fields_permissions: Feldberechtigungen
543 543 label_file_added: Datei hinzugefΓΌgt
544 544 label_file_plural: Dateien
545 545 label_filter_add: Filter hinzufΓΌgen
546 546 label_filter_plural: Filter
547 547 label_float: Fließkommazahl
548 548 label_follows: Nachfolger von
549 549 label_gantt: Gantt-Diagramm
550 550 label_gantt_progress_line: Fortschrittslinie
551 551 label_general: Allgemein
552 552 label_generate_key: Generieren
553 553 label_git_report_last_commit: Bericht des letzten Commits fΓΌr Dateien und Verzeichnisse
554 554 label_greater_or_equal: ">="
555 555 label_group: Gruppe
556 556 label_group_anonymous: Anonyme Benutzer
557 557 label_group_new: Neue Gruppe
558 558 label_group_non_member: Nichtmitglieder
559 559 label_group_plural: Gruppen
560 560 label_help: Hilfe
561 561 label_hidden: Versteckt
562 562 label_history: Historie
563 563 label_home: Hauptseite
564 564 label_in: in
565 565 label_in_less_than: in weniger als
566 566 label_in_more_than: in mehr als
567 567 label_in_the_next_days: in den nΓ€chsten
568 568 label_in_the_past_days: in den letzten
569 569 label_incoming_emails: Eingehende E-Mails
570 570 label_index_by_date: Seiten nach Datum sortiert
571 571 label_index_by_title: Seiten nach Titel sortiert
572 572 label_information: Information
573 573 label_information_plural: Informationen
574 574 label_integer: Zahl
575 575 label_internal: Intern
576 576 label_issue: Ticket
577 577 label_issue_added: Ticket hinzugefΓΌgt
578 578 label_issue_assigned_to_updated: Bearbeiter aktualisiert
579 579 label_issue_category: Ticket-Kategorie
580 580 label_issue_category_new: Neue Kategorie
581 581 label_issue_category_plural: Ticket-Kategorien
582 582 label_issue_new: Neues Ticket
583 583 label_issue_note_added: Notiz hinzugefΓΌgt
584 584 label_issue_plural: Tickets
585 585 label_issue_priority_updated: PrioritΓ€t aktualisiert
586 586 label_issue_status: Ticket-Status
587 587 label_issue_status_new: Neuer Status
588 588 label_issue_status_plural: Ticket-Status
589 589 label_issue_status_updated: Status aktualisiert
590 590 label_issue_tracking: Tickets
591 591 label_issue_updated: Ticket aktualisiert
592 592 label_issue_view_all: Alle Tickets anzeigen
593 593 label_issue_watchers: Beobachter
594 594 label_issues_by: "Tickets pro %{value}"
595 595 label_issues_visibility_all: Alle Tickets
596 596 label_issues_visibility_own: Tickets die folgender Benutzer erstellt hat oder die ihm zugewiesen sind
597 597 label_issues_visibility_public: Alle ΓΆffentlichen Tickets
598 598 label_item_position: "%{position}/%{count}"
599 599 label_jump_to_a_project: Zu einem Projekt springen...
600 600 label_language_based: SprachabhΓ€ngig
601 601 label_last_changes: "%{count} letzte Γ„nderungen"
602 602 label_last_login: Letzte Anmeldung
603 603 label_last_month: voriger Monat
604 604 label_last_n_days: "die letzten %{count} Tage"
605 605 label_last_n_weeks: letzte %{count} Wochen
606 606 label_last_week: vorige Woche
607 607 label_latest_compatible_version: Letzte kompatible Version
608 608 label_latest_revision: Aktuellste Revision
609 609 label_latest_revision_plural: Aktuellste Revisionen
610 610 label_ldap_authentication: LDAP-Authentifizierung
611 611 label_less_or_equal: "<="
612 612 label_less_than_ago: vor weniger als
613 613 label_link: Link
614 614 label_link_copied_issue: Kopierte Tickets verlinken
615 615 label_link_values_to: Werte mit URL verknΓΌpfen
616 616 label_list: Liste
617 617 label_loading: Lade...
618 618 label_logged_as: Angemeldet als
619 619 label_login: Anmelden
620 620 label_login_with_open_id_option: oder mit OpenID anmelden
621 621 label_logout: Abmelden
622 622 label_only: nur
623 623 label_max_size: Maximale Grâße
624 624 label_me: ich
625 625 label_member: Mitglied
626 626 label_member_new: Neues Mitglied
627 627 label_member_plural: Mitglieder
628 628 label_message_last: Letzter Forenbeitrag
629 629 label_message_new: Neues Thema
630 630 label_message_plural: ForenbeitrΓ€ge
631 631 label_message_posted: Forenbeitrag hinzugefΓΌgt
632 632 label_min_max_length: LΓ€nge (Min. - Max.)
633 633 label_missing_api_access_key: Der API-ZugriffsschlΓΌssel fehlt.
634 634 label_missing_feeds_access_key: Der Atom-ZugriffsschlΓΌssel fehlt.
635 635 label_modified: geΓ€ndert
636 636 label_module_plural: Module
637 637 label_month: Monat
638 638 label_months_from: Monate ab
639 639 label_more: Mehr
640 640 label_more_than_ago: vor mehr als
641 641 label_my_account: Mein Konto
642 642 label_my_page: Meine Seite
643 643 label_my_page_block: VerfΓΌgbare Widgets
644 644 label_my_projects: Meine Projekte
645 645 label_my_queries: Meine eigenen Abfragen
646 646 label_new: Neu
647 647 label_new_statuses_allowed: Neue Berechtigungen
648 648 label_news: News
649 649 label_news_added: News hinzugefΓΌgt
650 650 label_news_comment_added: Kommentar zu einer News hinzugefΓΌgt
651 651 label_news_latest: Letzte News
652 652 label_news_new: News hinzufΓΌgen
653 653 label_news_plural: News
654 654 label_news_view_all: Alle News anzeigen
655 655 label_next: Weiter
656 656 label_no_change_option: (Keine Γ„nderung)
657 657 label_no_data: Nichts anzuzeigen
658 658 label_no_issues_in_project: keine Tickets im Projekt
659 659 label_nobody: Niemand
660 660 label_none: kein
661 661 label_not_contains: enthΓ€lt nicht
662 662 label_not_equals: ist nicht
663 663 label_open_issues: offen
664 664 label_open_issues_plural: offen
665 665 label_optional_description: Beschreibung (optional)
666 666 label_options: Optionen
667 667 label_overall_activity: AktivitΓ€ten aller Projekte anzeigen
668 668 label_overall_spent_time: Aufgewendete Zeit aller Projekte anzeigen
669 669 label_overview: Übersicht
670 670 label_parent_revision: VorgΓ€nger
671 671 label_password_lost: Kennwort vergessen
672 672 label_password_required: Bitte geben Sie Ihr Kennwort ein
673 673 label_permissions: Berechtigungen
674 674 label_permissions_report: BerechtigungsΓΌbersicht
675 675 label_personalize_page: Diese Seite anpassen
676 676 label_planning: Terminplanung
677 677 label_please_login: Anmelden
678 678 label_plugins: Plugins
679 679 label_precedes: VorgΓ€nger von
680 680 label_preferences: PrΓ€ferenzen
681 681 label_preview: Vorschau
682 682 label_previous: ZurΓΌck
683 683 label_principal_search: "Nach Benutzer oder Gruppe suchen:"
684 684 label_profile: Profil
685 685 label_project: Projekt
686 686 label_project_all: Alle Projekte
687 687 label_project_copy_notifications: Sende Mailbenachrichtigungen beim Kopieren des Projekts.
688 688 label_project_latest: Neueste Projekte
689 689 label_project_new: Neues Projekt
690 690 label_project_plural: Projekte
691 691 label_public_projects: Γ–ffentliche Projekte
692 692 label_query: Benutzerdefinierte Abfrage
693 693 label_query_new: Neue Abfrage
694 694 label_query_plural: Benutzerdefinierte Abfragen
695 695 label_radio_buttons: Radio-Buttons
696 696 label_read: Lesen...
697 697 label_readonly: Nur-Lese-Zugriff
698 698 label_register: Registrieren
699 699 label_registered_on: Angemeldet am
700 700 label_registration_activation_by_email: Kontoaktivierung durch E-Mail
701 701 label_registration_automatic_activation: Automatische Kontoaktivierung
702 702 label_registration_manual_activation: Manuelle Kontoaktivierung
703 703 label_related_issues: ZugehΓΆrige Tickets
704 704 label_relates_to: Beziehung mit
705 705 label_relation_delete: Beziehung lΓΆschen
706 706 label_relation_new: Neue Beziehung
707 707 label_renamed: umbenannt
708 708 label_reply_plural: Antworten
709 709 label_report: Bericht
710 710 label_report_plural: Berichte
711 711 label_reported_issues: Erstellte Tickets
712 712 label_repository: Projektarchiv
713 713 label_repository_new: Neues Repository
714 714 label_repository_plural: Projektarchive
715 715 label_required: Erforderlich
716 716 label_result_plural: Resultate
717 717 label_reverse_chronological_order: in umgekehrter zeitlicher Reihenfolge
718 718 label_revision: Revision
719 719 label_revision_id: Revision %{value}
720 720 label_revision_plural: Revisionen
721 721 label_roadmap: Roadmap
722 722 label_roadmap_due_in: "FΓ€llig in %{value}"
723 723 label_roadmap_no_issues: Keine Tickets fΓΌr diese Version
724 724 label_roadmap_overdue: "seit %{value} verspΓ€tet"
725 725 label_role: Rolle
726 726 label_role_and_permissions: Rollen und Rechte
727 727 label_role_anonymous: Anonymous
728 728 label_role_new: Neue Rolle
729 729 label_role_non_member: Nichtmitglied
730 730 label_role_plural: Rollen
731 731 label_scm: Versionskontrollsystem
732 732 label_search: Suche
733 733 label_search_for_watchers: Nach hinzufΓΌgbaren Beobachtern suchen
734 734 label_search_titles_only: Nur Titel durchsuchen
735 735 label_send_information: Sende Kontoinformationen an Benutzer
736 736 label_send_test_email: Test-E-Mail senden
737 737 label_session_expiration: Ende einer Sitzung
738 738 label_settings: Konfiguration
739 739 label_show_closed_projects: Geschlossene Projekte anzeigen
740 740 label_show_completed_versions: Abgeschlossene Versionen anzeigen
741 741 label_sort: Sortierung
742 742 label_sort_by: "Sortiert nach %{value}"
743 743 label_sort_higher: Eins hΓΆher
744 744 label_sort_highest: An den Anfang
745 745 label_sort_lower: Eins tiefer
746 746 label_sort_lowest: Ans Ende
747 747 label_spent_time: Aufgewendete Zeit
748 748 label_statistics: Statistiken
749 749 label_status_transitions: StatusΓ€nderungen
750 750 label_stay_logged_in: Angemeldet bleiben
751 751 label_string: Text
752 752 label_subproject_new: Neues Unterprojekt
753 753 label_subproject_plural: Unterprojekte
754 754 label_subtask_plural: Unteraufgaben
755 755 label_tag: Markierung
756 756 label_text: Langer Text
757 757 label_theme: Stil
758 758 label_this_month: aktueller Monat
759 759 label_this_week: aktuelle Woche
760 760 label_this_year: aktuelles Jahr
761 761 label_time_entry_plural: BenΓΆtigte Zeit
762 762 label_time_tracking: Zeiterfassung
763 763 label_today: heute
764 764 label_topic_plural: Themen
765 765 label_total: Gesamtzahl
766 766 label_total_time: Gesamtzeit
767 767 label_tracker: Tracker
768 768 label_tracker_new: Neuer Tracker
769 769 label_tracker_plural: Tracker
770 770 label_unknown_plugin: Unbekanntes Plugin
771 771 label_update_issue_done_ratios: Ticket-Fortschritt aktualisieren
772 772 label_updated_time: "Vor %{value} aktualisiert"
773 773 label_updated_time_by: "Von %{author} vor %{age} aktualisiert"
774 774 label_used_by: Benutzt von
775 775 label_user: Benutzer
776 776 label_user_activity: "AktivitΓ€t von %{value}"
777 777 label_user_anonymous: Anonym
778 778 label_user_mail_no_self_notified: "Ich mΓΆchte nicht ΓΌber Γ„nderungen benachrichtigt werden, die ich selbst durchfΓΌhre."
779 779 label_user_mail_option_all: "FΓΌr alle Ereignisse in all meinen Projekten"
780 780 label_user_mail_option_none: Keine Ereignisse
781 781 label_user_mail_option_only_assigned: Nur fΓΌr Aufgaben fΓΌr die ich zustΓ€ndig bin
782 782 label_user_mail_option_only_my_events: Nur fΓΌr Aufgaben die ich beobachte oder an welchen ich mitarbeite
783 783 label_user_mail_option_only_owner: Nur fΓΌr Aufgaben die ich angelegt habe
784 784 label_user_mail_option_selected: "FΓΌr alle Ereignisse in den ausgewΓ€hlten Projekten"
785 785 label_user_new: Neuer Benutzer
786 786 label_user_plural: Benutzer
787 787 label_user_search: "Nach Benutzer suchen:"
788 788 label_users_visibility_all: Alle aktiven Benutzer
789 789 label_users_visibility_members_of_visible_projects: Mitglieder von sichtbaren Projekten
790 790 label_version: Version
791 791 label_version_new: Neue Version
792 792 label_version_plural: Versionen
793 793 label_version_sharing_descendants: Mit Unterprojekten
794 794 label_version_sharing_hierarchy: Mit Projekthierarchie
795 795 label_version_sharing_none: Nicht gemeinsam verwenden
796 796 label_version_sharing_system: Mit allen Projekten
797 797 label_version_sharing_tree: Mit Projektbaum
798 798 label_view_all_revisions: Alle Revisionen anzeigen
799 799 label_view_diff: Unterschiede anzeigen
800 800 label_view_revisions: Revisionen anzeigen
801 801 label_visibility_private: nur fΓΌr mich
802 802 label_visibility_public: fΓΌr jeden Benutzer
803 803 label_visibility_roles: nur fΓΌr diese Rollen
804 804 label_watched_issues: Beobachtete Tickets
805 805 label_week: Woche
806 806 label_wiki: Wiki
807 807 label_wiki_content_added: Wiki-Seite hinzugefΓΌgt
808 808 label_wiki_content_updated: Wiki-Seite aktualisiert
809 809 label_wiki_edit: Wiki-Bearbeitung
810 810 label_wiki_edit_plural: Wiki-Bearbeitungen
811 811 label_wiki_page: Wiki-Seite
812 812 label_wiki_page_plural: Wiki-Seiten
813 813 label_workflow: Workflow
814 814 label_x_closed_issues_abbr:
815 815 zero: 0 geschlossen
816 816 one: 1 geschlossen
817 817 other: "%{count} geschlossen"
818 818 label_x_comments:
819 819 zero: keine Kommentare
820 820 one: 1 Kommentar
821 821 other: "%{count} Kommentare"
822 822 label_x_issues:
823 823 zero: 0 Tickets
824 824 one: 1 Ticket
825 825 other: "%{count} Tickets"
826 826 label_x_open_issues_abbr:
827 827 zero: 0 offen
828 828 one: 1 offen
829 829 other: "%{count} offen"
830 830 label_x_projects:
831 831 zero: keine Projekte
832 832 one: 1 Projekt
833 833 other: "%{count} Projekte"
834 834 label_year: Jahr
835 835 label_yesterday: gestern
836 836
837 837 mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:"
838 838 mail_body_account_information: Ihre Konto-Informationen
839 839 mail_body_account_information_external: "Sie kΓΆnnen sich mit Ihrem Konto %{value} anmelden."
840 840 mail_body_lost_password: 'Benutzen Sie den folgenden Link, um Ihr Kennwort zu Γ€ndern:'
841 841 mail_body_register: 'Um Ihr Konto zu aktivieren, benutzen Sie folgenden Link:'
842 842 mail_body_reminder: "%{count} Tickets, die Ihnen zugewiesen sind, mΓΌssen in den nΓ€chsten %{days} Tagen abgegeben werden:"
843 843 mail_body_wiki_content_added: "Die Wiki-Seite '%{id}' wurde von %{author} hinzugefΓΌgt."
844 844 mail_body_wiki_content_updated: "Die Wiki-Seite '%{id}' wurde von %{author} aktualisiert."
845 845 mail_subject_account_activation_request: "Antrag auf %{value} Kontoaktivierung"
846 846 mail_subject_lost_password: "Ihr %{value} Kennwort"
847 847 mail_subject_register: "%{value} Kontoaktivierung"
848 848 mail_subject_reminder: "%{count} Tickets mΓΌssen in den nΓ€chsten %{days} Tagen abgegeben werden"
849 849 mail_subject_wiki_content_added: "Wiki-Seite '%{id}' hinzugefΓΌgt"
850 850 mail_subject_wiki_content_updated: "Wiki-Seite '%{id}' erfolgreich aktualisiert"
851 mail_subject_security_notification: "Sicherheitshinweis"
852 mail_body_security_notification_change: "%{field} wurde geÀndert."
853 mail_body_security_notification_change_to: "%{field} wurde geÀndert zu %{value}."
854 mail_body_security_notification_add: "%{field} %{value} wurde hinzugefügt."
855 mail_body_security_notification_remove: "%{field} %{value} wurde entfernt."
856 mail_body_security_notification_notify_enabled: "E-Mail-Adresse %{value} erhΓ€lt nun Benachrichtigungen."
857 mail_body_security_notification_notify_disabled: "E-Mail-Adresse %{value} erhΓ€lt keine Benachrichtigungen mehr."
851 858
852 859 notice_account_activated: Ihr Konto ist aktiviert. Sie kΓΆnnen sich jetzt anmelden.
853 860 notice_account_deleted: Ihr Benutzerkonto wurde unwiderruflich gelΓΆscht.
854 861 notice_account_invalid_credentials: Benutzer oder Kennwort ist ungΓΌltig.
855 862 notice_account_lost_email_sent: Eine E-Mail mit Anweisungen, ein neues Kennwort zu wΓ€hlen, wurde Ihnen geschickt.
856 863 notice_account_locked: Ihr Konto ist gesperrt.
857 864 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>.
858 865 notice_account_password_updated: Kennwort wurde erfolgreich aktualisiert.
859 866 notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators."
860 867 notice_account_register_done: Konto wurde erfolgreich angelegt. Eine E-Mail mit weiteren Instruktionen zur Kontoaktivierung wurde an %{email} gesendet.
861 868 notice_account_unknown_email: Unbekannter Benutzer.
862 869 notice_account_updated: Konto wurde erfolgreich aktualisiert.
863 870 notice_account_wrong_password: Falsches Kennwort.
864 871 notice_api_access_key_reseted: Ihr API-ZugriffsschlΓΌssel wurde zurΓΌckgesetzt.
865 872 notice_can_t_change_password: Dieses Konto verwendet eine externe Authentifizierungs-Quelle. UnmΓΆglich, das Kennwort zu Γ€ndern.
866 873 notice_default_data_loaded: Die Standard-Konfiguration wurde erfolgreich geladen.
867 874 notice_email_error: "Beim Senden einer E-Mail ist ein Fehler aufgetreten (%{value})."
868 875 notice_email_sent: "Eine E-Mail wurde an %{value} gesendet."
869 876 notice_failed_to_save_issues: "%{count} von %{total} ausgewΓ€hlten Tickets konnte(n) nicht gespeichert werden: %{ids}."
870 877 notice_failed_to_save_members: "Benutzer konnte nicht gespeichert werden: %{errors}."
871 878 notice_failed_to_save_time_entries: "Gescheitert %{count} ZeiteintrΓ€ge fΓΌr %{total} von ausgewΓ€hlten: %{ids} zu speichern."
872 879 notice_feeds_access_key_reseted: Ihr Atom-ZugriffsschlΓΌssel wurde zurΓΌckgesetzt.
873 880 notice_file_not_found: Anhang existiert nicht oder ist gelΓΆscht worden.
874 881 notice_gantt_chart_truncated: Die Grafik ist unvollstΓ€ndig, da das Maximum der anzeigbaren Aufgaben ΓΌberschritten wurde (%{max})
875 882 notice_issue_done_ratios_updated: Der Ticket-Fortschritt wurde aktualisiert.
876 883 notice_issue_successful_create: Ticket %{id} erstellt.
877 884 notice_issue_update_conflict: Das Ticket wurde wΓ€hrend Ihrer Bearbeitung von einem anderen Nutzer ΓΌberarbeitet.
878 885 notice_locking_conflict: Datum wurde von einem anderen Benutzer geΓ€ndert.
879 886 notice_new_password_must_be_different: Das neue Passwort muss sich vom dem aktuellen
880 887 unterscheiden
881 888 notice_no_issue_selected: "Kein Ticket ausgewΓ€hlt! Bitte wΓ€hlen Sie die Tickets, die Sie bearbeiten mΓΆchten."
882 889 notice_not_authorized: Sie sind nicht berechtigt, auf diese Seite zuzugreifen.
883 890 notice_not_authorized_archived_project: Das Projekt wurde archiviert und ist daher nicht nicht verfΓΌgbar.
884 891 notice_successful_connection: Verbindung erfolgreich.
885 892 notice_successful_create: Erfolgreich angelegt
886 893 notice_successful_delete: Erfolgreich gelΓΆscht.
887 894 notice_successful_update: Erfolgreich aktualisiert.
888 895 notice_unable_delete_time_entry: Der Zeiterfassungseintrag konnte nicht gelΓΆscht werden.
889 896 notice_unable_delete_version: Die Version konnte nicht gelΓΆscht werden.
890 897 notice_user_successful_create: Benutzer %{id} angelegt.
891 898
892 899 permission_add_issue_notes: Kommentare hinzufΓΌgen
893 900 permission_add_issue_watchers: Beobachter hinzufΓΌgen
894 901 permission_add_issues: Tickets hinzufΓΌgen
895 902 permission_add_messages: ForenbeitrΓ€ge hinzufΓΌgen
896 903 permission_add_project: Projekt erstellen
897 904 permission_add_subprojects: Unterprojekte erstellen
898 905 permission_add_documents: Dokumente hinzufΓΌgen
899 906 permission_browse_repository: Projektarchiv ansehen
900 907 permission_close_project: Schließen / erneutes Γ–ffnen eines Projekts
901 908 permission_comment_news: News kommentieren
902 909 permission_commit_access: Commit-Zugriff
903 910 permission_delete_issue_watchers: Beobachter lΓΆschen
904 911 permission_delete_issues: Tickets lΓΆschen
905 912 permission_delete_messages: ForenbeitrΓ€ge lΓΆschen
906 913 permission_delete_own_messages: Eigene ForenbeitrΓ€ge lΓΆschen
907 914 permission_delete_wiki_pages: Wiki-Seiten lΓΆschen
908 915 permission_delete_wiki_pages_attachments: AnhΓ€nge lΓΆschen
909 916 permission_delete_documents: Dokumente lΓΆschen
910 917 permission_edit_issue_notes: Kommentare bearbeiten
911 918 permission_edit_issues: Tickets bearbeiten
912 919 permission_edit_messages: ForenbeitrΓ€ge bearbeiten
913 920 permission_edit_own_issue_notes: Eigene Kommentare bearbeiten
914 921 permission_edit_own_messages: Eigene ForenbeitrΓ€ge bearbeiten
915 922 permission_edit_own_time_entries: Selbst gebuchte AufwΓ€nde bearbeiten
916 923 permission_edit_project: Projekt bearbeiten
917 924 permission_edit_time_entries: Gebuchte AufwΓ€nde bearbeiten
918 925 permission_edit_wiki_pages: Wiki-Seiten bearbeiten
919 926 permission_edit_documents: Dokumente bearbeiten
920 927 permission_export_wiki_pages: Wiki-Seiten exportieren
921 928 permission_log_time: AufwΓ€nde buchen
922 929 permission_manage_boards: Foren verwalten
923 930 permission_manage_categories: Ticket-Kategorien verwalten
924 931 permission_manage_files: Dateien verwalten
925 932 permission_manage_issue_relations: Ticket-Beziehungen verwalten
926 933 permission_manage_members: Mitglieder verwalten
927 934 permission_manage_news: News verwalten
928 935 permission_manage_project_activities: AktivitΓ€ten (Zeiterfassung) verwalten
929 936 permission_manage_public_queries: Γ–ffentliche Filter verwalten
930 937 permission_manage_related_issues: ZugehΓΆrige Tickets verwalten
931 938 permission_manage_repository: Projektarchiv verwalten
932 939 permission_manage_subtasks: Unteraufgaben verwalten
933 940 permission_manage_versions: Versionen verwalten
934 941 permission_manage_wiki: Wiki verwalten
935 942 permission_move_issues: Tickets verschieben
936 943 permission_protect_wiki_pages: Wiki-Seiten schΓΌtzen
937 944 permission_rename_wiki_pages: Wiki-Seiten umbenennen
938 945 permission_save_queries: Filter speichern
939 946 permission_select_project_modules: Projektmodule auswΓ€hlen
940 947 permission_set_issues_private: Tickets privat oder ΓΆffentlich markieren
941 948 permission_set_notes_private: Kommentar als privat markieren
942 949 permission_set_own_issues_private: Eigene Tickets privat oder ΓΆffentlich markieren
943 950 permission_view_calendar: Kalender ansehen
944 951 permission_view_changesets: Changesets ansehen
945 952 permission_view_documents: Dokumente ansehen
946 953 permission_view_files: Dateien ansehen
947 954 permission_view_gantt: Gantt-Diagramm ansehen
948 955 permission_view_issue_watchers: Liste der Beobachter ansehen
949 956 permission_view_issues: Tickets anzeigen
950 957 permission_view_messages: ForenbeitrΓ€ge ansehen
951 958 permission_view_private_notes: Private Kommentare sehen
952 959 permission_view_time_entries: Gebuchte AufwΓ€nde ansehen
953 960 permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen
954 961 permission_view_wiki_pages: Wiki ansehen
955 962
956 963 project_module_boards: Foren
957 964 project_module_calendar: Kalender
958 965 project_module_documents: Dokumente
959 966 project_module_files: Dateien
960 967 project_module_gantt: Gantt
961 968 project_module_issue_tracking: Ticket-Verfolgung
962 969 project_module_news: News
963 970 project_module_repository: Projektarchiv
964 971 project_module_time_tracking: Zeiterfassung
965 972 project_module_wiki: Wiki
966 973 project_status_active: aktiv
967 974 project_status_archived: archiviert
968 975 project_status_closed: geschlossen
969 976
970 977 setting_activity_days_default: Anzahl Tage pro Seite der Projekt-AktivitΓ€t
971 978 setting_app_subtitle: Applikations-Untertitel
972 979 setting_app_title: Applikations-Titel
973 980 setting_attachment_max_size: Max. Dateigrâße
974 981 setting_autofetch_changesets: Changesets automatisch abrufen
975 982 setting_autologin: Automatische Anmeldung lΓ€uft ab nach
976 983 setting_bcc_recipients: E-Mails als Blindkopie (BCC) senden
977 984 setting_cache_formatted_text: Formatierten Text im Cache speichern
978 985 setting_commit_cross_project_ref: Erlauben auf Tickets aller anderen Projekte zu referenzieren
979 986 setting_commit_fix_keywords: SchlΓΌsselwΓΆrter (Status)
980 987 setting_commit_logtime_activity_id: AktivitΓ€t fΓΌr die Zeiterfassung
981 988 setting_commit_logtime_enabled: Aktiviere Zeitlogging
982 989 setting_commit_ref_keywords: SchlΓΌsselwΓΆrter (Beziehungen)
983 990 setting_cross_project_issue_relations: Ticket-Beziehungen zwischen Projekten erlauben
984 991 setting_cross_project_subtasks: ProjektΓΌbergreifende Unteraufgaben erlauben
985 992 setting_date_format: Datumsformat
986 993 setting_default_issue_start_date_to_creation_date: Aktuelles Datum als Beginn fΓΌr neue Tickets verwenden
987 994 setting_default_language: Standardsprache
988 995 setting_default_notification_option: Standard Benachrichtigungsoptionen
989 996 setting_default_projects_modules: StandardmÀßig aktivierte Module für neue Projekte
990 997 setting_default_projects_public: Neue Projekte sind standardmÀßig âffentlich
991 998 setting_default_projects_tracker_ids: StandardmÀßig aktivierte Tracker für neue Projekte
992 999 setting_diff_max_lines_displayed: Maximale Anzahl anzuzeigender Diff-Zeilen
993 1000 setting_display_subprojects_issues: Tickets von Unterprojekten im Hauptprojekt anzeigen
994 1001 setting_emails_footer: E-Mail-Fußzeile
995 1002 setting_emails_header: E-Mail-Kopfzeile
996 1003 setting_enabled_scm: Aktivierte Versionskontrollsysteme
997 1004 setting_feeds_limit: Max. Anzahl EintrΓ€ge pro Atom-Feed
998 1005 setting_file_max_size_displayed: Maximale Grâße inline angezeigter Textdateien
999 1006 setting_force_default_language_for_anonymous: Standardsprache fΓΌr anonyme Benutzer erzwingen
1000 1007 setting_force_default_language_for_loggedin: Standardsprache fΓΌr angemeldete Benutzer erzwingen
1001 1008 setting_gantt_items_limit: Maximale Anzahl von Aufgaben die im Gantt-Chart angezeigt werden
1002 1009 setting_gravatar_default: Standard-Gravatar-Bild
1003 1010 setting_gravatar_enabled: Gravatar-Benutzerbilder benutzen
1004 1011 setting_host_name: Hostname
1005 1012 setting_issue_done_ratio: Berechne den Ticket-Fortschritt mittels
1006 1013 setting_issue_done_ratio_issue_field: Ticket-Feld %-erledigt
1007 1014 setting_issue_done_ratio_issue_status: Ticket-Status
1008 1015 setting_issue_group_assignment: Ticketzuweisung an Gruppen erlauben
1009 1016 setting_issue_list_default_columns: Standard-Spalten in der Ticket-Auflistung
1010 1017 setting_issues_export_limit: Max. Anzahl Tickets bei CSV/PDF-Export
1011 1018 setting_jsonp_enabled: JSONP UnterstΓΌtzung aktivieren
1012 1019 setting_link_copied_issue: Tickets beim kopieren verlinken
1013 1020 setting_login_required: Authentifizierung erforderlich
1014 1021 setting_mail_from: E-Mail-Absender
1015 1022 setting_mail_handler_api_enabled: Abruf eingehender E-Mails aktivieren
1016 1023 setting_mail_handler_api_key: API-SchlΓΌssel
1017 1024 setting_mail_handler_body_delimiters: "Schneide E-Mails nach einer dieser Zeilen ab"
1018 1025 setting_mail_handler_excluded_filenames: AnhÀnge nach Namen ausschließen
1019 1026 setting_new_project_user_role_id: Rolle, die einem Nicht-Administrator zugeordnet wird, der ein Projekt erstellt
1020 1027 setting_non_working_week_days: Arbeitsfreie Tage
1021 1028 setting_openid: Erlaube OpenID-Anmeldung und -Registrierung
1022 1029 setting_password_min_length: MindestlΓ€nge des Kennworts
1023 1030 setting_password_max_age: Erzwinge Passwortwechsel nach
1024 1031 setting_per_page_options: Objekte pro Seite
1025 1032 setting_plain_text_mail: Nur reinen Text (kein HTML) senden
1026 1033 setting_protocol: Protokoll
1027 1034 setting_repositories_encodings: Enkodierung von AnhΓ€ngen und Repositories
1028 1035 setting_repository_log_display_limit: Maximale Anzahl anzuzeigender Revisionen in der Historie einer Datei
1029 1036 setting_rest_api_enabled: REST-Schnittstelle aktivieren
1030 1037 setting_self_registration: Registrierung ermΓΆglichen
1031 1038 setting_sequential_project_identifiers: Fortlaufende Projektkennungen generieren
1032 1039 setting_session_lifetime: LΓ€ngste Dauer einer Sitzung
1033 1040 setting_session_timeout: ZeitΓΌberschreitung bei InaktivitΓ€t
1034 1041 setting_start_of_week: Wochenanfang
1035 1042 setting_sys_api_enabled: Webservice zur Verwaltung der Projektarchive benutzen
1036 1043 setting_text_formatting: Textformatierung
1037 1044 setting_thumbnails_enabled: Vorschaubilder von DateianhΓ€ngen anzeigen
1038 1045 setting_thumbnails_size: Grâße der Vorschaubilder (in Pixel)
1039 1046 setting_time_format: Zeitformat
1040 1047 setting_unsubscribe: Erlaubt Benutzern das eigene Benutzerkonto zu lΓΆschen
1041 1048 setting_user_format: Benutzer-Anzeigeformat
1042 1049 setting_welcome_text: Willkommenstext
1043 1050 setting_wiki_compression: Wiki-Historie komprimieren
1044 1051
1045 1052 status_active: aktiv
1046 1053 status_locked: gesperrt
1047 1054 status_registered: nicht aktivierte
1048 1055
1049 1056 text_account_destroy_confirmation: "MΓΆchten Sie wirklich fortfahren?\nIhr Benutzerkonto wird fΓΌr immer gelΓΆscht und kann nicht wiederhergestellt werden."
1050 1057 text_are_you_sure: Sind Sie sicher?
1051 1058 text_assign_time_entries_to_project: Gebuchte AufwΓ€nde dem Projekt zuweisen
1052 1059 text_caracters_maximum: "Max. %{count} Zeichen."
1053 1060 text_caracters_minimum: "Muss mindestens %{count} Zeichen lang sein."
1054 1061 text_comma_separated: Mehrere Werte erlaubt (durch Komma getrennt).
1055 1062 text_convert_available: ImageMagick Konvertierung verfΓΌgbar (optional)
1056 1063 text_custom_field_possible_values_info: 'Eine Zeile pro Wert'
1057 1064 text_default_administrator_account_changed: Administrator-Kennwort geΓ€ndert
1058 1065 text_destroy_time_entries: Gebuchte AufwΓ€nde lΓΆschen
1059 1066 text_destroy_time_entries_question: Es wurden bereits %{hours} Stunden auf dieses Ticket gebucht. Was soll mit den AufwΓ€nden geschehen?
1060 1067 text_diff_truncated: '... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen ΓΌberschreitet.'
1061 1068 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."
1062 1069 text_enumeration_category_reassign_to: 'Die Objekte stattdessen diesem Wert zuordnen:'
1063 1070 text_enumeration_destroy_question: "%{count} Objekt(e) sind diesem Wert zugeordnet."
1064 1071 text_file_repository_writable: Verzeichnis fΓΌr Dateien beschreibbar
1065 1072 text_git_repository_note: Repository steht fΓΌr sich alleine (bare) und liegt lokal (z.B. /gitrepo, c:\gitrepo)
1066 1073 text_issue_added: "Ticket %{id} wurde erstellt von %{author}."
1067 1074 text_issue_category_destroy_assignments: Kategorie-Zuordnung entfernen
1068 1075 text_issue_category_destroy_question: "Einige Tickets (%{count}) sind dieser Kategorie zugeodnet. Was mΓΆchten Sie tun?"
1069 1076 text_issue_category_reassign_to: Tickets dieser Kategorie zuordnen
1070 1077 text_issue_conflict_resolution_add_notes: Meine Γ„nderungen ΓΌbernehmen und alle anderen Γ„nderungen verwerfen
1071 1078 text_issue_conflict_resolution_cancel: Meine Γ„nderungen verwerfen und %{link} neu anzeigen
1072 1079 text_issue_conflict_resolution_overwrite: Meine Γ„nderungen trotzdem ΓΌbernehmen (vorherige Notizen bleiben erhalten aber manche kΓΆnnen ΓΌberschrieben werden)
1073 1080 text_issue_updated: "Ticket %{id} wurde aktualisiert von %{author}."
1074 1081 text_issues_destroy_confirmation: 'Sind Sie sicher, dass Sie die ausgewΓ€hlten Tickets lΓΆschen mΓΆchten?'
1075 1082 text_issues_destroy_descendants_confirmation: Dies wird auch %{count} Unteraufgabe/n lΓΆschen.
1076 1083 text_issues_ref_in_commit_messages: Ticket-Beziehungen und -Status in Commit-Log-Meldungen
1077 1084 text_journal_added: "%{label} %{value} wurde hinzugefΓΌgt"
1078 1085 text_journal_changed: "%{label} wurde von %{old} zu %{new} geΓ€ndert"
1079 1086 text_journal_changed_no_detail: "%{label} aktualisiert"
1080 1087 text_journal_deleted: "%{label} %{old} wurde gelΓΆscht"
1081 1088 text_journal_set_to: "%{label} wurde auf %{value} gesetzt"
1082 1089 text_length_between: "LΓ€nge zwischen %{min} und %{max} Zeichen."
1083 1090 text_line_separated: Mehrere Werte sind erlaubt (eine Zeile pro Wert).
1084 1091 text_load_default_configuration: Standard-Konfiguration laden
1085 1092 text_mercurial_repository_note: Lokales repository (e.g. /hgrepo, c:\hgrepo)
1086 1093 text_min_max_length_info: 0 heißt keine BeschrÀnkung
1087 1094 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."
1088 1095 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?"
1089 1096 text_plugin_assets_writable: Verzeichnis fΓΌr Plugin-Assets beschreibbar
1090 1097 text_project_closed: Dieses Projekt ist geschlossen und kann nicht bearbeitet werden.
1091 1098 text_project_destroy_confirmation: Sind Sie sicher, dass Sie das Projekt lΓΆschen wollen?
1092 1099 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.'
1093 1100 text_reassign_time_entries: 'Gebuchte AufwΓ€nde diesem Ticket zuweisen:'
1094 1101 text_regexp_info: z. B. ^[A-Z0-9]+$
1095 1102 text_repository_identifier_info: 'Kleinbuchstaben (a-z), Ziffern, Binde- und Unterstriche erlaubt.<br />Einmal gespeichert, kann die Kennung nicht mehr geΓ€ndert werden.'
1096 1103 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."
1097 1104 text_rmagick_available: RMagick verfΓΌgbar (optional)
1098 1105 text_scm_command: Kommando
1099 1106 text_scm_command_not_available: SCM-Kommando ist nicht verfΓΌgbar. Bitte prΓΌfen Sie die Einstellungen im Administrationspanel.
1100 1107 text_scm_command_version: Version
1101 1108 text_scm_config: Die SCM-Kommandos kânnen in der in config/configuration.yml konfiguriert werden. Redmine muss anschließend neu gestartet werden.
1102 1109 text_scm_path_encoding_note: "Standard: UTF-8"
1103 1110 text_select_mail_notifications: Bitte wΓ€hlen Sie die Aktionen aus, fΓΌr die eine Mailbenachrichtigung gesendet werden soll.
1104 1111 text_select_project_modules: 'Bitte wΓ€hlen Sie die Module aus, die in diesem Projekt aktiviert sein sollen:'
1105 1112 text_session_expiration_settings: "Achtung: Γ„nderungen kΓΆnnen aktuelle Sitzungen beenden, Ihre eingeschlossen!"
1106 1113 text_status_changed_by_changeset: "Status geΓ€ndert durch Changeset %{value}."
1107 1114 text_subprojects_destroy_warning: "Dessen Unterprojekte (%{value}) werden ebenfalls gelΓΆscht."
1108 1115 text_subversion_repository_note: 'Beispiele: file:///, http://, https://, svn://, svn+[tunnelscheme]://'
1109 1116 text_time_entries_destroy_confirmation: Sind Sie sicher, dass Sie die ausgewΓ€hlten ZeitaufwΓ€nde lΓΆschen mΓΆchten?
1110 1117 text_time_logged_by_changeset: Angewendet in Changeset %{value}.
1111 1118 text_tip_issue_begin_day: Aufgabe, die an diesem Tag beginnt
1112 1119 text_tip_issue_begin_end_day: Aufgabe, die an diesem Tag beginnt und endet
1113 1120 text_tip_issue_end_day: Aufgabe, die an diesem Tag endet
1114 1121 text_tracker_no_workflow: Kein Workflow fΓΌr diesen Tracker definiert.
1115 1122 text_turning_multiple_off: Wenn Sie die Mehrfachauswahl deaktivieren, werden Felder mit Mehrfachauswahl bereinigt.
1116 1123 Dadurch wird sichergestellt, dass lediglich ein Wert pro Feld ausgewΓ€hlt ist.
1117 1124 text_unallowed_characters: Nicht erlaubte Zeichen
1118 1125 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)."
1119 1126 text_user_wrote: "%{value} schrieb:"
1120 1127 text_warn_on_leaving_unsaved: Die aktuellen Γ„nderungen gehen verloren, wenn Sie diese Seite verlassen.
1121 1128 text_wiki_destroy_confirmation: Sind Sie sicher, dass Sie dieses Wiki mit sΓ€mtlichem Inhalt lΓΆschen mΓΆchten?
1122 1129 text_wiki_page_destroy_children: LΓΆsche alle Unterseiten
1123 1130 text_wiki_page_destroy_question: "Diese Seite hat %{descendants} Unterseite(n). Was mΓΆchten Sie tun?"
1124 1131 text_wiki_page_nullify_children: Verschiebe die Unterseiten auf die oberste Ebene
1125 1132 text_wiki_page_reassign_children: Ordne die Unterseiten dieser Seite zu
1126 1133 text_workflow_edit: Workflow zum Bearbeiten auswΓ€hlen
1127 1134 text_zoom_in: Ansicht vergrâßern
1128 1135 text_zoom_out: Ansicht verkleinern
1129 1136
1130 1137 version_status_closed: abgeschlossen
1131 1138 version_status_locked: gesperrt
1132 1139 version_status_open: offen
1133 1140
1134 1141 warning_attachments_not_saved: "%{count} Datei(en) konnten nicht gespeichert werden."
1135 1142 label_search_attachments_yes: Namen und Beschreibungen von AnhΓ€ngen durchsuchen
1136 1143 label_search_attachments_no: Keine AnhΓ€nge suchen
1137 1144 label_search_attachments_only: Nur AnhΓ€nge suchen
1138 1145 label_search_open_issues_only: Nur offene Tickets
1139 1146 field_address: E-Mail
1140 1147 setting_max_additional_emails: Maximale Anzahl zusΓ€tzlicher E-Mailadressen
1141 1148 label_email_address_plural: E-Mails
1142 1149 label_email_address_add: E-Mailadresse hinzufΓΌgen
1143 1150 label_enable_notifications: Benachrichtigungen aktivieren
1144 1151 label_disable_notifications: Benachrichtigungen deaktivieren
1145 1152 setting_search_results_per_page: Suchergebnisse pro Seite
1146 1153 label_blank_value: leer
1147 1154 permission_copy_issues: Tickets kopieren
1148 1155 error_password_expired: Your password has expired or the administrator requires you
1149 1156 to change it.
1150 1157 field_time_entries_visibility: Time logs visibility
1158 field_remote_ip: IP-Adresse
1151 1159 label_parent_task_attributes: Parent tasks attributes
1152 1160 label_parent_task_attributes_derived: Calculated from subtasks
1153 1161 label_parent_task_attributes_independent: Independent of subtasks
1154 1162 label_time_entries_visibility_all: All time entries
1155 1163 label_time_entries_visibility_own: Time entries created by the user
1156 1164 label_member_management: Member management
1157 1165 label_member_management_all_roles: Alle Rollen
1158 1166 label_member_management_selected_roles_only: Nur diese Rollen
1159 1167 label_total_spent_time: Aufgewendete Zeit aller Projekte anzeigen
1160 1168 notice_import_finished: Alle %{count} EintrΓ€ge wurden importiert.
1161 1169 notice_import_finished_with_errors: ! '%{count} von %{total} EintrΓ€gen konnten nicht
1162 1170 importiert werden.'
1163 1171 error_invalid_file_encoding: The file is not a valid %{encoding} encoded file
1164 1172 error_invalid_csv_file_or_settings: The file is not a CSV file or does not match the
1165 1173 settings below
1166 1174 error_can_not_read_import_file: Beim Einlesen der Datei ist ein Fehler aufgetreten
1167 1175 permission_import_issues: Tickets importieren
1168 1176 label_import_issues: Tickets importieren
1169 1177 label_select_file_to_import: Bitte wΓ€hlen Sie eine Datei fΓΌr den Import aus
1170 1178 label_fields_separator: Trennzeichen
1171 1179 label_fields_wrapper: Field wrapper
1172 1180 label_encoding: Encoding
1173 1181 label_comma_char: Komma
1174 1182 label_semi_colon_char: Semikolon
1175 1183 label_quote_char: AnfΓΌhrungszeichen
1176 1184 label_double_quote_char: Doppelte AnfΓΌhrungszeichen
1177 1185 label_fields_mapping: Fields mapping
1178 1186 label_file_content_preview: File content preview
1179 1187 label_create_missing_values: Create missing values
1180 1188 button_import: Importieren
1181 1189 field_total_estimated_hours: Total estimated time
1182 1190 label_api: API
1183 1191 label_total_plural: Totals
1184 1192 label_assigned_issues: Assigned issues
1185 1193 label_field_format_enumeration: Key/value list
1186 1194 label_f_hour_short: '%{value} h'
1187 1195 field_default_version: Standard-Version
1188 1196 error_attachment_extension_not_allowed: Attachment Erweiterung %{extension} ist nicht zugelassen
1189 1197 setting_attachment_extensions_allowed: Zugelassene Erweiterungen
1190 1198 setting_attachment_extensions_denied: Nicht zugelassene Erweiterungen
1191 1199 label_any_open_issues: any open issues
1192 1200 label_no_open_issues: no open issues
1193 1201 label_default_values_for_new_users: Standardwerte fΓΌr neue Benutzer
1194 1202 error_ldap_bind_credentials: Invalid LDAP Account/Password
@@ -1,1172 +1,1180
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_pdf_monospaced_fontname: freemono
146 146 general_first_day_of_week: '7'
147 147
148 148 notice_account_updated: Account was successfully updated.
149 149 notice_account_invalid_credentials: Invalid user or password
150 150 notice_account_password_updated: Password was successfully updated.
151 151 notice_account_wrong_password: Wrong password
152 152 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
153 153 notice_account_unknown_email: Unknown user.
154 154 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>.
155 155 notice_account_locked: Your account is locked.
156 156 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
157 157 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
158 158 notice_account_activated: Your account has been activated. You can now log in.
159 159 notice_successful_create: Successful creation.
160 160 notice_successful_update: Successful update.
161 161 notice_successful_delete: Successful deletion.
162 162 notice_successful_connection: Successful connection.
163 163 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
164 164 notice_locking_conflict: Data has been updated by another user.
165 165 notice_not_authorized: You are not authorized to access this page.
166 166 notice_not_authorized_archived_project: The project you're trying to access has been archived.
167 167 notice_email_sent: "An email was sent to %{value}"
168 168 notice_email_error: "An error occurred while sending mail (%{value})"
169 169 notice_feeds_access_key_reseted: Your Atom access key was reset.
170 170 notice_api_access_key_reseted: Your API access key was reset.
171 171 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
172 172 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
173 173 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
174 174 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
175 175 notice_account_pending: "Your account was created and is now pending administrator approval."
176 176 notice_default_data_loaded: Default configuration successfully loaded.
177 177 notice_unable_delete_version: Unable to delete version.
178 178 notice_unable_delete_time_entry: Unable to delete time log entry.
179 179 notice_issue_done_ratios_updated: Issue done ratios updated.
180 180 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
181 181 notice_issue_successful_create: "Issue %{id} created."
182 182 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
183 183 notice_account_deleted: "Your account has been permanently deleted."
184 184 notice_user_successful_create: "User %{id} created."
185 185 notice_new_password_must_be_different: The new password must be different from the current password
186 186 notice_import_finished: "All %{count} items have been imported."
187 187 notice_import_finished_with_errors: "%{count} out of %{total} items could not be imported."
188 188
189 189 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
190 190 error_scm_not_found: "The entry or revision was not found in the repository."
191 191 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
192 192 error_scm_annotate: "The entry does not exist or cannot be annotated."
193 193 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
194 194 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
195 195 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
196 196 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
197 197 error_can_not_delete_custom_field: Unable to delete custom field
198 198 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
199 199 error_can_not_remove_role: "This role is in use and cannot be deleted."
200 200 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
201 201 error_can_not_archive_project: This project cannot be archived
202 202 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
203 203 error_workflow_copy_source: 'Please select a source tracker or role'
204 204 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
205 205 error_unable_delete_issue_status: 'Unable to delete issue status'
206 206 error_unable_to_connect: "Unable to connect (%{value})"
207 207 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
208 208 error_session_expired: "Your session has expired. Please login again."
209 209 warning_attachments_not_saved: "%{count} file(s) could not be saved."
210 210 error_password_expired: "Your password has expired or the administrator requires you to change it."
211 211 error_invalid_file_encoding: "The file is not a valid %{encoding} encoded file"
212 212 error_invalid_csv_file_or_settings: "The file is not a CSV file or does not match the settings below"
213 213 error_can_not_read_import_file: "An error occurred while reading the file to import"
214 214 error_attachment_extension_not_allowed: "Attachment extension %{extension} is not allowed"
215 215 error_ldap_bind_credentials: "Invalid LDAP Account/Password"
216 216
217 217 mail_subject_lost_password: "Your %{value} password"
218 218 mail_body_lost_password: 'To change your password, click on the following link:'
219 219 mail_subject_register: "Your %{value} account activation"
220 220 mail_body_register: 'To activate your account, click on the following link:'
221 221 mail_body_account_information_external: "You can use your %{value} account to log in."
222 222 mail_body_account_information: Your account information
223 223 mail_subject_account_activation_request: "%{value} account activation request"
224 224 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
225 225 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
226 226 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
227 227 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
228 228 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
229 229 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
230 230 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
231 mail_subject_security_notification: "Security notification"
232 mail_body_security_notification_change: "%{field} was changed."
233 mail_body_security_notification_change_to: "%{field} was changed to %{value}."
234 mail_body_security_notification_add: "%{field} %{value} was added."
235 mail_body_security_notification_remove: "%{field} %{value} was removed."
236 mail_body_security_notification_notify_enabled: "Email address %{value} now receives notifications."
237 mail_body_security_notification_notify_disabled: "Email address %{value} no longer receives notifications."
231 238
232 239 field_name: Name
233 240 field_description: Description
234 241 field_summary: Summary
235 242 field_is_required: Required
236 243 field_firstname: First name
237 244 field_lastname: Last name
238 245 field_mail: Email
239 246 field_address: Email
240 247 field_filename: File
241 248 field_filesize: Size
242 249 field_downloads: Downloads
243 250 field_author: Author
244 251 field_created_on: Created
245 252 field_updated_on: Updated
246 253 field_closed_on: Closed
247 254 field_field_format: Format
248 255 field_is_for_all: For all projects
249 256 field_possible_values: Possible values
250 257 field_regexp: Regular expression
251 258 field_min_length: Minimum length
252 259 field_max_length: Maximum length
253 260 field_value: Value
254 261 field_category: Category
255 262 field_title: Title
256 263 field_project: Project
257 264 field_issue: Issue
258 265 field_status: Status
259 266 field_notes: Notes
260 267 field_is_closed: Issue closed
261 268 field_is_default: Default value
262 269 field_tracker: Tracker
263 270 field_subject: Subject
264 271 field_due_date: Due date
265 272 field_assigned_to: Assignee
266 273 field_priority: Priority
267 274 field_fixed_version: Target version
268 275 field_user: User
269 276 field_principal: Principal
270 277 field_role: Role
271 278 field_homepage: Homepage
272 279 field_is_public: Public
273 280 field_parent: Subproject of
274 281 field_is_in_roadmap: Issues displayed in roadmap
275 282 field_login: Login
276 283 field_mail_notification: Email notifications
277 284 field_admin: Administrator
278 285 field_last_login_on: Last connection
279 286 field_language: Language
280 287 field_effective_date: Date
281 288 field_password: Password
282 289 field_new_password: New password
283 290 field_password_confirmation: Confirmation
284 291 field_version: Version
285 292 field_type: Type
286 293 field_host: Host
287 294 field_port: Port
288 295 field_account: Account
289 296 field_base_dn: Base DN
290 297 field_attr_login: Login attribute
291 298 field_attr_firstname: Firstname attribute
292 299 field_attr_lastname: Lastname attribute
293 300 field_attr_mail: Email attribute
294 301 field_onthefly: On-the-fly user creation
295 302 field_start_date: Start date
296 303 field_done_ratio: "% Done"
297 304 field_auth_source: Authentication mode
298 305 field_hide_mail: Hide my email address
299 306 field_comments: Comment
300 307 field_url: URL
301 308 field_start_page: Start page
302 309 field_subproject: Subproject
303 310 field_hours: Hours
304 311 field_activity: Activity
305 312 field_spent_on: Date
306 313 field_identifier: Identifier
307 314 field_is_filter: Used as a filter
308 315 field_issue_to: Related issue
309 316 field_delay: Delay
310 317 field_assignable: Issues can be assigned to this role
311 318 field_redirect_existing_links: Redirect existing links
312 319 field_estimated_hours: Estimated time
313 320 field_column_names: Columns
314 321 field_time_entries: Log time
315 322 field_time_zone: Time zone
316 323 field_searchable: Searchable
317 324 field_default_value: Default value
318 325 field_comments_sorting: Display comments
319 326 field_parent_title: Parent page
320 327 field_editable: Editable
321 328 field_watcher: Watcher
322 329 field_identity_url: OpenID URL
323 330 field_content: Content
324 331 field_group_by: Group results by
325 332 field_sharing: Sharing
326 333 field_parent_issue: Parent task
327 334 field_member_of_group: "Assignee's group"
328 335 field_assigned_to_role: "Assignee's role"
329 336 field_text: Text field
330 337 field_visible: Visible
331 338 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
332 339 field_issues_visibility: Issues visibility
333 340 field_is_private: Private
334 341 field_commit_logs_encoding: Commit messages encoding
335 342 field_scm_path_encoding: Path encoding
336 343 field_path_to_repository: Path to repository
337 344 field_root_directory: Root directory
338 345 field_cvsroot: CVSROOT
339 346 field_cvs_module: Module
340 347 field_repository_is_default: Main repository
341 348 field_multiple: Multiple values
342 349 field_auth_source_ldap_filter: LDAP filter
343 350 field_core_fields: Standard fields
344 351 field_timeout: "Timeout (in seconds)"
345 352 field_board_parent: Parent forum
346 353 field_private_notes: Private notes
347 354 field_inherit_members: Inherit members
348 355 field_generate_password: Generate password
349 356 field_must_change_passwd: Must change password at next logon
350 357 field_default_status: Default status
351 358 field_users_visibility: Users visibility
352 359 field_time_entries_visibility: Time logs visibility
353 360 field_total_estimated_hours: Total estimated time
354 361 field_default_version: Default version
362 field_remote_ip: IP address
355 363
356 364 setting_app_title: Application title
357 365 setting_app_subtitle: Application subtitle
358 366 setting_welcome_text: Welcome text
359 367 setting_default_language: Default language
360 368 setting_login_required: Authentication required
361 369 setting_self_registration: Self-registration
362 370 setting_attachment_max_size: Maximum attachment size
363 371 setting_issues_export_limit: Issues export limit
364 372 setting_mail_from: Emission email address
365 373 setting_bcc_recipients: Blind carbon copy recipients (bcc)
366 374 setting_plain_text_mail: Plain text mail (no HTML)
367 375 setting_host_name: Host name and path
368 376 setting_text_formatting: Text formatting
369 377 setting_wiki_compression: Wiki history compression
370 378 setting_feeds_limit: Maximum number of items in Atom feeds
371 379 setting_default_projects_public: New projects are public by default
372 380 setting_autofetch_changesets: Fetch commits automatically
373 381 setting_sys_api_enabled: Enable WS for repository management
374 382 setting_commit_ref_keywords: Referencing keywords
375 383 setting_commit_fix_keywords: Fixing keywords
376 384 setting_autologin: Autologin
377 385 setting_date_format: Date format
378 386 setting_time_format: Time format
379 387 setting_cross_project_issue_relations: Allow cross-project issue relations
380 388 setting_cross_project_subtasks: Allow cross-project subtasks
381 389 setting_issue_list_default_columns: Default columns displayed on the issue list
382 390 setting_repositories_encodings: Attachments and repositories encodings
383 391 setting_emails_header: Email header
384 392 setting_emails_footer: Email footer
385 393 setting_protocol: Protocol
386 394 setting_per_page_options: Objects per page options
387 395 setting_user_format: Users display format
388 396 setting_activity_days_default: Days displayed on project activity
389 397 setting_display_subprojects_issues: Display subprojects issues on main projects by default
390 398 setting_enabled_scm: Enabled SCM
391 399 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
392 400 setting_mail_handler_api_enabled: Enable WS for incoming emails
393 401 setting_mail_handler_api_key: API key
394 402 setting_sequential_project_identifiers: Generate sequential project identifiers
395 403 setting_gravatar_enabled: Use Gravatar user icons
396 404 setting_gravatar_default: Default Gravatar image
397 405 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
398 406 setting_file_max_size_displayed: Maximum size of text files displayed inline
399 407 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
400 408 setting_openid: Allow OpenID login and registration
401 409 setting_password_max_age: Require password change after
402 410 setting_password_min_length: Minimum password length
403 411 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
404 412 setting_default_projects_modules: Default enabled modules for new projects
405 413 setting_issue_done_ratio: Calculate the issue done ratio with
406 414 setting_issue_done_ratio_issue_field: Use the issue field
407 415 setting_issue_done_ratio_issue_status: Use the issue status
408 416 setting_start_of_week: Start calendars on
409 417 setting_rest_api_enabled: Enable REST web service
410 418 setting_cache_formatted_text: Cache formatted text
411 419 setting_default_notification_option: Default notification option
412 420 setting_commit_logtime_enabled: Enable time logging
413 421 setting_commit_logtime_activity_id: Activity for logged time
414 422 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
415 423 setting_issue_group_assignment: Allow issue assignment to groups
416 424 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
417 425 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
418 426 setting_unsubscribe: Allow users to delete their own account
419 427 setting_session_lifetime: Session maximum lifetime
420 428 setting_session_timeout: Session inactivity timeout
421 429 setting_thumbnails_enabled: Display attachment thumbnails
422 430 setting_thumbnails_size: Thumbnails size (in pixels)
423 431 setting_non_working_week_days: Non-working days
424 432 setting_jsonp_enabled: Enable JSONP support
425 433 setting_default_projects_tracker_ids: Default trackers for new projects
426 434 setting_mail_handler_excluded_filenames: Exclude attachments by name
427 435 setting_force_default_language_for_anonymous: Force default language for anonymous users
428 436 setting_force_default_language_for_loggedin: Force default language for logged-in users
429 437 setting_link_copied_issue: Link issues on copy
430 438 setting_max_additional_emails: Maximum number of additional email addresses
431 439 setting_search_results_per_page: Search results per page
432 440 setting_attachment_extensions_allowed: Allowed extensions
433 441 setting_attachment_extensions_denied: Disallowed extensions
434 442
435 443 permission_add_project: Create project
436 444 permission_add_subprojects: Create subprojects
437 445 permission_edit_project: Edit project
438 446 permission_close_project: Close / reopen the project
439 447 permission_select_project_modules: Select project modules
440 448 permission_manage_members: Manage members
441 449 permission_manage_project_activities: Manage project activities
442 450 permission_manage_versions: Manage versions
443 451 permission_manage_categories: Manage issue categories
444 452 permission_view_issues: View Issues
445 453 permission_add_issues: Add issues
446 454 permission_edit_issues: Edit issues
447 455 permission_copy_issues: Copy issues
448 456 permission_manage_issue_relations: Manage issue relations
449 457 permission_set_issues_private: Set issues public or private
450 458 permission_set_own_issues_private: Set own issues public or private
451 459 permission_add_issue_notes: Add notes
452 460 permission_edit_issue_notes: Edit notes
453 461 permission_edit_own_issue_notes: Edit own notes
454 462 permission_view_private_notes: View private notes
455 463 permission_set_notes_private: Set notes as private
456 464 permission_move_issues: Move issues
457 465 permission_delete_issues: Delete issues
458 466 permission_manage_public_queries: Manage public queries
459 467 permission_save_queries: Save queries
460 468 permission_view_gantt: View gantt chart
461 469 permission_view_calendar: View calendar
462 470 permission_view_issue_watchers: View watchers list
463 471 permission_add_issue_watchers: Add watchers
464 472 permission_delete_issue_watchers: Delete watchers
465 473 permission_log_time: Log spent time
466 474 permission_view_time_entries: View spent time
467 475 permission_edit_time_entries: Edit time logs
468 476 permission_edit_own_time_entries: Edit own time logs
469 477 permission_manage_news: Manage news
470 478 permission_comment_news: Comment news
471 479 permission_view_documents: View documents
472 480 permission_add_documents: Add documents
473 481 permission_edit_documents: Edit documents
474 482 permission_delete_documents: Delete documents
475 483 permission_manage_files: Manage files
476 484 permission_view_files: View files
477 485 permission_manage_wiki: Manage wiki
478 486 permission_rename_wiki_pages: Rename wiki pages
479 487 permission_delete_wiki_pages: Delete wiki pages
480 488 permission_view_wiki_pages: View wiki
481 489 permission_view_wiki_edits: View wiki history
482 490 permission_edit_wiki_pages: Edit wiki pages
483 491 permission_delete_wiki_pages_attachments: Delete attachments
484 492 permission_protect_wiki_pages: Protect wiki pages
485 493 permission_manage_repository: Manage repository
486 494 permission_browse_repository: Browse repository
487 495 permission_view_changesets: View changesets
488 496 permission_commit_access: Commit access
489 497 permission_manage_boards: Manage forums
490 498 permission_view_messages: View messages
491 499 permission_add_messages: Post messages
492 500 permission_edit_messages: Edit messages
493 501 permission_edit_own_messages: Edit own messages
494 502 permission_delete_messages: Delete messages
495 503 permission_delete_own_messages: Delete own messages
496 504 permission_export_wiki_pages: Export wiki pages
497 505 permission_manage_subtasks: Manage subtasks
498 506 permission_manage_related_issues: Manage related issues
499 507 permission_import_issues: Import issues
500 508
501 509 project_module_issue_tracking: Issue tracking
502 510 project_module_time_tracking: Time tracking
503 511 project_module_news: News
504 512 project_module_documents: Documents
505 513 project_module_files: Files
506 514 project_module_wiki: Wiki
507 515 project_module_repository: Repository
508 516 project_module_boards: Forums
509 517 project_module_calendar: Calendar
510 518 project_module_gantt: Gantt
511 519
512 520 label_user: User
513 521 label_user_plural: Users
514 522 label_user_new: New user
515 523 label_user_anonymous: Anonymous
516 524 label_project: Project
517 525 label_project_new: New project
518 526 label_project_plural: Projects
519 527 label_x_projects:
520 528 zero: no projects
521 529 one: 1 project
522 530 other: "%{count} projects"
523 531 label_project_all: All Projects
524 532 label_project_latest: Latest projects
525 533 label_issue: Issue
526 534 label_issue_new: New issue
527 535 label_issue_plural: Issues
528 536 label_issue_view_all: View all issues
529 537 label_issues_by: "Issues by %{value}"
530 538 label_issue_added: Issue added
531 539 label_issue_updated: Issue updated
532 540 label_issue_note_added: Note added
533 541 label_issue_status_updated: Status updated
534 542 label_issue_assigned_to_updated: Assignee updated
535 543 label_issue_priority_updated: Priority updated
536 544 label_document: Document
537 545 label_document_new: New document
538 546 label_document_plural: Documents
539 547 label_document_added: Document added
540 548 label_role: Role
541 549 label_role_plural: Roles
542 550 label_role_new: New role
543 551 label_role_and_permissions: Roles and permissions
544 552 label_role_anonymous: Anonymous
545 553 label_role_non_member: Non member
546 554 label_member: Member
547 555 label_member_new: New member
548 556 label_member_plural: Members
549 557 label_tracker: Tracker
550 558 label_tracker_plural: Trackers
551 559 label_tracker_new: New tracker
552 560 label_workflow: Workflow
553 561 label_issue_status: Issue status
554 562 label_issue_status_plural: Issue statuses
555 563 label_issue_status_new: New status
556 564 label_issue_category: Issue category
557 565 label_issue_category_plural: Issue categories
558 566 label_issue_category_new: New category
559 567 label_custom_field: Custom field
560 568 label_custom_field_plural: Custom fields
561 569 label_custom_field_new: New custom field
562 570 label_enumerations: Enumerations
563 571 label_enumeration_new: New value
564 572 label_information: Information
565 573 label_information_plural: Information
566 574 label_please_login: Please log in
567 575 label_register: Register
568 576 label_login_with_open_id_option: or login with OpenID
569 577 label_password_lost: Lost password
570 578 label_password_required: Confirm your password to continue
571 579 label_home: Home
572 580 label_my_page: My page
573 581 label_my_account: My account
574 582 label_my_projects: My projects
575 583 label_my_page_block: My page block
576 584 label_administration: Administration
577 585 label_login: Sign in
578 586 label_logout: Sign out
579 587 label_help: Help
580 588 label_reported_issues: Reported issues
581 589 label_assigned_issues: Assigned issues
582 590 label_assigned_to_me_issues: Issues assigned to me
583 591 label_last_login: Last connection
584 592 label_registered_on: Registered on
585 593 label_activity: Activity
586 594 label_overall_activity: Overall activity
587 595 label_user_activity: "%{value}'s activity"
588 596 label_new: New
589 597 label_logged_as: Logged in as
590 598 label_environment: Environment
591 599 label_authentication: Authentication
592 600 label_auth_source: Authentication mode
593 601 label_auth_source_new: New authentication mode
594 602 label_auth_source_plural: Authentication modes
595 603 label_subproject_plural: Subprojects
596 604 label_subproject_new: New subproject
597 605 label_and_its_subprojects: "%{value} and its subprojects"
598 606 label_min_max_length: Min - Max length
599 607 label_list: List
600 608 label_date: Date
601 609 label_integer: Integer
602 610 label_float: Float
603 611 label_boolean: Boolean
604 612 label_string: Text
605 613 label_text: Long text
606 614 label_attribute: Attribute
607 615 label_attribute_plural: Attributes
608 616 label_no_data: No data to display
609 617 label_change_status: Change status
610 618 label_history: History
611 619 label_attachment: File
612 620 label_attachment_new: New file
613 621 label_attachment_delete: Delete file
614 622 label_attachment_plural: Files
615 623 label_file_added: File added
616 624 label_report: Report
617 625 label_report_plural: Reports
618 626 label_news: News
619 627 label_news_new: Add news
620 628 label_news_plural: News
621 629 label_news_latest: Latest news
622 630 label_news_view_all: View all news
623 631 label_news_added: News added
624 632 label_news_comment_added: Comment added to a news
625 633 label_settings: Settings
626 634 label_overview: Overview
627 635 label_version: Version
628 636 label_version_new: New version
629 637 label_version_plural: Versions
630 638 label_close_versions: Close completed versions
631 639 label_confirmation: Confirmation
632 640 label_export_to: 'Also available in:'
633 641 label_read: Read...
634 642 label_public_projects: Public projects
635 643 label_open_issues: open
636 644 label_open_issues_plural: open
637 645 label_closed_issues: closed
638 646 label_closed_issues_plural: closed
639 647 label_x_open_issues_abbr:
640 648 zero: 0 open
641 649 one: 1 open
642 650 other: "%{count} open"
643 651 label_x_closed_issues_abbr:
644 652 zero: 0 closed
645 653 one: 1 closed
646 654 other: "%{count} closed"
647 655 label_x_issues:
648 656 zero: 0 issues
649 657 one: 1 issue
650 658 other: "%{count} issues"
651 659 label_total: Total
652 660 label_total_plural: Totals
653 661 label_total_time: Total time
654 662 label_permissions: Permissions
655 663 label_current_status: Current status
656 664 label_new_statuses_allowed: New statuses allowed
657 665 label_all: all
658 666 label_any: any
659 667 label_none: none
660 668 label_nobody: nobody
661 669 label_next: Next
662 670 label_previous: Previous
663 671 label_used_by: Used by
664 672 label_details: Details
665 673 label_add_note: Add a note
666 674 label_calendar: Calendar
667 675 label_months_from: months from
668 676 label_gantt: Gantt
669 677 label_internal: Internal
670 678 label_last_changes: "last %{count} changes"
671 679 label_change_view_all: View all changes
672 680 label_personalize_page: Personalize this page
673 681 label_comment: Comment
674 682 label_comment_plural: Comments
675 683 label_x_comments:
676 684 zero: no comments
677 685 one: 1 comment
678 686 other: "%{count} comments"
679 687 label_comment_add: Add a comment
680 688 label_comment_added: Comment added
681 689 label_comment_delete: Delete comments
682 690 label_query: Custom query
683 691 label_query_plural: Custom queries
684 692 label_query_new: New query
685 693 label_my_queries: My custom queries
686 694 label_filter_add: Add filter
687 695 label_filter_plural: Filters
688 696 label_equals: is
689 697 label_not_equals: is not
690 698 label_in_less_than: in less than
691 699 label_in_more_than: in more than
692 700 label_in_the_next_days: in the next
693 701 label_in_the_past_days: in the past
694 702 label_greater_or_equal: '>='
695 703 label_less_or_equal: '<='
696 704 label_between: between
697 705 label_in: in
698 706 label_today: today
699 707 label_all_time: all time
700 708 label_yesterday: yesterday
701 709 label_this_week: this week
702 710 label_last_week: last week
703 711 label_last_n_weeks: "last %{count} weeks"
704 712 label_last_n_days: "last %{count} days"
705 713 label_this_month: this month
706 714 label_last_month: last month
707 715 label_this_year: this year
708 716 label_date_range: Date range
709 717 label_less_than_ago: less than days ago
710 718 label_more_than_ago: more than days ago
711 719 label_ago: days ago
712 720 label_contains: contains
713 721 label_not_contains: doesn't contain
714 722 label_any_issues_in_project: any issues in project
715 723 label_any_issues_not_in_project: any issues not in project
716 724 label_no_issues_in_project: no issues in project
717 725 label_any_open_issues: any open issues
718 726 label_no_open_issues: no open issues
719 727 label_day_plural: days
720 728 label_repository: Repository
721 729 label_repository_new: New repository
722 730 label_repository_plural: Repositories
723 731 label_browse: Browse
724 732 label_branch: Branch
725 733 label_tag: Tag
726 734 label_revision: Revision
727 735 label_revision_plural: Revisions
728 736 label_revision_id: "Revision %{value}"
729 737 label_associated_revisions: Associated revisions
730 738 label_added: added
731 739 label_modified: modified
732 740 label_copied: copied
733 741 label_renamed: renamed
734 742 label_deleted: deleted
735 743 label_latest_revision: Latest revision
736 744 label_latest_revision_plural: Latest revisions
737 745 label_view_revisions: View revisions
738 746 label_view_all_revisions: View all revisions
739 747 label_max_size: Maximum size
740 748 label_sort_highest: Move to top
741 749 label_sort_higher: Move up
742 750 label_sort_lower: Move down
743 751 label_sort_lowest: Move to bottom
744 752 label_roadmap: Roadmap
745 753 label_roadmap_due_in: "Due in %{value}"
746 754 label_roadmap_overdue: "%{value} late"
747 755 label_roadmap_no_issues: No issues for this version
748 756 label_search: Search
749 757 label_result_plural: Results
750 758 label_all_words: All words
751 759 label_wiki: Wiki
752 760 label_wiki_edit: Wiki edit
753 761 label_wiki_edit_plural: Wiki edits
754 762 label_wiki_page: Wiki page
755 763 label_wiki_page_plural: Wiki pages
756 764 label_index_by_title: Index by title
757 765 label_index_by_date: Index by date
758 766 label_current_version: Current version
759 767 label_preview: Preview
760 768 label_feed_plural: Feeds
761 769 label_changes_details: Details of all changes
762 770 label_issue_tracking: Issue tracking
763 771 label_spent_time: Spent time
764 772 label_total_spent_time: Total spent time
765 773 label_overall_spent_time: Overall spent time
766 774 label_f_hour: "%{value} hour"
767 775 label_f_hour_plural: "%{value} hours"
768 776 label_f_hour_short: "%{value} h"
769 777 label_time_tracking: Time tracking
770 778 label_change_plural: Changes
771 779 label_statistics: Statistics
772 780 label_commits_per_month: Commits per month
773 781 label_commits_per_author: Commits per author
774 782 label_diff: diff
775 783 label_view_diff: View differences
776 784 label_diff_inline: inline
777 785 label_diff_side_by_side: side by side
778 786 label_options: Options
779 787 label_copy_workflow_from: Copy workflow from
780 788 label_permissions_report: Permissions report
781 789 label_watched_issues: Watched issues
782 790 label_related_issues: Related issues
783 791 label_applied_status: Applied status
784 792 label_loading: Loading...
785 793 label_relation_new: New relation
786 794 label_relation_delete: Delete relation
787 795 label_relates_to: Related to
788 796 label_duplicates: Duplicates
789 797 label_duplicated_by: Duplicated by
790 798 label_blocks: Blocks
791 799 label_blocked_by: Blocked by
792 800 label_precedes: Precedes
793 801 label_follows: Follows
794 802 label_copied_to: Copied to
795 803 label_copied_from: Copied from
796 804 label_stay_logged_in: Stay logged in
797 805 label_disabled: disabled
798 806 label_show_completed_versions: Show completed versions
799 807 label_me: me
800 808 label_board: Forum
801 809 label_board_new: New forum
802 810 label_board_plural: Forums
803 811 label_board_locked: Locked
804 812 label_board_sticky: Sticky
805 813 label_topic_plural: Topics
806 814 label_message_plural: Messages
807 815 label_message_last: Last message
808 816 label_message_new: New message
809 817 label_message_posted: Message added
810 818 label_reply_plural: Replies
811 819 label_send_information: Send account information to the user
812 820 label_year: Year
813 821 label_month: Month
814 822 label_week: Week
815 823 label_date_from: From
816 824 label_date_to: To
817 825 label_language_based: Based on user's language
818 826 label_sort_by: "Sort by %{value}"
819 827 label_send_test_email: Send a test email
820 828 label_feeds_access_key: Atom access key
821 829 label_missing_feeds_access_key: Missing a Atom access key
822 830 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
823 831 label_module_plural: Modules
824 832 label_added_time_by: "Added by %{author} %{age} ago"
825 833 label_updated_time_by: "Updated by %{author} %{age} ago"
826 834 label_updated_time: "Updated %{value} ago"
827 835 label_jump_to_a_project: Jump to a project...
828 836 label_file_plural: Files
829 837 label_changeset_plural: Changesets
830 838 label_default_columns: Default columns
831 839 label_no_change_option: (No change)
832 840 label_bulk_edit_selected_issues: Bulk edit selected issues
833 841 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
834 842 label_theme: Theme
835 843 label_default: Default
836 844 label_search_titles_only: Search titles only
837 845 label_user_mail_option_all: "For any event on all my projects"
838 846 label_user_mail_option_selected: "For any event on the selected projects only..."
839 847 label_user_mail_option_none: "No events"
840 848 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
841 849 label_user_mail_option_only_assigned: "Only for things I am assigned to"
842 850 label_user_mail_option_only_owner: "Only for things I am the owner of"
843 851 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
844 852 label_registration_activation_by_email: account activation by email
845 853 label_registration_manual_activation: manual account activation
846 854 label_registration_automatic_activation: automatic account activation
847 855 label_display_per_page: "Per page: %{value}"
848 856 label_age: Age
849 857 label_change_properties: Change properties
850 858 label_general: General
851 859 label_more: More
852 860 label_scm: SCM
853 861 label_plugins: Plugins
854 862 label_ldap_authentication: LDAP authentication
855 863 label_downloads_abbr: D/L
856 864 label_optional_description: Optional description
857 865 label_add_another_file: Add another file
858 866 label_preferences: Preferences
859 867 label_chronological_order: In chronological order
860 868 label_reverse_chronological_order: In reverse chronological order
861 869 label_planning: Planning
862 870 label_incoming_emails: Incoming emails
863 871 label_generate_key: Generate a key
864 872 label_issue_watchers: Watchers
865 873 label_example: Example
866 874 label_display: Display
867 875 label_sort: Sort
868 876 label_ascending: Ascending
869 877 label_descending: Descending
870 878 label_date_from_to: From %{start} to %{end}
871 879 label_wiki_content_added: Wiki page added
872 880 label_wiki_content_updated: Wiki page updated
873 881 label_group: Group
874 882 label_group_plural: Groups
875 883 label_group_new: New group
876 884 label_group_anonymous: Anonymous users
877 885 label_group_non_member: Non member users
878 886 label_time_entry_plural: Spent time
879 887 label_version_sharing_none: Not shared
880 888 label_version_sharing_descendants: With subprojects
881 889 label_version_sharing_hierarchy: With project hierarchy
882 890 label_version_sharing_tree: With project tree
883 891 label_version_sharing_system: With all projects
884 892 label_update_issue_done_ratios: Update issue done ratios
885 893 label_copy_source: Source
886 894 label_copy_target: Target
887 895 label_copy_same_as_target: Same as target
888 896 label_display_used_statuses_only: Only display statuses that are used by this tracker
889 897 label_api_access_key: API access key
890 898 label_missing_api_access_key: Missing an API access key
891 899 label_api_access_key_created_on: "API access key created %{value} ago"
892 900 label_profile: Profile
893 901 label_subtask_plural: Subtasks
894 902 label_project_copy_notifications: Send email notifications during the project copy
895 903 label_principal_search: "Search for user or group:"
896 904 label_user_search: "Search for user:"
897 905 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
898 906 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
899 907 label_issues_visibility_all: All issues
900 908 label_issues_visibility_public: All non private issues
901 909 label_issues_visibility_own: Issues created by or assigned to the user
902 910 label_git_report_last_commit: Report last commit for files and directories
903 911 label_parent_revision: Parent
904 912 label_child_revision: Child
905 913 label_export_options: "%{export_format} export options"
906 914 label_copy_attachments: Copy attachments
907 915 label_copy_subtasks: Copy subtasks
908 916 label_item_position: "%{position} of %{count}"
909 917 label_completed_versions: Completed versions
910 918 label_search_for_watchers: Search for watchers to add
911 919 label_session_expiration: Session expiration
912 920 label_show_closed_projects: View closed projects
913 921 label_status_transitions: Status transitions
914 922 label_fields_permissions: Fields permissions
915 923 label_readonly: Read-only
916 924 label_required: Required
917 925 label_hidden: Hidden
918 926 label_attribute_of_project: "Project's %{name}"
919 927 label_attribute_of_issue: "Issue's %{name}"
920 928 label_attribute_of_author: "Author's %{name}"
921 929 label_attribute_of_assigned_to: "Assignee's %{name}"
922 930 label_attribute_of_user: "User's %{name}"
923 931 label_attribute_of_fixed_version: "Target version's %{name}"
924 932 label_cross_project_descendants: With subprojects
925 933 label_cross_project_tree: With project tree
926 934 label_cross_project_hierarchy: With project hierarchy
927 935 label_cross_project_system: With all projects
928 936 label_gantt_progress_line: Progress line
929 937 label_visibility_private: to me only
930 938 label_visibility_roles: to these roles only
931 939 label_visibility_public: to any users
932 940 label_link: Link
933 941 label_only: only
934 942 label_drop_down_list: drop-down list
935 943 label_checkboxes: checkboxes
936 944 label_radio_buttons: radio buttons
937 945 label_link_values_to: Link values to URL
938 946 label_custom_field_select_type: Select the type of object to which the custom field is to be attached
939 947 label_check_for_updates: Check for updates
940 948 label_latest_compatible_version: Latest compatible version
941 949 label_unknown_plugin: Unknown plugin
942 950 label_add_projects: Add projects
943 951 label_users_visibility_all: All active users
944 952 label_users_visibility_members_of_visible_projects: Members of visible projects
945 953 label_edit_attachments: Edit attached files
946 954 label_link_copied_issue: Link copied issue
947 955 label_ask: Ask
948 956 label_search_attachments_yes: Search attachment filenames and descriptions
949 957 label_search_attachments_no: Do not search attachments
950 958 label_search_attachments_only: Search attachments only
951 959 label_search_open_issues_only: Open issues only
952 960 label_email_address_plural: Emails
953 961 label_email_address_add: Add email address
954 962 label_enable_notifications: Enable notifications
955 963 label_disable_notifications: Disable notifications
956 964 label_blank_value: blank
957 965 label_parent_task_attributes: Parent tasks attributes
958 966 label_parent_task_attributes_derived: Calculated from subtasks
959 967 label_parent_task_attributes_independent: Independent of subtasks
960 968 label_time_entries_visibility_all: All time entries
961 969 label_time_entries_visibility_own: Time entries created by the user
962 970 label_member_management: Member management
963 971 label_member_management_all_roles: All roles
964 972 label_member_management_selected_roles_only: Only these roles
965 973 label_import_issues: Import issues
966 974 label_select_file_to_import: Select the file to import
967 975 label_fields_separator: Field separator
968 976 label_fields_wrapper: Field wrapper
969 977 label_encoding: Encoding
970 978 label_comma_char: Comma
971 979 label_semi_colon_char: Semi colon
972 980 label_quote_char: Quote
973 981 label_double_quote_char: Double quote
974 982 label_fields_mapping: Fields mapping
975 983 label_file_content_preview: File content preview
976 984 label_create_missing_values: Create missing values
977 985 label_api: API
978 986 label_field_format_enumeration: Key/value list
979 987 label_default_values_for_new_users: Default values for new users
980 988
981 989 button_login: Login
982 990 button_submit: Submit
983 991 button_save: Save
984 992 button_check_all: Check all
985 993 button_uncheck_all: Uncheck all
986 994 button_collapse_all: Collapse all
987 995 button_expand_all: Expand all
988 996 button_delete: Delete
989 997 button_create: Create
990 998 button_create_and_continue: Create and continue
991 999 button_test: Test
992 1000 button_edit: Edit
993 1001 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
994 1002 button_add: Add
995 1003 button_change: Change
996 1004 button_apply: Apply
997 1005 button_clear: Clear
998 1006 button_lock: Lock
999 1007 button_unlock: Unlock
1000 1008 button_download: Download
1001 1009 button_list: List
1002 1010 button_view: View
1003 1011 button_move: Move
1004 1012 button_move_and_follow: Move and follow
1005 1013 button_back: Back
1006 1014 button_cancel: Cancel
1007 1015 button_activate: Activate
1008 1016 button_sort: Sort
1009 1017 button_log_time: Log time
1010 1018 button_rollback: Rollback to this version
1011 1019 button_watch: Watch
1012 1020 button_unwatch: Unwatch
1013 1021 button_reply: Reply
1014 1022 button_archive: Archive
1015 1023 button_unarchive: Unarchive
1016 1024 button_reset: Reset
1017 1025 button_rename: Rename
1018 1026 button_change_password: Change password
1019 1027 button_copy: Copy
1020 1028 button_copy_and_follow: Copy and follow
1021 1029 button_annotate: Annotate
1022 1030 button_update: Update
1023 1031 button_configure: Configure
1024 1032 button_quote: Quote
1025 1033 button_duplicate: Duplicate
1026 1034 button_show: Show
1027 1035 button_hide: Hide
1028 1036 button_edit_section: Edit this section
1029 1037 button_export: Export
1030 1038 button_delete_my_account: Delete my account
1031 1039 button_close: Close
1032 1040 button_reopen: Reopen
1033 1041 button_import: Import
1034 1042
1035 1043 status_active: active
1036 1044 status_registered: registered
1037 1045 status_locked: locked
1038 1046
1039 1047 project_status_active: active
1040 1048 project_status_closed: closed
1041 1049 project_status_archived: archived
1042 1050
1043 1051 version_status_open: open
1044 1052 version_status_locked: locked
1045 1053 version_status_closed: closed
1046 1054
1047 1055 field_active: Active
1048 1056
1049 1057 text_select_mail_notifications: Select actions for which email notifications should be sent.
1050 1058 text_regexp_info: eg. ^[A-Z0-9]+$
1051 1059 text_min_max_length_info: 0 means no restriction
1052 1060 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
1053 1061 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
1054 1062 text_workflow_edit: Select a role and a tracker to edit the workflow
1055 1063 text_are_you_sure: Are you sure?
1056 1064 text_journal_changed: "%{label} changed from %{old} to %{new}"
1057 1065 text_journal_changed_no_detail: "%{label} updated"
1058 1066 text_journal_set_to: "%{label} set to %{value}"
1059 1067 text_journal_deleted: "%{label} deleted (%{old})"
1060 1068 text_journal_added: "%{label} %{value} added"
1061 1069 text_tip_issue_begin_day: issue beginning this day
1062 1070 text_tip_issue_end_day: issue ending this day
1063 1071 text_tip_issue_begin_end_day: issue beginning and ending this day
1064 1072 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.'
1065 1073 text_caracters_maximum: "%{count} characters maximum."
1066 1074 text_caracters_minimum: "Must be at least %{count} characters long."
1067 1075 text_length_between: "Length between %{min} and %{max} characters."
1068 1076 text_tracker_no_workflow: No workflow defined for this tracker
1069 1077 text_unallowed_characters: Unallowed characters
1070 1078 text_comma_separated: Multiple values allowed (comma separated).
1071 1079 text_line_separated: Multiple values allowed (one line for each value).
1072 1080 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
1073 1081 text_issue_added: "Issue %{id} has been reported by %{author}."
1074 1082 text_issue_updated: "Issue %{id} has been updated by %{author}."
1075 1083 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
1076 1084 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
1077 1085 text_issue_category_destroy_assignments: Remove category assignments
1078 1086 text_issue_category_reassign_to: Reassign issues to this category
1079 1087 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)."
1080 1088 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."
1081 1089 text_load_default_configuration: Load the default configuration
1082 1090 text_status_changed_by_changeset: "Applied in changeset %{value}."
1083 1091 text_time_logged_by_changeset: "Applied in changeset %{value}."
1084 1092 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1085 1093 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1086 1094 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1087 1095 text_select_project_modules: 'Select modules to enable for this project:'
1088 1096 text_default_administrator_account_changed: Default administrator account changed
1089 1097 text_file_repository_writable: Attachments directory writable
1090 1098 text_plugin_assets_writable: Plugin assets directory writable
1091 1099 text_rmagick_available: RMagick available (optional)
1092 1100 text_convert_available: ImageMagick convert available (optional)
1093 1101 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
1094 1102 text_destroy_time_entries: Delete reported hours
1095 1103 text_assign_time_entries_to_project: Assign reported hours to the project
1096 1104 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1097 1105 text_user_wrote: "%{value} wrote:"
1098 1106 text_enumeration_destroy_question: "%{count} objects are assigned to the value β€œ%{name}”."
1099 1107 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1100 1108 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."
1101 1109 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."
1102 1110 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1103 1111 text_custom_field_possible_values_info: 'One line for each value'
1104 1112 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1105 1113 text_wiki_page_nullify_children: "Keep child pages as root pages"
1106 1114 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1107 1115 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1108 1116 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?"
1109 1117 text_zoom_in: Zoom in
1110 1118 text_zoom_out: Zoom out
1111 1119 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1112 1120 text_scm_path_encoding_note: "Default: UTF-8"
1113 1121 text_subversion_repository_note: "Examples: file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1114 1122 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1115 1123 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1116 1124 text_scm_command: Command
1117 1125 text_scm_command_version: Version
1118 1126 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1119 1127 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1120 1128 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1121 1129 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1122 1130 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1123 1131 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1124 1132 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1125 1133 text_project_closed: This project is closed and read-only.
1126 1134 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
1127 1135
1128 1136 default_role_manager: Manager
1129 1137 default_role_developer: Developer
1130 1138 default_role_reporter: Reporter
1131 1139 default_tracker_bug: Bug
1132 1140 default_tracker_feature: Feature
1133 1141 default_tracker_support: Support
1134 1142 default_issue_status_new: New
1135 1143 default_issue_status_in_progress: In Progress
1136 1144 default_issue_status_resolved: Resolved
1137 1145 default_issue_status_feedback: Feedback
1138 1146 default_issue_status_closed: Closed
1139 1147 default_issue_status_rejected: Rejected
1140 1148 default_doc_category_user: User documentation
1141 1149 default_doc_category_tech: Technical documentation
1142 1150 default_priority_low: Low
1143 1151 default_priority_normal: Normal
1144 1152 default_priority_high: High
1145 1153 default_priority_urgent: Urgent
1146 1154 default_priority_immediate: Immediate
1147 1155 default_activity_design: Design
1148 1156 default_activity_development: Development
1149 1157
1150 1158 enumeration_issue_priorities: Issue priorities
1151 1159 enumeration_doc_categories: Document categories
1152 1160 enumeration_activities: Activities (time tracking)
1153 1161 enumeration_system_activity: System Activity
1154 1162 description_filter: Filter
1155 1163 description_search: Searchfield
1156 1164 description_choose_project: Projects
1157 1165 description_project_scope: Search scope
1158 1166 description_notes: Notes
1159 1167 description_message_content: Message content
1160 1168 description_query_sort_criteria_attribute: Sort attribute
1161 1169 description_query_sort_criteria_direction: Sort direction
1162 1170 description_user_mail_notification: Mail notification settings
1163 1171 description_available_columns: Available Columns
1164 1172 description_selected_columns: Selected Columns
1165 1173 description_all_columns: All Columns
1166 1174 description_issue_category_reassign: Choose issue category
1167 1175 description_wiki_subpages_reassign: Choose new parent page
1168 1176 description_date_range_list: Choose range from list
1169 1177 description_date_range_interval: Choose range by selecting start and end date
1170 1178 description_date_from: Enter start date
1171 1179 description_date_to: Enter end date
1172 1180 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,462 +1,467
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 AccountControllerTest < ActionController::TestCase
21 21 fixtures :users, :email_addresses, :roles
22 22
23 23 def setup
24 24 User.current = nil
25 25 end
26 26
27 27 def test_get_login
28 28 get :login
29 29 assert_response :success
30 30 assert_template 'login'
31 31
32 32 assert_select 'input[name=username]'
33 33 assert_select 'input[name=password]'
34 34 end
35 35
36 36 def test_get_login_while_logged_in_should_redirect_to_back_url_if_present
37 37 @request.session[:user_id] = 2
38 38 @request.env["HTTP_REFERER"] = 'http://test.host/issues/show/1'
39 39
40 40 get :login, :back_url => 'http://test.host/issues/show/1'
41 41 assert_redirected_to '/issues/show/1'
42 42 assert_equal 2, @request.session[:user_id]
43 43 end
44 44
45 45 def test_get_login_while_logged_in_should_redirect_to_referer_without_back_url
46 46 @request.session[:user_id] = 2
47 47 @request.env["HTTP_REFERER"] = 'http://test.host/issues/show/1'
48 48
49 49 get :login
50 50 assert_redirected_to '/issues/show/1'
51 51 assert_equal 2, @request.session[:user_id]
52 52 end
53 53
54 54 def test_get_login_while_logged_in_should_redirect_to_home_by_default
55 55 @request.session[:user_id] = 2
56 56
57 57 get :login
58 58 assert_redirected_to '/'
59 59 assert_equal 2, @request.session[:user_id]
60 60 end
61 61
62 62 def test_login_should_redirect_to_back_url_param
63 63 # request.uri is "test.host" in test environment
64 64 back_urls = [
65 65 'http://test.host/issues/show/1',
66 66 'http://test.host/',
67 67 '/'
68 68 ]
69 69 back_urls.each do |back_url|
70 70 post :login, :username => 'jsmith', :password => 'jsmith', :back_url => back_url
71 71 assert_redirected_to back_url
72 72 end
73 73 end
74 74
75 75 def test_login_with_suburi_should_redirect_to_back_url_param
76 76 @relative_url_root = Redmine::Utils.relative_url_root
77 77 Redmine::Utils.relative_url_root = '/redmine'
78 78
79 79 back_urls = [
80 80 'http://test.host/redmine/issues/show/1',
81 81 '/redmine'
82 82 ]
83 83 back_urls.each do |back_url|
84 84 post :login, :username => 'jsmith', :password => 'jsmith', :back_url => back_url
85 85 assert_redirected_to back_url
86 86 end
87 87 ensure
88 88 Redmine::Utils.relative_url_root = @relative_url_root
89 89 end
90 90
91 91 def test_login_should_not_redirect_to_another_host
92 92 back_urls = [
93 93 'http://test.foo/fake',
94 94 '//test.foo/fake'
95 95 ]
96 96 back_urls.each do |back_url|
97 97 post :login, :username => 'jsmith', :password => 'jsmith', :back_url => back_url
98 98 assert_redirected_to '/my/page'
99 99 end
100 100 end
101 101
102 102 def test_login_with_suburi_should_not_redirect_to_another_suburi
103 103 @relative_url_root = Redmine::Utils.relative_url_root
104 104 Redmine::Utils.relative_url_root = '/redmine'
105 105
106 106 back_urls = [
107 107 'http://test.host/',
108 108 'http://test.host/fake',
109 109 'http://test.host/fake/issues',
110 110 'http://test.host/redmine/../fake',
111 111 'http://test.host/redmine/../fake/issues',
112 112 'http://test.host/redmine/%2e%2e/fake',
113 113 '//test.foo/fake',
114 114 'http://test.host//fake',
115 115 'http://test.host/\n//fake',
116 116 '//bar@test.foo',
117 117 '//test.foo',
118 118 '////test.foo',
119 119 '@test.foo',
120 120 'fake@test.foo',
121 121 '.test.foo'
122 122 ]
123 123 back_urls.each do |back_url|
124 124 post :login, :username => 'jsmith', :password => 'jsmith', :back_url => back_url
125 125 assert_redirected_to '/my/page'
126 126 end
127 127 ensure
128 128 Redmine::Utils.relative_url_root = @relative_url_root
129 129 end
130 130
131 131 def test_login_with_wrong_password
132 132 post :login, :username => 'admin', :password => 'bad'
133 133 assert_response :success
134 134 assert_template 'login'
135 135
136 136 assert_select 'div.flash.error', :text => /Invalid user or password/
137 137 assert_select 'input[name=username][value=admin]'
138 138 assert_select 'input[name=password]'
139 139 assert_select 'input[name=password][value]', 0
140 140 end
141 141
142 142 def test_login_with_locked_account_should_fail
143 143 User.find(2).update_attribute :status, User::STATUS_LOCKED
144 144
145 145 post :login, :username => 'jsmith', :password => 'jsmith'
146 146 assert_redirected_to '/login'
147 147 assert_include 'locked', flash[:error]
148 148 assert_nil @request.session[:user_id]
149 149 end
150 150
151 151 def test_login_as_registered_user_with_manual_activation_should_inform_user
152 152 User.find(2).update_attribute :status, User::STATUS_REGISTERED
153 153
154 154 with_settings :self_registration => '2', :default_language => 'en' do
155 155 post :login, :username => 'jsmith', :password => 'jsmith'
156 156 assert_redirected_to '/login'
157 157 assert_include 'pending administrator approval', flash[:error]
158 158 end
159 159 end
160 160
161 161 def test_login_as_registered_user_with_email_activation_should_propose_new_activation_email
162 162 User.find(2).update_attribute :status, User::STATUS_REGISTERED
163 163
164 164 with_settings :self_registration => '1', :default_language => 'en' do
165 165 post :login, :username => 'jsmith', :password => 'jsmith'
166 166 assert_redirected_to '/login'
167 167 assert_equal 2, @request.session[:registered_user_id]
168 168 assert_include 'new activation email', flash[:error]
169 169 end
170 170 end
171 171
172 172 def test_login_should_rescue_auth_source_exception
173 173 source = AuthSource.create!(:name => 'Test')
174 174 User.find(2).update_attribute :auth_source_id, source.id
175 175 AuthSource.any_instance.stubs(:authenticate).raises(AuthSourceException.new("Something wrong"))
176 176
177 177 post :login, :username => 'jsmith', :password => 'jsmith'
178 178 assert_response 500
179 179 assert_select_error /Something wrong/
180 180 end
181 181
182 182 def test_login_should_reset_session
183 183 @controller.expects(:reset_session).once
184 184
185 185 post :login, :username => 'jsmith', :password => 'jsmith'
186 186 assert_response 302
187 187 end
188 188
189 189 def test_get_logout_should_not_logout
190 190 @request.session[:user_id] = 2
191 191 get :logout
192 192 assert_response :success
193 193 assert_template 'logout'
194 194
195 195 assert_equal 2, @request.session[:user_id]
196 196 end
197 197
198 198 def test_get_logout_with_anonymous_should_redirect
199 199 get :logout
200 200 assert_redirected_to '/'
201 201 end
202 202
203 203 def test_logout
204 204 @request.session[:user_id] = 2
205 205 post :logout
206 206 assert_redirected_to '/'
207 207 assert_nil @request.session[:user_id]
208 208 end
209 209
210 210 def test_logout_should_reset_session
211 211 @controller.expects(:reset_session).once
212 212
213 213 @request.session[:user_id] = 2
214 214 post :logout
215 215 assert_response 302
216 216 end
217 217
218 218 def test_get_register_with_registration_on
219 219 with_settings :self_registration => '3' do
220 220 get :register
221 221 assert_response :success
222 222 assert_template 'register'
223 223 assert_not_nil assigns(:user)
224 224
225 225 assert_select 'input[name=?]', 'user[password]'
226 226 assert_select 'input[name=?]', 'user[password_confirmation]'
227 227 end
228 228 end
229 229
230 230 def test_get_register_should_detect_user_language
231 231 with_settings :self_registration => '3' do
232 232 @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
233 233 get :register
234 234 assert_response :success
235 235 assert_not_nil assigns(:user)
236 236 assert_equal 'fr', assigns(:user).language
237 237 assert_select 'select[name=?]', 'user[language]' do
238 238 assert_select 'option[value=fr][selected=selected]'
239 239 end
240 240 end
241 241 end
242 242
243 243 def test_get_register_with_registration_off_should_redirect
244 244 with_settings :self_registration => '0' do
245 245 get :register
246 246 assert_redirected_to '/'
247 247 end
248 248 end
249 249
250 250 def test_get_register_should_show_hide_mail_preference
251 251 get :register
252 252 assert_select 'input[name=?][checked=checked]', 'pref[hide_mail]'
253 253 end
254 254
255 255 def test_get_register_should_show_hide_mail_preference_with_setting_turned_off
256 256 with_settings :default_users_hide_mail => '0' do
257 257 get :register
258 258 assert_select 'input[name=?]:not([checked=checked])', 'pref[hide_mail]'
259 259 end
260 260 end
261 261
262 262 # See integration/account_test.rb for the full test
263 263 def test_post_register_with_registration_on
264 264 with_settings :self_registration => '3' do
265 265 assert_difference 'User.count' do
266 266 post :register, :user => {
267 267 :login => 'register',
268 268 :password => 'secret123',
269 269 :password_confirmation => 'secret123',
270 270 :firstname => 'John',
271 271 :lastname => 'Doe',
272 272 :mail => 'register@example.com'
273 273 }
274 274 assert_redirected_to '/my/account'
275 275 end
276 276 user = User.order('id DESC').first
277 277 assert_equal 'register', user.login
278 278 assert_equal 'John', user.firstname
279 279 assert_equal 'Doe', user.lastname
280 280 assert_equal 'register@example.com', user.mail
281 281 assert user.check_password?('secret123')
282 282 assert user.active?
283 283 end
284 284 end
285 285
286 286 def test_post_register_with_registration_off_should_redirect
287 287 with_settings :self_registration => '0' do
288 288 assert_no_difference 'User.count' do
289 289 post :register, :user => {
290 290 :login => 'register',
291 291 :password => 'test',
292 292 :password_confirmation => 'test',
293 293 :firstname => 'John',
294 294 :lastname => 'Doe',
295 295 :mail => 'register@example.com'
296 296 }
297 297 assert_redirected_to '/'
298 298 end
299 299 end
300 300 end
301 301
302 302 def test_post_register_should_create_user_with_hide_mail_preference
303 303 with_settings :default_users_hide_mail => '0' do
304 304 user = new_record(User) do
305 305 post :register, :user => {
306 306 :login => 'register',
307 307 :password => 'secret123', :password_confirmation => 'secret123',
308 308 :firstname => 'John', :lastname => 'Doe',
309 309 :mail => 'register@example.com'
310 310 }, :pref => {
311 311 :hide_mail => '1'
312 312 }
313 313 end
314 314 assert_equal true, user.pref.hide_mail
315 315 end
316 316 end
317 317
318 318 def test_get_lost_password_should_display_lost_password_form
319 319 get :lost_password
320 320 assert_response :success
321 321 assert_select 'input[name=mail]'
322 322 end
323 323
324 324 def test_lost_password_for_active_user_should_create_a_token
325 325 Token.delete_all
326 326 ActionMailer::Base.deliveries.clear
327 327 assert_difference 'ActionMailer::Base.deliveries.size' do
328 328 assert_difference 'Token.count' do
329 329 with_settings :host_name => 'mydomain.foo', :protocol => 'http' do
330 330 post :lost_password, :mail => 'JSmith@somenet.foo'
331 331 assert_redirected_to '/login'
332 332 end
333 333 end
334 334 end
335 335
336 336 token = Token.order('id DESC').first
337 337 assert_equal User.find(2), token.user
338 338 assert_equal 'recovery', token.action
339 339
340 340 assert_select_email do
341 341 assert_select "a[href=?]", "http://mydomain.foo/account/lost_password?token=#{token.value}"
342 342 end
343 343 end
344 344
345 345 def test_lost_password_using_additional_email_address_should_send_email_to_the_address
346 346 EmailAddress.create!(:user_id => 2, :address => 'anotherAddress@foo.bar')
347 347 Token.delete_all
348 348
349 349 assert_difference 'ActionMailer::Base.deliveries.size' do
350 350 assert_difference 'Token.count' do
351 351 post :lost_password, :mail => 'ANOTHERaddress@foo.bar'
352 352 assert_redirected_to '/login'
353 353 end
354 354 end
355 355 mail = ActionMailer::Base.deliveries.last
356 356 assert_equal ['anotherAddress@foo.bar'], mail.bcc
357 357 end
358 358
359 359 def test_lost_password_for_unknown_user_should_fail
360 360 Token.delete_all
361 361 assert_no_difference 'Token.count' do
362 362 post :lost_password, :mail => 'invalid@somenet.foo'
363 363 assert_response :success
364 364 end
365 365 end
366 366
367 367 def test_lost_password_for_non_active_user_should_fail
368 368 Token.delete_all
369 369 assert User.find(2).lock!
370 370
371 371 assert_no_difference 'Token.count' do
372 372 post :lost_password, :mail => 'JSmith@somenet.foo'
373 373 assert_redirected_to '/account/lost_password'
374 374 end
375 375 end
376 376
377 377 def test_lost_password_for_user_who_cannot_change_password_should_fail
378 378 User.any_instance.stubs(:change_password_allowed?).returns(false)
379 379
380 380 assert_no_difference 'Token.count' do
381 381 post :lost_password, :mail => 'JSmith@somenet.foo'
382 382 assert_response :success
383 383 end
384 384 end
385 385
386 386 def test_get_lost_password_with_token_should_display_the_password_recovery_form
387 387 user = User.find(2)
388 388 token = Token.create!(:action => 'recovery', :user => user)
389 389
390 390 get :lost_password, :token => token.value
391 391 assert_response :success
392 392 assert_template 'password_recovery'
393 393
394 394 assert_select 'input[type=hidden][name=token][value=?]', token.value
395 395 end
396 396
397 397 def test_get_lost_password_with_invalid_token_should_redirect
398 398 get :lost_password, :token => "abcdef"
399 399 assert_redirected_to '/'
400 400 end
401 401
402 402 def test_post_lost_password_with_token_should_change_the_user_password
403 ActionMailer::Base.deliveries.clear
403 404 user = User.find(2)
404 405 token = Token.create!(:action => 'recovery', :user => user)
405 406
406 407 post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123'
407 408 assert_redirected_to '/login'
408 409 user.reload
409 410 assert user.check_password?('newpass123')
410 411 assert_nil Token.find_by_id(token.id), "Token was not deleted"
412 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
413 assert_select_email do
414 assert_select 'a[href^=?]', 'http://localhost:3000/my/password', :text => 'Change password'
415 end
411 416 end
412 417
413 418 def test_post_lost_password_with_token_for_non_active_user_should_fail
414 419 user = User.find(2)
415 420 token = Token.create!(:action => 'recovery', :user => user)
416 421 user.lock!
417 422
418 423 post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123'
419 424 assert_redirected_to '/'
420 425 assert ! user.check_password?('newpass123')
421 426 end
422 427
423 428 def test_post_lost_password_with_token_and_password_confirmation_failure_should_redisplay_the_form
424 429 user = User.find(2)
425 430 token = Token.create!(:action => 'recovery', :user => user)
426 431
427 432 post :lost_password, :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'wrongpass'
428 433 assert_response :success
429 434 assert_template 'password_recovery'
430 435 assert_not_nil Token.find_by_id(token.id), "Token was deleted"
431 436
432 437 assert_select 'input[type=hidden][name=token][value=?]', token.value
433 438 end
434 439
435 440 def test_post_lost_password_with_invalid_token_should_redirect
436 441 post :lost_password, :token => "abcdef", :new_password => 'newpass', :new_password_confirmation => 'newpass'
437 442 assert_redirected_to '/'
438 443 end
439 444
440 445 def test_activation_email_should_send_an_activation_email
441 446 User.find(2).update_attribute :status, User::STATUS_REGISTERED
442 447 @request.session[:registered_user_id] = 2
443 448
444 449 with_settings :self_registration => '1' do
445 450 assert_difference 'ActionMailer::Base.deliveries.size' do
446 451 get :activation_email
447 452 assert_redirected_to '/login'
448 453 end
449 454 end
450 455 end
451 456
452 457 def test_activation_email_without_session_data_should_fail
453 458 User.find(2).update_attribute :status, User::STATUS_REGISTERED
454 459
455 460 with_settings :self_registration => '1' do
456 461 assert_no_difference 'ActionMailer::Base.deliveries.size' do
457 462 get :activation_email
458 463 assert_redirected_to '/'
459 464 end
460 465 end
461 466 end
462 467 end
@@ -1,144 +1,189
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 25 end
26 26
27 27 def test_index_with_no_additional_emails
28 28 @request.session[:user_id] = 2
29 29 get :index, :user_id => 2
30 30 assert_response :success
31 31 assert_template 'index'
32 32 end
33 33
34 34 def test_index_with_additional_emails
35 35 @request.session[:user_id] = 2
36 36 EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
37 37
38 38 get :index, :user_id => 2
39 39 assert_response :success
40 40 assert_template 'index'
41 41 assert_select '.email', :text => 'another@somenet.foo'
42 42 end
43 43
44 44 def test_index_with_additional_emails_as_js
45 45 @request.session[:user_id] = 2
46 46 EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
47 47
48 48 xhr :get, :index, :user_id => 2
49 49 assert_response :success
50 50 assert_template 'index'
51 51 assert_include 'another@somenet.foo', response.body
52 52 end
53 53
54 54 def test_index_by_admin_should_be_allowed
55 55 @request.session[:user_id] = 1
56 56 get :index, :user_id => 2
57 57 assert_response :success
58 58 assert_template 'index'
59 59 end
60 60
61 61 def test_index_by_another_user_should_be_denied
62 62 @request.session[:user_id] = 3
63 63 get :index, :user_id => 2
64 64 assert_response 403
65 65 end
66 66
67 67 def test_create
68 68 @request.session[:user_id] = 2
69 69 assert_difference 'EmailAddress.count' do
70 70 post :create, :user_id => 2, :email_address => {:address => 'another@somenet.foo'}
71 71 assert_response 302
72 72 assert_redirected_to '/users/2/email_addresses'
73 73 end
74 74 email = EmailAddress.order('id DESC').first
75 75 assert_equal 2, email.user_id
76 76 assert_equal 'another@somenet.foo', email.address
77 77 end
78 78
79 79 def test_create_as_js
80 80 @request.session[:user_id] = 2
81 81 assert_difference 'EmailAddress.count' do
82 82 xhr :post, :create, :user_id => 2, :email_address => {:address => 'another@somenet.foo'}
83 83 assert_response 200
84 84 end
85 85 end
86 86
87 87 def test_create_with_failure
88 88 @request.session[:user_id] = 2
89 89 assert_no_difference 'EmailAddress.count' do
90 90 post :create, :user_id => 2, :email_address => {:address => 'invalid'}
91 91 assert_response 200
92 92 end
93 93 end
94 94
95 def test_create_should_send_security_notification
96 @request.session[:user_id] = 2
97 ActionMailer::Base.deliveries.clear
98 post :create, :user_id => 2, :email_address => {:address => 'something@example.fr'}
99
100 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
101 assert_mail_body_match '0.0.0.0', mail
102 assert_mail_body_match I18n.t(:mail_body_security_notification_add, field: I18n.t(:field_mail), value: 'something@example.fr'), mail
103 assert_select_email do
104 assert_select 'a[href^=?]', 'http://localhost:3000/my/account', :text => 'My account'
105 end
106 # The old email address should be notified about a new address for security purposes
107 assert [mail.bcc, mail.cc].flatten.include?(User.find(2).mail)
108 assert [mail.bcc, mail.cc].flatten.include?('something@example.fr')
109 end
110
95 111 def test_update
96 112 @request.session[:user_id] = 2
97 113 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
98 114
99 115 put :update, :user_id => 2, :id => email.id, :notify => '0'
100 116 assert_response 302
101 117
102 118 assert_equal false, email.reload.notify
103 119 end
104 120
105 121 def test_update_as_js
106 122 @request.session[:user_id] = 2
107 123 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
108 124
109 125 xhr :put, :update, :user_id => 2, :id => email.id, :notify => '0'
110 126 assert_response 200
111 127
112 128 assert_equal false, email.reload.notify
113 129 end
114 130
131 def test_update_should_send_security_notification
132 @request.session[:user_id] = 2
133 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
134
135 ActionMailer::Base.deliveries.clear
136 xhr :put, :update, :user_id => 2, :id => email.id, :notify => '0'
137
138 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
139 assert_mail_body_match I18n.t(:mail_body_security_notification_notify_disabled, value: 'another@somenet.foo'), mail
140
141 # The changed address should be notified for security purposes
142 assert [mail.bcc, mail.cc].flatten.include?('another@somenet.foo')
143 end
144
145
115 146 def test_destroy
116 147 @request.session[:user_id] = 2
117 148 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
118 149
119 150 assert_difference 'EmailAddress.count', -1 do
120 151 delete :destroy, :user_id => 2, :id => email.id
121 152 assert_response 302
122 153 assert_redirected_to '/users/2/email_addresses'
123 154 end
124 155 end
125 156
126 157 def test_destroy_as_js
127 158 @request.session[:user_id] = 2
128 159 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
129 160
130 161 assert_difference 'EmailAddress.count', -1 do
131 162 xhr :delete, :destroy, :user_id => 2, :id => email.id
132 163 assert_response 200
133 164 end
134 165 end
135 166
136 167 def test_should_not_destroy_default
137 168 @request.session[:user_id] = 2
138 169
139 170 assert_no_difference 'EmailAddress.count' do
140 171 delete :destroy, :user_id => 2, :id => User.find(2).email_address.id
141 172 assert_response 404
142 173 end
143 174 end
175
176 def test_destroy_should_send_security_notification
177 @request.session[:user_id] = 2
178 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
179
180 ActionMailer::Base.deliveries.clear
181 xhr :delete, :destroy, :user_id => 2, :id => email.id
182
183 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
184 assert_mail_body_match I18n.t(:mail_body_security_notification_remove, field: I18n.t(:field_mail), value: 'another@somenet.foo'), mail
185
186 # The removed address should be notified for security purposes
187 assert [mail.bcc, mail.cc].flatten.include?('another@somenet.foo')
188 end
144 189 end
@@ -1,268 +1,299
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 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_response :success
31 31 assert_template 'page'
32 32 end
33 33
34 34 def test_page
35 35 get :page
36 36 assert_response :success
37 37 assert_template 'page'
38 38 end
39 39
40 40 def test_page_with_timelog_block
41 41 preferences = User.find(2).pref
42 42 preferences[:my_page_layout] = {'top' => ['timelog']}
43 43 preferences.save!
44 44 TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10)
45 45
46 46 get :page
47 47 assert_response :success
48 48 assert_select 'tr.time-entry' do
49 49 assert_select 'td.subject a[href="/issues/1"]'
50 50 assert_select 'td.hours', :text => '2.50'
51 51 end
52 52 end
53 53
54 54 def test_page_with_all_blocks
55 55 blocks = MyController::BLOCKS.keys
56 56 preferences = User.find(2).pref
57 57 preferences[:my_page_layout] = {'top' => blocks}
58 58 preferences.save!
59 59
60 60 get :page
61 61 assert_response :success
62 62 assert_select 'div.mypage-box', blocks.size
63 63 end
64 64
65 65 def test_my_account_should_show_editable_custom_fields
66 66 get :account
67 67 assert_response :success
68 68 assert_template 'account'
69 69 assert_equal User.find(2), assigns(:user)
70 70
71 71 assert_select 'input[name=?]', 'user[custom_field_values][4]'
72 72 end
73 73
74 74 def test_my_account_should_not_show_non_editable_custom_fields
75 75 UserCustomField.find(4).update_attribute :editable, false
76 76
77 77 get :account
78 78 assert_response :success
79 79 assert_template 'account'
80 80 assert_equal User.find(2), assigns(:user)
81 81
82 82 assert_select 'input[name=?]', 'user[custom_field_values][4]', 0
83 83 end
84 84
85 85 def test_my_account_should_show_language_select
86 86 get :account
87 87 assert_response :success
88 88 assert_select 'select[name=?]', 'user[language]'
89 89 end
90 90
91 91 def test_my_account_should_not_show_language_select_with_force_default_language_for_loggedin
92 92 with_settings :force_default_language_for_loggedin => '1' do
93 93 get :account
94 94 assert_response :success
95 95 assert_select 'select[name=?]', 'user[language]', 0
96 96 end
97 97 end
98 98
99 99 def test_update_account
100 100 post :account,
101 101 :user => {
102 102 :firstname => "Joe",
103 103 :login => "root",
104 104 :admin => 1,
105 105 :group_ids => ['10'],
106 106 :custom_field_values => {"4" => "0100562500"}
107 107 }
108 108
109 109 assert_redirected_to '/my/account'
110 110 user = User.find(2)
111 111 assert_equal user, assigns(:user)
112 112 assert_equal "Joe", user.firstname
113 113 assert_equal "jsmith", user.login
114 114 assert_equal "0100562500", user.custom_value_for(4).value
115 115 # ignored
116 116 assert !user.admin?
117 117 assert user.groups.empty?
118 118 end
119 119
120 def test_update_account_should_send_security_notification
121 ActionMailer::Base.deliveries.clear
122 post :account,
123 :user => {
124 :mail => 'foobar@example.com'
125 }
126
127 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
128 assert_mail_body_match '0.0.0.0', mail
129 assert_mail_body_match I18n.t(:mail_body_security_notification_change_to, field: I18n.t(:field_mail), value: 'foobar@example.com'), mail
130 assert_select_email do
131 assert_select 'a[href^=?]', 'http://localhost:3000/my/account', :text => 'My account'
132 end
133 # The old email address should be notified about the change for security purposes
134 assert [mail.bcc, mail.cc].flatten.include?(User.find(2).mail)
135 assert [mail.bcc, mail.cc].flatten.include?('foobar@example.com')
136 end
137
120 138 def test_my_account_should_show_destroy_link
121 139 get :account
122 140 assert_select 'a[href="/my/account/destroy"]'
123 141 end
124 142
125 143 def test_get_destroy_should_display_the_destroy_confirmation
126 144 get :destroy
127 145 assert_response :success
128 146 assert_template 'destroy'
129 147 assert_select 'form[action="/my/account/destroy"]' do
130 148 assert_select 'input[name=confirm]'
131 149 end
132 150 end
133 151
134 152 def test_post_destroy_without_confirmation_should_not_destroy_account
135 153 assert_no_difference 'User.count' do
136 154 post :destroy
137 155 end
138 156 assert_response :success
139 157 assert_template 'destroy'
140 158 end
141 159
142 160 def test_post_destroy_without_confirmation_should_destroy_account
143 161 assert_difference 'User.count', -1 do
144 162 post :destroy, :confirm => '1'
145 163 end
146 164 assert_redirected_to '/'
147 165 assert_match /deleted/i, flash[:notice]
148 166 end
149 167
150 168 def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account
151 169 User.any_instance.stubs(:own_account_deletable?).returns(false)
152 170
153 171 assert_no_difference 'User.count' do
154 172 post :destroy, :confirm => '1'
155 173 end
156 174 assert_redirected_to '/my/account'
157 175 end
158 176
159 177 def test_change_password
160 178 get :password
161 179 assert_response :success
162 180 assert_template 'password'
163 181
164 182 # non matching password confirmation
165 183 post :password, :password => 'jsmith',
166 184 :new_password => 'secret123',
167 185 :new_password_confirmation => 'secret1234'
168 186 assert_response :success
169 187 assert_template 'password'
170 188 assert_select_error /Password doesn.*t match confirmation/
171 189
172 190 # wrong password
173 191 post :password, :password => 'wrongpassword',
174 192 :new_password => 'secret123',
175 193 :new_password_confirmation => 'secret123'
176 194 assert_response :success
177 195 assert_template 'password'
178 196 assert_equal 'Wrong password', flash[:error]
179 197
180 198 # good password
181 199 post :password, :password => 'jsmith',
182 200 :new_password => 'secret123',
183 201 :new_password_confirmation => 'secret123'
184 202 assert_redirected_to '/my/account'
185 203 assert User.try_to_login('jsmith', 'secret123')
186 204 end
187 205
188 206 def test_change_password_should_redirect_if_user_cannot_change_its_password
189 207 User.find(2).update_attribute(:auth_source_id, 1)
190 208
191 209 get :password
192 210 assert_not_nil flash[:error]
193 211 assert_redirected_to '/my/account'
194 212 end
195 213
214 def test_change_password_should_send_security_notification
215 ActionMailer::Base.deliveries.clear
216 post :password, :password => 'jsmith',
217 :new_password => 'secret123',
218 :new_password_confirmation => 'secret123'
219
220 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
221 assert_mail_body_no_match 'secret123', mail # just to be sure: pw should never be sent!
222 assert_select_email do
223 assert_select 'a[href^=?]', 'http://localhost:3000/my/password', :text => 'Change password'
224 end
225 end
226
196 227 def test_page_layout
197 228 get :page_layout
198 229 assert_response :success
199 230 assert_template 'page_layout'
200 231 end
201 232
202 233 def test_add_block
203 234 post :add_block, :block => 'issuesreportedbyme'
204 235 assert_redirected_to '/my/page_layout'
205 236 assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme')
206 237 end
207 238
208 239 def test_add_invalid_block_should_redirect
209 240 post :add_block, :block => 'invalid'
210 241 assert_redirected_to '/my/page_layout'
211 242 end
212 243
213 244 def test_remove_block
214 245 post :remove_block, :block => 'issuesassignedtome'
215 246 assert_redirected_to '/my/page_layout'
216 247 assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome')
217 248 end
218 249
219 250 def test_order_blocks
220 251 xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews']
221 252 assert_response :success
222 253 assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left']
223 254 end
224 255
225 256 def test_reset_rss_key_with_existing_key
226 257 @previous_token_value = User.find(2).rss_key # Will generate one if it's missing
227 258 post :reset_rss_key
228 259
229 260 assert_not_equal @previous_token_value, User.find(2).rss_key
230 261 assert User.find(2).rss_token
231 262 assert_match /reset/, flash[:notice]
232 263 assert_redirected_to '/my/account'
233 264 end
234 265
235 266 def test_reset_rss_key_without_existing_key
236 267 assert_nil User.find(2).rss_token
237 268 post :reset_rss_key
238 269
239 270 assert User.find(2).rss_token
240 271 assert_match /reset/, flash[:notice]
241 272 assert_redirected_to '/my/account'
242 273 end
243 274
244 275 def test_show_api_key
245 276 get :show_api_key
246 277 assert_response :success
247 278 assert_select 'pre', User.find(2).api_key
248 279 end
249 280
250 281 def test_reset_api_key_with_existing_key
251 282 @previous_token_value = User.find(2).api_key # Will generate one if it's missing
252 283 post :reset_api_key
253 284
254 285 assert_not_equal @previous_token_value, User.find(2).api_key
255 286 assert User.find(2).api_token
256 287 assert_match /reset/, flash[:notice]
257 288 assert_redirected_to '/my/account'
258 289 end
259 290
260 291 def test_reset_api_key_without_existing_key
261 292 assert_nil User.find(2).api_token
262 293 post :reset_api_key
263 294
264 295 assert User.find(2).api_token
265 296 assert_match /reset/, flash[:notice]
266 297 assert_redirected_to '/my/account'
267 298 end
268 299 end
@@ -1,853 +1,898
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 MailerTest < ActiveSupport::TestCase
21 21 include Redmine::I18n
22 22 include Rails::Dom::Testing::Assertions
23 23 fixtures :projects, :enabled_modules, :issues, :users, :email_addresses, :members,
24 24 :member_roles, :roles, :documents, :attachments, :news,
25 25 :tokens, :journals, :journal_details, :changesets,
26 26 :trackers, :projects_trackers,
27 27 :issue_statuses, :enumerations, :messages, :boards, :repositories,
28 28 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
29 29 :versions,
30 30 :comments
31 31
32 32 def setup
33 33 ActionMailer::Base.deliveries.clear
34 34 Setting.host_name = 'mydomain.foo'
35 35 Setting.protocol = 'http'
36 36 Setting.plain_text_mail = '0'
37 37 Setting.default_language = 'en'
38 38 User.current = nil
39 39 end
40 40
41 41 def test_generated_links_in_emails
42 42 Setting.host_name = 'mydomain.foo'
43 43 Setting.protocol = 'https'
44 44
45 45 journal = Journal.find(3)
46 46 assert Mailer.deliver_issue_edit(journal)
47 47
48 48 mail = last_email
49 49 assert_not_nil mail
50 50
51 51 assert_select_email do
52 52 # link to the main ticket
53 53 assert_select 'a[href=?]',
54 54 'https://mydomain.foo/issues/2#change-3',
55 55 :text => 'Feature request #2: Add ingredients categories'
56 56 # link to a referenced ticket
57 57 assert_select 'a[href=?][title=?]',
58 58 'https://mydomain.foo/issues/1',
59 59 "Bug: Cannot print recipes (New)",
60 60 :text => '#1'
61 61 # link to a changeset
62 62 assert_select 'a[href=?][title=?]',
63 63 'https://mydomain.foo/projects/ecookbook/repository/revisions/2',
64 64 'This commit fixes #1, #2 and references #1 & #3',
65 65 :text => 'r2'
66 66 # link to a description diff
67 67 assert_select 'a[href^=?][title=?]',
68 68 # should be https://mydomain.foo/journals/diff/3?detail_id=4
69 69 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
70 70 # attribute value
71 71 'https://mydomain.foo/journals/3/diff',
72 72 'View differences',
73 73 :text => 'diff'
74 74 # link to an attachment
75 75 assert_select 'a[href=?]',
76 76 'https://mydomain.foo/attachments/download/4/source.rb',
77 77 :text => 'source.rb'
78 78 end
79 79 end
80 80
81 81 def test_generated_links_with_prefix
82 82 relative_url_root = Redmine::Utils.relative_url_root
83 83 Setting.host_name = 'mydomain.foo/rdm'
84 84 Setting.protocol = 'http'
85 85
86 86 journal = Journal.find(3)
87 87 assert Mailer.deliver_issue_edit(journal)
88 88
89 89 mail = last_email
90 90 assert_not_nil mail
91 91
92 92 assert_select_email do
93 93 # link to the main ticket
94 94 assert_select 'a[href=?]',
95 95 'http://mydomain.foo/rdm/issues/2#change-3',
96 96 :text => 'Feature request #2: Add ingredients categories'
97 97 # link to a referenced ticket
98 98 assert_select 'a[href=?][title=?]',
99 99 'http://mydomain.foo/rdm/issues/1',
100 100 "Bug: Cannot print recipes (New)",
101 101 :text => '#1'
102 102 # link to a changeset
103 103 assert_select 'a[href=?][title=?]',
104 104 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
105 105 'This commit fixes #1, #2 and references #1 & #3',
106 106 :text => 'r2'
107 107 # link to a description diff
108 108 assert_select 'a[href^=?][title=?]',
109 109 # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4
110 110 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
111 111 # attribute value
112 112 'http://mydomain.foo/rdm/journals/3/diff',
113 113 'View differences',
114 114 :text => 'diff'
115 115 # link to an attachment
116 116 assert_select 'a[href=?]',
117 117 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
118 118 :text => 'source.rb'
119 119 end
120 120 end
121 121
122 122 def test_generated_links_with_port_and_prefix
123 123 with_settings :host_name => '10.0.0.1:81/redmine', :protocol => 'http' do
124 124 Mailer.test_email(User.find(1)).deliver
125 125 mail = last_email
126 126 assert_not_nil mail
127 127 assert_include 'http://10.0.0.1:81/redmine', mail_body(mail)
128 128 end
129 129 end
130 130
131 131 def test_generated_links_with_port
132 132 with_settings :host_name => '10.0.0.1:81', :protocol => 'http' do
133 133 Mailer.test_email(User.find(1)).deliver
134 134 mail = last_email
135 135 assert_not_nil mail
136 136 assert_include 'http://10.0.0.1:81', mail_body(mail)
137 137 end
138 138 end
139 139
140 140 def test_issue_edit_should_generate_url_with_hostname_for_relations
141 141 journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now)
142 142 journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2)
143 143 Mailer.deliver_issue_edit(journal)
144 144 assert_not_nil last_email
145 145 assert_select_email do
146 146 assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2'
147 147 end
148 148 end
149 149
150 150 def test_generated_links_with_prefix_and_no_relative_url_root
151 151 relative_url_root = Redmine::Utils.relative_url_root
152 152 Setting.host_name = 'mydomain.foo/rdm'
153 153 Setting.protocol = 'http'
154 154 Redmine::Utils.relative_url_root = nil
155 155
156 156 journal = Journal.find(3)
157 157 assert Mailer.deliver_issue_edit(journal)
158 158
159 159 mail = last_email
160 160 assert_not_nil mail
161 161
162 162 assert_select_email do
163 163 # link to the main ticket
164 164 assert_select 'a[href=?]',
165 165 'http://mydomain.foo/rdm/issues/2#change-3',
166 166 :text => 'Feature request #2: Add ingredients categories'
167 167 # link to a referenced ticket
168 168 assert_select 'a[href=?][title=?]',
169 169 'http://mydomain.foo/rdm/issues/1',
170 170 "Bug: Cannot print recipes (New)",
171 171 :text => '#1'
172 172 # link to a changeset
173 173 assert_select 'a[href=?][title=?]',
174 174 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
175 175 'This commit fixes #1, #2 and references #1 & #3',
176 176 :text => 'r2'
177 177 # link to a description diff
178 178 assert_select 'a[href^=?][title=?]',
179 179 # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4
180 180 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
181 181 # attribute value
182 182 'http://mydomain.foo/rdm/journals/3/diff',
183 183 'View differences',
184 184 :text => 'diff'
185 185 # link to an attachment
186 186 assert_select 'a[href=?]',
187 187 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
188 188 :text => 'source.rb'
189 189 end
190 190 ensure
191 191 # restore it
192 192 Redmine::Utils.relative_url_root = relative_url_root
193 193 end
194 194
195 195 def test_email_headers
196 196 issue = Issue.find(1)
197 197 Mailer.deliver_issue_add(issue)
198 198 mail = last_email
199 199 assert_not_nil mail
200 200 assert_equal 'All', mail.header['X-Auto-Response-Suppress'].to_s
201 201 assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s
202 202 assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s
203 203 end
204 204
205 205 def test_email_headers_should_include_sender
206 206 issue = Issue.find(1)
207 207 Mailer.deliver_issue_add(issue)
208 208 mail = last_email
209 209 assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
210 210 end
211 211
212 212 def test_plain_text_mail
213 213 Setting.plain_text_mail = 1
214 214 journal = Journal.find(2)
215 215 Mailer.deliver_issue_edit(journal)
216 216 mail = last_email
217 217 assert_equal "text/plain; charset=UTF-8", mail.content_type
218 218 assert_equal 0, mail.parts.size
219 219 assert !mail.encoded.include?('href')
220 220 end
221 221
222 222 def test_html_mail
223 223 Setting.plain_text_mail = 0
224 224 journal = Journal.find(2)
225 225 Mailer.deliver_issue_edit(journal)
226 226 mail = last_email
227 227 assert_equal 2, mail.parts.size
228 228 assert mail.encoded.include?('href')
229 229 end
230 230
231 231 def test_from_header
232 232 with_settings :mail_from => 'redmine@example.net' do
233 233 Mailer.test_email(User.find(1)).deliver
234 234 end
235 235 mail = last_email
236 236 assert_equal 'redmine@example.net', mail.from_addrs.first
237 237 end
238 238
239 239 def test_from_header_with_phrase
240 240 with_settings :mail_from => 'Redmine app <redmine@example.net>' do
241 241 Mailer.test_email(User.find(1)).deliver
242 242 end
243 243 mail = last_email
244 244 assert_equal 'redmine@example.net', mail.from_addrs.first
245 245 assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s
246 246 end
247 247
248 248 def test_should_not_send_email_without_recipient
249 249 news = News.first
250 250 user = news.author
251 251 # Remove members except news author
252 252 news.project.memberships.each {|m| m.destroy unless m.user == user}
253 253
254 254 user.pref.no_self_notified = false
255 255 user.pref.save
256 256 User.current = user
257 257 Mailer.news_added(news.reload).deliver
258 258 assert_equal 1, last_email.bcc.size
259 259
260 260 # nobody to notify
261 261 user.pref.no_self_notified = true
262 262 user.pref.save
263 263 User.current = user
264 264 ActionMailer::Base.deliveries.clear
265 265 Mailer.news_added(news.reload).deliver
266 266 assert ActionMailer::Base.deliveries.empty?
267 267 end
268 268
269 269 def test_issue_add_message_id
270 270 issue = Issue.find(2)
271 271 Mailer.deliver_issue_add(issue)
272 272 mail = last_email
273 273 assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
274 274 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
275 275 end
276 276
277 277 def test_issue_edit_message_id
278 278 journal = Journal.find(3)
279 279 journal.issue = Issue.find(2)
280 280
281 281 Mailer.deliver_issue_edit(journal)
282 282 mail = last_email
283 283 assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
284 284 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
285 285 assert_select_email do
286 286 # link to the update
287 287 assert_select "a[href=?]",
288 288 "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}"
289 289 end
290 290 end
291 291
292 292 def test_message_posted_message_id
293 293 message = Message.find(1)
294 294 Mailer.message_posted(message).deliver
295 295 mail = last_email
296 296 assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
297 297 assert_include "redmine.message-1.20070512151532@example.net", mail.references
298 298 assert_select_email do
299 299 # link to the message
300 300 assert_select "a[href=?]",
301 301 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}",
302 302 :text => message.subject
303 303 end
304 304 end
305 305
306 306 def test_reply_posted_message_id
307 307 message = Message.find(3)
308 308 Mailer.message_posted(message).deliver
309 309 mail = last_email
310 310 assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
311 311 assert_include "redmine.message-1.20070512151532@example.net", mail.references
312 312 assert_select_email do
313 313 # link to the reply
314 314 assert_select "a[href=?]",
315 315 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}",
316 316 :text => message.subject
317 317 end
318 318 end
319 319
320 320 test "#issue_add should notify project members" do
321 321 issue = Issue.find(1)
322 322 assert Mailer.deliver_issue_add(issue)
323 323 assert last_email.bcc.include?('dlopper@somenet.foo')
324 324 end
325 325
326 326 def test_issue_add_should_send_mail_to_all_user_email_address
327 327 EmailAddress.create!(:user_id => 3, :address => 'otheremail@somenet.foo')
328 328 issue = Issue.find(1)
329 329 assert Mailer.deliver_issue_add(issue)
330 330 assert last_email.bcc.include?('dlopper@somenet.foo')
331 331 assert last_email.bcc.include?('otheremail@somenet.foo')
332 332 end
333 333
334 334 test "#issue_add should not notify project members that are not allow to view the issue" do
335 335 issue = Issue.find(1)
336 336 Role.find(2).remove_permission!(:view_issues)
337 337 assert Mailer.deliver_issue_add(issue)
338 338 assert !last_email.bcc.include?('dlopper@somenet.foo')
339 339 end
340 340
341 341 test "#issue_add should notify issue watchers" do
342 342 issue = Issue.find(1)
343 343 user = User.find(9)
344 344 # minimal email notification options
345 345 user.pref.no_self_notified = '1'
346 346 user.pref.save
347 347 user.mail_notification = false
348 348 user.save
349 349
350 350 Watcher.create!(:watchable => issue, :user => user)
351 351 assert Mailer.deliver_issue_add(issue)
352 352 assert last_email.bcc.include?(user.mail)
353 353 end
354 354
355 355 test "#issue_add should not notify watchers not allowed to view the issue" do
356 356 issue = Issue.find(1)
357 357 user = User.find(9)
358 358 Watcher.create!(:watchable => issue, :user => user)
359 359 Role.non_member.remove_permission!(:view_issues)
360 360 assert Mailer.deliver_issue_add(issue)
361 361 assert !last_email.bcc.include?(user.mail)
362 362 end
363 363
364 364 def test_issue_add_should_include_enabled_fields
365 365 issue = Issue.find(2)
366 366 assert Mailer.deliver_issue_add(issue)
367 367 assert_mail_body_match '* Target version: 1.0', last_email
368 368 assert_select_email do
369 369 assert_select 'li', :text => 'Target version: 1.0'
370 370 end
371 371 end
372 372
373 373 def test_issue_add_should_not_include_disabled_fields
374 374 issue = Issue.find(2)
375 375 tracker = issue.tracker
376 376 tracker.core_fields -= ['fixed_version_id']
377 377 tracker.save!
378 378 assert Mailer.deliver_issue_add(issue)
379 379 assert_mail_body_no_match 'Target version', last_email
380 380 assert_select_email do
381 381 assert_select 'li', :text => /Target version/, :count => 0
382 382 end
383 383 end
384 384
385 385 # test mailer methods for each language
386 386 def test_issue_add
387 387 issue = Issue.find(1)
388 388 with_each_language_as_default do
389 389 assert Mailer.deliver_issue_add(issue)
390 390 end
391 391 end
392 392
393 393 def test_issue_edit
394 394 journal = Journal.find(1)
395 395 with_each_language_as_default do
396 396 assert Mailer.deliver_issue_edit(journal)
397 397 end
398 398 end
399 399
400 400 def test_issue_edit_should_send_private_notes_to_users_with_permission_only
401 401 journal = Journal.find(1)
402 402 journal.private_notes = true
403 403 journal.save!
404 404
405 405 Role.find(2).add_permission! :view_private_notes
406 406 Mailer.deliver_issue_edit(journal)
407 407 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
408 408
409 409 Role.find(2).remove_permission! :view_private_notes
410 410 Mailer.deliver_issue_edit(journal)
411 411 assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
412 412 end
413 413
414 414 def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only
415 415 Issue.find(1).set_watcher(User.find_by_login('someone'))
416 416 journal = Journal.find(1)
417 417 journal.private_notes = true
418 418 journal.save!
419 419
420 420 Role.non_member.add_permission! :view_private_notes
421 421 Mailer.deliver_issue_edit(journal)
422 422 assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
423 423
424 424 Role.non_member.remove_permission! :view_private_notes
425 425 Mailer.deliver_issue_edit(journal)
426 426 assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
427 427 end
428 428
429 429 def test_issue_edit_should_mark_private_notes
430 430 journal = Journal.find(2)
431 431 journal.private_notes = true
432 432 journal.save!
433 433
434 434 with_settings :default_language => 'en' do
435 435 Mailer.deliver_issue_edit(journal)
436 436 end
437 437 assert_mail_body_match '(Private notes)', last_email
438 438 end
439 439
440 440 def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue
441 441 issue = Issue.generate!
442 442 issue.init_journal(User.find(1))
443 443 private_issue = Issue.generate!(:is_private => true)
444 444 IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates')
445 445 issue.reload
446 446 assert_equal 1, issue.journals.size
447 447 journal = issue.journals.first
448 448 ActionMailer::Base.deliveries.clear
449 449
450 450 Mailer.deliver_issue_edit(journal)
451 451 last_email.bcc.each do |email|
452 452 user = User.find_by_mail(email)
453 453 assert private_issue.visible?(user), "Issue was not visible to #{user}"
454 454 end
455 455 end
456 456
457 457 def test_document_added
458 458 document = Document.find(1)
459 459 with_each_language_as_default do
460 460 assert Mailer.document_added(document).deliver
461 461 end
462 462 end
463 463
464 464 def test_attachments_added
465 465 attachements = [ Attachment.find_by_container_type('Document') ]
466 466 with_each_language_as_default do
467 467 assert Mailer.attachments_added(attachements).deliver
468 468 end
469 469 end
470 470
471 471 def test_version_file_added
472 472 attachements = [ Attachment.find_by_container_type('Version') ]
473 473 assert Mailer.attachments_added(attachements).deliver
474 474 assert_not_nil last_email.bcc
475 475 assert last_email.bcc.any?
476 476 assert_select_email do
477 477 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
478 478 end
479 479 end
480 480
481 481 def test_project_file_added
482 482 attachements = [ Attachment.find_by_container_type('Project') ]
483 483 assert Mailer.attachments_added(attachements).deliver
484 484 assert_not_nil last_email.bcc
485 485 assert last_email.bcc.any?
486 486 assert_select_email do
487 487 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
488 488 end
489 489 end
490 490
491 491 def test_news_added
492 492 news = News.first
493 493 with_each_language_as_default do
494 494 assert Mailer.news_added(news).deliver
495 495 end
496 496 end
497 497
498 498 def test_news_added_should_notify_project_news_watchers
499 499 user1 = User.generate!
500 500 user2 = User.generate!
501 501 news = News.find(1)
502 502 news.project.enabled_module('news').add_watcher(user1)
503 503
504 504 Mailer.news_added(news).deliver
505 505 assert_include user1.mail, last_email.bcc
506 506 assert_not_include user2.mail, last_email.bcc
507 507 end
508 508
509 509 def test_news_comment_added
510 510 comment = Comment.find(2)
511 511 with_each_language_as_default do
512 512 assert Mailer.news_comment_added(comment).deliver
513 513 end
514 514 end
515 515
516 516 def test_message_posted
517 517 message = Message.first
518 518 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
519 519 recipients = recipients.compact.uniq
520 520 with_each_language_as_default do
521 521 assert Mailer.message_posted(message).deliver
522 522 end
523 523 end
524 524
525 525 def test_wiki_content_added
526 526 content = WikiContent.find(1)
527 527 with_each_language_as_default do
528 528 assert_difference 'ActionMailer::Base.deliveries.size' do
529 529 assert Mailer.wiki_content_added(content).deliver
530 530 assert_select_email do
531 531 assert_select 'a[href=?]',
532 532 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
533 533 :text => 'CookBook documentation'
534 534 end
535 535 end
536 536 end
537 537 end
538 538
539 539 def test_wiki_content_updated
540 540 content = WikiContent.find(1)
541 541 with_each_language_as_default do
542 542 assert_difference 'ActionMailer::Base.deliveries.size' do
543 543 assert Mailer.wiki_content_updated(content).deliver
544 544 assert_select_email do
545 545 assert_select 'a[href=?]',
546 546 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
547 547 :text => 'CookBook documentation'
548 548 end
549 549 end
550 550 end
551 551 end
552 552
553 553 def test_account_information
554 554 user = User.find(2)
555 555 valid_languages.each do |lang|
556 556 user.update_attribute :language, lang.to_s
557 557 user.reload
558 558 assert Mailer.account_information(user, 'pAsswORd').deliver
559 559 end
560 560 end
561 561
562 562 def test_lost_password
563 563 token = Token.find(2)
564 564 valid_languages.each do |lang|
565 565 token.user.update_attribute :language, lang.to_s
566 566 token.reload
567 567 assert Mailer.lost_password(token).deliver
568 568 end
569 569 end
570 570
571 571 def test_register
572 572 token = Token.find(1)
573 573 Setting.host_name = 'redmine.foo'
574 574 Setting.protocol = 'https'
575 575
576 576 valid_languages.each do |lang|
577 577 token.user.update_attribute :language, lang.to_s
578 578 token.reload
579 579 ActionMailer::Base.deliveries.clear
580 580 assert Mailer.register(token).deliver
581 581 mail = last_email
582 582 assert_select_email do
583 583 assert_select "a[href=?]",
584 584 "https://redmine.foo/account/activate?token=#{token.value}",
585 585 :text => "https://redmine.foo/account/activate?token=#{token.value}"
586 586 end
587 587 end
588 588 end
589 589
590 590 def test_test
591 591 user = User.find(1)
592 592 valid_languages.each do |lang|
593 593 user.update_attribute :language, lang.to_s
594 594 assert Mailer.test_email(user).deliver
595 595 end
596 596 end
597 597
598 598 def test_reminders
599 599 Mailer.reminders(:days => 42)
600 600 assert_equal 1, ActionMailer::Base.deliveries.size
601 601 mail = last_email
602 602 assert mail.bcc.include?('dlopper@somenet.foo')
603 603 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
604 604 assert_equal '1 issue(s) due in the next 42 days', mail.subject
605 605 end
606 606
607 607 def test_reminders_should_not_include_closed_issues
608 608 with_settings :default_language => 'en' do
609 609 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5,
610 610 :subject => 'Closed issue', :assigned_to_id => 3,
611 611 :due_date => 5.days.from_now,
612 612 :author_id => 2)
613 613 ActionMailer::Base.deliveries.clear
614 614
615 615 Mailer.reminders(:days => 42)
616 616 assert_equal 1, ActionMailer::Base.deliveries.size
617 617 mail = last_email
618 618 assert mail.bcc.include?('dlopper@somenet.foo')
619 619 assert_mail_body_no_match 'Closed issue', mail
620 620 end
621 621 end
622 622
623 623 def test_reminders_for_users
624 624 Mailer.reminders(:days => 42, :users => ['5'])
625 625 assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper
626 626 Mailer.reminders(:days => 42, :users => ['3'])
627 627 assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper
628 628 mail = last_email
629 629 assert mail.bcc.include?('dlopper@somenet.foo')
630 630 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
631 631 end
632 632
633 633 def test_reminder_should_include_issues_assigned_to_groups
634 634 with_settings :default_language => 'en' do
635 635 group = Group.generate!
636 636 group.users << User.find(2)
637 637 group.users << User.find(3)
638 638
639 639 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
640 640 :subject => 'Assigned to group', :assigned_to => group,
641 641 :due_date => 5.days.from_now,
642 642 :author_id => 2)
643 643 ActionMailer::Base.deliveries.clear
644 644
645 645 Mailer.reminders(:days => 7)
646 646 assert_equal 2, ActionMailer::Base.deliveries.size
647 647 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort
648 648 ActionMailer::Base.deliveries.each do |mail|
649 649 assert_mail_body_match 'Assigned to group', mail
650 650 end
651 651 end
652 652 end
653 653
654 654 def test_reminders_with_version_option
655 655 with_settings :default_language => 'en' do
656 656 version = Version.generate!(:name => 'Acme', :project_id => 1)
657 657 Issue.generate!(:assigned_to => User.find(2), :due_date => 5.days.from_now)
658 658 Issue.generate!(:assigned_to => User.find(3), :due_date => 5.days.from_now, :fixed_version => version)
659 659 ActionMailer::Base.deliveries.clear
660 660
661 661 Mailer.reminders(:days => 42, :version => 'acme')
662 662 assert_equal 1, ActionMailer::Base.deliveries.size
663 663
664 664 mail = last_email
665 665 assert mail.bcc.include?('dlopper@somenet.foo')
666 666 end
667 667 end
668 668
669 def test_security_notification
670 set_language_if_valid User.find(1).language
671 with_settings :emails_footer => "footer without link" do
672 User.current.remote_ip = '192.168.1.1'
673 assert Mailer.security_notification(User.find(1), message: :notice_account_password_updated).deliver
674 mail = last_email
675 assert_not_nil mail
676 assert_mail_body_match '192.168.1.1', mail
677 assert_mail_body_match I18n.t(:notice_account_password_updated), mail
678 assert_select_email do
679 assert_select "h1", false
680 assert_select "a", false
681 end
682 end
683 end
684
685 def test_security_notification_should_include_title
686 set_language_if_valid User.find(2).language
687 with_settings :emails_footer => "footer without link" do
688 assert Mailer.security_notification(User.find(2),
689 message: :notice_account_password_updated,
690 title: :label_my_account
691 ).deliver
692 assert_select_email do
693 assert_select "a", false
694 assert_select "h1", :text => I18n.t(:label_my_account)
695 end
696 end
697 end
698
699 def test_security_notification_should_include_link
700 set_language_if_valid User.find(3).language
701 with_settings :emails_footer => "footer without link" do
702 assert Mailer.security_notification(User.find(3),
703 message: :notice_account_password_updated,
704 title: :label_my_account,
705 url: {controller: 'my', action: 'account'}
706 ).deliver
707 assert_select_email do
708 assert_select "h1", false
709 assert_select 'a[href=?]', 'http://mydomain.foo/my/account', :text => I18n.t(:label_my_account)
710 end
711 end
712 end
713
669 714 def test_mailer_should_not_change_locale
670 715 # Set current language to italian
671 716 set_language_if_valid 'it'
672 717 # Send an email to a french user
673 718 user = User.find(1)
674 719 user.language = 'fr'
675 720 Mailer.account_activated(user).deliver
676 721 mail = last_email
677 722 assert_mail_body_match 'Votre compte', mail
678 723
679 724 assert_equal :it, current_language
680 725 end
681 726
682 727 def test_with_deliveries_off
683 728 Mailer.with_deliveries false do
684 729 Mailer.test_email(User.find(1)).deliver
685 730 end
686 731 assert ActionMailer::Base.deliveries.empty?
687 732 # should restore perform_deliveries
688 733 assert ActionMailer::Base.perform_deliveries
689 734 end
690 735
691 736 def test_token_for_should_strip_trailing_gt_from_address_with_full_name
692 737 with_settings :mail_from => "Redmine Mailer<no-reply@redmine.org>" do
693 738 assert_match /\Aredmine.issue-\d+\.\d+\.[0-9a-f]+@redmine.org\z/, Mailer.token_for(Issue.generate!)
694 739 end
695 740 end
696 741
697 742 def test_layout_should_include_the_emails_header
698 743 with_settings :emails_header => "*Header content*" do
699 744 with_settings :plain_text_mail => 0 do
700 745 assert Mailer.test_email(User.find(1)).deliver
701 746 assert_select_email do
702 747 assert_select ".header" do
703 748 assert_select "strong", :text => "Header content"
704 749 end
705 750 end
706 751 end
707 752 with_settings :plain_text_mail => 1 do
708 753 assert Mailer.test_email(User.find(1)).deliver
709 754 mail = last_email
710 755 assert_not_nil mail
711 756 assert_include "*Header content*", mail.body.decoded
712 757 end
713 758 end
714 759 end
715 760
716 761 def test_layout_should_not_include_empty_emails_header
717 762 with_settings :emails_header => "", :plain_text_mail => 0 do
718 763 assert Mailer.test_email(User.find(1)).deliver
719 764 assert_select_email do
720 765 assert_select ".header", false
721 766 end
722 767 end
723 768 end
724 769
725 770 def test_layout_should_include_the_emails_footer
726 771 with_settings :emails_footer => "*Footer content*" do
727 772 with_settings :plain_text_mail => 0 do
728 773 assert Mailer.test_email(User.find(1)).deliver
729 774 assert_select_email do
730 775 assert_select ".footer" do
731 776 assert_select "strong", :text => "Footer content"
732 777 end
733 778 end
734 779 end
735 780 with_settings :plain_text_mail => 1 do
736 781 assert Mailer.test_email(User.find(1)).deliver
737 782 mail = last_email
738 783 assert_not_nil mail
739 784 assert_include "\n-- \n", mail.body.decoded
740 785 assert_include "*Footer content*", mail.body.decoded
741 786 end
742 787 end
743 788 end
744 789
745 790 def test_layout_should_not_include_empty_emails_footer
746 791 with_settings :emails_footer => "" do
747 792 with_settings :plain_text_mail => 0 do
748 793 assert Mailer.test_email(User.find(1)).deliver
749 794 assert_select_email do
750 795 assert_select ".footer", false
751 796 end
752 797 end
753 798 with_settings :plain_text_mail => 1 do
754 799 assert Mailer.test_email(User.find(1)).deliver
755 800 mail = last_email
756 801 assert_not_nil mail
757 802 assert_not_include "\n-- \n", mail.body.decoded
758 803 end
759 804 end
760 805 end
761 806
762 807 def test_should_escape_html_templates_only
763 808 Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>')
764 809 mail = last_email
765 810 assert_equal 2, mail.parts.size
766 811 assert_include '<tag>', text_part.body.encoded
767 812 assert_include '&lt;tag&gt;', html_part.body.encoded
768 813 end
769 814
770 815 def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true
771 816 mail = Mailer.test_email(User.find(1))
772 817 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
773 818
774 819 ActionMailer::Base.raise_delivery_errors = true
775 820 assert_raise Exception, "delivery error" do
776 821 mail.deliver
777 822 end
778 823 ensure
779 824 ActionMailer::Base.raise_delivery_errors = false
780 825 end
781 826
782 827 def test_should_log_delivery_errors_when_raise_delivery_errors_is_false
783 828 mail = Mailer.test_email(User.find(1))
784 829 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
785 830
786 831 Rails.logger.expects(:error).with("Email delivery error: delivery error")
787 832 ActionMailer::Base.raise_delivery_errors = false
788 833 assert_nothing_raised do
789 834 mail.deliver
790 835 end
791 836 end
792 837
793 838 def test_with_synched_deliveries_should_yield_with_synced_deliveries
794 839 ActionMailer::Base.delivery_method = :async_smtp
795 840 ActionMailer::Base.async_smtp_settings = {:foo => 'bar'}
796 841
797 842 Mailer.with_synched_deliveries do
798 843 assert_equal :smtp, ActionMailer::Base.delivery_method
799 844 assert_equal({:foo => 'bar'}, ActionMailer::Base.smtp_settings)
800 845 end
801 846 assert_equal :async_smtp, ActionMailer::Base.delivery_method
802 847 ensure
803 848 ActionMailer::Base.delivery_method = :test
804 849 end
805 850
806 851 def test_email_addresses_should_keep_addresses
807 852 assert_equal ["foo@example.net"],
808 853 Mailer.email_addresses("foo@example.net")
809 854
810 855 assert_equal ["foo@example.net", "bar@example.net"],
811 856 Mailer.email_addresses(["foo@example.net", "bar@example.net"])
812 857 end
813 858
814 859 def test_email_addresses_should_replace_users_with_their_email_addresses
815 860 assert_equal ["admin@somenet.foo"],
816 861 Mailer.email_addresses(User.find(1))
817 862
818 863 assert_equal ["admin@somenet.foo", "jsmith@somenet.foo"],
819 864 Mailer.email_addresses(User.where(:id => [1,2])).sort
820 865 end
821 866
822 867 def test_email_addresses_should_include_notified_emails_addresses_only
823 868 EmailAddress.create!(:user_id => 2, :address => "another@somenet.foo", :notify => false)
824 869 EmailAddress.create!(:user_id => 2, :address => "another2@somenet.foo")
825 870
826 871 assert_equal ["another2@somenet.foo", "jsmith@somenet.foo"],
827 872 Mailer.email_addresses(User.find(2)).sort
828 873 end
829 874
830 875 private
831 876
832 877 def last_email
833 878 mail = ActionMailer::Base.deliveries.last
834 879 assert_not_nil mail
835 880 mail
836 881 end
837 882
838 883 def text_part
839 884 last_email.parts.detect {|part| part.content_type.include?('text/plain')}
840 885 end
841 886
842 887 def html_part
843 888 last_email.parts.detect {|part| part.content_type.include?('text/html')}
844 889 end
845 890
846 891 def with_each_language_as_default(&block)
847 892 valid_languages.each do |lang|
848 893 with_settings :default_language => lang.to_s do
849 894 yield lang
850 895 end
851 896 end
852 897 end
853 898 end
General Comments 0
You need to be logged in to leave comments. Login now