@@ -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 '<tag>', 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 | |
|
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