@@ -0,0 +1,10 | |||
|
1 | <h2><%= l :label_api_access_key %></h2> | |
|
2 | ||
|
3 | <div class="box"> | |
|
4 | <pre><%= @user.api_key %></pre> | |
|
5 | </div> | |
|
6 | ||
|
7 | <p><%= link_to l(:button_back), action: 'account' %></p> | |
|
8 | ||
|
9 | ||
|
10 |
@@ -0,0 +1,1 | |||
|
1 | $('#api-access-key').html('<%= escape_javascript @user.api_key %>').toggle(); |
@@ -0,0 +1,19 | |||
|
1 | <h3 class="title"><%= l(:label_password_required) %></h3> | |
|
2 | <%= form_tag({}, remote: true) do %> | |
|
3 | ||
|
4 | <%= hidden_field_tag '_method', request.request_method %> | |
|
5 | <%= hash_to_hidden_fields @sudo_form.original_fields %> | |
|
6 | <%= render_flash_messages %> | |
|
7 | <div class="box tabular"> | |
|
8 | <p> | |
|
9 | <label for="sudo_password"><%= l :field_password %><span class="required">*</span></label> | |
|
10 | <%= password_field_tag :sudo_password, nil, size: 25 %> | |
|
11 | </p> | |
|
12 | </div> | |
|
13 | ||
|
14 | <p class="buttons"> | |
|
15 | <%= submit_tag l(:button_confirm_password), onclick: "hideModal(this);" %> | |
|
16 | <%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %> | |
|
17 | </p> | |
|
18 | <% end %> | |
|
19 |
@@ -0,0 +1,17 | |||
|
1 | <h2><%= l :label_password_required %></h2> | |
|
2 | <%= form_tag({}, class: 'tabular') do %> | |
|
3 | ||
|
4 | <%= hidden_field_tag '_method', request.request_method %> | |
|
5 | <%= hash_to_hidden_fields @sudo_form.original_fields %> | |
|
6 | ||
|
7 | <div class="box"> | |
|
8 | <p> | |
|
9 | <label for="sudo_password"><%= l :field_password %><span class="required">*</span></label> | |
|
10 | <%= password_field_tag :sudo_password, nil, size: 25 %> | |
|
11 | </p> | |
|
12 | </div> | |
|
13 | <%= submit_tag l(:button_confirm_password) %> | |
|
14 | <% end %> | |
|
15 | <%= javascript_tag "$('#sudo_password').focus();" %> | |
|
16 | ||
|
17 |
@@ -0,0 +1,4 | |||
|
1 | $('#ajax-modal').html('<%= escape_javascript render partial: 'sudo_mode/new_modal' %>'); | |
|
2 | showModal('ajax-modal', '400px'); | |
|
3 | $('#sudo_password').focus(); | |
|
4 |
@@ -0,0 +1,224 | |||
|
1 | require 'active_support/core_ext/object/to_query' | |
|
2 | require 'rack/utils' | |
|
3 | ||
|
4 | module Redmine | |
|
5 | module SudoMode | |
|
6 | ||
|
7 | # timespan after which sudo mode expires when unused. | |
|
8 | MAX_INACTIVITY = 15.minutes | |
|
9 | ||
|
10 | ||
|
11 | class SudoRequired < StandardError | |
|
12 | end | |
|
13 | ||
|
14 | ||
|
15 | class Form | |
|
16 | include ActiveModel::Validations | |
|
17 | ||
|
18 | attr_accessor :password, :original_fields | |
|
19 | validate :check_password | |
|
20 | ||
|
21 | def initialize(password = nil) | |
|
22 | self.password = password | |
|
23 | end | |
|
24 | ||
|
25 | def check_password | |
|
26 | unless password.present? && User.current.check_password?(password) | |
|
27 | errors[:password] << :invalid | |
|
28 | end | |
|
29 | end | |
|
30 | end | |
|
31 | ||
|
32 | ||
|
33 | module Helper | |
|
34 | # Represents params data from hash as hidden fields | |
|
35 | # | |
|
36 | # taken from https://github.com/brianhempel/hash_to_hidden_fields | |
|
37 | def hash_to_hidden_fields(hash) | |
|
38 | cleaned_hash = hash.reject { |k, v| v.nil? } | |
|
39 | pairs = cleaned_hash.to_query.split(Rack::Utils::DEFAULT_SEP) | |
|
40 | tags = pairs.map do |pair| | |
|
41 | key, value = pair.split('=', 2).map { |str| Rack::Utils.unescape(str) } | |
|
42 | hidden_field_tag(key, value) | |
|
43 | end | |
|
44 | tags.join("\n").html_safe | |
|
45 | end | |
|
46 | end | |
|
47 | ||
|
48 | ||
|
49 | module Controller | |
|
50 | extend ActiveSupport::Concern | |
|
51 | ||
|
52 | included do | |
|
53 | around_filter :sudo_mode | |
|
54 | end | |
|
55 | ||
|
56 | # Sudo mode Around Filter | |
|
57 | # | |
|
58 | # Checks the 'last used' timestamp from session and sets the | |
|
59 | # SudoMode::active? flag accordingly. | |
|
60 | # | |
|
61 | # After the request refreshes the timestamp if sudo mode was used during | |
|
62 | # this request. | |
|
63 | def sudo_mode | |
|
64 | if api_request? | |
|
65 | SudoMode.disable! | |
|
66 | elsif sudo_timestamp_valid? | |
|
67 | SudoMode.active! | |
|
68 | end | |
|
69 | yield | |
|
70 | update_sudo_timestamp! if SudoMode.was_used? | |
|
71 | end | |
|
72 | ||
|
73 | # This renders the sudo mode form / handles sudo form submission. | |
|
74 | # | |
|
75 | # Call this method in controller actions if sudo permissions are required | |
|
76 | # for processing this request. This approach is good in cases where the | |
|
77 | # action needs to be protected in any case or where the check is simple. | |
|
78 | # | |
|
79 | # In cases where this decision depends on complex conditions in the model, | |
|
80 | # consider the declarative approach using the require_sudo_mode class | |
|
81 | # method and a corresponding declaration in the model that causes it to throw | |
|
82 | # a SudoRequired Error when necessary. | |
|
83 | # | |
|
84 | # All parameter names given are included as hidden fields to be resubmitted | |
|
85 | # along with the password. | |
|
86 | # | |
|
87 | # Returns true when processing the action should continue, false otherwise. | |
|
88 | # If false is returned, render has already been called for display of the | |
|
89 | # password form. | |
|
90 | # | |
|
91 | # if @user.mail_changed? | |
|
92 | # require_sudo_mode :user or return | |
|
93 | # end | |
|
94 | # | |
|
95 | def require_sudo_mode(*param_names) | |
|
96 | return true if SudoMode.active? | |
|
97 | ||
|
98 | if param_names.blank? | |
|
99 | param_names = params.keys - %w(id action controller sudo_password) | |
|
100 | end | |
|
101 | ||
|
102 | process_sudo_form | |
|
103 | ||
|
104 | if SudoMode.active? | |
|
105 | true | |
|
106 | else | |
|
107 | render_sudo_form param_names | |
|
108 | false | |
|
109 | end | |
|
110 | end | |
|
111 | ||
|
112 | # display the sudo password form | |
|
113 | def render_sudo_form(param_names) | |
|
114 | @sudo_form ||= SudoMode::Form.new | |
|
115 | @sudo_form.original_fields = params.slice( *param_names ) | |
|
116 | # a simple 'render "sudo_mode/new"' works when used directly inside an | |
|
117 | # action, but not when called from a before_filter: | |
|
118 | respond_to do |format| | |
|
119 | format.html { render 'sudo_mode/new' } | |
|
120 | format.js { render 'sudo_mode/new' } | |
|
121 | end | |
|
122 | end | |
|
123 | ||
|
124 | # handle sudo password form submit | |
|
125 | def process_sudo_form | |
|
126 | if params[:sudo_password] | |
|
127 | @sudo_form = SudoMode::Form.new(params[:sudo_password]) | |
|
128 | if @sudo_form.valid? | |
|
129 | SudoMode.active! | |
|
130 | else | |
|
131 | flash.now[:error] = l(:notice_account_wrong_password) | |
|
132 | end | |
|
133 | end | |
|
134 | end | |
|
135 | ||
|
136 | def sudo_timestamp_valid? | |
|
137 | session[:sudo_timestamp].to_i > MAX_INACTIVITY.ago.to_i | |
|
138 | end | |
|
139 | ||
|
140 | def update_sudo_timestamp!(new_value = Time.now.to_i) | |
|
141 | session[:sudo_timestamp] = new_value | |
|
142 | end | |
|
143 | ||
|
144 | # Before Filter which is used by the require_sudo_mode class method. | |
|
145 | class SudoRequestFilter < Struct.new(:parameters, :request_methods) | |
|
146 | def before(controller) | |
|
147 | method_matches = request_methods.blank? || request_methods.include?(controller.request.method_symbol) | |
|
148 | if SudoMode.possible? && method_matches | |
|
149 | controller.require_sudo_mode( *parameters ) | |
|
150 | else | |
|
151 | true | |
|
152 | end | |
|
153 | end | |
|
154 | end | |
|
155 | ||
|
156 | module ClassMethods | |
|
157 | ||
|
158 | # Handles sudo requirements for the given actions, preserving the named | |
|
159 | # parameters, or any parameters if you omit the :parameters option. | |
|
160 | # | |
|
161 | # Sudo enforcement by default is active for all requests to an action | |
|
162 | # but may be limited to a certain subset of request methods via the | |
|
163 | # :only option. | |
|
164 | # | |
|
165 | # Examples: | |
|
166 | # | |
|
167 | # require_sudo_mode :account, only: :post | |
|
168 | # require_sudo_mode :update, :create, parameters: %w(role) | |
|
169 | # require_sudo_mode :destroy | |
|
170 | # | |
|
171 | def require_sudo_mode(*args) | |
|
172 | actions = args.dup | |
|
173 | options = actions.extract_options! | |
|
174 | filter = SudoRequestFilter.new Array(options[:parameters]), Array(options[:only]) | |
|
175 | before_filter filter, only: actions | |
|
176 | end | |
|
177 | end | |
|
178 | end | |
|
179 | ||
|
180 | ||
|
181 | # true if the sudo mode state was queried during this request | |
|
182 | def self.was_used? | |
|
183 | !!RequestStore.store[:sudo_mode_was_used] | |
|
184 | end | |
|
185 | ||
|
186 | # true if sudo mode is currently active. | |
|
187 | # | |
|
188 | # Calling this method also turns was_used? to true, therefore | |
|
189 | # it is important to only call this when sudo is actually needed, as the last | |
|
190 | # condition to determine wether a change can be done or not. | |
|
191 | # | |
|
192 | # If you do it wrong, timeout of the sudo mode will happen too late or not at | |
|
193 | # all. | |
|
194 | def self.active? | |
|
195 | if !!RequestStore.store[:sudo_mode] | |
|
196 | RequestStore.store[:sudo_mode_was_used] = true | |
|
197 | end | |
|
198 | end | |
|
199 | ||
|
200 | def self.active! | |
|
201 | RequestStore.store[:sudo_mode] = true | |
|
202 | end | |
|
203 | ||
|
204 | def self.possible? | |
|
205 | !disabled? && User.current.logged? | |
|
206 | end | |
|
207 | ||
|
208 | # Turn off sudo mode (never require password entry). | |
|
209 | def self.disable! | |
|
210 | RequestStore.store[:sudo_mode_disabled] = true | |
|
211 | end | |
|
212 | ||
|
213 | # Turn sudo mode back on | |
|
214 | def self.enable! | |
|
215 | RequestStore.store[:sudo_mode_disabled] = nil | |
|
216 | end | |
|
217 | ||
|
218 | def self.disabled? | |
|
219 | !!RequestStore.store[:sudo_mode_disabled] | |
|
220 | end | |
|
221 | ||
|
222 | end | |
|
223 | end | |
|
224 |
@@ -0,0 +1,126 | |||
|
1 | require File.expand_path('../../test_helper', __FILE__) | |
|
2 | ||
|
3 | class SudoTest < Redmine::IntegrationTest | |
|
4 | fixtures :projects, :members, :member_roles, :roles, :users | |
|
5 | ||
|
6 | def setup | |
|
7 | Redmine::SudoMode.enable! | |
|
8 | end | |
|
9 | ||
|
10 | def teardown | |
|
11 | Redmine::SudoMode.disable! | |
|
12 | end | |
|
13 | ||
|
14 | def test_create_member_xhr | |
|
15 | log_user 'admin', 'admin' | |
|
16 | get '/projects/ecookbook/settings/members' | |
|
17 | assert_response :success | |
|
18 | ||
|
19 | assert_no_difference 'Member.count' do | |
|
20 | xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7} | |
|
21 | end | |
|
22 | ||
|
23 | assert_no_difference 'Member.count' do | |
|
24 | xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: '' | |
|
25 | end | |
|
26 | ||
|
27 | assert_no_difference 'Member.count' do | |
|
28 | xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'wrong' | |
|
29 | end | |
|
30 | ||
|
31 | assert_difference 'Member.count' do | |
|
32 | xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'admin' | |
|
33 | end | |
|
34 | assert User.find(7).member_of?(Project.find(1)) | |
|
35 | end | |
|
36 | ||
|
37 | def test_create_member | |
|
38 | log_user 'admin', 'admin' | |
|
39 | get '/projects/ecookbook/settings/members' | |
|
40 | assert_response :success | |
|
41 | ||
|
42 | assert_no_difference 'Member.count' do | |
|
43 | post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7} | |
|
44 | end | |
|
45 | ||
|
46 | assert_no_difference 'Member.count' do | |
|
47 | post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: '' | |
|
48 | end | |
|
49 | ||
|
50 | assert_no_difference 'Member.count' do | |
|
51 | post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'wrong' | |
|
52 | end | |
|
53 | ||
|
54 | assert_difference 'Member.count' do | |
|
55 | post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'admin' | |
|
56 | end | |
|
57 | ||
|
58 | assert_redirected_to '/projects/ecookbook/settings/members' | |
|
59 | assert User.find(7).member_of?(Project.find(1)) | |
|
60 | end | |
|
61 | ||
|
62 | def test_create_role | |
|
63 | log_user 'admin', 'admin' | |
|
64 | get '/roles' | |
|
65 | assert_response :success | |
|
66 | ||
|
67 | get '/roles/new' | |
|
68 | assert_response :success | |
|
69 | ||
|
70 | post '/roles', role: { } | |
|
71 | assert_response :success | |
|
72 | assert_select 'h2', 'Confirm your password to continue' | |
|
73 | assert_select 'form[action="/roles"]' | |
|
74 | assert assigns(:sudo_form).errors.blank? | |
|
75 | ||
|
76 | post '/roles', role: { name: 'new role', issues_visibility: 'all' } | |
|
77 | assert_response :success | |
|
78 | assert_select 'h2', 'Confirm your password to continue' | |
|
79 | assert_select 'form[action="/roles"]' | |
|
80 | assert_match /"new role"/, response.body | |
|
81 | assert assigns(:sudo_form).errors.blank? | |
|
82 | ||
|
83 | post '/roles', role: { name: 'new role', issues_visibility: 'all' }, sudo_password: 'wrong' | |
|
84 | assert_response :success | |
|
85 | assert_select 'h2', 'Confirm your password to continue' | |
|
86 | assert_select 'form[action="/roles"]' | |
|
87 | assert_match /"new role"/, response.body | |
|
88 | assert assigns(:sudo_form).errors[:password].present? | |
|
89 | ||
|
90 | assert_difference 'Role.count' do | |
|
91 | post '/roles', role: { name: 'new role', issues_visibility: 'all', assignable: '1', permissions: %w(view_calendar) }, sudo_password: 'admin' | |
|
92 | end | |
|
93 | assert_redirected_to '/roles' | |
|
94 | end | |
|
95 | ||
|
96 | def test_update_email_address | |
|
97 | log_user 'jsmith', 'jsmith' | |
|
98 | get '/my/account' | |
|
99 | assert_response :success | |
|
100 | post '/my/account', user: { mail: 'newmail@test.com' } | |
|
101 | assert_response :success | |
|
102 | assert_select 'h2', 'Confirm your password to continue' | |
|
103 | assert_select 'form[action="/my/account"]' | |
|
104 | assert_match /"newmail@test\.com"/, response.body | |
|
105 | assert assigns(:sudo_form).errors.blank? | |
|
106 | ||
|
107 | # wrong password | |
|
108 | post '/my/account', user: { mail: 'newmail@test.com' }, sudo_password: 'wrong' | |
|
109 | assert_response :success | |
|
110 | assert_select 'h2', 'Confirm your password to continue' | |
|
111 | assert_select 'form[action="/my/account"]' | |
|
112 | assert_match /"newmail@test\.com"/, response.body | |
|
113 | assert assigns(:sudo_form).errors[:password].present? | |
|
114 | ||
|
115 | # correct password | |
|
116 | post '/my/account', user: { mail: 'newmail@test.com' }, sudo_password: 'jsmith' | |
|
117 | assert_redirected_to '/my/account' | |
|
118 | assert_equal 'newmail@test.com', User.find_by_login('jsmith').mail | |
|
119 | ||
|
120 | # sudo mode should now be active and not require password again | |
|
121 | post '/my/account', user: { mail: 'even.newer.mail@test.com' } | |
|
122 | assert_redirected_to '/my/account' | |
|
123 | assert_equal 'even.newer.mail@test.com', User.find_by_login('jsmith').mail | |
|
124 | end | |
|
125 | ||
|
126 | end |
@@ -1,660 +1,662 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require 'uri' |
|
19 | 19 | require 'cgi' |
|
20 | 20 | |
|
21 | 21 | class Unauthorized < Exception; end |
|
22 | 22 | |
|
23 | 23 | class ApplicationController < ActionController::Base |
|
24 | 24 | include Redmine::I18n |
|
25 | 25 | include Redmine::Pagination |
|
26 | 26 | include RoutesHelper |
|
27 | 27 | helper :routes |
|
28 | 28 | |
|
29 | 29 | class_attribute :accept_api_auth_actions |
|
30 | 30 | class_attribute :accept_rss_auth_actions |
|
31 | 31 | class_attribute :model_object |
|
32 | 32 | |
|
33 | 33 | layout 'base' |
|
34 | 34 | |
|
35 | 35 | protect_from_forgery |
|
36 | 36 | |
|
37 | 37 | def verify_authenticity_token |
|
38 | 38 | unless api_request? |
|
39 | 39 | super |
|
40 | 40 | end |
|
41 | 41 | end |
|
42 | 42 | |
|
43 | 43 | def handle_unverified_request |
|
44 | 44 | unless api_request? |
|
45 | 45 | super |
|
46 | 46 | cookies.delete(autologin_cookie_name) |
|
47 | 47 | self.logged_user = nil |
|
48 | 48 | set_localization |
|
49 | 49 | render_error :status => 422, :message => "Invalid form authenticity token." |
|
50 | 50 | end |
|
51 | 51 | end |
|
52 | 52 | |
|
53 | 53 | before_filter :session_expiration, :user_setup, :force_logout_if_password_changed, :check_if_login_required, :check_password_change, :set_localization |
|
54 | 54 | |
|
55 | 55 | rescue_from ::Unauthorized, :with => :deny_access |
|
56 | 56 | rescue_from ::ActionView::MissingTemplate, :with => :missing_template |
|
57 | 57 | |
|
58 | 58 | include Redmine::Search::Controller |
|
59 | 59 | include Redmine::MenuManager::MenuController |
|
60 | 60 | helper Redmine::MenuManager::MenuHelper |
|
61 | 61 | |
|
62 | include Redmine::SudoMode::Controller | |
|
63 | ||
|
62 | 64 | def session_expiration |
|
63 | 65 | if session[:user_id] |
|
64 | 66 | if session_expired? && !try_to_autologin |
|
65 | 67 | set_localization(User.active.find_by_id(session[:user_id])) |
|
66 | 68 | self.logged_user = nil |
|
67 | 69 | flash[:error] = l(:error_session_expired) |
|
68 | 70 | require_login |
|
69 | 71 | else |
|
70 | 72 | session[:atime] = Time.now.utc.to_i |
|
71 | 73 | end |
|
72 | 74 | end |
|
73 | 75 | end |
|
74 | 76 | |
|
75 | 77 | def session_expired? |
|
76 | 78 | if Setting.session_lifetime? |
|
77 | 79 | unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60) |
|
78 | 80 | return true |
|
79 | 81 | end |
|
80 | 82 | end |
|
81 | 83 | if Setting.session_timeout? |
|
82 | 84 | unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60) |
|
83 | 85 | return true |
|
84 | 86 | end |
|
85 | 87 | end |
|
86 | 88 | false |
|
87 | 89 | end |
|
88 | 90 | |
|
89 | 91 | def start_user_session(user) |
|
90 | 92 | session[:user_id] = user.id |
|
91 | 93 | session[:ctime] = Time.now.utc.to_i |
|
92 | 94 | session[:atime] = Time.now.utc.to_i |
|
93 | 95 | if user.must_change_password? |
|
94 | 96 | session[:pwd] = '1' |
|
95 | 97 | end |
|
96 | 98 | end |
|
97 | 99 | |
|
98 | 100 | def user_setup |
|
99 | 101 | # Check the settings cache for each request |
|
100 | 102 | Setting.check_cache |
|
101 | 103 | # Find the current user |
|
102 | 104 | User.current = find_current_user |
|
103 | 105 | logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger |
|
104 | 106 | end |
|
105 | 107 | |
|
106 | 108 | # Returns the current user or nil if no user is logged in |
|
107 | 109 | # and starts a session if needed |
|
108 | 110 | def find_current_user |
|
109 | 111 | user = nil |
|
110 | 112 | unless api_request? |
|
111 | 113 | if session[:user_id] |
|
112 | 114 | # existing session |
|
113 | 115 | user = (User.active.find(session[:user_id]) rescue nil) |
|
114 | 116 | elsif autologin_user = try_to_autologin |
|
115 | 117 | user = autologin_user |
|
116 | 118 | elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth? |
|
117 | 119 | # RSS key authentication does not start a session |
|
118 | 120 | user = User.find_by_rss_key(params[:key]) |
|
119 | 121 | end |
|
120 | 122 | end |
|
121 | 123 | if user.nil? && Setting.rest_api_enabled? && accept_api_auth? |
|
122 | 124 | if (key = api_key_from_request) |
|
123 | 125 | # Use API key |
|
124 | 126 | user = User.find_by_api_key(key) |
|
125 | 127 | elsif request.authorization.to_s =~ /\ABasic /i |
|
126 | 128 | # HTTP Basic, either username/password or API key/random |
|
127 | 129 | authenticate_with_http_basic do |username, password| |
|
128 | 130 | user = User.try_to_login(username, password) || User.find_by_api_key(username) |
|
129 | 131 | end |
|
130 | 132 | if user && user.must_change_password? |
|
131 | 133 | render_error :message => 'You must change your password', :status => 403 |
|
132 | 134 | return |
|
133 | 135 | end |
|
134 | 136 | end |
|
135 | 137 | # Switch user if requested by an admin user |
|
136 | 138 | if user && user.admin? && (username = api_switch_user_from_request) |
|
137 | 139 | su = User.find_by_login(username) |
|
138 | 140 | if su && su.active? |
|
139 | 141 | logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger |
|
140 | 142 | user = su |
|
141 | 143 | else |
|
142 | 144 | render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412 |
|
143 | 145 | end |
|
144 | 146 | end |
|
145 | 147 | end |
|
146 | 148 | user |
|
147 | 149 | end |
|
148 | 150 | |
|
149 | 151 | def force_logout_if_password_changed |
|
150 | 152 | passwd_changed_on = User.current.passwd_changed_on || Time.at(0) |
|
151 | 153 | # Make sure we force logout only for web browser sessions, not API calls |
|
152 | 154 | # if the password was changed after the session creation. |
|
153 | 155 | if session[:user_id] && passwd_changed_on.utc.to_i > session[:ctime].to_i |
|
154 | 156 | reset_session |
|
155 | 157 | set_localization |
|
156 | 158 | flash[:error] = l(:error_session_expired) |
|
157 | 159 | redirect_to signin_url |
|
158 | 160 | end |
|
159 | 161 | end |
|
160 | 162 | |
|
161 | 163 | def autologin_cookie_name |
|
162 | 164 | Redmine::Configuration['autologin_cookie_name'].presence || 'autologin' |
|
163 | 165 | end |
|
164 | 166 | |
|
165 | 167 | def try_to_autologin |
|
166 | 168 | if cookies[autologin_cookie_name] && Setting.autologin? |
|
167 | 169 | # auto-login feature starts a new session |
|
168 | 170 | user = User.try_to_autologin(cookies[autologin_cookie_name]) |
|
169 | 171 | if user |
|
170 | 172 | reset_session |
|
171 | 173 | start_user_session(user) |
|
172 | 174 | end |
|
173 | 175 | user |
|
174 | 176 | end |
|
175 | 177 | end |
|
176 | 178 | |
|
177 | 179 | # Sets the logged in user |
|
178 | 180 | def logged_user=(user) |
|
179 | 181 | reset_session |
|
180 | 182 | if user && user.is_a?(User) |
|
181 | 183 | User.current = user |
|
182 | 184 | start_user_session(user) |
|
183 | 185 | else |
|
184 | 186 | User.current = User.anonymous |
|
185 | 187 | end |
|
186 | 188 | end |
|
187 | 189 | |
|
188 | 190 | # Logs out current user |
|
189 | 191 | def logout_user |
|
190 | 192 | if User.current.logged? |
|
191 | 193 | cookies.delete(autologin_cookie_name) |
|
192 | 194 | Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) |
|
193 | 195 | self.logged_user = nil |
|
194 | 196 | end |
|
195 | 197 | end |
|
196 | 198 | |
|
197 | 199 | # check if login is globally required to access the application |
|
198 | 200 | def check_if_login_required |
|
199 | 201 | # no check needed if user is already logged in |
|
200 | 202 | return true if User.current.logged? |
|
201 | 203 | require_login if Setting.login_required? |
|
202 | 204 | end |
|
203 | 205 | |
|
204 | 206 | def check_password_change |
|
205 | 207 | if session[:pwd] |
|
206 | 208 | if User.current.must_change_password? |
|
207 | 209 | flash[:error] = l(:error_password_expired) |
|
208 | 210 | redirect_to my_password_path |
|
209 | 211 | else |
|
210 | 212 | session.delete(:pwd) |
|
211 | 213 | end |
|
212 | 214 | end |
|
213 | 215 | end |
|
214 | 216 | |
|
215 | 217 | def set_localization(user=User.current) |
|
216 | 218 | lang = nil |
|
217 | 219 | if user && user.logged? |
|
218 | 220 | lang = find_language(user.language) |
|
219 | 221 | end |
|
220 | 222 | if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE'] |
|
221 | 223 | accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first |
|
222 | 224 | if !accept_lang.blank? |
|
223 | 225 | accept_lang = accept_lang.downcase |
|
224 | 226 | lang = find_language(accept_lang) || find_language(accept_lang.split('-').first) |
|
225 | 227 | end |
|
226 | 228 | end |
|
227 | 229 | lang ||= Setting.default_language |
|
228 | 230 | set_language_if_valid(lang) |
|
229 | 231 | end |
|
230 | 232 | |
|
231 | 233 | def require_login |
|
232 | 234 | if !User.current.logged? |
|
233 | 235 | # Extract only the basic url parameters on non-GET requests |
|
234 | 236 | if request.get? |
|
235 | 237 | url = url_for(params) |
|
236 | 238 | else |
|
237 | 239 | url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id]) |
|
238 | 240 | end |
|
239 | 241 | respond_to do |format| |
|
240 | 242 | format.html { |
|
241 | 243 | if request.xhr? |
|
242 | 244 | head :unauthorized |
|
243 | 245 | else |
|
244 | 246 | redirect_to signin_path(:back_url => url) |
|
245 | 247 | end |
|
246 | 248 | } |
|
247 | 249 | format.any(:atom, :pdf, :csv) { |
|
248 | 250 | redirect_to signin_path(:back_url => url) |
|
249 | 251 | } |
|
250 | 252 | format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } |
|
251 | 253 | format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } |
|
252 | 254 | format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } |
|
253 | 255 | format.any { head :unauthorized } |
|
254 | 256 | end |
|
255 | 257 | return false |
|
256 | 258 | end |
|
257 | 259 | true |
|
258 | 260 | end |
|
259 | 261 | |
|
260 | 262 | def require_admin |
|
261 | 263 | return unless require_login |
|
262 | 264 | if !User.current.admin? |
|
263 | 265 | render_403 |
|
264 | 266 | return false |
|
265 | 267 | end |
|
266 | 268 | true |
|
267 | 269 | end |
|
268 | 270 | |
|
269 | 271 | def deny_access |
|
270 | 272 | User.current.logged? ? render_403 : require_login |
|
271 | 273 | end |
|
272 | 274 | |
|
273 | 275 | # Authorize the user for the requested action |
|
274 | 276 | def authorize(ctrl = params[:controller], action = params[:action], global = false) |
|
275 | 277 | allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global) |
|
276 | 278 | if allowed |
|
277 | 279 | true |
|
278 | 280 | else |
|
279 | 281 | if @project && @project.archived? |
|
280 | 282 | render_403 :message => :notice_not_authorized_archived_project |
|
281 | 283 | else |
|
282 | 284 | deny_access |
|
283 | 285 | end |
|
284 | 286 | end |
|
285 | 287 | end |
|
286 | 288 | |
|
287 | 289 | # Authorize the user for the requested action outside a project |
|
288 | 290 | def authorize_global(ctrl = params[:controller], action = params[:action], global = true) |
|
289 | 291 | authorize(ctrl, action, global) |
|
290 | 292 | end |
|
291 | 293 | |
|
292 | 294 | # Find project of id params[:id] |
|
293 | 295 | def find_project |
|
294 | 296 | @project = Project.find(params[:id]) |
|
295 | 297 | rescue ActiveRecord::RecordNotFound |
|
296 | 298 | render_404 |
|
297 | 299 | end |
|
298 | 300 | |
|
299 | 301 | # Find project of id params[:project_id] |
|
300 | 302 | def find_project_by_project_id |
|
301 | 303 | @project = Project.find(params[:project_id]) |
|
302 | 304 | rescue ActiveRecord::RecordNotFound |
|
303 | 305 | render_404 |
|
304 | 306 | end |
|
305 | 307 | |
|
306 | 308 | # Find a project based on params[:project_id] |
|
307 | 309 | # TODO: some subclasses override this, see about merging their logic |
|
308 | 310 | def find_optional_project |
|
309 | 311 | @project = Project.find(params[:project_id]) unless params[:project_id].blank? |
|
310 | 312 | allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true) |
|
311 | 313 | allowed ? true : deny_access |
|
312 | 314 | rescue ActiveRecord::RecordNotFound |
|
313 | 315 | render_404 |
|
314 | 316 | end |
|
315 | 317 | |
|
316 | 318 | # Finds and sets @project based on @object.project |
|
317 | 319 | def find_project_from_association |
|
318 | 320 | render_404 unless @object.present? |
|
319 | 321 | |
|
320 | 322 | @project = @object.project |
|
321 | 323 | end |
|
322 | 324 | |
|
323 | 325 | def find_model_object |
|
324 | 326 | model = self.class.model_object |
|
325 | 327 | if model |
|
326 | 328 | @object = model.find(params[:id]) |
|
327 | 329 | self.instance_variable_set('@' + controller_name.singularize, @object) if @object |
|
328 | 330 | end |
|
329 | 331 | rescue ActiveRecord::RecordNotFound |
|
330 | 332 | render_404 |
|
331 | 333 | end |
|
332 | 334 | |
|
333 | 335 | def self.model_object(model) |
|
334 | 336 | self.model_object = model |
|
335 | 337 | end |
|
336 | 338 | |
|
337 | 339 | # Find the issue whose id is the :id parameter |
|
338 | 340 | # Raises a Unauthorized exception if the issue is not visible |
|
339 | 341 | def find_issue |
|
340 | 342 | # Issue.visible.find(...) can not be used to redirect user to the login form |
|
341 | 343 | # if the issue actually exists but requires authentication |
|
342 | 344 | @issue = Issue.find(params[:id]) |
|
343 | 345 | raise Unauthorized unless @issue.visible? |
|
344 | 346 | @project = @issue.project |
|
345 | 347 | rescue ActiveRecord::RecordNotFound |
|
346 | 348 | render_404 |
|
347 | 349 | end |
|
348 | 350 | |
|
349 | 351 | # Find issues with a single :id param or :ids array param |
|
350 | 352 | # Raises a Unauthorized exception if one of the issues is not visible |
|
351 | 353 | def find_issues |
|
352 | 354 | @issues = Issue.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a |
|
353 | 355 | raise ActiveRecord::RecordNotFound if @issues.empty? |
|
354 | 356 | raise Unauthorized unless @issues.all?(&:visible?) |
|
355 | 357 | @projects = @issues.collect(&:project).compact.uniq |
|
356 | 358 | @project = @projects.first if @projects.size == 1 |
|
357 | 359 | rescue ActiveRecord::RecordNotFound |
|
358 | 360 | render_404 |
|
359 | 361 | end |
|
360 | 362 | |
|
361 | 363 | def find_attachments |
|
362 | 364 | if (attachments = params[:attachments]).present? |
|
363 | 365 | att = attachments.values.collect do |attachment| |
|
364 | 366 | Attachment.find_by_token( attachment[:token] ) if attachment[:token].present? |
|
365 | 367 | end |
|
366 | 368 | att.compact! |
|
367 | 369 | end |
|
368 | 370 | @attachments = att || [] |
|
369 | 371 | end |
|
370 | 372 | |
|
371 | 373 | # make sure that the user is a member of the project (or admin) if project is private |
|
372 | 374 | # used as a before_filter for actions that do not require any particular permission on the project |
|
373 | 375 | def check_project_privacy |
|
374 | 376 | if @project && !@project.archived? |
|
375 | 377 | if @project.visible? |
|
376 | 378 | true |
|
377 | 379 | else |
|
378 | 380 | deny_access |
|
379 | 381 | end |
|
380 | 382 | else |
|
381 | 383 | @project = nil |
|
382 | 384 | render_404 |
|
383 | 385 | false |
|
384 | 386 | end |
|
385 | 387 | end |
|
386 | 388 | |
|
387 | 389 | def back_url |
|
388 | 390 | url = params[:back_url] |
|
389 | 391 | if url.nil? && referer = request.env['HTTP_REFERER'] |
|
390 | 392 | url = CGI.unescape(referer.to_s) |
|
391 | 393 | end |
|
392 | 394 | url |
|
393 | 395 | end |
|
394 | 396 | |
|
395 | 397 | def redirect_back_or_default(default, options={}) |
|
396 | 398 | back_url = params[:back_url].to_s |
|
397 | 399 | if back_url.present? && valid_back_url?(back_url) |
|
398 | 400 | redirect_to(back_url) |
|
399 | 401 | return |
|
400 | 402 | elsif options[:referer] |
|
401 | 403 | redirect_to_referer_or default |
|
402 | 404 | return |
|
403 | 405 | end |
|
404 | 406 | redirect_to default |
|
405 | 407 | false |
|
406 | 408 | end |
|
407 | 409 | |
|
408 | 410 | # Returns true if back_url is a valid url for redirection, otherwise false |
|
409 | 411 | def valid_back_url?(back_url) |
|
410 | 412 | if CGI.unescape(back_url).include?('..') |
|
411 | 413 | return false |
|
412 | 414 | end |
|
413 | 415 | |
|
414 | 416 | begin |
|
415 | 417 | uri = URI.parse(back_url) |
|
416 | 418 | rescue URI::InvalidURIError |
|
417 | 419 | return false |
|
418 | 420 | end |
|
419 | 421 | |
|
420 | 422 | if uri.host.present? && uri.host != request.host |
|
421 | 423 | return false |
|
422 | 424 | end |
|
423 | 425 | |
|
424 | 426 | if uri.path.match(%r{/(login|account/register)}) |
|
425 | 427 | return false |
|
426 | 428 | end |
|
427 | 429 | |
|
428 | 430 | if relative_url_root.present? && !uri.path.starts_with?(relative_url_root) |
|
429 | 431 | return false |
|
430 | 432 | end |
|
431 | 433 | |
|
432 | 434 | return true |
|
433 | 435 | end |
|
434 | 436 | private :valid_back_url? |
|
435 | 437 | |
|
436 | 438 | # Redirects to the request referer if present, redirects to args or call block otherwise. |
|
437 | 439 | def redirect_to_referer_or(*args, &block) |
|
438 | 440 | redirect_to :back |
|
439 | 441 | rescue ::ActionController::RedirectBackError |
|
440 | 442 | if args.any? |
|
441 | 443 | redirect_to *args |
|
442 | 444 | elsif block_given? |
|
443 | 445 | block.call |
|
444 | 446 | else |
|
445 | 447 | raise "#redirect_to_referer_or takes arguments or a block" |
|
446 | 448 | end |
|
447 | 449 | end |
|
448 | 450 | |
|
449 | 451 | def render_403(options={}) |
|
450 | 452 | @project = nil |
|
451 | 453 | render_error({:message => :notice_not_authorized, :status => 403}.merge(options)) |
|
452 | 454 | return false |
|
453 | 455 | end |
|
454 | 456 | |
|
455 | 457 | def render_404(options={}) |
|
456 | 458 | render_error({:message => :notice_file_not_found, :status => 404}.merge(options)) |
|
457 | 459 | return false |
|
458 | 460 | end |
|
459 | 461 | |
|
460 | 462 | # Renders an error response |
|
461 | 463 | def render_error(arg) |
|
462 | 464 | arg = {:message => arg} unless arg.is_a?(Hash) |
|
463 | 465 | |
|
464 | 466 | @message = arg[:message] |
|
465 | 467 | @message = l(@message) if @message.is_a?(Symbol) |
|
466 | 468 | @status = arg[:status] || 500 |
|
467 | 469 | |
|
468 | 470 | respond_to do |format| |
|
469 | 471 | format.html { |
|
470 | 472 | render :template => 'common/error', :layout => use_layout, :status => @status |
|
471 | 473 | } |
|
472 | 474 | format.any { head @status } |
|
473 | 475 | end |
|
474 | 476 | end |
|
475 | 477 | |
|
476 | 478 | # Handler for ActionView::MissingTemplate exception |
|
477 | 479 | def missing_template |
|
478 | 480 | logger.warn "Missing template, responding with 404" |
|
479 | 481 | @project = nil |
|
480 | 482 | render_404 |
|
481 | 483 | end |
|
482 | 484 | |
|
483 | 485 | # Filter for actions that provide an API response |
|
484 | 486 | # but have no HTML representation for non admin users |
|
485 | 487 | def require_admin_or_api_request |
|
486 | 488 | return true if api_request? |
|
487 | 489 | if User.current.admin? |
|
488 | 490 | true |
|
489 | 491 | elsif User.current.logged? |
|
490 | 492 | render_error(:status => 406) |
|
491 | 493 | else |
|
492 | 494 | deny_access |
|
493 | 495 | end |
|
494 | 496 | end |
|
495 | 497 | |
|
496 | 498 | # Picks which layout to use based on the request |
|
497 | 499 | # |
|
498 | 500 | # @return [boolean, string] name of the layout to use or false for no layout |
|
499 | 501 | def use_layout |
|
500 | 502 | request.xhr? ? false : 'base' |
|
501 | 503 | end |
|
502 | 504 | |
|
503 | 505 | def render_feed(items, options={}) |
|
504 | 506 | @items = (items || []).to_a |
|
505 | 507 | @items.sort! {|x,y| y.event_datetime <=> x.event_datetime } |
|
506 | 508 | @items = @items.slice(0, Setting.feeds_limit.to_i) |
|
507 | 509 | @title = options[:title] || Setting.app_title |
|
508 | 510 | render :template => "common/feed", :formats => [:atom], :layout => false, |
|
509 | 511 | :content_type => 'application/atom+xml' |
|
510 | 512 | end |
|
511 | 513 | |
|
512 | 514 | def self.accept_rss_auth(*actions) |
|
513 | 515 | if actions.any? |
|
514 | 516 | self.accept_rss_auth_actions = actions |
|
515 | 517 | else |
|
516 | 518 | self.accept_rss_auth_actions || [] |
|
517 | 519 | end |
|
518 | 520 | end |
|
519 | 521 | |
|
520 | 522 | def accept_rss_auth?(action=action_name) |
|
521 | 523 | self.class.accept_rss_auth.include?(action.to_sym) |
|
522 | 524 | end |
|
523 | 525 | |
|
524 | 526 | def self.accept_api_auth(*actions) |
|
525 | 527 | if actions.any? |
|
526 | 528 | self.accept_api_auth_actions = actions |
|
527 | 529 | else |
|
528 | 530 | self.accept_api_auth_actions || [] |
|
529 | 531 | end |
|
530 | 532 | end |
|
531 | 533 | |
|
532 | 534 | def accept_api_auth?(action=action_name) |
|
533 | 535 | self.class.accept_api_auth.include?(action.to_sym) |
|
534 | 536 | end |
|
535 | 537 | |
|
536 | 538 | # Returns the number of objects that should be displayed |
|
537 | 539 | # on the paginated list |
|
538 | 540 | def per_page_option |
|
539 | 541 | per_page = nil |
|
540 | 542 | if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i) |
|
541 | 543 | per_page = params[:per_page].to_s.to_i |
|
542 | 544 | session[:per_page] = per_page |
|
543 | 545 | elsif session[:per_page] |
|
544 | 546 | per_page = session[:per_page] |
|
545 | 547 | else |
|
546 | 548 | per_page = Setting.per_page_options_array.first || 25 |
|
547 | 549 | end |
|
548 | 550 | per_page |
|
549 | 551 | end |
|
550 | 552 | |
|
551 | 553 | # Returns offset and limit used to retrieve objects |
|
552 | 554 | # for an API response based on offset, limit and page parameters |
|
553 | 555 | def api_offset_and_limit(options=params) |
|
554 | 556 | if options[:offset].present? |
|
555 | 557 | offset = options[:offset].to_i |
|
556 | 558 | if offset < 0 |
|
557 | 559 | offset = 0 |
|
558 | 560 | end |
|
559 | 561 | end |
|
560 | 562 | limit = options[:limit].to_i |
|
561 | 563 | if limit < 1 |
|
562 | 564 | limit = 25 |
|
563 | 565 | elsif limit > 100 |
|
564 | 566 | limit = 100 |
|
565 | 567 | end |
|
566 | 568 | if offset.nil? && options[:page].present? |
|
567 | 569 | offset = (options[:page].to_i - 1) * limit |
|
568 | 570 | offset = 0 if offset < 0 |
|
569 | 571 | end |
|
570 | 572 | offset ||= 0 |
|
571 | 573 | |
|
572 | 574 | [offset, limit] |
|
573 | 575 | end |
|
574 | 576 | |
|
575 | 577 | # qvalues http header parser |
|
576 | 578 | # code taken from webrick |
|
577 | 579 | def parse_qvalues(value) |
|
578 | 580 | tmp = [] |
|
579 | 581 | if value |
|
580 | 582 | parts = value.split(/,\s*/) |
|
581 | 583 | parts.each {|part| |
|
582 | 584 | if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part) |
|
583 | 585 | val = m[1] |
|
584 | 586 | q = (m[2] or 1).to_f |
|
585 | 587 | tmp.push([val, q]) |
|
586 | 588 | end |
|
587 | 589 | } |
|
588 | 590 | tmp = tmp.sort_by{|val, q| -q} |
|
589 | 591 | tmp.collect!{|val, q| val} |
|
590 | 592 | end |
|
591 | 593 | return tmp |
|
592 | 594 | rescue |
|
593 | 595 | nil |
|
594 | 596 | end |
|
595 | 597 | |
|
596 | 598 | # Returns a string that can be used as filename value in Content-Disposition header |
|
597 | 599 | def filename_for_content_disposition(name) |
|
598 | 600 | request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident)} ? ERB::Util.url_encode(name) : name |
|
599 | 601 | end |
|
600 | 602 | |
|
601 | 603 | def api_request? |
|
602 | 604 | %w(xml json).include? params[:format] |
|
603 | 605 | end |
|
604 | 606 | |
|
605 | 607 | # Returns the API key present in the request |
|
606 | 608 | def api_key_from_request |
|
607 | 609 | if params[:key].present? |
|
608 | 610 | params[:key].to_s |
|
609 | 611 | elsif request.headers["X-Redmine-API-Key"].present? |
|
610 | 612 | request.headers["X-Redmine-API-Key"].to_s |
|
611 | 613 | end |
|
612 | 614 | end |
|
613 | 615 | |
|
614 | 616 | # Returns the API 'switch user' value if present |
|
615 | 617 | def api_switch_user_from_request |
|
616 | 618 | request.headers["X-Redmine-Switch-User"].to_s.presence |
|
617 | 619 | end |
|
618 | 620 | |
|
619 | 621 | # Renders a warning flash if obj has unsaved attachments |
|
620 | 622 | def render_attachment_warning_if_needed(obj) |
|
621 | 623 | flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present? |
|
622 | 624 | end |
|
623 | 625 | |
|
624 | 626 | # Rescues an invalid query statement. Just in case... |
|
625 | 627 | def query_statement_invalid(exception) |
|
626 | 628 | logger.error "Query::StatementInvalid: #{exception.message}" if logger |
|
627 | 629 | session.delete(:query) |
|
628 | 630 | sort_clear if respond_to?(:sort_clear) |
|
629 | 631 | render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator." |
|
630 | 632 | end |
|
631 | 633 | |
|
632 | 634 | # Renders a 200 response for successfull updates or deletions via the API |
|
633 | 635 | def render_api_ok |
|
634 | 636 | render_api_head :ok |
|
635 | 637 | end |
|
636 | 638 | |
|
637 | 639 | # Renders a head API response |
|
638 | 640 | def render_api_head(status) |
|
639 | 641 | # #head would return a response body with one space |
|
640 | 642 | render :text => '', :status => status, :layout => nil |
|
641 | 643 | end |
|
642 | 644 | |
|
643 | 645 | # Renders API response on validation failure |
|
644 | 646 | # for an object or an array of objects |
|
645 | 647 | def render_validation_errors(objects) |
|
646 | 648 | messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten |
|
647 | 649 | render_api_errors(messages) |
|
648 | 650 | end |
|
649 | 651 | |
|
650 | 652 | def render_api_errors(*messages) |
|
651 | 653 | @error_messages = messages.flatten |
|
652 | 654 | render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil |
|
653 | 655 | end |
|
654 | 656 | |
|
655 | 657 | # Overrides #_include_layout? so that #render with no arguments |
|
656 | 658 | # doesn't use the layout for api requests |
|
657 | 659 | def _include_layout?(*args) |
|
658 | 660 | api_request? ? false : super |
|
659 | 661 | end |
|
660 | 662 | end |
@@ -1,96 +1,97 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class AuthSourcesController < ApplicationController |
|
19 | 19 | layout 'admin' |
|
20 | 20 | menu_item :ldap_authentication |
|
21 | 21 | |
|
22 | 22 | before_filter :require_admin |
|
23 | 23 | before_filter :find_auth_source, :only => [:edit, :update, :test_connection, :destroy] |
|
24 | require_sudo_mode :update, :destroy | |
|
24 | 25 | |
|
25 | 26 | def index |
|
26 | 27 | @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25 |
|
27 | 28 | end |
|
28 | 29 | |
|
29 | 30 | def new |
|
30 | 31 | klass_name = params[:type] || 'AuthSourceLdap' |
|
31 | 32 | @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source]) |
|
32 | 33 | render_404 unless @auth_source |
|
33 | 34 | end |
|
34 | 35 | |
|
35 | 36 | def create |
|
36 | 37 | @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source]) |
|
37 | 38 | if @auth_source.save |
|
38 | 39 | flash[:notice] = l(:notice_successful_create) |
|
39 | 40 | redirect_to auth_sources_path |
|
40 | 41 | else |
|
41 | 42 | render :action => 'new' |
|
42 | 43 | end |
|
43 | 44 | end |
|
44 | 45 | |
|
45 | 46 | def edit |
|
46 | 47 | end |
|
47 | 48 | |
|
48 | 49 | def update |
|
49 | 50 | if @auth_source.update_attributes(params[:auth_source]) |
|
50 | 51 | flash[:notice] = l(:notice_successful_update) |
|
51 | 52 | redirect_to auth_sources_path |
|
52 | 53 | else |
|
53 | 54 | render :action => 'edit' |
|
54 | 55 | end |
|
55 | 56 | end |
|
56 | 57 | |
|
57 | 58 | def test_connection |
|
58 | 59 | begin |
|
59 | 60 | @auth_source.test_connection |
|
60 | 61 | flash[:notice] = l(:notice_successful_connection) |
|
61 | 62 | rescue Exception => e |
|
62 | 63 | flash[:error] = l(:error_unable_to_connect, e.message) |
|
63 | 64 | end |
|
64 | 65 | redirect_to auth_sources_path |
|
65 | 66 | end |
|
66 | 67 | |
|
67 | 68 | def destroy |
|
68 | 69 | unless @auth_source.users.exists? |
|
69 | 70 | @auth_source.destroy |
|
70 | 71 | flash[:notice] = l(:notice_successful_delete) |
|
71 | 72 | end |
|
72 | 73 | redirect_to auth_sources_path |
|
73 | 74 | end |
|
74 | 75 | |
|
75 | 76 | def autocomplete_for_new_user |
|
76 | 77 | results = AuthSource.search(params[:term]) |
|
77 | 78 | |
|
78 | 79 | render :json => results.map {|result| { |
|
79 | 80 | 'value' => result[:login], |
|
80 | 81 | 'label' => "#{result[:login]} (#{result[:firstname]} #{result[:lastname]})", |
|
81 | 82 | 'login' => result[:login].to_s, |
|
82 | 83 | 'firstname' => result[:firstname].to_s, |
|
83 | 84 | 'lastname' => result[:lastname].to_s, |
|
84 | 85 | 'mail' => result[:mail].to_s, |
|
85 | 86 | 'auth_source_id' => result[:auth_source_id].to_s |
|
86 | 87 | }} |
|
87 | 88 | end |
|
88 | 89 | |
|
89 | 90 | private |
|
90 | 91 | |
|
91 | 92 | def find_auth_source |
|
92 | 93 | @auth_source = AuthSource.find(params[:id]) |
|
93 | 94 | rescue ActiveRecord::RecordNotFound |
|
94 | 95 | render_404 |
|
95 | 96 | end |
|
96 | 97 | end |
@@ -1,105 +1,106 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class EmailAddressesController < ApplicationController |
|
19 | 19 | before_filter :find_user, :require_admin_or_current_user |
|
20 | 20 | before_filter :find_email_address, :only => [:update, :destroy] |
|
21 | require_sudo_mode :create, :update, :destroy | |
|
21 | 22 | |
|
22 | 23 | def index |
|
23 | 24 | @addresses = @user.email_addresses.order(:id).where(:is_default => false).to_a |
|
24 | 25 | @address ||= EmailAddress.new |
|
25 | 26 | end |
|
26 | 27 | |
|
27 | 28 | def create |
|
28 | 29 | saved = false |
|
29 | 30 | if @user.email_addresses.count <= Setting.max_additional_emails.to_i |
|
30 | 31 | @address = EmailAddress.new(:user => @user, :is_default => false) |
|
31 | 32 | attrs = params[:email_address] |
|
32 | 33 | if attrs.is_a?(Hash) |
|
33 | 34 | @address.address = attrs[:address].to_s |
|
34 | 35 | end |
|
35 | 36 | saved = @address.save |
|
36 | 37 | end |
|
37 | 38 | |
|
38 | 39 | respond_to do |format| |
|
39 | 40 | format.html { |
|
40 | 41 | if saved |
|
41 | 42 | redirect_to user_email_addresses_path(@user) |
|
42 | 43 | else |
|
43 | 44 | index |
|
44 | 45 | render :action => 'index' |
|
45 | 46 | end |
|
46 | 47 | } |
|
47 | 48 | format.js { |
|
48 | 49 | @address = nil if saved |
|
49 | 50 | index |
|
50 | 51 | render :action => 'index' |
|
51 | 52 | } |
|
52 | 53 | end |
|
53 | 54 | end |
|
54 | 55 | |
|
55 | 56 | def update |
|
56 | 57 | if params[:notify].present? |
|
57 | 58 | @address.notify = params[:notify].to_s |
|
58 | 59 | end |
|
59 | 60 | @address.save |
|
60 | 61 | |
|
61 | 62 | respond_to do |format| |
|
62 | 63 | format.html { |
|
63 | 64 | redirect_to user_email_addresses_path(@user) |
|
64 | 65 | } |
|
65 | 66 | format.js { |
|
66 | 67 | @address = nil |
|
67 | 68 | index |
|
68 | 69 | render :action => 'index' |
|
69 | 70 | } |
|
70 | 71 | end |
|
71 | 72 | end |
|
72 | 73 | |
|
73 | 74 | def destroy |
|
74 | 75 | @address.destroy |
|
75 | 76 | |
|
76 | 77 | respond_to do |format| |
|
77 | 78 | format.html { |
|
78 | 79 | redirect_to user_email_addresses_path(@user) |
|
79 | 80 | } |
|
80 | 81 | format.js { |
|
81 | 82 | @address = nil |
|
82 | 83 | index |
|
83 | 84 | render :action => 'index' |
|
84 | 85 | } |
|
85 | 86 | end |
|
86 | 87 | end |
|
87 | 88 | |
|
88 | 89 | private |
|
89 | 90 | |
|
90 | 91 | def find_user |
|
91 | 92 | @user = User.find(params[:user_id]) |
|
92 | 93 | end |
|
93 | 94 | |
|
94 | 95 | def find_email_address |
|
95 | 96 | @address = @user.email_addresses.where(:is_default => false).find(params[:id]) |
|
96 | 97 | rescue ActiveRecord::RecordNotFound |
|
97 | 98 | render_404 |
|
98 | 99 | end |
|
99 | 100 | |
|
100 | 101 | def require_admin_or_current_user |
|
101 | 102 | unless @user == User.current |
|
102 | 103 | require_admin |
|
103 | 104 | end |
|
104 | 105 | end |
|
105 | 106 | end |
@@ -1,147 +1,149 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class GroupsController < ApplicationController |
|
19 | 19 | layout 'admin' |
|
20 | 20 | |
|
21 | 21 | before_filter :require_admin |
|
22 | 22 | before_filter :find_group, :except => [:index, :new, :create] |
|
23 | 23 | accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user |
|
24 | 24 | |
|
25 | require_sudo_mode :add_users, :remove_user, :create, :update, :destroy, :edit_membership, :destroy_membership | |
|
26 | ||
|
25 | 27 | helper :custom_fields |
|
26 | 28 | helper :principal_memberships |
|
27 | 29 | |
|
28 | 30 | def index |
|
29 | 31 | respond_to do |format| |
|
30 | 32 | format.html { |
|
31 | 33 | @groups = Group.sorted.to_a |
|
32 | 34 | @user_count_by_group_id = user_count_by_group_id |
|
33 | 35 | } |
|
34 | 36 | format.api { |
|
35 | 37 | scope = Group.sorted |
|
36 | 38 | scope = scope.givable unless params[:builtin] == '1' |
|
37 | 39 | @groups = scope.to_a |
|
38 | 40 | } |
|
39 | 41 | end |
|
40 | 42 | end |
|
41 | 43 | |
|
42 | 44 | def show |
|
43 | 45 | respond_to do |format| |
|
44 | 46 | format.html |
|
45 | 47 | format.api |
|
46 | 48 | end |
|
47 | 49 | end |
|
48 | 50 | |
|
49 | 51 | def new |
|
50 | 52 | @group = Group.new |
|
51 | 53 | end |
|
52 | 54 | |
|
53 | 55 | def create |
|
54 | 56 | @group = Group.new |
|
55 | 57 | @group.safe_attributes = params[:group] |
|
56 | 58 | |
|
57 | 59 | respond_to do |format| |
|
58 | 60 | if @group.save |
|
59 | 61 | format.html { |
|
60 | 62 | flash[:notice] = l(:notice_successful_create) |
|
61 | 63 | redirect_to(params[:continue] ? new_group_path : groups_path) |
|
62 | 64 | } |
|
63 | 65 | format.api { render :action => 'show', :status => :created, :location => group_url(@group) } |
|
64 | 66 | else |
|
65 | 67 | format.html { render :action => "new" } |
|
66 | 68 | format.api { render_validation_errors(@group) } |
|
67 | 69 | end |
|
68 | 70 | end |
|
69 | 71 | end |
|
70 | 72 | |
|
71 | 73 | def edit |
|
72 | 74 | end |
|
73 | 75 | |
|
74 | 76 | def update |
|
75 | 77 | @group.safe_attributes = params[:group] |
|
76 | 78 | |
|
77 | 79 | respond_to do |format| |
|
78 | 80 | if @group.save |
|
79 | 81 | flash[:notice] = l(:notice_successful_update) |
|
80 | 82 | format.html { redirect_to(groups_path) } |
|
81 | 83 | format.api { render_api_ok } |
|
82 | 84 | else |
|
83 | 85 | format.html { render :action => "edit" } |
|
84 | 86 | format.api { render_validation_errors(@group) } |
|
85 | 87 | end |
|
86 | 88 | end |
|
87 | 89 | end |
|
88 | 90 | |
|
89 | 91 | def destroy |
|
90 | 92 | @group.destroy |
|
91 | 93 | |
|
92 | 94 | respond_to do |format| |
|
93 | 95 | format.html { redirect_to(groups_path) } |
|
94 | 96 | format.api { render_api_ok } |
|
95 | 97 | end |
|
96 | 98 | end |
|
97 | 99 | |
|
98 | 100 | def new_users |
|
99 | 101 | end |
|
100 | 102 | |
|
101 | 103 | def add_users |
|
102 | 104 | @users = User.not_in_group(@group).where(:id => (params[:user_id] || params[:user_ids])).to_a |
|
103 | 105 | @group.users << @users |
|
104 | 106 | respond_to do |format| |
|
105 | 107 | format.html { redirect_to edit_group_path(@group, :tab => 'users') } |
|
106 | 108 | format.js |
|
107 | 109 | format.api { |
|
108 | 110 | if @users.any? |
|
109 | 111 | render_api_ok |
|
110 | 112 | else |
|
111 | 113 | render_api_errors "#{l(:label_user)} #{l('activerecord.errors.messages.invalid')}" |
|
112 | 114 | end |
|
113 | 115 | } |
|
114 | 116 | end |
|
115 | 117 | end |
|
116 | 118 | |
|
117 | 119 | def remove_user |
|
118 | 120 | @group.users.delete(User.find(params[:user_id])) if request.delete? |
|
119 | 121 | respond_to do |format| |
|
120 | 122 | format.html { redirect_to edit_group_path(@group, :tab => 'users') } |
|
121 | 123 | format.js |
|
122 | 124 | format.api { render_api_ok } |
|
123 | 125 | end |
|
124 | 126 | end |
|
125 | 127 | |
|
126 | 128 | def autocomplete_for_user |
|
127 | 129 | respond_to do |format| |
|
128 | 130 | format.js |
|
129 | 131 | end |
|
130 | 132 | end |
|
131 | 133 | |
|
132 | 134 | private |
|
133 | 135 | |
|
134 | 136 | def find_group |
|
135 | 137 | @group = Group.find(params[:id]) |
|
136 | 138 | rescue ActiveRecord::RecordNotFound |
|
137 | 139 | render_404 |
|
138 | 140 | end |
|
139 | 141 | |
|
140 | 142 | def user_count_by_group_id |
|
141 | 143 | h = User.joins(:groups).group('group_id').count |
|
142 | 144 | h.keys.each do |key| |
|
143 | 145 | h[key.to_i] = h.delete(key) |
|
144 | 146 | end |
|
145 | 147 | h |
|
146 | 148 | end |
|
147 | 149 | end |
@@ -1,127 +1,129 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class MembersController < ApplicationController |
|
19 | 19 | model_object Member |
|
20 | 20 | before_filter :find_model_object, :except => [:index, :new, :create, :autocomplete] |
|
21 | 21 | before_filter :find_project_from_association, :except => [:index, :new, :create, :autocomplete] |
|
22 | 22 | before_filter :find_project_by_project_id, :only => [:index, :new, :create, :autocomplete] |
|
23 | 23 | before_filter :authorize |
|
24 | 24 | accept_api_auth :index, :show, :create, :update, :destroy |
|
25 | 25 | |
|
26 | require_sudo_mode :create, :update, :destroy | |
|
27 | ||
|
26 | 28 | def index |
|
27 | 29 | scope = @project.memberships.active |
|
28 | 30 | @offset, @limit = api_offset_and_limit |
|
29 | 31 | @member_count = scope.count |
|
30 | 32 | @member_pages = Paginator.new @member_count, @limit, params['page'] |
|
31 | 33 | @offset ||= @member_pages.offset |
|
32 | 34 | @members = scope.order(:id).limit(@limit).offset(@offset).to_a |
|
33 | 35 | |
|
34 | 36 | respond_to do |format| |
|
35 | 37 | format.html { head 406 } |
|
36 | 38 | format.api |
|
37 | 39 | end |
|
38 | 40 | end |
|
39 | 41 | |
|
40 | 42 | def show |
|
41 | 43 | respond_to do |format| |
|
42 | 44 | format.html { head 406 } |
|
43 | 45 | format.api |
|
44 | 46 | end |
|
45 | 47 | end |
|
46 | 48 | |
|
47 | 49 | def new |
|
48 | 50 | @member = Member.new |
|
49 | 51 | end |
|
50 | 52 | |
|
51 | 53 | def create |
|
52 | 54 | members = [] |
|
53 | 55 | if params[:membership] |
|
54 | 56 | user_ids = Array.wrap(params[:membership][:user_id] || params[:membership][:user_ids]) |
|
55 | 57 | user_ids << nil if user_ids.empty? |
|
56 | 58 | user_ids.each do |user_id| |
|
57 | 59 | member = Member.new(:project => @project, :user_id => user_id) |
|
58 | 60 | member.set_editable_role_ids(params[:membership][:role_ids]) |
|
59 | 61 | members << member |
|
60 | 62 | end |
|
61 | 63 | @project.members << members |
|
62 | 64 | end |
|
63 | 65 | |
|
64 | 66 | respond_to do |format| |
|
65 | 67 | format.html { redirect_to_settings_in_projects } |
|
66 | 68 | format.js { |
|
67 | 69 | @members = members |
|
68 | 70 | @member = Member.new |
|
69 | 71 | } |
|
70 | 72 | format.api { |
|
71 | 73 | @member = members.first |
|
72 | 74 | if @member.valid? |
|
73 | 75 | render :action => 'show', :status => :created, :location => membership_url(@member) |
|
74 | 76 | else |
|
75 | 77 | render_validation_errors(@member) |
|
76 | 78 | end |
|
77 | 79 | } |
|
78 | 80 | end |
|
79 | 81 | end |
|
80 | 82 | |
|
81 | 83 | def update |
|
82 | 84 | if params[:membership] |
|
83 | 85 | @member.set_editable_role_ids(params[:membership][:role_ids]) |
|
84 | 86 | end |
|
85 | 87 | saved = @member.save |
|
86 | 88 | respond_to do |format| |
|
87 | 89 | format.html { redirect_to_settings_in_projects } |
|
88 | 90 | format.js |
|
89 | 91 | format.api { |
|
90 | 92 | if saved |
|
91 | 93 | render_api_ok |
|
92 | 94 | else |
|
93 | 95 | render_validation_errors(@member) |
|
94 | 96 | end |
|
95 | 97 | } |
|
96 | 98 | end |
|
97 | 99 | end |
|
98 | 100 | |
|
99 | 101 | def destroy |
|
100 | 102 | if @member.deletable? |
|
101 | 103 | @member.destroy |
|
102 | 104 | end |
|
103 | 105 | respond_to do |format| |
|
104 | 106 | format.html { redirect_to_settings_in_projects } |
|
105 | 107 | format.js |
|
106 | 108 | format.api { |
|
107 | 109 | if @member.destroyed? |
|
108 | 110 | render_api_ok |
|
109 | 111 | else |
|
110 | 112 | head :unprocessable_entity |
|
111 | 113 | end |
|
112 | 114 | } |
|
113 | 115 | end |
|
114 | 116 | end |
|
115 | 117 | |
|
116 | 118 | def autocomplete |
|
117 | 119 | respond_to do |format| |
|
118 | 120 | format.js |
|
119 | 121 | end |
|
120 | 122 | end |
|
121 | 123 | |
|
122 | 124 | private |
|
123 | 125 | |
|
124 | 126 | def redirect_to_settings_in_projects |
|
125 | 127 | redirect_to settings_project_path(@project, :tab => 'members') |
|
126 | 128 | end |
|
127 | 129 | end |
@@ -1,204 +1,211 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class MyController < ApplicationController |
|
19 | 19 | before_filter :require_login |
|
20 | 20 | # let user change user's password when user has to |
|
21 | 21 | skip_before_filter :check_password_change, :only => :password |
|
22 | 22 | |
|
23 | require_sudo_mode :account, only: :post | |
|
24 | require_sudo_mode :reset_rss_key, :reset_api_key, :show_api_key, :destroy | |
|
25 | ||
|
23 | 26 | helper :issues |
|
24 | 27 | helper :users |
|
25 | 28 | helper :custom_fields |
|
26 | 29 | |
|
27 | 30 | BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, |
|
28 | 31 | 'issuesreportedbyme' => :label_reported_issues, |
|
29 | 32 | 'issueswatched' => :label_watched_issues, |
|
30 | 33 | 'news' => :label_news_latest, |
|
31 | 34 | 'calendar' => :label_calendar, |
|
32 | 35 | 'documents' => :label_document_plural, |
|
33 | 36 | 'timelog' => :label_spent_time |
|
34 | 37 | }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze |
|
35 | 38 | |
|
36 | 39 | DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], |
|
37 | 40 | 'right' => ['issuesreportedbyme'] |
|
38 | 41 | }.freeze |
|
39 | 42 | |
|
40 | 43 | def index |
|
41 | 44 | page |
|
42 | 45 | render :action => 'page' |
|
43 | 46 | end |
|
44 | 47 | |
|
45 | 48 | # Show user's page |
|
46 | 49 | def page |
|
47 | 50 | @user = User.current |
|
48 | 51 | @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT |
|
49 | 52 | end |
|
50 | 53 | |
|
51 | 54 | # Edit user's account |
|
52 | 55 | def account |
|
53 | 56 | @user = User.current |
|
54 | 57 | @pref = @user.pref |
|
55 | 58 | if request.post? |
|
56 | 59 | @user.safe_attributes = params[:user] if params[:user] |
|
57 | 60 | @user.pref.attributes = params[:pref] if params[:pref] |
|
58 | 61 | if @user.save |
|
59 | 62 | @user.pref.save |
|
60 | 63 | set_language_if_valid @user.language |
|
61 | 64 | flash[:notice] = l(:notice_account_updated) |
|
62 | 65 | redirect_to my_account_path |
|
63 | 66 | return |
|
64 | 67 | end |
|
65 | 68 | end |
|
66 | 69 | end |
|
67 | 70 | |
|
68 | 71 | # Destroys user's account |
|
69 | 72 | def destroy |
|
70 | 73 | @user = User.current |
|
71 | 74 | unless @user.own_account_deletable? |
|
72 | 75 | redirect_to my_account_path |
|
73 | 76 | return |
|
74 | 77 | end |
|
75 | 78 | |
|
76 | 79 | if request.post? && params[:confirm] |
|
77 | 80 | @user.destroy |
|
78 | 81 | if @user.destroyed? |
|
79 | 82 | logout_user |
|
80 | 83 | flash[:notice] = l(:notice_account_deleted) |
|
81 | 84 | end |
|
82 | 85 | redirect_to home_path |
|
83 | 86 | end |
|
84 | 87 | end |
|
85 | 88 | |
|
86 | 89 | # Manage user's password |
|
87 | 90 | def password |
|
88 | 91 | @user = User.current |
|
89 | 92 | unless @user.change_password_allowed? |
|
90 | 93 | flash[:error] = l(:notice_can_t_change_password) |
|
91 | 94 | redirect_to my_account_path |
|
92 | 95 | return |
|
93 | 96 | end |
|
94 | 97 | if request.post? |
|
95 | 98 | if !@user.check_password?(params[:password]) |
|
96 | 99 | flash.now[:error] = l(:notice_account_wrong_password) |
|
97 | 100 | elsif params[:password] == params[:new_password] |
|
98 | 101 | flash.now[:error] = l(:notice_new_password_must_be_different) |
|
99 | 102 | else |
|
100 | 103 | @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] |
|
101 | 104 | @user.must_change_passwd = false |
|
102 | 105 | if @user.save |
|
103 | 106 | # Reset the session creation time to not log out this session on next |
|
104 | 107 | # request due to ApplicationController#force_logout_if_password_changed |
|
105 | 108 | session[:ctime] = User.current.passwd_changed_on.utc.to_i |
|
106 | 109 | flash[:notice] = l(:notice_account_password_updated) |
|
107 | 110 | redirect_to my_account_path |
|
108 | 111 | end |
|
109 | 112 | end |
|
110 | 113 | end |
|
111 | 114 | end |
|
112 | 115 | |
|
113 | 116 | # Create a new feeds key |
|
114 | 117 | def reset_rss_key |
|
115 | 118 | if request.post? |
|
116 | 119 | if User.current.rss_token |
|
117 | 120 | User.current.rss_token.destroy |
|
118 | 121 | User.current.reload |
|
119 | 122 | end |
|
120 | 123 | User.current.rss_key |
|
121 | 124 | flash[:notice] = l(:notice_feeds_access_key_reseted) |
|
122 | 125 | end |
|
123 | 126 | redirect_to my_account_path |
|
124 | 127 | end |
|
125 | 128 | |
|
129 | def show_api_key | |
|
130 | @user = User.current | |
|
131 | end | |
|
132 | ||
|
126 | 133 | # Create a new API key |
|
127 | 134 | def reset_api_key |
|
128 | 135 | if request.post? |
|
129 | 136 | if User.current.api_token |
|
130 | 137 | User.current.api_token.destroy |
|
131 | 138 | User.current.reload |
|
132 | 139 | end |
|
133 | 140 | User.current.api_key |
|
134 | 141 | flash[:notice] = l(:notice_api_access_key_reseted) |
|
135 | 142 | end |
|
136 | 143 | redirect_to my_account_path |
|
137 | 144 | end |
|
138 | 145 | |
|
139 | 146 | # User's page layout configuration |
|
140 | 147 | def page_layout |
|
141 | 148 | @user = User.current |
|
142 | 149 | @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup |
|
143 | 150 | @block_options = [] |
|
144 | 151 | BLOCKS.each do |k, v| |
|
145 | 152 | unless @blocks.values.flatten.include?(k) |
|
146 | 153 | @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize] |
|
147 | 154 | end |
|
148 | 155 | end |
|
149 | 156 | end |
|
150 | 157 | |
|
151 | 158 | # Add a block to user's page |
|
152 | 159 | # The block is added on top of the page |
|
153 | 160 | # params[:block] : id of the block to add |
|
154 | 161 | def add_block |
|
155 | 162 | block = params[:block].to_s.underscore |
|
156 | 163 | if block.present? && BLOCKS.key?(block) |
|
157 | 164 | @user = User.current |
|
158 | 165 | layout = @user.pref[:my_page_layout] || {} |
|
159 | 166 | # remove if already present in a group |
|
160 | 167 | %w(top left right).each {|f| (layout[f] ||= []).delete block } |
|
161 | 168 | # add it on top |
|
162 | 169 | layout['top'].unshift block |
|
163 | 170 | @user.pref[:my_page_layout] = layout |
|
164 | 171 | @user.pref.save |
|
165 | 172 | end |
|
166 | 173 | redirect_to my_page_layout_path |
|
167 | 174 | end |
|
168 | 175 | |
|
169 | 176 | # Remove a block to user's page |
|
170 | 177 | # params[:block] : id of the block to remove |
|
171 | 178 | def remove_block |
|
172 | 179 | block = params[:block].to_s.underscore |
|
173 | 180 | @user = User.current |
|
174 | 181 | # remove block in all groups |
|
175 | 182 | layout = @user.pref[:my_page_layout] || {} |
|
176 | 183 | %w(top left right).each {|f| (layout[f] ||= []).delete block } |
|
177 | 184 | @user.pref[:my_page_layout] = layout |
|
178 | 185 | @user.pref.save |
|
179 | 186 | redirect_to my_page_layout_path |
|
180 | 187 | end |
|
181 | 188 | |
|
182 | 189 | # Change blocks order on user's page |
|
183 | 190 | # params[:group] : group to order (top, left or right) |
|
184 | 191 | # params[:list-(top|left|right)] : array of block ids of the group |
|
185 | 192 | def order_blocks |
|
186 | 193 | group = params[:group] |
|
187 | 194 | @user = User.current |
|
188 | 195 | if group.is_a?(String) |
|
189 | 196 | group_items = (params["blocks"] || []).collect(&:underscore) |
|
190 | 197 | group_items.each {|s| s.sub!(/^block_/, '')} |
|
191 | 198 | if group_items and group_items.is_a? Array |
|
192 | 199 | layout = @user.pref[:my_page_layout] || {} |
|
193 | 200 | # remove group blocks if they are presents in other groups |
|
194 | 201 | %w(top left right).each {|f| |
|
195 | 202 | layout[f] = (layout[f] || []) - group_items |
|
196 | 203 | } |
|
197 | 204 | layout[group] = group_items |
|
198 | 205 | @user.pref[:my_page_layout] = layout |
|
199 | 206 | @user.pref.save |
|
200 | 207 | end |
|
201 | 208 | end |
|
202 | 209 | render :nothing => true |
|
203 | 210 | end |
|
204 | 211 | end |
@@ -1,233 +1,234 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class ProjectsController < ApplicationController |
|
19 | 19 | menu_item :overview |
|
20 | 20 | menu_item :settings, :only => :settings |
|
21 | 21 | |
|
22 | 22 | before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ] |
|
23 | 23 | before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy] |
|
24 | 24 | before_filter :authorize_global, :only => [:new, :create] |
|
25 | 25 | before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ] |
|
26 | 26 | accept_rss_auth :index |
|
27 | 27 | accept_api_auth :index, :show, :create, :update, :destroy |
|
28 | require_sudo_mode :destroy | |
|
28 | 29 | |
|
29 | 30 | after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller| |
|
30 | 31 | if controller.request.post? |
|
31 | 32 | controller.send :expire_action, :controller => 'welcome', :action => 'robots' |
|
32 | 33 | end |
|
33 | 34 | end |
|
34 | 35 | |
|
35 | 36 | helper :custom_fields |
|
36 | 37 | helper :issues |
|
37 | 38 | helper :queries |
|
38 | 39 | helper :repositories |
|
39 | 40 | helper :members |
|
40 | 41 | |
|
41 | 42 | # Lists visible projects |
|
42 | 43 | def index |
|
43 | 44 | scope = Project.visible.sorted |
|
44 | 45 | |
|
45 | 46 | respond_to do |format| |
|
46 | 47 | format.html { |
|
47 | 48 | unless params[:closed] |
|
48 | 49 | scope = scope.active |
|
49 | 50 | end |
|
50 | 51 | @projects = scope.to_a |
|
51 | 52 | } |
|
52 | 53 | format.api { |
|
53 | 54 | @offset, @limit = api_offset_and_limit |
|
54 | 55 | @project_count = scope.count |
|
55 | 56 | @projects = scope.offset(@offset).limit(@limit).to_a |
|
56 | 57 | } |
|
57 | 58 | format.atom { |
|
58 | 59 | projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a |
|
59 | 60 | render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") |
|
60 | 61 | } |
|
61 | 62 | end |
|
62 | 63 | end |
|
63 | 64 | |
|
64 | 65 | def new |
|
65 | 66 | @issue_custom_fields = IssueCustomField.sorted.to_a |
|
66 | 67 | @trackers = Tracker.sorted.to_a |
|
67 | 68 | @project = Project.new |
|
68 | 69 | @project.safe_attributes = params[:project] |
|
69 | 70 | end |
|
70 | 71 | |
|
71 | 72 | def create |
|
72 | 73 | @issue_custom_fields = IssueCustomField.sorted.to_a |
|
73 | 74 | @trackers = Tracker.sorted.to_a |
|
74 | 75 | @project = Project.new |
|
75 | 76 | @project.safe_attributes = params[:project] |
|
76 | 77 | |
|
77 | 78 | if @project.save |
|
78 | 79 | unless User.current.admin? |
|
79 | 80 | @project.add_default_member(User.current) |
|
80 | 81 | end |
|
81 | 82 | respond_to do |format| |
|
82 | 83 | format.html { |
|
83 | 84 | flash[:notice] = l(:notice_successful_create) |
|
84 | 85 | if params[:continue] |
|
85 | 86 | attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?} |
|
86 | 87 | redirect_to new_project_path(attrs) |
|
87 | 88 | else |
|
88 | 89 | redirect_to settings_project_path(@project) |
|
89 | 90 | end |
|
90 | 91 | } |
|
91 | 92 | format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) } |
|
92 | 93 | end |
|
93 | 94 | else |
|
94 | 95 | respond_to do |format| |
|
95 | 96 | format.html { render :action => 'new' } |
|
96 | 97 | format.api { render_validation_errors(@project) } |
|
97 | 98 | end |
|
98 | 99 | end |
|
99 | 100 | end |
|
100 | 101 | |
|
101 | 102 | def copy |
|
102 | 103 | @issue_custom_fields = IssueCustomField.sorted.to_a |
|
103 | 104 | @trackers = Tracker.sorted.to_a |
|
104 | 105 | @source_project = Project.find(params[:id]) |
|
105 | 106 | if request.get? |
|
106 | 107 | @project = Project.copy_from(@source_project) |
|
107 | 108 | @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? |
|
108 | 109 | else |
|
109 | 110 | Mailer.with_deliveries(params[:notifications] == '1') do |
|
110 | 111 | @project = Project.new |
|
111 | 112 | @project.safe_attributes = params[:project] |
|
112 | 113 | if @project.copy(@source_project, :only => params[:only]) |
|
113 | 114 | flash[:notice] = l(:notice_successful_create) |
|
114 | 115 | redirect_to settings_project_path(@project) |
|
115 | 116 | elsif !@project.new_record? |
|
116 | 117 | # Project was created |
|
117 | 118 | # But some objects were not copied due to validation failures |
|
118 | 119 | # (eg. issues from disabled trackers) |
|
119 | 120 | # TODO: inform about that |
|
120 | 121 | redirect_to settings_project_path(@project) |
|
121 | 122 | end |
|
122 | 123 | end |
|
123 | 124 | end |
|
124 | 125 | rescue ActiveRecord::RecordNotFound |
|
125 | 126 | # source_project not found |
|
126 | 127 | render_404 |
|
127 | 128 | end |
|
128 | 129 | |
|
129 | 130 | # Show @project |
|
130 | 131 | def show |
|
131 | 132 | # try to redirect to the requested menu item |
|
132 | 133 | if params[:jump] && redirect_to_project_menu_item(@project, params[:jump]) |
|
133 | 134 | return |
|
134 | 135 | end |
|
135 | 136 | |
|
136 | 137 | @users_by_role = @project.users_by_role |
|
137 | 138 | @subprojects = @project.children.visible.to_a |
|
138 | 139 | @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a |
|
139 | 140 | @trackers = @project.rolled_up_trackers |
|
140 | 141 | |
|
141 | 142 | cond = @project.project_condition(Setting.display_subprojects_issues?) |
|
142 | 143 | |
|
143 | 144 | @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count |
|
144 | 145 | @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count |
|
145 | 146 | |
|
146 | 147 | if User.current.allowed_to_view_all_time_entries?(@project) |
|
147 | 148 | @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f |
|
148 | 149 | end |
|
149 | 150 | |
|
150 | 151 | @key = User.current.rss_key |
|
151 | 152 | |
|
152 | 153 | respond_to do |format| |
|
153 | 154 | format.html |
|
154 | 155 | format.api |
|
155 | 156 | end |
|
156 | 157 | end |
|
157 | 158 | |
|
158 | 159 | def settings |
|
159 | 160 | @issue_custom_fields = IssueCustomField.sorted.to_a |
|
160 | 161 | @issue_category ||= IssueCategory.new |
|
161 | 162 | @member ||= @project.members.new |
|
162 | 163 | @trackers = Tracker.sorted.to_a |
|
163 | 164 | @wiki ||= @project.wiki || Wiki.new(:project => @project) |
|
164 | 165 | end |
|
165 | 166 | |
|
166 | 167 | def edit |
|
167 | 168 | end |
|
168 | 169 | |
|
169 | 170 | def update |
|
170 | 171 | @project.safe_attributes = params[:project] |
|
171 | 172 | if @project.save |
|
172 | 173 | respond_to do |format| |
|
173 | 174 | format.html { |
|
174 | 175 | flash[:notice] = l(:notice_successful_update) |
|
175 | 176 | redirect_to settings_project_path(@project) |
|
176 | 177 | } |
|
177 | 178 | format.api { render_api_ok } |
|
178 | 179 | end |
|
179 | 180 | else |
|
180 | 181 | respond_to do |format| |
|
181 | 182 | format.html { |
|
182 | 183 | settings |
|
183 | 184 | render :action => 'settings' |
|
184 | 185 | } |
|
185 | 186 | format.api { render_validation_errors(@project) } |
|
186 | 187 | end |
|
187 | 188 | end |
|
188 | 189 | end |
|
189 | 190 | |
|
190 | 191 | def modules |
|
191 | 192 | @project.enabled_module_names = params[:enabled_module_names] |
|
192 | 193 | flash[:notice] = l(:notice_successful_update) |
|
193 | 194 | redirect_to settings_project_path(@project, :tab => 'modules') |
|
194 | 195 | end |
|
195 | 196 | |
|
196 | 197 | def archive |
|
197 | 198 | unless @project.archive |
|
198 | 199 | flash[:error] = l(:error_can_not_archive_project) |
|
199 | 200 | end |
|
200 | 201 | redirect_to admin_projects_path(:status => params[:status]) |
|
201 | 202 | end |
|
202 | 203 | |
|
203 | 204 | def unarchive |
|
204 | 205 | unless @project.active? |
|
205 | 206 | @project.unarchive |
|
206 | 207 | end |
|
207 | 208 | redirect_to admin_projects_path(:status => params[:status]) |
|
208 | 209 | end |
|
209 | 210 | |
|
210 | 211 | def close |
|
211 | 212 | @project.close |
|
212 | 213 | redirect_to project_path(@project) |
|
213 | 214 | end |
|
214 | 215 | |
|
215 | 216 | def reopen |
|
216 | 217 | @project.reopen |
|
217 | 218 | redirect_to project_path(@project) |
|
218 | 219 | end |
|
219 | 220 | |
|
220 | 221 | # Delete @project |
|
221 | 222 | def destroy |
|
222 | 223 | @project_to_destroy = @project |
|
223 | 224 | if api_request? || params[:confirm] |
|
224 | 225 | @project_to_destroy.destroy |
|
225 | 226 | respond_to do |format| |
|
226 | 227 | format.html { redirect_to admin_projects_path } |
|
227 | 228 | format.api { render_api_ok } |
|
228 | 229 | end |
|
229 | 230 | end |
|
230 | 231 | # hide project in layout |
|
231 | 232 | @project = nil |
|
232 | 233 | end |
|
233 | 234 | end |
@@ -1,108 +1,110 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class RolesController < ApplicationController |
|
19 | 19 | layout 'admin' |
|
20 | 20 | |
|
21 | 21 | before_filter :require_admin, :except => [:index, :show] |
|
22 | 22 | before_filter :require_admin_or_api_request, :only => [:index, :show] |
|
23 | 23 | before_filter :find_role, :only => [:show, :edit, :update, :destroy] |
|
24 | 24 | accept_api_auth :index, :show |
|
25 | 25 | |
|
26 | require_sudo_mode :create, :update, :destroy | |
|
27 | ||
|
26 | 28 | def index |
|
27 | 29 | respond_to do |format| |
|
28 | 30 | format.html { |
|
29 | 31 | @role_pages, @roles = paginate Role.sorted, :per_page => 25 |
|
30 | 32 | render :action => "index", :layout => false if request.xhr? |
|
31 | 33 | } |
|
32 | 34 | format.api { |
|
33 | 35 | @roles = Role.givable.to_a |
|
34 | 36 | } |
|
35 | 37 | end |
|
36 | 38 | end |
|
37 | 39 | |
|
38 | 40 | def show |
|
39 | 41 | respond_to do |format| |
|
40 | 42 | format.api |
|
41 | 43 | end |
|
42 | 44 | end |
|
43 | 45 | |
|
44 | 46 | def new |
|
45 | 47 | # Prefills the form with 'Non member' role permissions by default |
|
46 | 48 | @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions}) |
|
47 | 49 | if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy]) |
|
48 | 50 | @role.copy_from(@copy_from) |
|
49 | 51 | end |
|
50 | 52 | @roles = Role.sorted.to_a |
|
51 | 53 | end |
|
52 | 54 | |
|
53 | 55 | def create |
|
54 | 56 | @role = Role.new(params[:role]) |
|
55 | 57 | if request.post? && @role.save |
|
56 | 58 | # workflow copy |
|
57 | 59 | if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from])) |
|
58 | 60 | @role.workflow_rules.copy(copy_from) |
|
59 | 61 | end |
|
60 | 62 | flash[:notice] = l(:notice_successful_create) |
|
61 | 63 | redirect_to roles_path |
|
62 | 64 | else |
|
63 | 65 | @roles = Role.sorted.to_a |
|
64 | 66 | render :action => 'new' |
|
65 | 67 | end |
|
66 | 68 | end |
|
67 | 69 | |
|
68 | 70 | def edit |
|
69 | 71 | end |
|
70 | 72 | |
|
71 | 73 | def update |
|
72 | 74 | if @role.update_attributes(params[:role]) |
|
73 | 75 | flash[:notice] = l(:notice_successful_update) |
|
74 | 76 | redirect_to roles_path(:page => params[:page]) |
|
75 | 77 | else |
|
76 | 78 | render :action => 'edit' |
|
77 | 79 | end |
|
78 | 80 | end |
|
79 | 81 | |
|
80 | 82 | def destroy |
|
81 | 83 | @role.destroy |
|
82 | 84 | redirect_to roles_path |
|
83 | 85 | rescue |
|
84 | 86 | flash[:error] = l(:error_can_not_remove_role) |
|
85 | 87 | redirect_to roles_path |
|
86 | 88 | end |
|
87 | 89 | |
|
88 | 90 | def permissions |
|
89 | 91 | @roles = Role.sorted.to_a |
|
90 | 92 | @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? } |
|
91 | 93 | if request.post? |
|
92 | 94 | @roles.each do |role| |
|
93 | 95 | role.permissions = params[:permissions][role.id.to_s] |
|
94 | 96 | role.save |
|
95 | 97 | end |
|
96 | 98 | flash[:notice] = l(:notice_successful_update) |
|
97 | 99 | redirect_to roles_path |
|
98 | 100 | end |
|
99 | 101 | end |
|
100 | 102 | |
|
101 | 103 | private |
|
102 | 104 | |
|
103 | 105 | def find_role |
|
104 | 106 | @role = Role.find(params[:id]) |
|
105 | 107 | rescue ActiveRecord::RecordNotFound |
|
106 | 108 | render_404 |
|
107 | 109 | end |
|
108 | 110 | end |
@@ -1,74 +1,76 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class SettingsController < ApplicationController |
|
19 | 19 | layout 'admin' |
|
20 | 20 | menu_item :plugins, :only => :plugin |
|
21 | 21 | |
|
22 | 22 | helper :queries |
|
23 | 23 | |
|
24 | 24 | before_filter :require_admin |
|
25 | 25 | |
|
26 | require_sudo_mode :index, :edit, :plugin | |
|
27 | ||
|
26 | 28 | def index |
|
27 | 29 | edit |
|
28 | 30 | render :action => 'edit' |
|
29 | 31 | end |
|
30 | 32 | |
|
31 | 33 | def edit |
|
32 | 34 | @notifiables = Redmine::Notifiable.all |
|
33 | 35 | if request.post? && params[:settings] && params[:settings].is_a?(Hash) |
|
34 | 36 | settings = (params[:settings] || {}).dup.symbolize_keys |
|
35 | 37 | settings.each do |name, value| |
|
36 | 38 | Setting.set_from_params name, value |
|
37 | 39 | end |
|
38 | 40 | flash[:notice] = l(:notice_successful_update) |
|
39 | 41 | redirect_to settings_path(:tab => params[:tab]) |
|
40 | 42 | else |
|
41 | 43 | @options = {} |
|
42 | 44 | user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]} |
|
43 | 45 | @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]} |
|
44 | 46 | @deliveries = ActionMailer::Base.perform_deliveries |
|
45 | 47 | |
|
46 | 48 | @guessed_host_and_path = request.host_with_port.dup |
|
47 | 49 | @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank? |
|
48 | 50 | |
|
49 | 51 | @commit_update_keywords = Setting.commit_update_keywords.dup |
|
50 | 52 | @commit_update_keywords = [{}] unless @commit_update_keywords.is_a?(Array) && @commit_update_keywords.any? |
|
51 | 53 | |
|
52 | 54 | Redmine::Themes.rescan |
|
53 | 55 | end |
|
54 | 56 | end |
|
55 | 57 | |
|
56 | 58 | def plugin |
|
57 | 59 | @plugin = Redmine::Plugin.find(params[:id]) |
|
58 | 60 | unless @plugin.configurable? |
|
59 | 61 | render_404 |
|
60 | 62 | return |
|
61 | 63 | end |
|
62 | 64 | |
|
63 | 65 | if request.post? |
|
64 | 66 | Setting.send "plugin_#{@plugin.id}=", params[:settings] |
|
65 | 67 | flash[:notice] = l(:notice_successful_update) |
|
66 | 68 | redirect_to plugin_settings_path(@plugin) |
|
67 | 69 | else |
|
68 | 70 | @partial = @plugin.settings[:partial] |
|
69 | 71 | @settings = Setting.send "plugin_#{@plugin.id}" |
|
70 | 72 | end |
|
71 | 73 | rescue Redmine::PluginNotFound |
|
72 | 74 | render_404 |
|
73 | 75 | end |
|
74 | 76 | end |
@@ -1,188 +1,190 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class UsersController < ApplicationController |
|
19 | 19 | layout 'admin' |
|
20 | 20 | |
|
21 | 21 | before_filter :require_admin, :except => :show |
|
22 | 22 | before_filter :find_user, :only => [:show, :edit, :update, :destroy] |
|
23 | 23 | accept_api_auth :index, :show, :create, :update, :destroy |
|
24 | 24 | |
|
25 | 25 | helper :sort |
|
26 | 26 | include SortHelper |
|
27 | 27 | helper :custom_fields |
|
28 | 28 | include CustomFieldsHelper |
|
29 | 29 | helper :principal_memberships |
|
30 | 30 | |
|
31 | require_sudo_mode :create, :update, :destroy | |
|
32 | ||
|
31 | 33 | def index |
|
32 | 34 | sort_init 'login', 'asc' |
|
33 | 35 | sort_update %w(login firstname lastname admin created_on last_login_on) |
|
34 | 36 | |
|
35 | 37 | case params[:format] |
|
36 | 38 | when 'xml', 'json' |
|
37 | 39 | @offset, @limit = api_offset_and_limit |
|
38 | 40 | else |
|
39 | 41 | @limit = per_page_option |
|
40 | 42 | end |
|
41 | 43 | |
|
42 | 44 | @status = params[:status] || 1 |
|
43 | 45 | |
|
44 | 46 | scope = User.logged.status(@status).preload(:email_address) |
|
45 | 47 | scope = scope.like(params[:name]) if params[:name].present? |
|
46 | 48 | scope = scope.in_group(params[:group_id]) if params[:group_id].present? |
|
47 | 49 | |
|
48 | 50 | @user_count = scope.count |
|
49 | 51 | @user_pages = Paginator.new @user_count, @limit, params['page'] |
|
50 | 52 | @offset ||= @user_pages.offset |
|
51 | 53 | @users = scope.order(sort_clause).limit(@limit).offset(@offset).to_a |
|
52 | 54 | |
|
53 | 55 | respond_to do |format| |
|
54 | 56 | format.html { |
|
55 | 57 | @groups = Group.all.sort |
|
56 | 58 | render :layout => !request.xhr? |
|
57 | 59 | } |
|
58 | 60 | format.api |
|
59 | 61 | end |
|
60 | 62 | end |
|
61 | 63 | |
|
62 | 64 | def show |
|
63 | 65 | unless @user.visible? |
|
64 | 66 | render_404 |
|
65 | 67 | return |
|
66 | 68 | end |
|
67 | 69 | |
|
68 | 70 | # show projects based on current user visibility |
|
69 | 71 | @memberships = @user.memberships.where(Project.visible_condition(User.current)).to_a |
|
70 | 72 | |
|
71 | 73 | respond_to do |format| |
|
72 | 74 | format.html { |
|
73 | 75 | events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10) |
|
74 | 76 | @events_by_day = events.group_by(&:event_date) |
|
75 | 77 | render :layout => 'base' |
|
76 | 78 | } |
|
77 | 79 | format.api |
|
78 | 80 | end |
|
79 | 81 | end |
|
80 | 82 | |
|
81 | 83 | def new |
|
82 | 84 | @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option) |
|
83 | 85 | @user.safe_attributes = params[:user] |
|
84 | 86 | @auth_sources = AuthSource.all |
|
85 | 87 | end |
|
86 | 88 | |
|
87 | 89 | def create |
|
88 | 90 | @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option) |
|
89 | 91 | @user.safe_attributes = params[:user] |
|
90 | 92 | @user.admin = params[:user][:admin] || false |
|
91 | 93 | @user.login = params[:user][:login] |
|
92 | 94 | @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id |
|
93 | 95 | @user.pref.attributes = params[:pref] if params[:pref] |
|
94 | 96 | |
|
95 | 97 | if @user.save |
|
96 | 98 | Mailer.account_information(@user, @user.password).deliver if params[:send_information] |
|
97 | 99 | |
|
98 | 100 | respond_to do |format| |
|
99 | 101 | format.html { |
|
100 | 102 | flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user))) |
|
101 | 103 | if params[:continue] |
|
102 | 104 | attrs = params[:user].slice(:generate_password) |
|
103 | 105 | redirect_to new_user_path(:user => attrs) |
|
104 | 106 | else |
|
105 | 107 | redirect_to edit_user_path(@user) |
|
106 | 108 | end |
|
107 | 109 | } |
|
108 | 110 | format.api { render :action => 'show', :status => :created, :location => user_url(@user) } |
|
109 | 111 | end |
|
110 | 112 | else |
|
111 | 113 | @auth_sources = AuthSource.all |
|
112 | 114 | # Clear password input |
|
113 | 115 | @user.password = @user.password_confirmation = nil |
|
114 | 116 | |
|
115 | 117 | respond_to do |format| |
|
116 | 118 | format.html { render :action => 'new' } |
|
117 | 119 | format.api { render_validation_errors(@user) } |
|
118 | 120 | end |
|
119 | 121 | end |
|
120 | 122 | end |
|
121 | 123 | |
|
122 | 124 | def edit |
|
123 | 125 | @auth_sources = AuthSource.all |
|
124 | 126 | @membership ||= Member.new |
|
125 | 127 | end |
|
126 | 128 | |
|
127 | 129 | def update |
|
128 | 130 | @user.admin = params[:user][:admin] if params[:user][:admin] |
|
129 | 131 | @user.login = params[:user][:login] if params[:user][:login] |
|
130 | 132 | if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?) |
|
131 | 133 | @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] |
|
132 | 134 | end |
|
133 | 135 | @user.safe_attributes = params[:user] |
|
134 | 136 | # Was the account actived ? (do it before User#save clears the change) |
|
135 | 137 | was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE]) |
|
136 | 138 | # TODO: Similar to My#account |
|
137 | 139 | @user.pref.attributes = params[:pref] if params[:pref] |
|
138 | 140 | |
|
139 | 141 | if @user.save |
|
140 | 142 | @user.pref.save |
|
141 | 143 | |
|
142 | 144 | if was_activated |
|
143 | 145 | Mailer.account_activated(@user).deliver |
|
144 | 146 | elsif @user.active? && params[:send_information] && @user.password.present? && @user.auth_source_id.nil? |
|
145 | 147 | Mailer.account_information(@user, @user.password).deliver |
|
146 | 148 | end |
|
147 | 149 | |
|
148 | 150 | respond_to do |format| |
|
149 | 151 | format.html { |
|
150 | 152 | flash[:notice] = l(:notice_successful_update) |
|
151 | 153 | redirect_to_referer_or edit_user_path(@user) |
|
152 | 154 | } |
|
153 | 155 | format.api { render_api_ok } |
|
154 | 156 | end |
|
155 | 157 | else |
|
156 | 158 | @auth_sources = AuthSource.all |
|
157 | 159 | @membership ||= Member.new |
|
158 | 160 | # Clear password input |
|
159 | 161 | @user.password = @user.password_confirmation = nil |
|
160 | 162 | |
|
161 | 163 | respond_to do |format| |
|
162 | 164 | format.html { render :action => :edit } |
|
163 | 165 | format.api { render_validation_errors(@user) } |
|
164 | 166 | end |
|
165 | 167 | end |
|
166 | 168 | end |
|
167 | 169 | |
|
168 | 170 | def destroy |
|
169 | 171 | @user.destroy |
|
170 | 172 | respond_to do |format| |
|
171 | 173 | format.html { redirect_back_or_default(users_path) } |
|
172 | 174 | format.api { render_api_ok } |
|
173 | 175 | end |
|
174 | 176 | end |
|
175 | 177 | |
|
176 | 178 | private |
|
177 | 179 | |
|
178 | 180 | def find_user |
|
179 | 181 | if params[:id] == 'current' |
|
180 | 182 | require_login || return |
|
181 | 183 | @user = User.current |
|
182 | 184 | else |
|
183 | 185 | @user = User.find(params[:id]) |
|
184 | 186 | end |
|
185 | 187 | rescue ActiveRecord::RecordNotFound |
|
186 | 188 | render_404 |
|
187 | 189 | end |
|
188 | 190 | end |
@@ -1,1326 +1,1327 | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | 2 | # |
|
3 | 3 | # Redmine - project management software |
|
4 | 4 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
5 | 5 | # |
|
6 | 6 | # This program is free software; you can redistribute it and/or |
|
7 | 7 | # modify it under the terms of the GNU General Public License |
|
8 | 8 | # as published by the Free Software Foundation; either version 2 |
|
9 | 9 | # of the License, or (at your option) any later version. |
|
10 | 10 | # |
|
11 | 11 | # This program is distributed in the hope that it will be useful, |
|
12 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 | 14 | # GNU General Public License for more details. |
|
15 | 15 | # |
|
16 | 16 | # You should have received a copy of the GNU General Public License |
|
17 | 17 | # along with this program; if not, write to the Free Software |
|
18 | 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
19 | 19 | |
|
20 | 20 | require 'forwardable' |
|
21 | 21 | require 'cgi' |
|
22 | 22 | |
|
23 | 23 | module ApplicationHelper |
|
24 | 24 | include Redmine::WikiFormatting::Macros::Definitions |
|
25 | 25 | include Redmine::I18n |
|
26 | 26 | include GravatarHelper::PublicMethods |
|
27 | 27 | include Redmine::Pagination::Helper |
|
28 | include Redmine::SudoMode::Helper | |
|
28 | 29 | |
|
29 | 30 | extend Forwardable |
|
30 | 31 | def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter |
|
31 | 32 | |
|
32 | 33 | # Return true if user is authorized for controller/action, otherwise false |
|
33 | 34 | def authorize_for(controller, action) |
|
34 | 35 | User.current.allowed_to?({:controller => controller, :action => action}, @project) |
|
35 | 36 | end |
|
36 | 37 | |
|
37 | 38 | # Display a link if user is authorized |
|
38 | 39 | # |
|
39 | 40 | # @param [String] name Anchor text (passed to link_to) |
|
40 | 41 | # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized |
|
41 | 42 | # @param [optional, Hash] html_options Options passed to link_to |
|
42 | 43 | # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to |
|
43 | 44 | def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) |
|
44 | 45 | link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) |
|
45 | 46 | end |
|
46 | 47 | |
|
47 | 48 | # Displays a link to user's account page if active |
|
48 | 49 | def link_to_user(user, options={}) |
|
49 | 50 | if user.is_a?(User) |
|
50 | 51 | name = h(user.name(options[:format])) |
|
51 | 52 | if user.active? || (User.current.admin? && user.logged?) |
|
52 | 53 | link_to name, user_path(user), :class => user.css_classes |
|
53 | 54 | else |
|
54 | 55 | name |
|
55 | 56 | end |
|
56 | 57 | else |
|
57 | 58 | h(user.to_s) |
|
58 | 59 | end |
|
59 | 60 | end |
|
60 | 61 | |
|
61 | 62 | # Displays a link to +issue+ with its subject. |
|
62 | 63 | # Examples: |
|
63 | 64 | # |
|
64 | 65 | # link_to_issue(issue) # => Defect #6: This is the subject |
|
65 | 66 | # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... |
|
66 | 67 | # link_to_issue(issue, :subject => false) # => Defect #6 |
|
67 | 68 | # link_to_issue(issue, :project => true) # => Foo - Defect #6 |
|
68 | 69 | # link_to_issue(issue, :subject => false, :tracker => false) # => #6 |
|
69 | 70 | # |
|
70 | 71 | def link_to_issue(issue, options={}) |
|
71 | 72 | title = nil |
|
72 | 73 | subject = nil |
|
73 | 74 | text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}" |
|
74 | 75 | if options[:subject] == false |
|
75 | 76 | title = issue.subject.truncate(60) |
|
76 | 77 | else |
|
77 | 78 | subject = issue.subject |
|
78 | 79 | if truncate_length = options[:truncate] |
|
79 | 80 | subject = subject.truncate(truncate_length) |
|
80 | 81 | end |
|
81 | 82 | end |
|
82 | 83 | only_path = options[:only_path].nil? ? true : options[:only_path] |
|
83 | 84 | s = link_to(text, issue_url(issue, :only_path => only_path), |
|
84 | 85 | :class => issue.css_classes, :title => title) |
|
85 | 86 | s << h(": #{subject}") if subject |
|
86 | 87 | s = h("#{issue.project} - ") + s if options[:project] |
|
87 | 88 | s |
|
88 | 89 | end |
|
89 | 90 | |
|
90 | 91 | # Generates a link to an attachment. |
|
91 | 92 | # Options: |
|
92 | 93 | # * :text - Link text (default to attachment filename) |
|
93 | 94 | # * :download - Force download (default: false) |
|
94 | 95 | def link_to_attachment(attachment, options={}) |
|
95 | 96 | text = options.delete(:text) || attachment.filename |
|
96 | 97 | route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url |
|
97 | 98 | html_options = options.slice!(:only_path) |
|
98 | 99 | options[:only_path] = true unless options.key?(:only_path) |
|
99 | 100 | url = send(route_method, attachment, attachment.filename, options) |
|
100 | 101 | link_to text, url, html_options |
|
101 | 102 | end |
|
102 | 103 | |
|
103 | 104 | # Generates a link to a SCM revision |
|
104 | 105 | # Options: |
|
105 | 106 | # * :text - Link text (default to the formatted revision) |
|
106 | 107 | def link_to_revision(revision, repository, options={}) |
|
107 | 108 | if repository.is_a?(Project) |
|
108 | 109 | repository = repository.repository |
|
109 | 110 | end |
|
110 | 111 | text = options.delete(:text) || format_revision(revision) |
|
111 | 112 | rev = revision.respond_to?(:identifier) ? revision.identifier : revision |
|
112 | 113 | link_to( |
|
113 | 114 | h(text), |
|
114 | 115 | {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, |
|
115 | 116 | :title => l(:label_revision_id, format_revision(revision)), |
|
116 | 117 | :accesskey => options[:accesskey] |
|
117 | 118 | ) |
|
118 | 119 | end |
|
119 | 120 | |
|
120 | 121 | # Generates a link to a message |
|
121 | 122 | def link_to_message(message, options={}, html_options = nil) |
|
122 | 123 | link_to( |
|
123 | 124 | message.subject.truncate(60), |
|
124 | 125 | board_message_url(message.board_id, message.parent_id || message.id, { |
|
125 | 126 | :r => (message.parent_id && message.id), |
|
126 | 127 | :anchor => (message.parent_id ? "message-#{message.id}" : nil), |
|
127 | 128 | :only_path => true |
|
128 | 129 | }.merge(options)), |
|
129 | 130 | html_options |
|
130 | 131 | ) |
|
131 | 132 | end |
|
132 | 133 | |
|
133 | 134 | # Generates a link to a project if active |
|
134 | 135 | # Examples: |
|
135 | 136 | # |
|
136 | 137 | # link_to_project(project) # => link to the specified project overview |
|
137 | 138 | # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options |
|
138 | 139 | # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) |
|
139 | 140 | # |
|
140 | 141 | def link_to_project(project, options={}, html_options = nil) |
|
141 | 142 | if project.archived? |
|
142 | 143 | h(project.name) |
|
143 | 144 | else |
|
144 | 145 | link_to project.name, |
|
145 | 146 | project_url(project, {:only_path => true}.merge(options)), |
|
146 | 147 | html_options |
|
147 | 148 | end |
|
148 | 149 | end |
|
149 | 150 | |
|
150 | 151 | # Generates a link to a project settings if active |
|
151 | 152 | def link_to_project_settings(project, options={}, html_options=nil) |
|
152 | 153 | if project.active? |
|
153 | 154 | link_to project.name, settings_project_path(project, options), html_options |
|
154 | 155 | elsif project.archived? |
|
155 | 156 | h(project.name) |
|
156 | 157 | else |
|
157 | 158 | link_to project.name, project_path(project, options), html_options |
|
158 | 159 | end |
|
159 | 160 | end |
|
160 | 161 | |
|
161 | 162 | # Generates a link to a version |
|
162 | 163 | def link_to_version(version, options = {}) |
|
163 | 164 | return '' unless version && version.is_a?(Version) |
|
164 | 165 | options = {:title => format_date(version.effective_date)}.merge(options) |
|
165 | 166 | link_to_if version.visible?, format_version_name(version), version_path(version), options |
|
166 | 167 | end |
|
167 | 168 | |
|
168 | 169 | # Helper that formats object for html or text rendering |
|
169 | 170 | def format_object(object, html=true, &block) |
|
170 | 171 | if block_given? |
|
171 | 172 | object = yield object |
|
172 | 173 | end |
|
173 | 174 | case object.class.name |
|
174 | 175 | when 'Array' |
|
175 | 176 | object.map {|o| format_object(o, html)}.join(', ').html_safe |
|
176 | 177 | when 'Time' |
|
177 | 178 | format_time(object) |
|
178 | 179 | when 'Date' |
|
179 | 180 | format_date(object) |
|
180 | 181 | when 'Fixnum' |
|
181 | 182 | object.to_s |
|
182 | 183 | when 'Float' |
|
183 | 184 | sprintf "%.2f", object |
|
184 | 185 | when 'User' |
|
185 | 186 | html ? link_to_user(object) : object.to_s |
|
186 | 187 | when 'Project' |
|
187 | 188 | html ? link_to_project(object) : object.to_s |
|
188 | 189 | when 'Version' |
|
189 | 190 | html ? link_to_version(object) : object.to_s |
|
190 | 191 | when 'TrueClass' |
|
191 | 192 | l(:general_text_Yes) |
|
192 | 193 | when 'FalseClass' |
|
193 | 194 | l(:general_text_No) |
|
194 | 195 | when 'Issue' |
|
195 | 196 | object.visible? && html ? link_to_issue(object) : "##{object.id}" |
|
196 | 197 | when 'CustomValue', 'CustomFieldValue' |
|
197 | 198 | if object.custom_field |
|
198 | 199 | f = object.custom_field.format.formatted_custom_value(self, object, html) |
|
199 | 200 | if f.nil? || f.is_a?(String) |
|
200 | 201 | f |
|
201 | 202 | else |
|
202 | 203 | format_object(f, html, &block) |
|
203 | 204 | end |
|
204 | 205 | else |
|
205 | 206 | object.value.to_s |
|
206 | 207 | end |
|
207 | 208 | else |
|
208 | 209 | html ? h(object) : object.to_s |
|
209 | 210 | end |
|
210 | 211 | end |
|
211 | 212 | |
|
212 | 213 | def wiki_page_path(page, options={}) |
|
213 | 214 | url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options)) |
|
214 | 215 | end |
|
215 | 216 | |
|
216 | 217 | def thumbnail_tag(attachment) |
|
217 | 218 | link_to image_tag(thumbnail_path(attachment)), |
|
218 | 219 | named_attachment_path(attachment, attachment.filename), |
|
219 | 220 | :title => attachment.filename |
|
220 | 221 | end |
|
221 | 222 | |
|
222 | 223 | def toggle_link(name, id, options={}) |
|
223 | 224 | onclick = "$('##{id}').toggle(); " |
|
224 | 225 | onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") |
|
225 | 226 | onclick << "return false;" |
|
226 | 227 | link_to(name, "#", :onclick => onclick) |
|
227 | 228 | end |
|
228 | 229 | |
|
229 | 230 | def format_activity_title(text) |
|
230 | 231 | h(truncate_single_line_raw(text, 100)) |
|
231 | 232 | end |
|
232 | 233 | |
|
233 | 234 | def format_activity_day(date) |
|
234 | 235 | date == User.current.today ? l(:label_today).titleize : format_date(date) |
|
235 | 236 | end |
|
236 | 237 | |
|
237 | 238 | def format_activity_description(text) |
|
238 | 239 | h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') |
|
239 | 240 | ).gsub(/[\r\n]+/, "<br />").html_safe |
|
240 | 241 | end |
|
241 | 242 | |
|
242 | 243 | def format_version_name(version) |
|
243 | 244 | if version.project == @project |
|
244 | 245 | h(version) |
|
245 | 246 | else |
|
246 | 247 | h("#{version.project} - #{version}") |
|
247 | 248 | end |
|
248 | 249 | end |
|
249 | 250 | |
|
250 | 251 | def due_date_distance_in_words(date) |
|
251 | 252 | if date |
|
252 | 253 | l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) |
|
253 | 254 | end |
|
254 | 255 | end |
|
255 | 256 | |
|
256 | 257 | # Renders a tree of projects as a nested set of unordered lists |
|
257 | 258 | # The given collection may be a subset of the whole project tree |
|
258 | 259 | # (eg. some intermediate nodes are private and can not be seen) |
|
259 | 260 | def render_project_nested_lists(projects, &block) |
|
260 | 261 | s = '' |
|
261 | 262 | if projects.any? |
|
262 | 263 | ancestors = [] |
|
263 | 264 | original_project = @project |
|
264 | 265 | projects.sort_by(&:lft).each do |project| |
|
265 | 266 | # set the project environment to please macros. |
|
266 | 267 | @project = project |
|
267 | 268 | if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) |
|
268 | 269 | s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n" |
|
269 | 270 | else |
|
270 | 271 | ancestors.pop |
|
271 | 272 | s << "</li>" |
|
272 | 273 | while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) |
|
273 | 274 | ancestors.pop |
|
274 | 275 | s << "</ul></li>\n" |
|
275 | 276 | end |
|
276 | 277 | end |
|
277 | 278 | classes = (ancestors.empty? ? 'root' : 'child') |
|
278 | 279 | s << "<li class='#{classes}'><div class='#{classes}'>" |
|
279 | 280 | s << h(block_given? ? capture(project, &block) : project.name) |
|
280 | 281 | s << "</div>\n" |
|
281 | 282 | ancestors << project |
|
282 | 283 | end |
|
283 | 284 | s << ("</li></ul>\n" * ancestors.size) |
|
284 | 285 | @project = original_project |
|
285 | 286 | end |
|
286 | 287 | s.html_safe |
|
287 | 288 | end |
|
288 | 289 | |
|
289 | 290 | def render_page_hierarchy(pages, node=nil, options={}) |
|
290 | 291 | content = '' |
|
291 | 292 | if pages[node] |
|
292 | 293 | content << "<ul class=\"pages-hierarchy\">\n" |
|
293 | 294 | pages[node].each do |page| |
|
294 | 295 | content << "<li>" |
|
295 | 296 | content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil}, |
|
296 | 297 | :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) |
|
297 | 298 | content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] |
|
298 | 299 | content << "</li>\n" |
|
299 | 300 | end |
|
300 | 301 | content << "</ul>\n" |
|
301 | 302 | end |
|
302 | 303 | content.html_safe |
|
303 | 304 | end |
|
304 | 305 | |
|
305 | 306 | # Renders flash messages |
|
306 | 307 | def render_flash_messages |
|
307 | 308 | s = '' |
|
308 | 309 | flash.each do |k,v| |
|
309 | 310 | s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") |
|
310 | 311 | end |
|
311 | 312 | s.html_safe |
|
312 | 313 | end |
|
313 | 314 | |
|
314 | 315 | # Renders tabs and their content |
|
315 | 316 | def render_tabs(tabs, selected=params[:tab]) |
|
316 | 317 | if tabs.any? |
|
317 | 318 | unless tabs.detect {|tab| tab[:name] == selected} |
|
318 | 319 | selected = nil |
|
319 | 320 | end |
|
320 | 321 | selected ||= tabs.first[:name] |
|
321 | 322 | render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected} |
|
322 | 323 | else |
|
323 | 324 | content_tag 'p', l(:label_no_data), :class => "nodata" |
|
324 | 325 | end |
|
325 | 326 | end |
|
326 | 327 | |
|
327 | 328 | # Renders the project quick-jump box |
|
328 | 329 | def render_project_jump_box |
|
329 | 330 | return unless User.current.logged? |
|
330 | 331 | projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a |
|
331 | 332 | if projects.any? |
|
332 | 333 | options = |
|
333 | 334 | ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" + |
|
334 | 335 | '<option value="" disabled="disabled">---</option>').html_safe |
|
335 | 336 | |
|
336 | 337 | options << project_tree_options_for_select(projects, :selected => @project) do |p| |
|
337 | 338 | { :value => project_path(:id => p, :jump => current_menu_item) } |
|
338 | 339 | end |
|
339 | 340 | |
|
340 | 341 | select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') |
|
341 | 342 | end |
|
342 | 343 | end |
|
343 | 344 | |
|
344 | 345 | def project_tree_options_for_select(projects, options = {}) |
|
345 | 346 | s = ''.html_safe |
|
346 | 347 | if blank_text = options[:include_blank] |
|
347 | 348 | if blank_text == true |
|
348 | 349 | blank_text = ' '.html_safe |
|
349 | 350 | end |
|
350 | 351 | s << content_tag('option', blank_text, :value => '') |
|
351 | 352 | end |
|
352 | 353 | project_tree(projects) do |project, level| |
|
353 | 354 | name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe |
|
354 | 355 | tag_options = {:value => project.id} |
|
355 | 356 | if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) |
|
356 | 357 | tag_options[:selected] = 'selected' |
|
357 | 358 | else |
|
358 | 359 | tag_options[:selected] = nil |
|
359 | 360 | end |
|
360 | 361 | tag_options.merge!(yield(project)) if block_given? |
|
361 | 362 | s << content_tag('option', name_prefix + h(project), tag_options) |
|
362 | 363 | end |
|
363 | 364 | s.html_safe |
|
364 | 365 | end |
|
365 | 366 | |
|
366 | 367 | # Yields the given block for each project with its level in the tree |
|
367 | 368 | # |
|
368 | 369 | # Wrapper for Project#project_tree |
|
369 | 370 | def project_tree(projects, &block) |
|
370 | 371 | Project.project_tree(projects, &block) |
|
371 | 372 | end |
|
372 | 373 | |
|
373 | 374 | def principals_check_box_tags(name, principals) |
|
374 | 375 | s = '' |
|
375 | 376 | principals.each do |principal| |
|
376 | 377 | s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n" |
|
377 | 378 | end |
|
378 | 379 | s.html_safe |
|
379 | 380 | end |
|
380 | 381 | |
|
381 | 382 | # Returns a string for users/groups option tags |
|
382 | 383 | def principals_options_for_select(collection, selected=nil) |
|
383 | 384 | s = '' |
|
384 | 385 | if collection.include?(User.current) |
|
385 | 386 | s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) |
|
386 | 387 | end |
|
387 | 388 | groups = '' |
|
388 | 389 | collection.sort.each do |element| |
|
389 | 390 | selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected |
|
390 | 391 | (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>) |
|
391 | 392 | end |
|
392 | 393 | unless groups.empty? |
|
393 | 394 | s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>) |
|
394 | 395 | end |
|
395 | 396 | s.html_safe |
|
396 | 397 | end |
|
397 | 398 | |
|
398 | 399 | def option_tag(name, text, value, selected=nil, options={}) |
|
399 | 400 | content_tag 'option', value, options.merge(:value => value, :selected => (value == selected)) |
|
400 | 401 | end |
|
401 | 402 | |
|
402 | 403 | def truncate_single_line_raw(string, length) |
|
403 | 404 | string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ') |
|
404 | 405 | end |
|
405 | 406 | |
|
406 | 407 | # Truncates at line break after 250 characters or options[:length] |
|
407 | 408 | def truncate_lines(string, options={}) |
|
408 | 409 | length = options[:length] || 250 |
|
409 | 410 | if string.to_s =~ /\A(.{#{length}}.*?)$/m |
|
410 | 411 | "#{$1}..." |
|
411 | 412 | else |
|
412 | 413 | string |
|
413 | 414 | end |
|
414 | 415 | end |
|
415 | 416 | |
|
416 | 417 | def anchor(text) |
|
417 | 418 | text.to_s.gsub(' ', '_') |
|
418 | 419 | end |
|
419 | 420 | |
|
420 | 421 | def html_hours(text) |
|
421 | 422 | text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe |
|
422 | 423 | end |
|
423 | 424 | |
|
424 | 425 | def authoring(created, author, options={}) |
|
425 | 426 | l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe |
|
426 | 427 | end |
|
427 | 428 | |
|
428 | 429 | def time_tag(time) |
|
429 | 430 | text = distance_of_time_in_words(Time.now, time) |
|
430 | 431 | if @project |
|
431 | 432 | link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time)) |
|
432 | 433 | else |
|
433 | 434 | content_tag('abbr', text, :title => format_time(time)) |
|
434 | 435 | end |
|
435 | 436 | end |
|
436 | 437 | |
|
437 | 438 | def syntax_highlight_lines(name, content) |
|
438 | 439 | lines = [] |
|
439 | 440 | syntax_highlight(name, content).each_line { |line| lines << line } |
|
440 | 441 | lines |
|
441 | 442 | end |
|
442 | 443 | |
|
443 | 444 | def syntax_highlight(name, content) |
|
444 | 445 | Redmine::SyntaxHighlighting.highlight_by_filename(content, name) |
|
445 | 446 | end |
|
446 | 447 | |
|
447 | 448 | def to_path_param(path) |
|
448 | 449 | str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") |
|
449 | 450 | str.blank? ? nil : str |
|
450 | 451 | end |
|
451 | 452 | |
|
452 | 453 | def reorder_links(name, url, method = :post) |
|
453 | 454 | link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), |
|
454 | 455 | url.merge({"#{name}[move_to]" => 'highest'}), |
|
455 | 456 | :method => method, :title => l(:label_sort_highest)) + |
|
456 | 457 | link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), |
|
457 | 458 | url.merge({"#{name}[move_to]" => 'higher'}), |
|
458 | 459 | :method => method, :title => l(:label_sort_higher)) + |
|
459 | 460 | link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), |
|
460 | 461 | url.merge({"#{name}[move_to]" => 'lower'}), |
|
461 | 462 | :method => method, :title => l(:label_sort_lower)) + |
|
462 | 463 | link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), |
|
463 | 464 | url.merge({"#{name}[move_to]" => 'lowest'}), |
|
464 | 465 | :method => method, :title => l(:label_sort_lowest)) |
|
465 | 466 | end |
|
466 | 467 | |
|
467 | 468 | def breadcrumb(*args) |
|
468 | 469 | elements = args.flatten |
|
469 | 470 | elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil |
|
470 | 471 | end |
|
471 | 472 | |
|
472 | 473 | def other_formats_links(&block) |
|
473 | 474 | concat('<p class="other-formats">'.html_safe + l(:label_export_to)) |
|
474 | 475 | yield Redmine::Views::OtherFormatsBuilder.new(self) |
|
475 | 476 | concat('</p>'.html_safe) |
|
476 | 477 | end |
|
477 | 478 | |
|
478 | 479 | def page_header_title |
|
479 | 480 | if @project.nil? || @project.new_record? |
|
480 | 481 | h(Setting.app_title) |
|
481 | 482 | else |
|
482 | 483 | b = [] |
|
483 | 484 | ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a) |
|
484 | 485 | if ancestors.any? |
|
485 | 486 | root = ancestors.shift |
|
486 | 487 | b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') |
|
487 | 488 | if ancestors.size > 2 |
|
488 | 489 | b << "\xe2\x80\xa6" |
|
489 | 490 | ancestors = ancestors[-2, 2] |
|
490 | 491 | end |
|
491 | 492 | b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } |
|
492 | 493 | end |
|
493 | 494 | b << h(@project) |
|
494 | 495 | b.join(" \xc2\xbb ").html_safe |
|
495 | 496 | end |
|
496 | 497 | end |
|
497 | 498 | |
|
498 | 499 | # Returns a h2 tag and sets the html title with the given arguments |
|
499 | 500 | def title(*args) |
|
500 | 501 | strings = args.map do |arg| |
|
501 | 502 | if arg.is_a?(Array) && arg.size >= 2 |
|
502 | 503 | link_to(*arg) |
|
503 | 504 | else |
|
504 | 505 | h(arg.to_s) |
|
505 | 506 | end |
|
506 | 507 | end |
|
507 | 508 | html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s} |
|
508 | 509 | content_tag('h2', strings.join(' » ').html_safe) |
|
509 | 510 | end |
|
510 | 511 | |
|
511 | 512 | # Sets the html title |
|
512 | 513 | # Returns the html title when called without arguments |
|
513 | 514 | # Current project name and app_title and automatically appended |
|
514 | 515 | # Exemples: |
|
515 | 516 | # html_title 'Foo', 'Bar' |
|
516 | 517 | # html_title # => 'Foo - Bar - My Project - Redmine' |
|
517 | 518 | def html_title(*args) |
|
518 | 519 | if args.empty? |
|
519 | 520 | title = @html_title || [] |
|
520 | 521 | title << @project.name if @project |
|
521 | 522 | title << Setting.app_title unless Setting.app_title == title.last |
|
522 | 523 | title.reject(&:blank?).join(' - ') |
|
523 | 524 | else |
|
524 | 525 | @html_title ||= [] |
|
525 | 526 | @html_title += args |
|
526 | 527 | end |
|
527 | 528 | end |
|
528 | 529 | |
|
529 | 530 | # Returns the theme, controller name, and action as css classes for the |
|
530 | 531 | # HTML body. |
|
531 | 532 | def body_css_classes |
|
532 | 533 | css = [] |
|
533 | 534 | if theme = Redmine::Themes.theme(Setting.ui_theme) |
|
534 | 535 | css << 'theme-' + theme.name |
|
535 | 536 | end |
|
536 | 537 | |
|
537 | 538 | css << 'project-' + @project.identifier if @project && @project.identifier.present? |
|
538 | 539 | css << 'controller-' + controller_name |
|
539 | 540 | css << 'action-' + action_name |
|
540 | 541 | css.join(' ') |
|
541 | 542 | end |
|
542 | 543 | |
|
543 | 544 | def accesskey(s) |
|
544 | 545 | @used_accesskeys ||= [] |
|
545 | 546 | key = Redmine::AccessKeys.key_for(s) |
|
546 | 547 | return nil if @used_accesskeys.include?(key) |
|
547 | 548 | @used_accesskeys << key |
|
548 | 549 | key |
|
549 | 550 | end |
|
550 | 551 | |
|
551 | 552 | # Formats text according to system settings. |
|
552 | 553 | # 2 ways to call this method: |
|
553 | 554 | # * with a String: textilizable(text, options) |
|
554 | 555 | # * with an object and one of its attribute: textilizable(issue, :description, options) |
|
555 | 556 | def textilizable(*args) |
|
556 | 557 | options = args.last.is_a?(Hash) ? args.pop : {} |
|
557 | 558 | case args.size |
|
558 | 559 | when 1 |
|
559 | 560 | obj = options[:object] |
|
560 | 561 | text = args.shift |
|
561 | 562 | when 2 |
|
562 | 563 | obj = args.shift |
|
563 | 564 | attr = args.shift |
|
564 | 565 | text = obj.send(attr).to_s |
|
565 | 566 | else |
|
566 | 567 | raise ArgumentError, 'invalid arguments to textilizable' |
|
567 | 568 | end |
|
568 | 569 | return '' if text.blank? |
|
569 | 570 | project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) |
|
570 | 571 | @only_path = only_path = options.delete(:only_path) == false ? false : true |
|
571 | 572 | |
|
572 | 573 | text = text.dup |
|
573 | 574 | macros = catch_macros(text) |
|
574 | 575 | text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) |
|
575 | 576 | |
|
576 | 577 | @parsed_headings = [] |
|
577 | 578 | @heading_anchors = {} |
|
578 | 579 | @current_section = 0 if options[:edit_section_links] |
|
579 | 580 | |
|
580 | 581 | parse_sections(text, project, obj, attr, only_path, options) |
|
581 | 582 | text = parse_non_pre_blocks(text, obj, macros) do |text| |
|
582 | 583 | [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| |
|
583 | 584 | send method_name, text, project, obj, attr, only_path, options |
|
584 | 585 | end |
|
585 | 586 | end |
|
586 | 587 | parse_headings(text, project, obj, attr, only_path, options) |
|
587 | 588 | |
|
588 | 589 | if @parsed_headings.any? |
|
589 | 590 | replace_toc(text, @parsed_headings) |
|
590 | 591 | end |
|
591 | 592 | |
|
592 | 593 | text.html_safe |
|
593 | 594 | end |
|
594 | 595 | |
|
595 | 596 | def parse_non_pre_blocks(text, obj, macros) |
|
596 | 597 | s = StringScanner.new(text) |
|
597 | 598 | tags = [] |
|
598 | 599 | parsed = '' |
|
599 | 600 | while !s.eos? |
|
600 | 601 | s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) |
|
601 | 602 | text, full_tag, closing, tag = s[1], s[2], s[3], s[4] |
|
602 | 603 | if tags.empty? |
|
603 | 604 | yield text |
|
604 | 605 | inject_macros(text, obj, macros) if macros.any? |
|
605 | 606 | else |
|
606 | 607 | inject_macros(text, obj, macros, false) if macros.any? |
|
607 | 608 | end |
|
608 | 609 | parsed << text |
|
609 | 610 | if tag |
|
610 | 611 | if closing |
|
611 | 612 | if tags.last == tag.downcase |
|
612 | 613 | tags.pop |
|
613 | 614 | end |
|
614 | 615 | else |
|
615 | 616 | tags << tag.downcase |
|
616 | 617 | end |
|
617 | 618 | parsed << full_tag |
|
618 | 619 | end |
|
619 | 620 | end |
|
620 | 621 | # Close any non closing tags |
|
621 | 622 | while tag = tags.pop |
|
622 | 623 | parsed << "</#{tag}>" |
|
623 | 624 | end |
|
624 | 625 | parsed |
|
625 | 626 | end |
|
626 | 627 | |
|
627 | 628 | def parse_inline_attachments(text, project, obj, attr, only_path, options) |
|
628 | 629 | return if options[:inline_attachments] == false |
|
629 | 630 | |
|
630 | 631 | # when using an image link, try to use an attachment, if possible |
|
631 | 632 | attachments = options[:attachments] || [] |
|
632 | 633 | attachments += obj.attachments if obj.respond_to?(:attachments) |
|
633 | 634 | if attachments.present? |
|
634 | 635 | text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| |
|
635 | 636 | filename, ext, alt, alttext = $1.downcase, $2, $3, $4 |
|
636 | 637 | # search for the picture in attachments |
|
637 | 638 | if found = Attachment.latest_attach(attachments, CGI.unescape(filename)) |
|
638 | 639 | image_url = download_named_attachment_url(found, found.filename, :only_path => only_path) |
|
639 | 640 | desc = found.description.to_s.gsub('"', '') |
|
640 | 641 | if !desc.blank? && alttext.blank? |
|
641 | 642 | alt = " title=\"#{desc}\" alt=\"#{desc}\"" |
|
642 | 643 | end |
|
643 | 644 | "src=\"#{image_url}\"#{alt}" |
|
644 | 645 | else |
|
645 | 646 | m |
|
646 | 647 | end |
|
647 | 648 | end |
|
648 | 649 | end |
|
649 | 650 | end |
|
650 | 651 | |
|
651 | 652 | # Wiki links |
|
652 | 653 | # |
|
653 | 654 | # Examples: |
|
654 | 655 | # [[mypage]] |
|
655 | 656 | # [[mypage|mytext]] |
|
656 | 657 | # wiki links can refer other project wikis, using project name or identifier: |
|
657 | 658 | # [[project:]] -> wiki starting page |
|
658 | 659 | # [[project:|mytext]] |
|
659 | 660 | # [[project:mypage]] |
|
660 | 661 | # [[project:mypage|mytext]] |
|
661 | 662 | def parse_wiki_links(text, project, obj, attr, only_path, options) |
|
662 | 663 | text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| |
|
663 | 664 | link_project = project |
|
664 | 665 | esc, all, page, title = $1, $2, $3, $5 |
|
665 | 666 | if esc.nil? |
|
666 | 667 | if page =~ /^([^\:]+)\:(.*)$/ |
|
667 | 668 | identifier, page = $1, $2 |
|
668 | 669 | link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier) |
|
669 | 670 | title ||= identifier if page.blank? |
|
670 | 671 | end |
|
671 | 672 | |
|
672 | 673 | if link_project && link_project.wiki |
|
673 | 674 | # extract anchor |
|
674 | 675 | anchor = nil |
|
675 | 676 | if page =~ /^(.+?)\#(.+)$/ |
|
676 | 677 | page, anchor = $1, $2 |
|
677 | 678 | end |
|
678 | 679 | anchor = sanitize_anchor_name(anchor) if anchor.present? |
|
679 | 680 | # check if page exists |
|
680 | 681 | wiki_page = link_project.wiki.find_page(page) |
|
681 | 682 | url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page |
|
682 | 683 | "##{anchor}" |
|
683 | 684 | else |
|
684 | 685 | case options[:wiki_links] |
|
685 | 686 | when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') |
|
686 | 687 | when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export |
|
687 | 688 | else |
|
688 | 689 | wiki_page_id = page.present? ? Wiki.titleize(page) : nil |
|
689 | 690 | parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil |
|
690 | 691 | url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, |
|
691 | 692 | :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent) |
|
692 | 693 | end |
|
693 | 694 | end |
|
694 | 695 | link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) |
|
695 | 696 | else |
|
696 | 697 | # project or wiki doesn't exist |
|
697 | 698 | all |
|
698 | 699 | end |
|
699 | 700 | else |
|
700 | 701 | all |
|
701 | 702 | end |
|
702 | 703 | end |
|
703 | 704 | end |
|
704 | 705 | |
|
705 | 706 | # Redmine links |
|
706 | 707 | # |
|
707 | 708 | # Examples: |
|
708 | 709 | # Issues: |
|
709 | 710 | # #52 -> Link to issue #52 |
|
710 | 711 | # Changesets: |
|
711 | 712 | # r52 -> Link to revision 52 |
|
712 | 713 | # commit:a85130f -> Link to scmid starting with a85130f |
|
713 | 714 | # Documents: |
|
714 | 715 | # document#17 -> Link to document with id 17 |
|
715 | 716 | # document:Greetings -> Link to the document with title "Greetings" |
|
716 | 717 | # document:"Some document" -> Link to the document with title "Some document" |
|
717 | 718 | # Versions: |
|
718 | 719 | # version#3 -> Link to version with id 3 |
|
719 | 720 | # version:1.0.0 -> Link to version named "1.0.0" |
|
720 | 721 | # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" |
|
721 | 722 | # Attachments: |
|
722 | 723 | # attachment:file.zip -> Link to the attachment of the current object named file.zip |
|
723 | 724 | # Source files: |
|
724 | 725 | # source:some/file -> Link to the file located at /some/file in the project's repository |
|
725 | 726 | # source:some/file@52 -> Link to the file's revision 52 |
|
726 | 727 | # source:some/file#L120 -> Link to line 120 of the file |
|
727 | 728 | # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 |
|
728 | 729 | # export:some/file -> Force the download of the file |
|
729 | 730 | # Forum messages: |
|
730 | 731 | # message#1218 -> Link to message with id 1218 |
|
731 | 732 | # Projects: |
|
732 | 733 | # project:someproject -> Link to project named "someproject" |
|
733 | 734 | # project#3 -> Link to project with id 3 |
|
734 | 735 | # |
|
735 | 736 | # Links can refer other objects from other projects, using project identifier: |
|
736 | 737 | # identifier:r52 |
|
737 | 738 | # identifier:document:"Some document" |
|
738 | 739 | # identifier:version:1.0.0 |
|
739 | 740 | # identifier:source:some/file |
|
740 | 741 | def parse_redmine_links(text, default_project, obj, attr, only_path, options) |
|
741 | 742 | text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m| |
|
742 | 743 | tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19 |
|
743 | 744 | if tag_content |
|
744 | 745 | $& |
|
745 | 746 | else |
|
746 | 747 | link = nil |
|
747 | 748 | project = default_project |
|
748 | 749 | if project_identifier |
|
749 | 750 | project = Project.visible.find_by_identifier(project_identifier) |
|
750 | 751 | end |
|
751 | 752 | if esc.nil? |
|
752 | 753 | if prefix.nil? && sep == 'r' |
|
753 | 754 | if project |
|
754 | 755 | repository = nil |
|
755 | 756 | if repo_identifier |
|
756 | 757 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} |
|
757 | 758 | else |
|
758 | 759 | repository = project.repository |
|
759 | 760 | end |
|
760 | 761 | # project.changesets.visible raises an SQL error because of a double join on repositories |
|
761 | 762 | if repository && |
|
762 | 763 | (changeset = Changeset.visible. |
|
763 | 764 | find_by_repository_id_and_revision(repository.id, identifier)) |
|
764 | 765 | link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), |
|
765 | 766 | {:only_path => only_path, :controller => 'repositories', |
|
766 | 767 | :action => 'revision', :id => project, |
|
767 | 768 | :repository_id => repository.identifier_param, |
|
768 | 769 | :rev => changeset.revision}, |
|
769 | 770 | :class => 'changeset', |
|
770 | 771 | :title => truncate_single_line_raw(changeset.comments, 100)) |
|
771 | 772 | end |
|
772 | 773 | end |
|
773 | 774 | elsif sep == '#' |
|
774 | 775 | oid = identifier.to_i |
|
775 | 776 | case prefix |
|
776 | 777 | when nil |
|
777 | 778 | if oid.to_s == identifier && |
|
778 | 779 | issue = Issue.visible.find_by_id(oid) |
|
779 | 780 | anchor = comment_id ? "note-#{comment_id}" : nil |
|
780 | 781 | link = link_to("##{oid}#{comment_suffix}", |
|
781 | 782 | issue_url(issue, :only_path => only_path, :anchor => anchor), |
|
782 | 783 | :class => issue.css_classes, |
|
783 | 784 | :title => "#{issue.subject.truncate(100)} (#{issue.status.name})") |
|
784 | 785 | end |
|
785 | 786 | when 'document' |
|
786 | 787 | if document = Document.visible.find_by_id(oid) |
|
787 | 788 | link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document') |
|
788 | 789 | end |
|
789 | 790 | when 'version' |
|
790 | 791 | if version = Version.visible.find_by_id(oid) |
|
791 | 792 | link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version') |
|
792 | 793 | end |
|
793 | 794 | when 'message' |
|
794 | 795 | if message = Message.visible.find_by_id(oid) |
|
795 | 796 | link = link_to_message(message, {:only_path => only_path}, :class => 'message') |
|
796 | 797 | end |
|
797 | 798 | when 'forum' |
|
798 | 799 | if board = Board.visible.find_by_id(oid) |
|
799 | 800 | link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board') |
|
800 | 801 | end |
|
801 | 802 | when 'news' |
|
802 | 803 | if news = News.visible.find_by_id(oid) |
|
803 | 804 | link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news') |
|
804 | 805 | end |
|
805 | 806 | when 'project' |
|
806 | 807 | if p = Project.visible.find_by_id(oid) |
|
807 | 808 | link = link_to_project(p, {:only_path => only_path}, :class => 'project') |
|
808 | 809 | end |
|
809 | 810 | end |
|
810 | 811 | elsif sep == ':' |
|
811 | 812 | # removes the double quotes if any |
|
812 | 813 | name = identifier.gsub(%r{^"(.*)"$}, "\\1") |
|
813 | 814 | name = CGI.unescapeHTML(name) |
|
814 | 815 | case prefix |
|
815 | 816 | when 'document' |
|
816 | 817 | if project && document = project.documents.visible.find_by_title(name) |
|
817 | 818 | link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document') |
|
818 | 819 | end |
|
819 | 820 | when 'version' |
|
820 | 821 | if project && version = project.versions.visible.find_by_name(name) |
|
821 | 822 | link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version') |
|
822 | 823 | end |
|
823 | 824 | when 'forum' |
|
824 | 825 | if project && board = project.boards.visible.find_by_name(name) |
|
825 | 826 | link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board') |
|
826 | 827 | end |
|
827 | 828 | when 'news' |
|
828 | 829 | if project && news = project.news.visible.find_by_title(name) |
|
829 | 830 | link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news') |
|
830 | 831 | end |
|
831 | 832 | when 'commit', 'source', 'export' |
|
832 | 833 | if project |
|
833 | 834 | repository = nil |
|
834 | 835 | if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$} |
|
835 | 836 | repo_prefix, repo_identifier, name = $1, $2, $3 |
|
836 | 837 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} |
|
837 | 838 | else |
|
838 | 839 | repository = project.repository |
|
839 | 840 | end |
|
840 | 841 | if prefix == 'commit' |
|
841 | 842 | if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first) |
|
842 | 843 | link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier}, |
|
843 | 844 | :class => 'changeset', |
|
844 | 845 | :title => truncate_single_line_raw(changeset.comments, 100) |
|
845 | 846 | end |
|
846 | 847 | else |
|
847 | 848 | if repository && User.current.allowed_to?(:browse_repository, project) |
|
848 | 849 | name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$} |
|
849 | 850 | path, rev, anchor = $1, $3, $5 |
|
850 | 851 | link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param, |
|
851 | 852 | :path => to_path_param(path), |
|
852 | 853 | :rev => rev, |
|
853 | 854 | :anchor => anchor}, |
|
854 | 855 | :class => (prefix == 'export' ? 'source download' : 'source') |
|
855 | 856 | end |
|
856 | 857 | end |
|
857 | 858 | repo_prefix = nil |
|
858 | 859 | end |
|
859 | 860 | when 'attachment' |
|
860 | 861 | attachments = options[:attachments] || [] |
|
861 | 862 | attachments += obj.attachments if obj.respond_to?(:attachments) |
|
862 | 863 | if attachments && attachment = Attachment.latest_attach(attachments, name) |
|
863 | 864 | link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment') |
|
864 | 865 | end |
|
865 | 866 | when 'project' |
|
866 | 867 | if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first |
|
867 | 868 | link = link_to_project(p, {:only_path => only_path}, :class => 'project') |
|
868 | 869 | end |
|
869 | 870 | end |
|
870 | 871 | end |
|
871 | 872 | end |
|
872 | 873 | (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) |
|
873 | 874 | end |
|
874 | 875 | end |
|
875 | 876 | end |
|
876 | 877 | |
|
877 | 878 | HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) |
|
878 | 879 | |
|
879 | 880 | def parse_sections(text, project, obj, attr, only_path, options) |
|
880 | 881 | return unless options[:edit_section_links] |
|
881 | 882 | text.gsub!(HEADING_RE) do |
|
882 | 883 | heading = $1 |
|
883 | 884 | @current_section += 1 |
|
884 | 885 | if @current_section > 1 |
|
885 | 886 | content_tag('div', |
|
886 | 887 | link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), |
|
887 | 888 | :class => 'contextual', |
|
888 | 889 | :title => l(:button_edit_section), |
|
889 | 890 | :id => "section-#{@current_section}") + heading.html_safe |
|
890 | 891 | else |
|
891 | 892 | heading |
|
892 | 893 | end |
|
893 | 894 | end |
|
894 | 895 | end |
|
895 | 896 | |
|
896 | 897 | # Headings and TOC |
|
897 | 898 | # Adds ids and links to headings unless options[:headings] is set to false |
|
898 | 899 | def parse_headings(text, project, obj, attr, only_path, options) |
|
899 | 900 | return if options[:headings] == false |
|
900 | 901 | |
|
901 | 902 | text.gsub!(HEADING_RE) do |
|
902 | 903 | level, attrs, content = $2.to_i, $3, $4 |
|
903 | 904 | item = strip_tags(content).strip |
|
904 | 905 | anchor = sanitize_anchor_name(item) |
|
905 | 906 | # used for single-file wiki export |
|
906 | 907 | anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) |
|
907 | 908 | @heading_anchors[anchor] ||= 0 |
|
908 | 909 | idx = (@heading_anchors[anchor] += 1) |
|
909 | 910 | if idx > 1 |
|
910 | 911 | anchor = "#{anchor}-#{idx}" |
|
911 | 912 | end |
|
912 | 913 | @parsed_headings << [level, anchor, item] |
|
913 | 914 | "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>" |
|
914 | 915 | end |
|
915 | 916 | end |
|
916 | 917 | |
|
917 | 918 | MACROS_RE = /( |
|
918 | 919 | (!)? # escaping |
|
919 | 920 | ( |
|
920 | 921 | \{\{ # opening tag |
|
921 | 922 | ([\w]+) # macro name |
|
922 | 923 | (\(([^\n\r]*?)\))? # optional arguments |
|
923 | 924 | ([\n\r].*?[\n\r])? # optional block of text |
|
924 | 925 | \}\} # closing tag |
|
925 | 926 | ) |
|
926 | 927 | )/mx unless const_defined?(:MACROS_RE) |
|
927 | 928 | |
|
928 | 929 | MACRO_SUB_RE = /( |
|
929 | 930 | \{\{ |
|
930 | 931 | macro\((\d+)\) |
|
931 | 932 | \}\} |
|
932 | 933 | )/x unless const_defined?(:MACRO_SUB_RE) |
|
933 | 934 | |
|
934 | 935 | # Extracts macros from text |
|
935 | 936 | def catch_macros(text) |
|
936 | 937 | macros = {} |
|
937 | 938 | text.gsub!(MACROS_RE) do |
|
938 | 939 | all, macro = $1, $4.downcase |
|
939 | 940 | if macro_exists?(macro) || all =~ MACRO_SUB_RE |
|
940 | 941 | index = macros.size |
|
941 | 942 | macros[index] = all |
|
942 | 943 | "{{macro(#{index})}}" |
|
943 | 944 | else |
|
944 | 945 | all |
|
945 | 946 | end |
|
946 | 947 | end |
|
947 | 948 | macros |
|
948 | 949 | end |
|
949 | 950 | |
|
950 | 951 | # Executes and replaces macros in text |
|
951 | 952 | def inject_macros(text, obj, macros, execute=true) |
|
952 | 953 | text.gsub!(MACRO_SUB_RE) do |
|
953 | 954 | all, index = $1, $2.to_i |
|
954 | 955 | orig = macros.delete(index) |
|
955 | 956 | if execute && orig && orig =~ MACROS_RE |
|
956 | 957 | esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) |
|
957 | 958 | if esc.nil? |
|
958 | 959 | h(exec_macro(macro, obj, args, block) || all) |
|
959 | 960 | else |
|
960 | 961 | h(all) |
|
961 | 962 | end |
|
962 | 963 | elsif orig |
|
963 | 964 | h(orig) |
|
964 | 965 | else |
|
965 | 966 | h(all) |
|
966 | 967 | end |
|
967 | 968 | end |
|
968 | 969 | end |
|
969 | 970 | |
|
970 | 971 | TOC_RE = /<p>\{\{((<|<)|(>|>))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE) |
|
971 | 972 | |
|
972 | 973 | # Renders the TOC with given headings |
|
973 | 974 | def replace_toc(text, headings) |
|
974 | 975 | text.gsub!(TOC_RE) do |
|
975 | 976 | left_align, right_align = $2, $3 |
|
976 | 977 | # Keep only the 4 first levels |
|
977 | 978 | headings = headings.select{|level, anchor, item| level <= 4} |
|
978 | 979 | if headings.empty? |
|
979 | 980 | '' |
|
980 | 981 | else |
|
981 | 982 | div_class = 'toc' |
|
982 | 983 | div_class << ' right' if right_align |
|
983 | 984 | div_class << ' left' if left_align |
|
984 | 985 | out = "<ul class=\"#{div_class}\"><li>" |
|
985 | 986 | root = headings.map(&:first).min |
|
986 | 987 | current = root |
|
987 | 988 | started = false |
|
988 | 989 | headings.each do |level, anchor, item| |
|
989 | 990 | if level > current |
|
990 | 991 | out << '<ul><li>' * (level - current) |
|
991 | 992 | elsif level < current |
|
992 | 993 | out << "</li></ul>\n" * (current - level) + "</li><li>" |
|
993 | 994 | elsif started |
|
994 | 995 | out << '</li><li>' |
|
995 | 996 | end |
|
996 | 997 | out << "<a href=\"##{anchor}\">#{item}</a>" |
|
997 | 998 | current = level |
|
998 | 999 | started = true |
|
999 | 1000 | end |
|
1000 | 1001 | out << '</li></ul>' * (current - root) |
|
1001 | 1002 | out << '</li></ul>' |
|
1002 | 1003 | end |
|
1003 | 1004 | end |
|
1004 | 1005 | end |
|
1005 | 1006 | |
|
1006 | 1007 | # Same as Rails' simple_format helper without using paragraphs |
|
1007 | 1008 | def simple_format_without_paragraph(text) |
|
1008 | 1009 | text.to_s. |
|
1009 | 1010 | gsub(/\r\n?/, "\n"). # \r\n and \r -> \n |
|
1010 | 1011 | gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br |
|
1011 | 1012 | gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br |
|
1012 | 1013 | html_safe |
|
1013 | 1014 | end |
|
1014 | 1015 | |
|
1015 | 1016 | def lang_options_for_select(blank=true) |
|
1016 | 1017 | (blank ? [["(auto)", ""]] : []) + languages_options |
|
1017 | 1018 | end |
|
1018 | 1019 | |
|
1019 | 1020 | def labelled_form_for(*args, &proc) |
|
1020 | 1021 | args << {} unless args.last.is_a?(Hash) |
|
1021 | 1022 | options = args.last |
|
1022 | 1023 | if args.first.is_a?(Symbol) |
|
1023 | 1024 | options.merge!(:as => args.shift) |
|
1024 | 1025 | end |
|
1025 | 1026 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) |
|
1026 | 1027 | form_for(*args, &proc) |
|
1027 | 1028 | end |
|
1028 | 1029 | |
|
1029 | 1030 | def labelled_fields_for(*args, &proc) |
|
1030 | 1031 | args << {} unless args.last.is_a?(Hash) |
|
1031 | 1032 | options = args.last |
|
1032 | 1033 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) |
|
1033 | 1034 | fields_for(*args, &proc) |
|
1034 | 1035 | end |
|
1035 | 1036 | |
|
1036 | 1037 | def error_messages_for(*objects) |
|
1037 | 1038 | html = "" |
|
1038 | 1039 | objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact |
|
1039 | 1040 | errors = objects.map {|o| o.errors.full_messages}.flatten |
|
1040 | 1041 | if errors.any? |
|
1041 | 1042 | html << "<div id='errorExplanation'><ul>\n" |
|
1042 | 1043 | errors.each do |error| |
|
1043 | 1044 | html << "<li>#{h error}</li>\n" |
|
1044 | 1045 | end |
|
1045 | 1046 | html << "</ul></div>\n" |
|
1046 | 1047 | end |
|
1047 | 1048 | html.html_safe |
|
1048 | 1049 | end |
|
1049 | 1050 | |
|
1050 | 1051 | def delete_link(url, options={}) |
|
1051 | 1052 | options = { |
|
1052 | 1053 | :method => :delete, |
|
1053 | 1054 | :data => {:confirm => l(:text_are_you_sure)}, |
|
1054 | 1055 | :class => 'icon icon-del' |
|
1055 | 1056 | }.merge(options) |
|
1056 | 1057 | |
|
1057 | 1058 | link_to l(:button_delete), url, options |
|
1058 | 1059 | end |
|
1059 | 1060 | |
|
1060 | 1061 | def preview_link(url, form, target='preview', options={}) |
|
1061 | 1062 | content_tag 'a', l(:label_preview), { |
|
1062 | 1063 | :href => "#", |
|
1063 | 1064 | :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, |
|
1064 | 1065 | :accesskey => accesskey(:preview) |
|
1065 | 1066 | }.merge(options) |
|
1066 | 1067 | end |
|
1067 | 1068 | |
|
1068 | 1069 | def link_to_function(name, function, html_options={}) |
|
1069 | 1070 | content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) |
|
1070 | 1071 | end |
|
1071 | 1072 | |
|
1072 | 1073 | # Helper to render JSON in views |
|
1073 | 1074 | def raw_json(arg) |
|
1074 | 1075 | arg.to_json.to_s.gsub('/', '\/').html_safe |
|
1075 | 1076 | end |
|
1076 | 1077 | |
|
1077 | 1078 | def back_url |
|
1078 | 1079 | url = params[:back_url] |
|
1079 | 1080 | if url.nil? && referer = request.env['HTTP_REFERER'] |
|
1080 | 1081 | url = CGI.unescape(referer.to_s) |
|
1081 | 1082 | end |
|
1082 | 1083 | url |
|
1083 | 1084 | end |
|
1084 | 1085 | |
|
1085 | 1086 | def back_url_hidden_field_tag |
|
1086 | 1087 | url = back_url |
|
1087 | 1088 | hidden_field_tag('back_url', url, :id => nil) unless url.blank? |
|
1088 | 1089 | end |
|
1089 | 1090 | |
|
1090 | 1091 | def check_all_links(form_name) |
|
1091 | 1092 | link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + |
|
1092 | 1093 | " | ".html_safe + |
|
1093 | 1094 | link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") |
|
1094 | 1095 | end |
|
1095 | 1096 | |
|
1096 | 1097 | def toggle_checkboxes_link(selector) |
|
1097 | 1098 | link_to_function image_tag('toggle_check.png'), |
|
1098 | 1099 | "toggleCheckboxesBySelector('#{selector}')", |
|
1099 | 1100 | :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}" |
|
1100 | 1101 | end |
|
1101 | 1102 | |
|
1102 | 1103 | def progress_bar(pcts, options={}) |
|
1103 | 1104 | pcts = [pcts, pcts] unless pcts.is_a?(Array) |
|
1104 | 1105 | pcts = pcts.collect(&:round) |
|
1105 | 1106 | pcts[1] = pcts[1] - pcts[0] |
|
1106 | 1107 | pcts << (100 - pcts[1] - pcts[0]) |
|
1107 | 1108 | width = options[:width] || '100px;' |
|
1108 | 1109 | legend = options[:legend] || '' |
|
1109 | 1110 | content_tag('table', |
|
1110 | 1111 | content_tag('tr', |
|
1111 | 1112 | (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + |
|
1112 | 1113 | (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + |
|
1113 | 1114 | (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) |
|
1114 | 1115 | ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe + |
|
1115 | 1116 | content_tag('p', legend, :class => 'percent').html_safe |
|
1116 | 1117 | end |
|
1117 | 1118 | |
|
1118 | 1119 | def checked_image(checked=true) |
|
1119 | 1120 | if checked |
|
1120 | 1121 | @checked_image_tag ||= image_tag('toggle_check.png') |
|
1121 | 1122 | end |
|
1122 | 1123 | end |
|
1123 | 1124 | |
|
1124 | 1125 | def context_menu(url) |
|
1125 | 1126 | unless @context_menu_included |
|
1126 | 1127 | content_for :header_tags do |
|
1127 | 1128 | javascript_include_tag('context_menu') + |
|
1128 | 1129 | stylesheet_link_tag('context_menu') |
|
1129 | 1130 | end |
|
1130 | 1131 | if l(:direction) == 'rtl' |
|
1131 | 1132 | content_for :header_tags do |
|
1132 | 1133 | stylesheet_link_tag('context_menu_rtl') |
|
1133 | 1134 | end |
|
1134 | 1135 | end |
|
1135 | 1136 | @context_menu_included = true |
|
1136 | 1137 | end |
|
1137 | 1138 | javascript_tag "contextMenuInit('#{ url_for(url) }')" |
|
1138 | 1139 | end |
|
1139 | 1140 | |
|
1140 | 1141 | def calendar_for(field_id) |
|
1141 | 1142 | include_calendar_headers_tags |
|
1142 | 1143 | javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") |
|
1143 | 1144 | end |
|
1144 | 1145 | |
|
1145 | 1146 | def include_calendar_headers_tags |
|
1146 | 1147 | unless @calendar_headers_tags_included |
|
1147 | 1148 | tags = ''.html_safe |
|
1148 | 1149 | @calendar_headers_tags_included = true |
|
1149 | 1150 | content_for :header_tags do |
|
1150 | 1151 | start_of_week = Setting.start_of_week |
|
1151 | 1152 | start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? |
|
1152 | 1153 | # Redmine uses 1..7 (monday..sunday) in settings and locales |
|
1153 | 1154 | # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 |
|
1154 | 1155 | start_of_week = start_of_week.to_i % 7 |
|
1155 | 1156 | tags << javascript_tag( |
|
1156 | 1157 | "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + |
|
1157 | 1158 | "showOn: 'button', buttonImageOnly: true, buttonImage: '" + |
|
1158 | 1159 | path_to_image('/images/calendar.png') + |
|
1159 | 1160 | "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " + |
|
1160 | 1161 | "selectOtherMonths: true, changeMonth: true, changeYear: true, " + |
|
1161 | 1162 | "beforeShow: beforeShowDatePicker};") |
|
1162 | 1163 | jquery_locale = l('jquery.locale', :default => current_language.to_s) |
|
1163 | 1164 | unless jquery_locale == 'en' |
|
1164 | 1165 | tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js") |
|
1165 | 1166 | end |
|
1166 | 1167 | tags |
|
1167 | 1168 | end |
|
1168 | 1169 | end |
|
1169 | 1170 | end |
|
1170 | 1171 | |
|
1171 | 1172 | # Overrides Rails' stylesheet_link_tag with themes and plugins support. |
|
1172 | 1173 | # Examples: |
|
1173 | 1174 | # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults |
|
1174 | 1175 | # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets |
|
1175 | 1176 | # |
|
1176 | 1177 | def stylesheet_link_tag(*sources) |
|
1177 | 1178 | options = sources.last.is_a?(Hash) ? sources.pop : {} |
|
1178 | 1179 | plugin = options.delete(:plugin) |
|
1179 | 1180 | sources = sources.map do |source| |
|
1180 | 1181 | if plugin |
|
1181 | 1182 | "/plugin_assets/#{plugin}/stylesheets/#{source}" |
|
1182 | 1183 | elsif current_theme && current_theme.stylesheets.include?(source) |
|
1183 | 1184 | current_theme.stylesheet_path(source) |
|
1184 | 1185 | else |
|
1185 | 1186 | source |
|
1186 | 1187 | end |
|
1187 | 1188 | end |
|
1188 | 1189 | super *sources, options |
|
1189 | 1190 | end |
|
1190 | 1191 | |
|
1191 | 1192 | # Overrides Rails' image_tag with themes and plugins support. |
|
1192 | 1193 | # Examples: |
|
1193 | 1194 | # image_tag('image.png') # => picks image.png from the current theme or defaults |
|
1194 | 1195 | # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets |
|
1195 | 1196 | # |
|
1196 | 1197 | def image_tag(source, options={}) |
|
1197 | 1198 | if plugin = options.delete(:plugin) |
|
1198 | 1199 | source = "/plugin_assets/#{plugin}/images/#{source}" |
|
1199 | 1200 | elsif current_theme && current_theme.images.include?(source) |
|
1200 | 1201 | source = current_theme.image_path(source) |
|
1201 | 1202 | end |
|
1202 | 1203 | super source, options |
|
1203 | 1204 | end |
|
1204 | 1205 | |
|
1205 | 1206 | # Overrides Rails' javascript_include_tag with plugins support |
|
1206 | 1207 | # Examples: |
|
1207 | 1208 | # javascript_include_tag('scripts') # => picks scripts.js from defaults |
|
1208 | 1209 | # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets |
|
1209 | 1210 | # |
|
1210 | 1211 | def javascript_include_tag(*sources) |
|
1211 | 1212 | options = sources.last.is_a?(Hash) ? sources.pop : {} |
|
1212 | 1213 | if plugin = options.delete(:plugin) |
|
1213 | 1214 | sources = sources.map do |source| |
|
1214 | 1215 | if plugin |
|
1215 | 1216 | "/plugin_assets/#{plugin}/javascripts/#{source}" |
|
1216 | 1217 | else |
|
1217 | 1218 | source |
|
1218 | 1219 | end |
|
1219 | 1220 | end |
|
1220 | 1221 | end |
|
1221 | 1222 | super *sources, options |
|
1222 | 1223 | end |
|
1223 | 1224 | |
|
1224 | 1225 | def sidebar_content? |
|
1225 | 1226 | content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present? |
|
1226 | 1227 | end |
|
1227 | 1228 | |
|
1228 | 1229 | def view_layouts_base_sidebar_hook_response |
|
1229 | 1230 | @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) |
|
1230 | 1231 | end |
|
1231 | 1232 | |
|
1232 | 1233 | def email_delivery_enabled? |
|
1233 | 1234 | !!ActionMailer::Base.perform_deliveries |
|
1234 | 1235 | end |
|
1235 | 1236 | |
|
1236 | 1237 | # Returns the avatar image tag for the given +user+ if avatars are enabled |
|
1237 | 1238 | # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') |
|
1238 | 1239 | def avatar(user, options = { }) |
|
1239 | 1240 | if Setting.gravatar_enabled? |
|
1240 | 1241 | options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) |
|
1241 | 1242 | email = nil |
|
1242 | 1243 | if user.respond_to?(:mail) |
|
1243 | 1244 | email = user.mail |
|
1244 | 1245 | elsif user.to_s =~ %r{<(.+?)>} |
|
1245 | 1246 | email = $1 |
|
1246 | 1247 | end |
|
1247 | 1248 | return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil |
|
1248 | 1249 | else |
|
1249 | 1250 | '' |
|
1250 | 1251 | end |
|
1251 | 1252 | end |
|
1252 | 1253 | |
|
1253 | 1254 | def sanitize_anchor_name(anchor) |
|
1254 | 1255 | anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
1255 | 1256 | end |
|
1256 | 1257 | |
|
1257 | 1258 | # Returns the javascript tags that are included in the html layout head |
|
1258 | 1259 | def javascript_heads |
|
1259 | 1260 | tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application') |
|
1260 | 1261 | unless User.current.pref.warn_on_leaving_unsaved == '0' |
|
1261 | 1262 | tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") |
|
1262 | 1263 | end |
|
1263 | 1264 | tags |
|
1264 | 1265 | end |
|
1265 | 1266 | |
|
1266 | 1267 | def favicon |
|
1267 | 1268 | "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe |
|
1268 | 1269 | end |
|
1269 | 1270 | |
|
1270 | 1271 | # Returns the path to the favicon |
|
1271 | 1272 | def favicon_path |
|
1272 | 1273 | icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico' |
|
1273 | 1274 | image_path(icon) |
|
1274 | 1275 | end |
|
1275 | 1276 | |
|
1276 | 1277 | # Returns the full URL to the favicon |
|
1277 | 1278 | def favicon_url |
|
1278 | 1279 | # TODO: use #image_url introduced in Rails4 |
|
1279 | 1280 | path = favicon_path |
|
1280 | 1281 | base = url_for(:controller => 'welcome', :action => 'index', :only_path => false) |
|
1281 | 1282 | base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'') |
|
1282 | 1283 | end |
|
1283 | 1284 | |
|
1284 | 1285 | def robot_exclusion_tag |
|
1285 | 1286 | '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe |
|
1286 | 1287 | end |
|
1287 | 1288 | |
|
1288 | 1289 | # Returns true if arg is expected in the API response |
|
1289 | 1290 | def include_in_api_response?(arg) |
|
1290 | 1291 | unless @included_in_api_response |
|
1291 | 1292 | param = params[:include] |
|
1292 | 1293 | @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') |
|
1293 | 1294 | @included_in_api_response.collect!(&:strip) |
|
1294 | 1295 | end |
|
1295 | 1296 | @included_in_api_response.include?(arg.to_s) |
|
1296 | 1297 | end |
|
1297 | 1298 | |
|
1298 | 1299 | # Returns options or nil if nometa param or X-Redmine-Nometa header |
|
1299 | 1300 | # was set in the request |
|
1300 | 1301 | def api_meta(options) |
|
1301 | 1302 | if params[:nometa].present? || request.headers['X-Redmine-Nometa'] |
|
1302 | 1303 | # compatibility mode for activeresource clients that raise |
|
1303 | 1304 | # an error when deserializing an array with attributes |
|
1304 | 1305 | nil |
|
1305 | 1306 | else |
|
1306 | 1307 | options |
|
1307 | 1308 | end |
|
1308 | 1309 | end |
|
1309 | 1310 | |
|
1310 | 1311 | def generate_csv(&block) |
|
1311 | 1312 | decimal_separator = l(:general_csv_decimal_separator) |
|
1312 | 1313 | encoding = l(:general_csv_encoding) |
|
1313 | 1314 | end |
|
1314 | 1315 | |
|
1315 | 1316 | private |
|
1316 | 1317 | |
|
1317 | 1318 | def wiki_helper |
|
1318 | 1319 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
|
1319 | 1320 | extend helper |
|
1320 | 1321 | return self |
|
1321 | 1322 | end |
|
1322 | 1323 | |
|
1323 | 1324 | def link_to_content_update(text, url_params = {}, html_options = {}) |
|
1324 | 1325 | link_to(text, url_params, html_options) |
|
1325 | 1326 | end |
|
1326 | 1327 | end |
@@ -1,36 +1,36 | |||
|
1 | 1 | <h3><%=l(:label_my_account)%></h3> |
|
2 | 2 | |
|
3 | 3 | <p><%=l(:field_login)%>: <strong><%= link_to_user(@user, :format => :username) %></strong><br /> |
|
4 | 4 | <%=l(:field_created_on)%>: <%= format_time(@user.created_on) %></p> |
|
5 | 5 | |
|
6 | 6 | <% if @user.own_account_deletable? %> |
|
7 | 7 | <p><%= link_to(l(:button_delete_my_account), {:action => 'destroy'}, :class => 'icon icon-del') %></p> |
|
8 | 8 | <% end %> |
|
9 | 9 | |
|
10 | 10 | <h4><%= l(:label_feeds_access_key) %></h4> |
|
11 | 11 | |
|
12 | 12 | <p> |
|
13 | 13 | <% if @user.rss_token %> |
|
14 | 14 | <%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %> |
|
15 | 15 | <% else %> |
|
16 | 16 | <%= l(:label_missing_feeds_access_key) %> |
|
17 | 17 | <% end %> |
|
18 | 18 | (<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>) |
|
19 | 19 | </p> |
|
20 | 20 | |
|
21 | 21 | <% if Setting.rest_api_enabled? %> |
|
22 | 22 | <h4><%= l(:label_api_access_key) %></h4> |
|
23 | 23 | <div> |
|
24 | <%= link_to_function(l(:button_show), "$('#api-access-key').toggle();")%> | |
|
25 |
<pre id='api-access-key' class='autoscroll'>< |
|
|
24 | <%= link_to l(:button_show), {:action => 'show_api_key'}, :remote => true %> | |
|
25 | <pre id='api-access-key' class='autoscroll'></pre> | |
|
26 | 26 | </div> |
|
27 | 27 | <%= javascript_tag("$('#api-access-key').hide();") %> |
|
28 | 28 | <p> |
|
29 | 29 | <% if @user.api_token %> |
|
30 | 30 | <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %> |
|
31 | 31 | <% else %> |
|
32 | 32 | <%= l(:label_missing_api_access_key) %> |
|
33 | 33 | <% end %> |
|
34 | 34 | (<%= link_to l(:button_reset), {:action => 'reset_api_key'}, :method => :post %>) |
|
35 | 35 | </p> |
|
36 | 36 | <% end %> |
@@ -1,1164 +1,1166 | |||
|
1 | 1 | # German translations for Ruby on Rails |
|
2 | 2 | # by Clemens Kofler (clemens@railway.at) |
|
3 | 3 | # additions for Redmine 1.2 by Jens Martsch (jmartsch@gmail.com) |
|
4 | 4 | |
|
5 | 5 | de: |
|
6 | 6 | direction: ltr |
|
7 | 7 | date: |
|
8 | 8 | formats: |
|
9 | 9 | # Use the strftime parameters for formats. |
|
10 | 10 | # When no format has been given, it uses default. |
|
11 | 11 | # You can provide other formats here if you like! |
|
12 | 12 | default: "%d.%m.%Y" |
|
13 | 13 | short: "%e. %b" |
|
14 | 14 | long: "%e. %B %Y" |
|
15 | 15 | |
|
16 | 16 | day_names: [Sonntag, Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag] |
|
17 | 17 | abbr_day_names: [So, Mo, Di, Mi, Do, Fr, Sa] |
|
18 | 18 | |
|
19 | 19 | # Don't forget the nil at the beginning; there's no such thing as a 0th month |
|
20 | 20 | month_names: [~, Januar, Februar, März, April, Mai, Juni, Juli, August, September, Oktober, November, Dezember] |
|
21 | 21 | abbr_month_names: [~, Jan, Feb, Mär, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez] |
|
22 | 22 | # Used in date_select and datime_select. |
|
23 | 23 | order: |
|
24 | 24 | - :day |
|
25 | 25 | - :month |
|
26 | 26 | - :year |
|
27 | 27 | |
|
28 | 28 | time: |
|
29 | 29 | formats: |
|
30 | 30 | default: "%d.%m.%Y %H:%M" |
|
31 | 31 | time: "%H:%M" |
|
32 | 32 | short: "%e. %b %H:%M" |
|
33 | 33 | long: "%A, %e. %B %Y, %H:%M Uhr" |
|
34 | 34 | am: "vormittags" |
|
35 | 35 | pm: "nachmittags" |
|
36 | 36 | |
|
37 | 37 | datetime: |
|
38 | 38 | distance_in_words: |
|
39 | 39 | half_a_minute: 'eine halbe Minute' |
|
40 | 40 | less_than_x_seconds: |
|
41 | 41 | one: 'weniger als 1 Sekunde' |
|
42 | 42 | other: 'weniger als %{count} Sekunden' |
|
43 | 43 | x_seconds: |
|
44 | 44 | one: '1 Sekunde' |
|
45 | 45 | other: '%{count} Sekunden' |
|
46 | 46 | less_than_x_minutes: |
|
47 | 47 | one: 'weniger als 1 Minute' |
|
48 | 48 | other: 'weniger als %{count} Minuten' |
|
49 | 49 | x_minutes: |
|
50 | 50 | one: '1 Minute' |
|
51 | 51 | other: '%{count} Minuten' |
|
52 | 52 | about_x_hours: |
|
53 | 53 | one: 'etwa 1 Stunde' |
|
54 | 54 | other: 'etwa %{count} Stunden' |
|
55 | 55 | x_hours: |
|
56 | 56 | one: "1 Stunde" |
|
57 | 57 | other: "%{count} Stunden" |
|
58 | 58 | x_days: |
|
59 | 59 | one: '1 Tag' |
|
60 | 60 | other: '%{count} Tagen' |
|
61 | 61 | about_x_months: |
|
62 | 62 | one: 'etwa 1 Monat' |
|
63 | 63 | other: 'etwa %{count} Monaten' |
|
64 | 64 | x_months: |
|
65 | 65 | one: '1 Monat' |
|
66 | 66 | other: '%{count} Monaten' |
|
67 | 67 | about_x_years: |
|
68 | 68 | one: 'etwa 1 Jahr' |
|
69 | 69 | other: 'etwa %{count} Jahren' |
|
70 | 70 | over_x_years: |
|
71 | 71 | one: 'mehr als 1 Jahr' |
|
72 | 72 | other: 'mehr als %{count} Jahren' |
|
73 | 73 | almost_x_years: |
|
74 | 74 | one: "fast 1 Jahr" |
|
75 | 75 | other: "fast %{count} Jahren" |
|
76 | 76 | |
|
77 | 77 | number: |
|
78 | 78 | # Default format for numbers |
|
79 | 79 | format: |
|
80 | 80 | separator: ',' |
|
81 | 81 | delimiter: '.' |
|
82 | 82 | precision: 2 |
|
83 | 83 | currency: |
|
84 | 84 | format: |
|
85 | 85 | unit: '€' |
|
86 | 86 | format: '%n %u' |
|
87 | 87 | delimiter: '' |
|
88 | 88 | percentage: |
|
89 | 89 | format: |
|
90 | 90 | delimiter: "" |
|
91 | 91 | precision: |
|
92 | 92 | format: |
|
93 | 93 | delimiter: "" |
|
94 | 94 | human: |
|
95 | 95 | format: |
|
96 | 96 | delimiter: "" |
|
97 | 97 | precision: 3 |
|
98 | 98 | storage_units: |
|
99 | 99 | format: "%n %u" |
|
100 | 100 | units: |
|
101 | 101 | byte: |
|
102 | 102 | one: "Byte" |
|
103 | 103 | other: "Bytes" |
|
104 | 104 | kb: "KB" |
|
105 | 105 | mb: "MB" |
|
106 | 106 | gb: "GB" |
|
107 | 107 | tb: "TB" |
|
108 | 108 | |
|
109 | 109 | # Used in array.to_sentence. |
|
110 | 110 | support: |
|
111 | 111 | array: |
|
112 | 112 | sentence_connector: "und" |
|
113 | 113 | skip_last_comma: true |
|
114 | 114 | |
|
115 | 115 | activerecord: |
|
116 | 116 | errors: |
|
117 | 117 | template: |
|
118 | 118 | header: |
|
119 | 119 | one: "Dieses %{model}-Objekt konnte nicht gespeichert werden: %{count} Fehler." |
|
120 | 120 | other: "Dieses %{model}-Objekt konnte nicht gespeichert werden: %{count} Fehler." |
|
121 | 121 | body: "Bitte überprüfen Sie die folgenden Felder:" |
|
122 | 122 | |
|
123 | 123 | messages: |
|
124 | 124 | inclusion: "ist kein gültiger Wert" |
|
125 | 125 | exclusion: "ist nicht verfügbar" |
|
126 | 126 | invalid: "ist nicht gültig" |
|
127 | 127 | confirmation: "stimmt nicht mit der Bestätigung überein" |
|
128 | 128 | accepted: "muss akzeptiert werden" |
|
129 | 129 | empty: "muss ausgefüllt werden" |
|
130 | 130 | blank: "muss ausgefüllt werden" |
|
131 | 131 | too_long: "ist zu lang (nicht mehr als %{count} Zeichen)" |
|
132 | 132 | too_short: "ist zu kurz (nicht weniger als %{count} Zeichen)" |
|
133 | 133 | wrong_length: "hat die falsche Länge (muss genau %{count} Zeichen haben)" |
|
134 | 134 | taken: "ist bereits vergeben" |
|
135 | 135 | not_a_number: "ist keine Zahl" |
|
136 | 136 | not_a_date: "ist kein gültiges Datum" |
|
137 | 137 | greater_than: "muss größer als %{count} sein" |
|
138 | 138 | greater_than_or_equal_to: "muss größer oder gleich %{count} sein" |
|
139 | 139 | equal_to: "muss genau %{count} sein" |
|
140 | 140 | less_than: "muss kleiner als %{count} sein" |
|
141 | 141 | less_than_or_equal_to: "muss kleiner oder gleich %{count} sein" |
|
142 | 142 | odd: "muss ungerade sein" |
|
143 | 143 | even: "muss gerade sein" |
|
144 | 144 | greater_than_start_date: "muss größer als Anfangsdatum sein" |
|
145 | 145 | not_same_project: "gehört nicht zum selben Projekt" |
|
146 | 146 | circular_dependency: "Diese Beziehung würde eine zyklische Abhängigkeit erzeugen" |
|
147 | 147 | cant_link_an_issue_with_a_descendant: "Ein Ticket kann nicht mit einer Ihrer Unteraufgaben verlinkt werden" |
|
148 | 148 | earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" |
|
149 | 149 | |
|
150 | 150 | actionview_instancetag_blank_option: Bitte auswählen |
|
151 | 151 | |
|
152 | 152 | button_activate: Aktivieren |
|
153 | 153 | button_add: Hinzufügen |
|
154 | 154 | button_annotate: Annotieren |
|
155 | 155 | button_apply: Anwenden |
|
156 | 156 | button_archive: Archivieren |
|
157 | 157 | button_back: Zurück |
|
158 | 158 | button_cancel: Abbrechen |
|
159 | 159 | button_change: Wechseln |
|
160 | 160 | button_change_password: Kennwort ändern |
|
161 | 161 | button_check_all: Alles auswählen |
|
162 | 162 | button_clear: Zurücksetzen |
|
163 | 163 | button_close: Schließen |
|
164 | 164 | button_collapse_all: Alle einklappen |
|
165 | 165 | button_configure: Konfigurieren |
|
166 | button_confirm_password: Kennwort bestätigen | |
|
166 | 167 | button_copy: Kopieren |
|
167 | 168 | button_copy_and_follow: Kopieren und Ticket anzeigen |
|
168 | 169 | button_create: Anlegen |
|
169 | 170 | button_create_and_continue: Anlegen und weiter |
|
170 | 171 | button_delete: Löschen |
|
171 | 172 | button_delete_my_account: Mein Benutzerkonto löschen |
|
172 | 173 | button_download: Download |
|
173 | 174 | button_duplicate: Duplizieren |
|
174 | 175 | button_edit: Bearbeiten |
|
175 | 176 | button_edit_associated_wikipage: "Zugehörige Wikiseite bearbeiten: %{page_title}" |
|
176 | 177 | button_edit_section: Diesen Bereich bearbeiten |
|
177 | 178 | button_expand_all: Alle ausklappen |
|
178 | 179 | button_export: Exportieren |
|
179 | 180 | button_hide: Verstecken |
|
180 | 181 | button_list: Liste |
|
181 | 182 | button_lock: Sperren |
|
182 | 183 | button_log_time: Aufwand buchen |
|
183 | 184 | button_login: Anmelden |
|
184 | 185 | button_move: Verschieben |
|
185 | 186 | button_move_and_follow: Verschieben und Ticket anzeigen |
|
186 | 187 | button_quote: Zitieren |
|
187 | 188 | button_rename: Umbenennen |
|
188 | 189 | button_reopen: Öffnen |
|
189 | 190 | button_reply: Antworten |
|
190 | 191 | button_reset: Zurücksetzen |
|
191 | 192 | button_rollback: Auf diese Version zurücksetzen |
|
192 | 193 | button_save: Speichern |
|
193 | 194 | button_show: Anzeigen |
|
194 | 195 | button_sort: Sortieren |
|
195 | 196 | button_submit: OK |
|
196 | 197 | button_test: Testen |
|
197 | 198 | button_unarchive: Entarchivieren |
|
198 | 199 | button_uncheck_all: Alles abwählen |
|
199 | 200 | button_unlock: Entsperren |
|
200 | 201 | button_unwatch: Nicht beobachten |
|
201 | 202 | button_update: Aktualisieren |
|
202 | 203 | button_view: Anzeigen |
|
203 | 204 | button_watch: Beobachten |
|
204 | 205 | |
|
205 | 206 | default_activity_design: Design |
|
206 | 207 | default_activity_development: Entwicklung |
|
207 | 208 | default_doc_category_tech: Technische Dokumentation |
|
208 | 209 | default_doc_category_user: Benutzerdokumentation |
|
209 | 210 | default_issue_status_closed: Erledigt |
|
210 | 211 | default_issue_status_feedback: Feedback |
|
211 | 212 | default_issue_status_in_progress: In Bearbeitung |
|
212 | 213 | default_issue_status_new: Neu |
|
213 | 214 | default_issue_status_rejected: Abgewiesen |
|
214 | 215 | default_issue_status_resolved: Gelöst |
|
215 | 216 | default_priority_high: Hoch |
|
216 | 217 | default_priority_immediate: Sofort |
|
217 | 218 | default_priority_low: Niedrig |
|
218 | 219 | default_priority_normal: Normal |
|
219 | 220 | default_priority_urgent: Dringend |
|
220 | 221 | default_role_developer: Entwickler |
|
221 | 222 | default_role_manager: Manager |
|
222 | 223 | default_role_reporter: Reporter |
|
223 | 224 | default_tracker_bug: Fehler |
|
224 | 225 | default_tracker_feature: Feature |
|
225 | 226 | default_tracker_support: Unterstützung |
|
226 | 227 | |
|
227 | 228 | description_all_columns: Alle Spalten |
|
228 | 229 | description_available_columns: Verfügbare Spalten |
|
229 | 230 | description_choose_project: Projekte |
|
230 | 231 | description_date_from: Startdatum eintragen |
|
231 | 232 | description_date_range_interval: Zeitraum durch Start- und Enddatum festlegen |
|
232 | 233 | description_date_range_list: Zeitraum aus einer Liste wählen |
|
233 | 234 | description_date_to: Enddatum eintragen |
|
234 | 235 | description_filter: Filter |
|
235 | 236 | description_issue_category_reassign: Neue Kategorie wählen |
|
236 | 237 | description_message_content: Nachrichteninhalt |
|
237 | 238 | description_notes: Kommentare |
|
238 | 239 | description_project_scope: Suchbereich |
|
239 | 240 | description_query_sort_criteria_attribute: Sortierattribut |
|
240 | 241 | description_query_sort_criteria_direction: Sortierrichtung |
|
241 | 242 | description_search: Suchfeld |
|
242 | 243 | description_selected_columns: Ausgewählte Spalten |
|
243 | 244 | |
|
244 | 245 | description_user_mail_notification: Mailbenachrichtigungseinstellung |
|
245 | 246 | description_wiki_subpages_reassign: Neue Elternseite wählen |
|
246 | 247 | |
|
247 | 248 | enumeration_activities: Aktivitäten (Zeiterfassung) |
|
248 | 249 | enumeration_doc_categories: Dokumentenkategorien |
|
249 | 250 | enumeration_issue_priorities: Ticket-Prioritäten |
|
250 | 251 | enumeration_system_activity: System-Aktivität |
|
251 | 252 | |
|
252 | 253 | error_attachment_too_big: Diese Datei kann nicht hochgeladen werden, da sie die maximale Dateigröße von (%{max_size}) überschreitet. |
|
253 | 254 | error_can_not_archive_project: Dieses Projekt kann nicht archiviert werden. |
|
254 | 255 | error_can_not_delete_custom_field: Kann das benutzerdefinierte Feld nicht löschen. |
|
255 | 256 | error_can_not_delete_tracker: Dieser Tracker enthält Tickets und kann nicht gelöscht werden. |
|
256 | 257 | error_can_not_remove_role: Diese Rolle wird verwendet und kann nicht gelöscht werden. |
|
257 | 258 | error_can_not_reopen_issue_on_closed_version: Das Ticket ist einer abgeschlossenen Version zugeordnet und kann daher nicht wieder geöffnet werden. |
|
258 | 259 | error_can_t_load_default_data: "Die Standard-Konfiguration konnte nicht geladen werden: %{value}" |
|
259 | 260 | error_issue_done_ratios_not_updated: Der Ticket-Fortschritt wurde nicht aktualisiert. |
|
260 | 261 | error_issue_not_found_in_project: 'Das Ticket wurde nicht gefunden oder gehört nicht zu diesem Projekt.' |
|
261 | 262 | error_no_default_issue_status: Es ist kein Status als Standard definiert. Bitte überprüfen Sie Ihre Konfiguration (unter "Administration -> Ticket-Status"). |
|
262 | 263 | error_no_tracker_in_project: Diesem Projekt ist kein Tracker zugeordnet. Bitte überprüfen Sie die Projekteinstellungen. |
|
263 | 264 | error_scm_annotate: "Der Eintrag existiert nicht oder kann nicht annotiert werden." |
|
264 | 265 | error_scm_annotate_big_text_file: Der Eintrag kann nicht umgesetzt werden, da er die maximale Textlänge überschreitet. |
|
265 | 266 | error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %{value}" |
|
266 | 267 | error_scm_not_found: Eintrag und/oder Revision existiert nicht im Projektarchiv. |
|
267 | 268 | error_session_expired: Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an. |
|
268 | 269 | error_unable_delete_issue_status: "Der Ticket-Status konnte nicht gelöscht werden." |
|
269 | 270 | error_unable_to_connect: Fehler beim Verbinden (%{value}) |
|
270 | 271 | error_workflow_copy_source: Bitte wählen Sie einen Quell-Tracker und eine Quell-Rolle. |
|
271 | 272 | error_workflow_copy_target: Bitte wählen Sie die Ziel-Tracker und -Rollen. |
|
272 | 273 | |
|
273 | 274 | field_account: Konto |
|
274 | 275 | field_active: Aktiv |
|
275 | 276 | field_activity: Aktivität |
|
276 | 277 | field_admin: Administrator |
|
277 | 278 | field_assignable: Tickets können dieser Rolle zugewiesen werden |
|
278 | 279 | field_assigned_to: Zugewiesen an |
|
279 | 280 | field_assigned_to_role: Zuständigkeitsrolle |
|
280 | 281 | field_attr_firstname: Vorname-Attribut |
|
281 | 282 | field_attr_lastname: Name-Attribut |
|
282 | 283 | field_attr_login: Mitgliedsname-Attribut |
|
283 | 284 | field_attr_mail: E-Mail-Attribut |
|
284 | 285 | field_auth_source: Authentifizierungs-Modus |
|
285 | 286 | field_auth_source_ldap_filter: LDAP-Filter |
|
286 | 287 | field_author: Autor |
|
287 | 288 | field_base_dn: Base DN |
|
288 | 289 | field_board_parent: Übergeordnetes Forum |
|
289 | 290 | field_category: Kategorie |
|
290 | 291 | field_column_names: Spalten |
|
291 | 292 | field_closed_on: Geschlossen am |
|
292 | 293 | field_comments: Kommentar |
|
293 | 294 | field_comments_sorting: Kommentare anzeigen |
|
294 | 295 | field_commit_logs_encoding: Kodierung der Commit-Log-Meldungen |
|
295 | 296 | field_content: Inhalt |
|
296 | 297 | field_core_fields: Standardwerte |
|
297 | 298 | field_created_on: Angelegt |
|
298 | 299 | field_cvs_module: Modul |
|
299 | 300 | field_cvsroot: CVSROOT |
|
300 | 301 | field_default_value: Standardwert |
|
301 | 302 | field_default_status: Standardstatus |
|
302 | 303 | field_delay: Pufferzeit |
|
303 | 304 | field_description: Beschreibung |
|
304 | 305 | field_done_ratio: "% erledigt" |
|
305 | 306 | field_downloads: Downloads |
|
306 | 307 | field_due_date: Abgabedatum |
|
307 | 308 | field_editable: Bearbeitbar |
|
308 | 309 | field_effective_date: Datum |
|
309 | 310 | field_estimated_hours: Geschätzter Aufwand |
|
310 | 311 | field_field_format: Format |
|
311 | 312 | field_filename: Datei |
|
312 | 313 | field_filesize: Größe |
|
313 | 314 | field_firstname: Vorname |
|
314 | 315 | field_fixed_version: Zielversion |
|
315 | 316 | field_generate_password: Passwort generieren |
|
316 | 317 | field_group_by: Gruppiere Ergebnisse nach |
|
317 | 318 | field_hide_mail: E-Mail-Adresse nicht anzeigen |
|
318 | 319 | field_homepage: Projekt-Homepage |
|
319 | 320 | field_host: Host |
|
320 | 321 | field_hours: Stunden |
|
321 | 322 | field_identifier: Kennung |
|
322 | 323 | field_identity_url: OpenID-URL |
|
323 | 324 | field_inherit_members: Benutzer erben |
|
324 | 325 | field_is_closed: Ticket geschlossen |
|
325 | 326 | field_is_default: Standardeinstellung |
|
326 | 327 | field_is_filter: Als Filter benutzen |
|
327 | 328 | field_is_for_all: Für alle Projekte |
|
328 | 329 | field_is_in_roadmap: In der Roadmap anzeigen |
|
329 | 330 | field_is_private: Privat |
|
330 | 331 | field_is_public: Öffentlich |
|
331 | 332 | field_is_required: Erforderlich |
|
332 | 333 | field_issue: Ticket |
|
333 | 334 | field_issue_to: Zugehöriges Ticket |
|
334 | 335 | field_issues_visibility: Ticket Sichtbarkeit |
|
335 | 336 | field_language: Sprache |
|
336 | 337 | field_last_login_on: Letzte Anmeldung |
|
337 | 338 | field_lastname: Nachname |
|
338 | 339 | field_login: Mitgliedsname |
|
339 | 340 | field_mail: E-Mail |
|
340 | 341 | field_mail_notification: Mailbenachrichtigung |
|
341 | 342 | field_max_length: Maximale Länge |
|
342 | 343 | field_member_of_group: Zuständigkeitsgruppe |
|
343 | 344 | field_min_length: Minimale Länge |
|
344 | 345 | field_multiple: Mehrere Werte |
|
345 | 346 | field_must_change_passwd: Passwort beim nächsten Login ändern |
|
346 | 347 | field_name: Name |
|
347 | 348 | field_new_password: Neues Kennwort |
|
348 | 349 | field_notes: Kommentare |
|
349 | 350 | field_onthefly: On-the-fly-Benutzererstellung |
|
350 | 351 | field_parent: Unterprojekt von |
|
351 | 352 | field_parent_issue: Übergeordnete Aufgabe |
|
352 | 353 | field_parent_title: Übergeordnete Seite |
|
353 | 354 | field_password: Kennwort |
|
354 | 355 | field_password_confirmation: Bestätigung |
|
355 | 356 | field_path_to_repository: Pfad zum Repository |
|
356 | 357 | field_port: Port |
|
357 | 358 | field_possible_values: Mögliche Werte |
|
358 | 359 | field_principal: Auftraggeber |
|
359 | 360 | field_priority: Priorität |
|
360 | 361 | field_private_notes: Privater Kommentar |
|
361 | 362 | field_project: Projekt |
|
362 | 363 | field_redirect_existing_links: Existierende Links umleiten |
|
363 | 364 | field_regexp: Regulärer Ausdruck |
|
364 | 365 | field_repository_is_default: Haupt-Repository |
|
365 | 366 | field_role: Rolle |
|
366 | 367 | field_root_directory: Wurzelverzeichnis |
|
367 | 368 | field_scm_path_encoding: Pfad-Kodierung |
|
368 | 369 | field_searchable: Durchsuchbar |
|
369 | 370 | field_sharing: Gemeinsame Verwendung |
|
370 | 371 | field_spent_on: Datum |
|
371 | 372 | field_start_date: Beginn |
|
372 | 373 | field_start_page: Hauptseite |
|
373 | 374 | field_status: Status |
|
374 | 375 | field_subject: Thema |
|
375 | 376 | field_subproject: Unterprojekt von |
|
376 | 377 | field_summary: Zusammenfassung |
|
377 | 378 | field_text: Textfeld |
|
378 | 379 | field_time_entries: Logzeit |
|
379 | 380 | field_time_zone: Zeitzone |
|
380 | 381 | field_timeout: Auszeit (in Sekunden) |
|
381 | 382 | field_title: Titel |
|
382 | 383 | field_tracker: Tracker |
|
383 | 384 | field_type: Typ |
|
384 | 385 | field_updated_on: Aktualisiert |
|
385 | 386 | field_url: URL |
|
386 | 387 | field_user: Benutzer |
|
387 | 388 | field_users_visibility: Benutzer Sichtbarkeit |
|
388 | 389 | field_value: Wert |
|
389 | 390 | field_version: Version |
|
390 | 391 | field_visible: Sichtbar |
|
391 | 392 | field_warn_on_leaving_unsaved: Vor dem Verlassen einer Seite mit ungesichertem Text im Editor warnen |
|
392 | 393 | field_watcher: Beobachter |
|
393 | 394 | |
|
394 | 395 | general_csv_decimal_separator: ',' |
|
395 | 396 | general_csv_encoding: ISO-8859-1 |
|
396 | 397 | general_csv_separator: ';' |
|
397 | 398 | general_pdf_fontname: freesans |
|
398 | 399 | general_first_day_of_week: '1' |
|
399 | 400 | general_lang_name: 'German (Deutsch)' |
|
400 | 401 | general_text_No: 'Nein' |
|
401 | 402 | general_text_Yes: 'Ja' |
|
402 | 403 | general_text_no: 'nein' |
|
403 | 404 | general_text_yes: 'ja' |
|
404 | 405 | |
|
405 | 406 | label_activity: Aktivität |
|
406 | 407 | label_add_another_file: Eine weitere Datei hinzufügen |
|
407 | 408 | label_add_note: Kommentar hinzufügen |
|
408 | 409 | label_add_projects: Projekt hinzufügen |
|
409 | 410 | label_added: hinzugefügt |
|
410 | 411 | label_added_time_by: "Von %{author} vor %{age} hinzugefügt" |
|
411 | 412 | label_additional_workflow_transitions_for_assignee: Zusätzliche Berechtigungen wenn der Benutzer der Zugewiesene ist |
|
412 | 413 | label_additional_workflow_transitions_for_author: Zusätzliche Berechtigungen wenn der Benutzer der Autor ist |
|
413 | 414 | label_administration: Administration |
|
414 | 415 | label_age: Geändert vor |
|
415 | 416 | label_ago: vor |
|
416 | 417 | label_all: alle |
|
417 | 418 | label_all_time: gesamter Zeitraum |
|
418 | 419 | label_all_words: Alle Wörter |
|
419 | 420 | label_and_its_subprojects: "%{value} und dessen Unterprojekte" |
|
420 | 421 | label_any: alle |
|
421 | 422 | label_any_issues_in_project: irgendein Ticket im Projekt |
|
422 | 423 | label_any_issues_not_in_project: irgendein Ticket nicht im Projekt |
|
423 | 424 | label_api_access_key: API-Zugriffsschlüssel |
|
424 | 425 | label_api_access_key_created_on: Der API-Zugriffsschlüssel wurde vor %{value} erstellt |
|
425 | 426 | label_applied_status: Zugewiesener Status |
|
426 | 427 | label_ascending: Aufsteigend |
|
427 | 428 | label_ask: Nachfragen |
|
428 | 429 | label_assigned_to_me_issues: Mir zugewiesene Tickets |
|
429 | 430 | label_associated_revisions: Zugehörige Revisionen |
|
430 | 431 | label_attachment: Datei |
|
431 | 432 | label_attachment_delete: Anhang löschen |
|
432 | 433 | label_attachment_new: Neue Datei |
|
433 | 434 | label_attachment_plural: Dateien |
|
434 | 435 | label_attribute: Attribut |
|
435 | 436 | label_attribute_of_assigned_to: "%{name} des Bearbeiters" |
|
436 | 437 | label_attribute_of_author: "%{name} des Autors" |
|
437 | 438 | label_attribute_of_fixed_version: "%{name} der Zielversion" |
|
438 | 439 | label_attribute_of_issue: "%{name} des Tickets" |
|
439 | 440 | label_attribute_of_project: "%{name} des Projekts" |
|
440 | 441 | label_attribute_of_user: "%{name} des Benutzers" |
|
441 | 442 | label_attribute_plural: Attribute |
|
442 | 443 | label_auth_source: Authentifizierungs-Modus |
|
443 | 444 | label_auth_source_new: Neuer Authentifizierungs-Modus |
|
444 | 445 | label_auth_source_plural: Authentifizierungs-Arten |
|
445 | 446 | label_authentication: Authentifizierung |
|
446 | 447 | label_between: zwischen |
|
447 | 448 | label_blocked_by: Blockiert durch |
|
448 | 449 | label_blocks: Blockiert |
|
449 | 450 | label_board: Forum |
|
450 | 451 | label_board_locked: Gesperrt |
|
451 | 452 | label_board_new: Neues Forum |
|
452 | 453 | label_board_plural: Foren |
|
453 | 454 | label_board_sticky: Wichtig (immer oben) |
|
454 | 455 | label_boolean: Boolean |
|
455 | 456 | label_branch: Zweig |
|
456 | 457 | label_browse: Codebrowser |
|
457 | 458 | label_bulk_edit_selected_issues: Alle ausgewählten Tickets bearbeiten |
|
458 | 459 | label_bulk_edit_selected_time_entries: Ausgewählte Zeitaufwände bearbeiten |
|
459 | 460 | label_calendar: Kalender |
|
460 | 461 | label_change_plural: Änderungen |
|
461 | 462 | label_change_properties: Eigenschaften ändern |
|
462 | 463 | label_change_status: Statuswechsel |
|
463 | 464 | label_change_view_all: Alle Änderungen anzeigen |
|
464 | 465 | label_changes_details: Details aller Änderungen |
|
465 | 466 | label_changeset_plural: Changesets |
|
466 | 467 | label_checkboxes: Checkboxen |
|
467 | 468 | label_check_for_updates: Auf Updates prüfen |
|
468 | 469 | label_child_revision: Nachfolger |
|
469 | 470 | label_chronological_order: in zeitlicher Reihenfolge |
|
470 | 471 | label_close_versions: Vollständige Versionen schließen |
|
471 | 472 | label_closed_issues: geschlossen |
|
472 | 473 | label_closed_issues_plural: geschlossen |
|
473 | 474 | label_comment: Kommentar |
|
474 | 475 | label_comment_add: Kommentar hinzufügen |
|
475 | 476 | label_comment_added: Kommentar hinzugefügt |
|
476 | 477 | label_comment_delete: Kommentar löschen |
|
477 | 478 | label_comment_plural: Kommentare |
|
478 | 479 | label_commits_per_author: Übertragungen pro Autor |
|
479 | 480 | label_commits_per_month: Übertragungen pro Monat |
|
480 | 481 | label_completed_versions: Abgeschlossene Versionen |
|
481 | 482 | label_confirmation: Bestätigung |
|
482 | 483 | label_contains: enthält |
|
483 | 484 | label_copied: kopiert |
|
484 | 485 | label_copied_from: Kopiert von |
|
485 | 486 | label_copied_to: Kopiert nach |
|
486 | 487 | label_copy_attachments: Anhänge kopieren |
|
487 | 488 | label_copy_same_as_target: So wie das Ziel |
|
488 | 489 | label_copy_source: Quelle |
|
489 | 490 | label_copy_subtasks: Unteraufgaben kopieren |
|
490 | 491 | label_copy_target: Ziel |
|
491 | 492 | label_copy_workflow_from: Workflow kopieren von |
|
492 | 493 | label_cross_project_descendants: Mit Unterprojekten |
|
493 | 494 | label_cross_project_hierarchy: Mit Projekthierarchie |
|
494 | 495 | label_cross_project_system: Mit allen Projekten |
|
495 | 496 | label_cross_project_tree: Mit Projektbaum |
|
496 | 497 | label_current_status: Gegenwärtiger Status |
|
497 | 498 | label_current_version: Gegenwärtige Version |
|
498 | 499 | label_custom_field: Benutzerdefiniertes Feld |
|
499 | 500 | label_custom_field_new: Neues Feld |
|
500 | 501 | label_custom_field_plural: Benutzerdefinierte Felder |
|
501 | 502 | label_custom_field_select_type: Bitte wählen Sie den Objekttyp, zu dem das benutzerdefinierte Feld hinzugefügt werden soll |
|
502 | 503 | label_date: Datum |
|
503 | 504 | label_date_from: Von |
|
504 | 505 | label_date_from_to: von %{start} bis %{end} |
|
505 | 506 | label_date_range: Zeitraum |
|
506 | 507 | label_date_to: Bis |
|
507 | 508 | label_day_plural: Tage |
|
508 | 509 | label_default: Standard |
|
509 | 510 | label_default_columns: Standard-Spalten |
|
510 | 511 | label_deleted: gelöscht |
|
511 | 512 | label_descending: Absteigend |
|
512 | 513 | label_details: Details |
|
513 | 514 | label_diff: diff |
|
514 | 515 | label_diff_inline: einspaltig |
|
515 | 516 | label_diff_side_by_side: nebeneinander |
|
516 | 517 | label_disabled: gesperrt |
|
517 | 518 | label_display: Anzeige |
|
518 | 519 | label_display_per_page: "Pro Seite: %{value}" |
|
519 | 520 | label_display_used_statuses_only: Zeige nur Status an, die von diesem Tracker verwendet werden |
|
520 | 521 | label_document: Dokument |
|
521 | 522 | label_document_added: Dokument hinzugefügt |
|
522 | 523 | label_document_new: Neues Dokument |
|
523 | 524 | label_document_plural: Dokumente |
|
524 | 525 | label_downloads_abbr: D/L |
|
525 | 526 | label_drop_down_list: Dropdown-Liste |
|
526 | 527 | label_duplicated_by: Dupliziert durch |
|
527 | 528 | label_duplicates: Duplikat von |
|
528 | 529 | label_edit_attachments: Angehängte Dateien bearbeiten |
|
529 | 530 | label_end_to_end: Ende - Ende |
|
530 | 531 | label_end_to_start: Ende - Anfang |
|
531 | 532 | label_enumeration_new: Neuer Wert |
|
532 | 533 | label_enumerations: Aufzählungen |
|
533 | 534 | label_environment: Umgebung |
|
534 | 535 | label_equals: ist |
|
535 | 536 | label_example: Beispiel |
|
536 | 537 | label_export_options: "%{export_format} Export-Eigenschaften" |
|
537 | 538 | label_export_to: "Auch abrufbar als:" |
|
538 | 539 | label_f_hour: "%{value} Stunde" |
|
539 | 540 | label_f_hour_plural: "%{value} Stunden" |
|
540 | 541 | label_feed_plural: Feeds |
|
541 | 542 | label_feeds_access_key: Atom-Zugriffsschlüssel |
|
542 | 543 | label_feeds_access_key_created_on: "Atom-Zugriffsschlüssel vor %{value} erstellt" |
|
543 | 544 | label_fields_permissions: Feldberechtigungen |
|
544 | 545 | label_file_added: Datei hinzugefügt |
|
545 | 546 | label_file_plural: Dateien |
|
546 | 547 | label_filter_add: Filter hinzufügen |
|
547 | 548 | label_filter_plural: Filter |
|
548 | 549 | label_float: Fließkommazahl |
|
549 | 550 | label_follows: Nachfolger von |
|
550 | 551 | label_gantt: Gantt-Diagramm |
|
551 | 552 | label_gantt_progress_line: Fortschrittslinie |
|
552 | 553 | label_general: Allgemein |
|
553 | 554 | label_generate_key: Generieren |
|
554 | 555 | label_git_report_last_commit: Bericht des letzten Commits für Dateien und Verzeichnisse |
|
555 | 556 | label_greater_or_equal: ">=" |
|
556 | 557 | label_group: Gruppe |
|
557 | 558 | label_group_anonymous: Anonyme Benutzer |
|
558 | 559 | label_group_new: Neue Gruppe |
|
559 | 560 | label_group_non_member: Nichtmitglieder |
|
560 | 561 | label_group_plural: Gruppen |
|
561 | 562 | label_help: Hilfe |
|
562 | 563 | label_hidden: Versteckt |
|
563 | 564 | label_history: Historie |
|
564 | 565 | label_home: Hauptseite |
|
565 | 566 | label_in: in |
|
566 | 567 | label_in_less_than: in weniger als |
|
567 | 568 | label_in_more_than: in mehr als |
|
568 | 569 | label_in_the_next_days: in den nächsten |
|
569 | 570 | label_in_the_past_days: in den letzten |
|
570 | 571 | label_incoming_emails: Eingehende E-Mails |
|
571 | 572 | label_index_by_date: Seiten nach Datum sortiert |
|
572 | 573 | label_index_by_title: Seiten nach Titel sortiert |
|
573 | 574 | label_information: Information |
|
574 | 575 | label_information_plural: Informationen |
|
575 | 576 | label_integer: Zahl |
|
576 | 577 | label_internal: Intern |
|
577 | 578 | label_issue: Ticket |
|
578 | 579 | label_issue_added: Ticket hinzugefügt |
|
579 | 580 | label_issue_assigned_to_updated: Bearbeiter aktualisiert |
|
580 | 581 | label_issue_category: Ticket-Kategorie |
|
581 | 582 | label_issue_category_new: Neue Kategorie |
|
582 | 583 | label_issue_category_plural: Ticket-Kategorien |
|
583 | 584 | label_issue_new: Neues Ticket |
|
584 | 585 | label_issue_note_added: Notiz hinzugefügt |
|
585 | 586 | label_issue_plural: Tickets |
|
586 | 587 | label_issue_priority_updated: Priorität aktualisiert |
|
587 | 588 | label_issue_status: Ticket-Status |
|
588 | 589 | label_issue_status_new: Neuer Status |
|
589 | 590 | label_issue_status_plural: Ticket-Status |
|
590 | 591 | label_issue_status_updated: Status aktualisiert |
|
591 | 592 | label_issue_tracking: Tickets |
|
592 | 593 | label_issue_updated: Ticket aktualisiert |
|
593 | 594 | label_issue_view_all: Alle Tickets anzeigen |
|
594 | 595 | label_issue_watchers: Beobachter |
|
595 | 596 | label_issues_by: "Tickets pro %{value}" |
|
596 | 597 | label_issues_visibility_all: Alle Tickets |
|
597 | 598 | label_issues_visibility_own: Tickets die folgender Benutzer erstellt hat oder die ihm zugewiesen sind |
|
598 | 599 | label_issues_visibility_public: Alle öffentlichen Tickets |
|
599 | 600 | label_item_position: "%{position}/%{count}" |
|
600 | 601 | label_jump_to_a_project: Zu einem Projekt springen... |
|
601 | 602 | label_language_based: Sprachabhängig |
|
602 | 603 | label_last_changes: "%{count} letzte Änderungen" |
|
603 | 604 | label_last_login: Letzte Anmeldung |
|
604 | 605 | label_last_month: voriger Monat |
|
605 | 606 | label_last_n_days: "die letzten %{count} Tage" |
|
606 | 607 | label_last_n_weeks: letzte %{count} Wochen |
|
607 | 608 | label_last_week: vorige Woche |
|
608 | 609 | label_latest_compatible_version: Letzte kompatible Version |
|
609 | 610 | label_latest_revision: Aktuellste Revision |
|
610 | 611 | label_latest_revision_plural: Aktuellste Revisionen |
|
611 | 612 | label_ldap_authentication: LDAP-Authentifizierung |
|
612 | 613 | label_less_or_equal: "<=" |
|
613 | 614 | label_less_than_ago: vor weniger als |
|
614 | 615 | label_link: Link |
|
615 | 616 | label_link_copied_issue: Kopierte Tickets verlinken |
|
616 | 617 | label_link_values_to: Werte mit URL verknüpfen |
|
617 | 618 | label_list: Liste |
|
618 | 619 | label_loading: Lade... |
|
619 | 620 | label_logged_as: Angemeldet als |
|
620 | 621 | label_login: Anmelden |
|
621 | 622 | label_login_with_open_id_option: oder mit OpenID anmelden |
|
622 | 623 | label_logout: Abmelden |
|
623 | 624 | label_only: nur |
|
624 | 625 | label_max_size: Maximale Größe |
|
625 | 626 | label_me: ich |
|
626 | 627 | label_member: Mitglied |
|
627 | 628 | label_member_new: Neues Mitglied |
|
628 | 629 | label_member_plural: Mitglieder |
|
629 | 630 | label_message_last: Letzter Forenbeitrag |
|
630 | 631 | label_message_new: Neues Thema |
|
631 | 632 | label_message_plural: Forenbeiträge |
|
632 | 633 | label_message_posted: Forenbeitrag hinzugefügt |
|
633 | 634 | label_min_max_length: Länge (Min. - Max.) |
|
634 | 635 | label_missing_api_access_key: Der API-Zugriffsschlüssel fehlt. |
|
635 | 636 | label_missing_feeds_access_key: Der Atom-Zugriffsschlüssel fehlt. |
|
636 | 637 | label_modified: geändert |
|
637 | 638 | label_module_plural: Module |
|
638 | 639 | label_month: Monat |
|
639 | 640 | label_months_from: Monate ab |
|
640 | 641 | label_more: Mehr |
|
641 | 642 | label_more_than_ago: vor mehr als |
|
642 | 643 | label_my_account: Mein Konto |
|
643 | 644 | label_my_page: Meine Seite |
|
644 | 645 | label_my_page_block: Verfügbare Widgets |
|
645 | 646 | label_my_projects: Meine Projekte |
|
646 | 647 | label_my_queries: Meine eigenen Abfragen |
|
647 | 648 | label_new: Neu |
|
648 | 649 | label_new_statuses_allowed: Neue Berechtigungen |
|
649 | 650 | label_news: News |
|
650 | 651 | label_news_added: News hinzugefügt |
|
651 | 652 | label_news_comment_added: Kommentar zu einer News hinzugefügt |
|
652 | 653 | label_news_latest: Letzte News |
|
653 | 654 | label_news_new: News hinzufügen |
|
654 | 655 | label_news_plural: News |
|
655 | 656 | label_news_view_all: Alle News anzeigen |
|
656 | 657 | label_next: Weiter |
|
657 | 658 | label_no_change_option: (Keine Änderung) |
|
658 | 659 | label_no_data: Nichts anzuzeigen |
|
659 | 660 | label_no_issues_in_project: keine Tickets im Projekt |
|
660 | 661 | label_nobody: Niemand |
|
661 | 662 | label_none: kein |
|
662 | 663 | label_not_contains: enthält nicht |
|
663 | 664 | label_not_equals: ist nicht |
|
664 | 665 | label_open_issues: offen |
|
665 | 666 | label_open_issues_plural: offen |
|
666 | 667 | label_optional_description: Beschreibung (optional) |
|
667 | 668 | label_options: Optionen |
|
668 | 669 | label_overall_activity: Aktivitäten aller Projekte anzeigen |
|
669 | 670 | label_overall_spent_time: Aufgewendete Zeit aller Projekte anzeigen |
|
670 | 671 | label_overview: Übersicht |
|
671 | 672 | label_parent_revision: Vorgänger |
|
672 | 673 | label_password_lost: Kennwort vergessen |
|
674 | label_password_required: Bitte geben Sie Ihr Kennwort ein | |
|
673 | 675 | label_permissions: Berechtigungen |
|
674 | 676 | label_permissions_report: Berechtigungsübersicht |
|
675 | 677 | label_personalize_page: Diese Seite anpassen |
|
676 | 678 | label_planning: Terminplanung |
|
677 | 679 | label_please_login: Anmelden |
|
678 | 680 | label_plugins: Plugins |
|
679 | 681 | label_precedes: Vorgänger von |
|
680 | 682 | label_preferences: Präferenzen |
|
681 | 683 | label_preview: Vorschau |
|
682 | 684 | label_previous: Zurück |
|
683 | 685 | label_principal_search: "Nach Benutzer oder Gruppe suchen:" |
|
684 | 686 | label_profile: Profil |
|
685 | 687 | label_project: Projekt |
|
686 | 688 | label_project_all: Alle Projekte |
|
687 | 689 | label_project_copy_notifications: Sende Mailbenachrichtigungen beim Kopieren des Projekts. |
|
688 | 690 | label_project_latest: Neueste Projekte |
|
689 | 691 | label_project_new: Neues Projekt |
|
690 | 692 | label_project_plural: Projekte |
|
691 | 693 | label_public_projects: Öffentliche Projekte |
|
692 | 694 | label_query: Benutzerdefinierte Abfrage |
|
693 | 695 | label_query_new: Neue Abfrage |
|
694 | 696 | label_query_plural: Benutzerdefinierte Abfragen |
|
695 | 697 | label_radio_buttons: Radio-Buttons |
|
696 | 698 | label_read: Lesen... |
|
697 | 699 | label_readonly: Nur-Lese-Zugriff |
|
698 | 700 | label_register: Registrieren |
|
699 | 701 | label_registered_on: Angemeldet am |
|
700 | 702 | label_registration_activation_by_email: Kontoaktivierung durch E-Mail |
|
701 | 703 | label_registration_automatic_activation: Automatische Kontoaktivierung |
|
702 | 704 | label_registration_manual_activation: Manuelle Kontoaktivierung |
|
703 | 705 | label_related_issues: Zugehörige Tickets |
|
704 | 706 | label_relates_to: Beziehung mit |
|
705 | 707 | label_relation_delete: Beziehung löschen |
|
706 | 708 | label_relation_new: Neue Beziehung |
|
707 | 709 | label_renamed: umbenannt |
|
708 | 710 | label_reply_plural: Antworten |
|
709 | 711 | label_report: Bericht |
|
710 | 712 | label_report_plural: Berichte |
|
711 | 713 | label_reported_issues: Erstellte Tickets |
|
712 | 714 | label_repository: Projektarchiv |
|
713 | 715 | label_repository_new: Neues Repository |
|
714 | 716 | label_repository_plural: Projektarchive |
|
715 | 717 | label_required: Erforderlich |
|
716 | 718 | label_result_plural: Resultate |
|
717 | 719 | label_reverse_chronological_order: in umgekehrter zeitlicher Reihenfolge |
|
718 | 720 | label_revision: Revision |
|
719 | 721 | label_revision_id: Revision %{value} |
|
720 | 722 | label_revision_plural: Revisionen |
|
721 | 723 | label_roadmap: Roadmap |
|
722 | 724 | label_roadmap_due_in: "Fällig in %{value}" |
|
723 | 725 | label_roadmap_no_issues: Keine Tickets für diese Version |
|
724 | 726 | label_roadmap_overdue: "seit %{value} verspätet" |
|
725 | 727 | label_role: Rolle |
|
726 | 728 | label_role_and_permissions: Rollen und Rechte |
|
727 | 729 | label_role_anonymous: Anonymous |
|
728 | 730 | label_role_new: Neue Rolle |
|
729 | 731 | label_role_non_member: Nichtmitglied |
|
730 | 732 | label_role_plural: Rollen |
|
731 | 733 | label_scm: Versionskontrollsystem |
|
732 | 734 | label_search: Suche |
|
733 | 735 | label_search_for_watchers: Nach hinzufügbaren Beobachtern suchen |
|
734 | 736 | label_search_titles_only: Nur Titel durchsuchen |
|
735 | 737 | label_send_information: Sende Kontoinformationen an Benutzer |
|
736 | 738 | label_send_test_email: Test-E-Mail senden |
|
737 | 739 | label_session_expiration: Ende einer Sitzung |
|
738 | 740 | label_settings: Konfiguration |
|
739 | 741 | label_show_closed_projects: Geschlossene Projekte anzeigen |
|
740 | 742 | label_show_completed_versions: Abgeschlossene Versionen anzeigen |
|
741 | 743 | label_sort: Sortierung |
|
742 | 744 | label_sort_by: "Sortiert nach %{value}" |
|
743 | 745 | label_sort_higher: Eins höher |
|
744 | 746 | label_sort_highest: An den Anfang |
|
745 | 747 | label_sort_lower: Eins tiefer |
|
746 | 748 | label_sort_lowest: Ans Ende |
|
747 | 749 | label_spent_time: Aufgewendete Zeit |
|
748 | 750 | label_start_to_end: Anfang - Ende |
|
749 | 751 | label_start_to_start: Anfang - Anfang |
|
750 | 752 | label_statistics: Statistiken |
|
751 | 753 | label_status_transitions: Statusänderungen |
|
752 | 754 | label_stay_logged_in: Angemeldet bleiben |
|
753 | 755 | label_string: Text |
|
754 | 756 | label_subproject_new: Neues Unterprojekt |
|
755 | 757 | label_subproject_plural: Unterprojekte |
|
756 | 758 | label_subtask_plural: Unteraufgaben |
|
757 | 759 | label_tag: Markierung |
|
758 | 760 | label_text: Langer Text |
|
759 | 761 | label_theme: Stil |
|
760 | 762 | label_this_month: aktueller Monat |
|
761 | 763 | label_this_week: aktuelle Woche |
|
762 | 764 | label_this_year: aktuelles Jahr |
|
763 | 765 | label_time_entry_plural: Benötigte Zeit |
|
764 | 766 | label_time_tracking: Zeiterfassung |
|
765 | 767 | label_today: heute |
|
766 | 768 | label_topic_plural: Themen |
|
767 | 769 | label_total: Gesamtzahl |
|
768 | 770 | label_total_time: Gesamtzeit |
|
769 | 771 | label_tracker: Tracker |
|
770 | 772 | label_tracker_new: Neuer Tracker |
|
771 | 773 | label_tracker_plural: Tracker |
|
772 | 774 | label_unknown_plugin: Unbekanntes Plugin |
|
773 | 775 | label_update_issue_done_ratios: Ticket-Fortschritt aktualisieren |
|
774 | 776 | label_updated_time: "Vor %{value} aktualisiert" |
|
775 | 777 | label_updated_time_by: "Von %{author} vor %{age} aktualisiert" |
|
776 | 778 | label_used_by: Benutzt von |
|
777 | 779 | label_user: Benutzer |
|
778 | 780 | label_user_activity: "Aktivität von %{value}" |
|
779 | 781 | label_user_anonymous: Anonym |
|
780 | 782 | label_user_mail_no_self_notified: "Ich möchte nicht über Änderungen benachrichtigt werden, die ich selbst durchführe." |
|
781 | 783 | label_user_mail_option_all: "Für alle Ereignisse in all meinen Projekten" |
|
782 | 784 | label_user_mail_option_none: Keine Ereignisse |
|
783 | 785 | label_user_mail_option_only_assigned: Nur für Aufgaben für die ich zuständig bin |
|
784 | 786 | label_user_mail_option_only_my_events: Nur für Aufgaben die ich beobachte oder an welchen ich mitarbeite |
|
785 | 787 | label_user_mail_option_only_owner: Nur für Aufgaben die ich angelegt habe |
|
786 | 788 | label_user_mail_option_selected: "Für alle Ereignisse in den ausgewählten Projekten" |
|
787 | 789 | label_user_new: Neuer Benutzer |
|
788 | 790 | label_user_plural: Benutzer |
|
789 | 791 | label_user_search: "Nach Benutzer suchen:" |
|
790 | 792 | label_users_visibility_all: Alle aktiven Benutzer |
|
791 | 793 | label_users_visibility_members_of_visible_projects: Mitglieder von sichtbaren Projekten |
|
792 | 794 | label_version: Version |
|
793 | 795 | label_version_new: Neue Version |
|
794 | 796 | label_version_plural: Versionen |
|
795 | 797 | label_version_sharing_descendants: Mit Unterprojekten |
|
796 | 798 | label_version_sharing_hierarchy: Mit Projekthierarchie |
|
797 | 799 | label_version_sharing_none: Nicht gemeinsam verwenden |
|
798 | 800 | label_version_sharing_system: Mit allen Projekten |
|
799 | 801 | label_version_sharing_tree: Mit Projektbaum |
|
800 | 802 | label_view_all_revisions: Alle Revisionen anzeigen |
|
801 | 803 | label_view_diff: Unterschiede anzeigen |
|
802 | 804 | label_view_revisions: Revisionen anzeigen |
|
803 | 805 | label_visibility_private: nur für mich |
|
804 | 806 | label_visibility_public: für jeden Benutzer |
|
805 | 807 | label_visibility_roles: nur für diese Rollen |
|
806 | 808 | label_watched_issues: Beobachtete Tickets |
|
807 | 809 | label_week: Woche |
|
808 | 810 | label_wiki: Wiki |
|
809 | 811 | label_wiki_content_added: Wiki-Seite hinzugefügt |
|
810 | 812 | label_wiki_content_updated: Wiki-Seite aktualisiert |
|
811 | 813 | label_wiki_edit: Wiki-Bearbeitung |
|
812 | 814 | label_wiki_edit_plural: Wiki-Bearbeitungen |
|
813 | 815 | label_wiki_page: Wiki-Seite |
|
814 | 816 | label_wiki_page_plural: Wiki-Seiten |
|
815 | 817 | label_workflow: Workflow |
|
816 | 818 | label_x_closed_issues_abbr: |
|
817 | 819 | zero: 0 geschlossen |
|
818 | 820 | one: 1 geschlossen |
|
819 | 821 | other: "%{count} geschlossen" |
|
820 | 822 | label_x_comments: |
|
821 | 823 | zero: keine Kommentare |
|
822 | 824 | one: 1 Kommentar |
|
823 | 825 | other: "%{count} Kommentare" |
|
824 | 826 | label_x_issues: |
|
825 | 827 | zero: 0 Tickets |
|
826 | 828 | one: 1 Ticket |
|
827 | 829 | other: "%{count} Tickets" |
|
828 | 830 | label_x_open_issues_abbr: |
|
829 | 831 | zero: 0 offen |
|
830 | 832 | one: 1 offen |
|
831 | 833 | other: "%{count} offen" |
|
832 | 834 | label_x_open_issues_abbr_on_total: |
|
833 | 835 | zero: 0 offen / %{total} |
|
834 | 836 | one: 1 offen / %{total} |
|
835 | 837 | other: "%{count} offen / %{total}" |
|
836 | 838 | label_x_projects: |
|
837 | 839 | zero: keine Projekte |
|
838 | 840 | one: 1 Projekt |
|
839 | 841 | other: "%{count} Projekte" |
|
840 | 842 | label_year: Jahr |
|
841 | 843 | label_yesterday: gestern |
|
842 | 844 | |
|
843 | 845 | mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:" |
|
844 | 846 | mail_body_account_information: Ihre Konto-Informationen |
|
845 | 847 | mail_body_account_information_external: "Sie können sich mit Ihrem Konto %{value} anmelden." |
|
846 | 848 | mail_body_lost_password: 'Benutzen Sie den folgenden Link, um Ihr Kennwort zu ändern:' |
|
847 | 849 | mail_body_register: 'Um Ihr Konto zu aktivieren, benutzen Sie folgenden Link:' |
|
848 | 850 | mail_body_reminder: "%{count} Tickets, die Ihnen zugewiesen sind, müssen in den nächsten %{days} Tagen abgegeben werden:" |
|
849 | 851 | mail_body_wiki_content_added: "Die Wiki-Seite '%{id}' wurde von %{author} hinzugefügt." |
|
850 | 852 | mail_body_wiki_content_updated: "Die Wiki-Seite '%{id}' wurde von %{author} aktualisiert." |
|
851 | 853 | mail_subject_account_activation_request: "Antrag auf %{value} Kontoaktivierung" |
|
852 | 854 | mail_subject_lost_password: "Ihr %{value} Kennwort" |
|
853 | 855 | mail_subject_register: "%{value} Kontoaktivierung" |
|
854 | 856 | mail_subject_reminder: "%{count} Tickets müssen in den nächsten %{days} Tagen abgegeben werden" |
|
855 | 857 | mail_subject_wiki_content_added: "Wiki-Seite '%{id}' hinzugefügt" |
|
856 | 858 | mail_subject_wiki_content_updated: "Wiki-Seite '%{id}' erfolgreich aktualisiert" |
|
857 | 859 | |
|
858 | 860 | notice_account_activated: Ihr Konto ist aktiviert. Sie können sich jetzt anmelden. |
|
859 | 861 | notice_account_deleted: Ihr Benutzerkonto wurde unwiderruflich gelöscht. |
|
860 | 862 | notice_account_invalid_creditentials: Benutzer oder Kennwort ist ungültig. |
|
861 | 863 | notice_account_lost_email_sent: Eine E-Mail mit Anweisungen, ein neues Kennwort zu wählen, wurde Ihnen geschickt. |
|
862 | 864 | notice_account_locked: Ihr Konto ist gesperrt. |
|
863 | 865 | notice_account_not_activated_yet: Sie haben Ihr Konto noch nicht aktiviert. Wenn Sie die Aktivierungsmail erneut erhalten wollen, <a href="%{url}">klicken Sie bitte hier</a>. |
|
864 | 866 | notice_account_password_updated: Kennwort wurde erfolgreich aktualisiert. |
|
865 | 867 | notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators." |
|
866 | 868 | notice_account_register_done: Konto wurde erfolgreich angelegt. Eine E-Mail mit weiteren Instruktionen zur Kontoaktivierung wurde an %{email} gesendet. |
|
867 | 869 | notice_account_unknown_email: Unbekannter Benutzer. |
|
868 | 870 | notice_account_updated: Konto wurde erfolgreich aktualisiert. |
|
869 | 871 | notice_account_wrong_password: Falsches Kennwort. |
|
870 | 872 | notice_api_access_key_reseted: Ihr API-Zugriffsschlüssel wurde zurückgesetzt. |
|
871 | 873 | notice_can_t_change_password: Dieses Konto verwendet eine externe Authentifizierungs-Quelle. Unmöglich, das Kennwort zu ändern. |
|
872 | 874 | notice_default_data_loaded: Die Standard-Konfiguration wurde erfolgreich geladen. |
|
873 | 875 | notice_email_error: "Beim Senden einer E-Mail ist ein Fehler aufgetreten (%{value})." |
|
874 | 876 | notice_email_sent: "Eine E-Mail wurde an %{value} gesendet." |
|
875 | 877 | notice_failed_to_save_issues: "%{count} von %{total} ausgewählten Tickets konnte(n) nicht gespeichert werden: %{ids}." |
|
876 | 878 | notice_failed_to_save_members: "Benutzer konnte nicht gespeichert werden: %{errors}." |
|
877 | 879 | notice_failed_to_save_time_entries: "Gescheitert %{count} Zeiteinträge für %{total} von ausgewählten: %{ids} zu speichern." |
|
878 | 880 | notice_feeds_access_key_reseted: Ihr Atom-Zugriffsschlüssel wurde zurückgesetzt. |
|
879 | 881 | notice_file_not_found: Anhang existiert nicht oder ist gelöscht worden. |
|
880 | 882 | notice_gantt_chart_truncated: Die Grafik ist unvollständig, da das Maximum der anzeigbaren Aufgaben überschritten wurde (%{max}) |
|
881 | 883 | notice_issue_done_ratios_updated: Der Ticket-Fortschritt wurde aktualisiert. |
|
882 | 884 | notice_issue_successful_create: Ticket %{id} erstellt. |
|
883 | 885 | notice_issue_update_conflict: Das Ticket wurde während Ihrer Bearbeitung von einem anderen Nutzer überarbeitet. |
|
884 | 886 | notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert. |
|
885 | 887 | notice_new_password_must_be_different: Das neue Passwort muss sich vom dem aktuellen |
|
886 | 888 | unterscheiden |
|
887 | 889 | notice_no_issue_selected: "Kein Ticket ausgewählt! Bitte wählen Sie die Tickets, die Sie bearbeiten möchten." |
|
888 | 890 | notice_not_authorized: Sie sind nicht berechtigt, auf diese Seite zuzugreifen. |
|
889 | 891 | notice_not_authorized_archived_project: Das Projekt wurde archiviert und ist daher nicht nicht verfügbar. |
|
890 | 892 | notice_successful_connection: Verbindung erfolgreich. |
|
891 | 893 | notice_successful_create: Erfolgreich angelegt |
|
892 | 894 | notice_successful_delete: Erfolgreich gelöscht. |
|
893 | 895 | notice_successful_update: Erfolgreich aktualisiert. |
|
894 | 896 | notice_unable_delete_time_entry: Der Zeiterfassungseintrag konnte nicht gelöscht werden. |
|
895 | 897 | notice_unable_delete_version: Die Version konnte nicht gelöscht werden. |
|
896 | 898 | notice_user_successful_create: Benutzer %{id} angelegt. |
|
897 | 899 | |
|
898 | 900 | permission_add_issue_notes: Kommentare hinzufügen |
|
899 | 901 | permission_add_issue_watchers: Beobachter hinzufügen |
|
900 | 902 | permission_add_issues: Tickets hinzufügen |
|
901 | 903 | permission_add_messages: Forenbeiträge hinzufügen |
|
902 | 904 | permission_add_project: Projekt erstellen |
|
903 | 905 | permission_add_subprojects: Unterprojekte erstellen |
|
904 | 906 | permission_add_documents: Dokumente hinzufügen |
|
905 | 907 | permission_browse_repository: Projektarchiv ansehen |
|
906 | 908 | permission_close_project: Schließen / erneutes Öffnen eines Projekts |
|
907 | 909 | permission_comment_news: News kommentieren |
|
908 | 910 | permission_commit_access: Commit-Zugriff |
|
909 | 911 | permission_delete_issue_watchers: Beobachter löschen |
|
910 | 912 | permission_delete_issues: Tickets löschen |
|
911 | 913 | permission_delete_messages: Forenbeiträge löschen |
|
912 | 914 | permission_delete_own_messages: Eigene Forenbeiträge löschen |
|
913 | 915 | permission_delete_wiki_pages: Wiki-Seiten löschen |
|
914 | 916 | permission_delete_wiki_pages_attachments: Anhänge löschen |
|
915 | 917 | permission_delete_documents: Dokumente löschen |
|
916 | 918 | permission_edit_issue_notes: Kommentare bearbeiten |
|
917 | 919 | permission_edit_issues: Tickets bearbeiten |
|
918 | 920 | permission_edit_messages: Forenbeiträge bearbeiten |
|
919 | 921 | permission_edit_own_issue_notes: Eigene Kommentare bearbeiten |
|
920 | 922 | permission_edit_own_messages: Eigene Forenbeiträge bearbeiten |
|
921 | 923 | permission_edit_own_time_entries: Selbst gebuchte Aufwände bearbeiten |
|
922 | 924 | permission_edit_project: Projekt bearbeiten |
|
923 | 925 | permission_edit_time_entries: Gebuchte Aufwände bearbeiten |
|
924 | 926 | permission_edit_wiki_pages: Wiki-Seiten bearbeiten |
|
925 | 927 | permission_edit_documents: Dokumente bearbeiten |
|
926 | 928 | permission_export_wiki_pages: Wiki-Seiten exportieren |
|
927 | 929 | permission_log_time: Aufwände buchen |
|
928 | 930 | permission_manage_boards: Foren verwalten |
|
929 | 931 | permission_manage_categories: Ticket-Kategorien verwalten |
|
930 | 932 | permission_manage_files: Dateien verwalten |
|
931 | 933 | permission_manage_issue_relations: Ticket-Beziehungen verwalten |
|
932 | 934 | permission_manage_members: Mitglieder verwalten |
|
933 | 935 | permission_manage_news: News verwalten |
|
934 | 936 | permission_manage_project_activities: Aktivitäten (Zeiterfassung) verwalten |
|
935 | 937 | permission_manage_public_queries: Öffentliche Filter verwalten |
|
936 | 938 | permission_manage_related_issues: Zugehörige Tickets verwalten |
|
937 | 939 | permission_manage_repository: Projektarchiv verwalten |
|
938 | 940 | permission_manage_subtasks: Unteraufgaben verwalten |
|
939 | 941 | permission_manage_versions: Versionen verwalten |
|
940 | 942 | permission_manage_wiki: Wiki verwalten |
|
941 | 943 | permission_move_issues: Tickets verschieben |
|
942 | 944 | permission_protect_wiki_pages: Wiki-Seiten schützen |
|
943 | 945 | permission_rename_wiki_pages: Wiki-Seiten umbenennen |
|
944 | 946 | permission_save_queries: Filter speichern |
|
945 | 947 | permission_select_project_modules: Projektmodule auswählen |
|
946 | 948 | permission_set_issues_private: Tickets privat oder öffentlich markieren |
|
947 | 949 | permission_set_notes_private: Kommentar als privat markieren |
|
948 | 950 | permission_set_own_issues_private: Eigene Tickets privat oder öffentlich markieren |
|
949 | 951 | permission_view_calendar: Kalender ansehen |
|
950 | 952 | permission_view_changesets: Changesets ansehen |
|
951 | 953 | permission_view_documents: Dokumente ansehen |
|
952 | 954 | permission_view_files: Dateien ansehen |
|
953 | 955 | permission_view_gantt: Gantt-Diagramm ansehen |
|
954 | 956 | permission_view_issue_watchers: Liste der Beobachter ansehen |
|
955 | 957 | permission_view_issues: Tickets anzeigen |
|
956 | 958 | permission_view_messages: Forenbeiträge ansehen |
|
957 | 959 | permission_view_private_notes: Private Kommentare sehen |
|
958 | 960 | permission_view_time_entries: Gebuchte Aufwände ansehen |
|
959 | 961 | permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen |
|
960 | 962 | permission_view_wiki_pages: Wiki ansehen |
|
961 | 963 | |
|
962 | 964 | project_module_boards: Foren |
|
963 | 965 | project_module_calendar: Kalender |
|
964 | 966 | project_module_documents: Dokumente |
|
965 | 967 | project_module_files: Dateien |
|
966 | 968 | project_module_gantt: Gantt |
|
967 | 969 | project_module_issue_tracking: Ticket-Verfolgung |
|
968 | 970 | project_module_news: News |
|
969 | 971 | project_module_repository: Projektarchiv |
|
970 | 972 | project_module_time_tracking: Zeiterfassung |
|
971 | 973 | project_module_wiki: Wiki |
|
972 | 974 | project_status_active: aktiv |
|
973 | 975 | project_status_archived: archiviert |
|
974 | 976 | project_status_closed: geschlossen |
|
975 | 977 | |
|
976 | 978 | setting_activity_days_default: Anzahl Tage pro Seite der Projekt-Aktivität |
|
977 | 979 | setting_app_subtitle: Applikations-Untertitel |
|
978 | 980 | setting_app_title: Applikations-Titel |
|
979 | 981 | setting_attachment_max_size: Max. Dateigröße |
|
980 | 982 | setting_autofetch_changesets: Changesets automatisch abrufen |
|
981 | 983 | setting_autologin: Automatische Anmeldung läuft ab nach |
|
982 | 984 | setting_bcc_recipients: E-Mails als Blindkopie (BCC) senden |
|
983 | 985 | setting_cache_formatted_text: Formatierten Text im Cache speichern |
|
984 | 986 | setting_commit_cross_project_ref: Erlauben auf Tickets aller anderen Projekte zu referenzieren |
|
985 | 987 | setting_commit_fix_keywords: Schlüsselwörter (Status) |
|
986 | 988 | setting_commit_logtime_activity_id: Aktivität für die Zeiterfassung |
|
987 | 989 | setting_commit_logtime_enabled: Aktiviere Zeitlogging |
|
988 | 990 | setting_commit_ref_keywords: Schlüsselwörter (Beziehungen) |
|
989 | 991 | setting_cross_project_issue_relations: Ticket-Beziehungen zwischen Projekten erlauben |
|
990 | 992 | setting_cross_project_subtasks: Projektübergreifende Unteraufgaben erlauben |
|
991 | 993 | setting_date_format: Datumsformat |
|
992 | 994 | setting_default_issue_start_date_to_creation_date: Aktuelles Datum als Beginn für neue Tickets verwenden |
|
993 | 995 | setting_default_language: Standardsprache |
|
994 | 996 | setting_default_notification_option: Standard Benachrichtigungsoptionen |
|
995 | 997 | setting_default_projects_modules: Standardmäßig aktivierte Module für neue Projekte |
|
996 | 998 | setting_default_projects_public: Neue Projekte sind standardmäßig öffentlich |
|
997 | 999 | setting_default_projects_tracker_ids: Standardmäßig aktivierte Tracker für neue Projekte |
|
998 | 1000 | setting_diff_max_lines_displayed: Maximale Anzahl anzuzeigender Diff-Zeilen |
|
999 | 1001 | setting_display_subprojects_issues: Tickets von Unterprojekten im Hauptprojekt anzeigen |
|
1000 | 1002 | setting_emails_footer: E-Mail-Fußzeile |
|
1001 | 1003 | setting_emails_header: E-Mail-Kopfzeile |
|
1002 | 1004 | setting_enabled_scm: Aktivierte Versionskontrollsysteme |
|
1003 | 1005 | setting_feeds_limit: Max. Anzahl Einträge pro Atom-Feed |
|
1004 | 1006 | setting_file_max_size_displayed: Maximale Größe inline angezeigter Textdateien |
|
1005 | 1007 | setting_force_default_language_for_anonymous: Standardsprache für anonyme Benutzer erzwingen |
|
1006 | 1008 | setting_force_default_language_for_loggedin: Standardsprache für angemeldete Benutzer erzwingen |
|
1007 | 1009 | setting_gantt_items_limit: Maximale Anzahl von Aufgaben die im Gantt-Chart angezeigt werden |
|
1008 | 1010 | setting_gravatar_default: Standard-Gravatar-Bild |
|
1009 | 1011 | setting_gravatar_enabled: Gravatar-Benutzerbilder benutzen |
|
1010 | 1012 | setting_host_name: Hostname |
|
1011 | 1013 | setting_issue_done_ratio: Berechne den Ticket-Fortschritt mittels |
|
1012 | 1014 | setting_issue_done_ratio_issue_field: Ticket-Feld %-erledigt |
|
1013 | 1015 | setting_issue_done_ratio_issue_status: Ticket-Status |
|
1014 | 1016 | setting_issue_group_assignment: Ticketzuweisung an Gruppen erlauben |
|
1015 | 1017 | setting_issue_list_default_columns: Standard-Spalten in der Ticket-Auflistung |
|
1016 | 1018 | setting_issues_export_limit: Max. Anzahl Tickets bei CSV/PDF-Export |
|
1017 | 1019 | setting_jsonp_enabled: JSONP Unterstützung aktivieren |
|
1018 | 1020 | setting_link_copied_issue: Tickets beim kopieren verlinken |
|
1019 | 1021 | setting_login_required: Authentifizierung erforderlich |
|
1020 | 1022 | setting_mail_from: E-Mail-Absender |
|
1021 | 1023 | setting_mail_handler_api_enabled: Abruf eingehender E-Mails aktivieren |
|
1022 | 1024 | setting_mail_handler_api_key: API-Schlüssel |
|
1023 | 1025 | setting_mail_handler_body_delimiters: "Schneide E-Mails nach einer dieser Zeilen ab" |
|
1024 | 1026 | setting_mail_handler_excluded_filenames: Anhänge nach Namen ausschließen |
|
1025 | 1027 | setting_new_project_user_role_id: Rolle, die einem Nicht-Administrator zugeordnet wird, der ein Projekt erstellt |
|
1026 | 1028 | setting_non_working_week_days: Arbeitsfreie Tage |
|
1027 | 1029 | setting_openid: Erlaube OpenID-Anmeldung und -Registrierung |
|
1028 | 1030 | setting_password_min_length: Mindestlänge des Kennworts |
|
1029 | 1031 | setting_password_max_age: Erzwinge Passwortwechsel nach |
|
1030 | 1032 | setting_per_page_options: Objekte pro Seite |
|
1031 | 1033 | setting_plain_text_mail: Nur reinen Text (kein HTML) senden |
|
1032 | 1034 | setting_protocol: Protokoll |
|
1033 | 1035 | setting_repositories_encodings: Enkodierung von Anhängen und Repositories |
|
1034 | 1036 | setting_repository_log_display_limit: Maximale Anzahl anzuzeigender Revisionen in der Historie einer Datei |
|
1035 | 1037 | setting_rest_api_enabled: REST-Schnittstelle aktivieren |
|
1036 | 1038 | setting_self_registration: Registrierung ermöglichen |
|
1037 | 1039 | setting_sequential_project_identifiers: Fortlaufende Projektkennungen generieren |
|
1038 | 1040 | setting_session_lifetime: Längste Dauer einer Sitzung |
|
1039 | 1041 | setting_session_timeout: Zeitüberschreitung bei Inaktivität |
|
1040 | 1042 | setting_start_of_week: Wochenanfang |
|
1041 | 1043 | setting_sys_api_enabled: Webservice zur Verwaltung der Projektarchive benutzen |
|
1042 | 1044 | setting_text_formatting: Textformatierung |
|
1043 | 1045 | setting_thumbnails_enabled: Vorschaubilder von Dateianhängen anzeigen |
|
1044 | 1046 | setting_thumbnails_size: Größe der Vorschaubilder (in Pixel) |
|
1045 | 1047 | setting_time_format: Zeitformat |
|
1046 | 1048 | setting_unsubscribe: Erlaubt Benutzern das eigene Benutzerkonto zu löschen |
|
1047 | 1049 | setting_user_format: Benutzer-Anzeigeformat |
|
1048 | 1050 | setting_welcome_text: Willkommenstext |
|
1049 | 1051 | setting_wiki_compression: Wiki-Historie komprimieren |
|
1050 | 1052 | |
|
1051 | 1053 | status_active: aktiv |
|
1052 | 1054 | status_locked: gesperrt |
|
1053 | 1055 | status_registered: nicht aktivierte |
|
1054 | 1056 | |
|
1055 | 1057 | text_account_destroy_confirmation: "Möchten Sie wirklich fortfahren?\nIhr Benutzerkonto wird für immer gelöscht und kann nicht wiederhergestellt werden." |
|
1056 | 1058 | text_are_you_sure: Sind Sie sicher? |
|
1057 | 1059 | text_assign_time_entries_to_project: Gebuchte Aufwände dem Projekt zuweisen |
|
1058 | 1060 | text_caracters_maximum: "Max. %{count} Zeichen." |
|
1059 | 1061 | text_caracters_minimum: "Muss mindestens %{count} Zeichen lang sein." |
|
1060 | 1062 | text_comma_separated: Mehrere Werte erlaubt (durch Komma getrennt). |
|
1061 | 1063 | text_convert_available: ImageMagick Konvertierung verfügbar (optional) |
|
1062 | 1064 | text_custom_field_possible_values_info: 'Eine Zeile pro Wert' |
|
1063 | 1065 | text_default_administrator_account_changed: Administrator-Kennwort geändert |
|
1064 | 1066 | text_destroy_time_entries: Gebuchte Aufwände löschen |
|
1065 | 1067 | text_destroy_time_entries_question: Es wurden bereits %{hours} Stunden auf dieses Ticket gebucht. Was soll mit den Aufwänden geschehen? |
|
1066 | 1068 | text_diff_truncated: '... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.' |
|
1067 | 1069 | text_email_delivery_not_configured: "Der SMTP-Server ist nicht konfiguriert und Mailbenachrichtigungen sind ausgeschaltet.\nNehmen Sie die Einstellungen für Ihren SMTP-Server in config/configuration.yml vor und starten Sie die Applikation neu." |
|
1068 | 1070 | text_enumeration_category_reassign_to: 'Die Objekte stattdessen diesem Wert zuordnen:' |
|
1069 | 1071 | text_enumeration_destroy_question: "%{count} Objekt(e) sind diesem Wert zugeordnet." |
|
1070 | 1072 | text_file_repository_writable: Verzeichnis für Dateien beschreibbar |
|
1071 | 1073 | text_git_repository_note: Repository steht für sich alleine (bare) und liegt lokal (z.B. /gitrepo, c:\gitrepo) |
|
1072 | 1074 | text_issue_added: "Ticket %{id} wurde erstellt von %{author}." |
|
1073 | 1075 | text_issue_category_destroy_assignments: Kategorie-Zuordnung entfernen |
|
1074 | 1076 | text_issue_category_destroy_question: "Einige Tickets (%{count}) sind dieser Kategorie zugeodnet. Was möchten Sie tun?" |
|
1075 | 1077 | text_issue_category_reassign_to: Tickets dieser Kategorie zuordnen |
|
1076 | 1078 | text_issue_conflict_resolution_add_notes: Meine Änderungen übernehmen und alle anderen Änderungen verwerfen |
|
1077 | 1079 | text_issue_conflict_resolution_cancel: Meine Änderungen verwerfen und %{link} neu anzeigen |
|
1078 | 1080 | text_issue_conflict_resolution_overwrite: Meine Änderungen trotzdem übernehmen (vorherige Notizen bleiben erhalten aber manche können überschrieben werden) |
|
1079 | 1081 | text_issue_updated: "Ticket %{id} wurde aktualisiert von %{author}." |
|
1080 | 1082 | text_issues_destroy_confirmation: 'Sind Sie sicher, dass Sie die ausgewählten Tickets löschen möchten?' |
|
1081 | 1083 | text_issues_destroy_descendants_confirmation: Dies wird auch %{count} Unteraufgabe/n löschen. |
|
1082 | 1084 | text_issues_ref_in_commit_messages: Ticket-Beziehungen und -Status in Commit-Log-Meldungen |
|
1083 | 1085 | text_journal_added: "%{label} %{value} wurde hinzugefügt" |
|
1084 | 1086 | text_journal_changed: "%{label} wurde von %{old} zu %{new} geändert" |
|
1085 | 1087 | text_journal_changed_no_detail: "%{label} aktualisiert" |
|
1086 | 1088 | text_journal_deleted: "%{label} %{old} wurde gelöscht" |
|
1087 | 1089 | text_journal_set_to: "%{label} wurde auf %{value} gesetzt" |
|
1088 | 1090 | text_length_between: "Länge zwischen %{min} und %{max} Zeichen." |
|
1089 | 1091 | text_line_separated: Mehrere Werte sind erlaubt (eine Zeile pro Wert). |
|
1090 | 1092 | text_load_default_configuration: Standard-Konfiguration laden |
|
1091 | 1093 | text_mercurial_repository_note: Lokales repository (e.g. /hgrepo, c:\hgrepo) |
|
1092 | 1094 | text_min_max_length_info: 0 heißt keine Beschränkung |
|
1093 | 1095 | text_no_configuration_data: "Rollen, Tracker, Ticket-Status und Workflows wurden noch nicht konfiguriert.\nEs ist sehr zu empfehlen, die Standard-Konfiguration zu laden. Sobald sie geladen ist, können Sie diese abändern." |
|
1094 | 1096 | text_own_membership_delete_confirmation: "Sie sind dabei, einige oder alle Ihre Berechtigungen zu entfernen. Es ist möglich, dass Sie danach das Projekt nicht mehr ansehen oder bearbeiten dürfen.\nSind Sie sicher, dass Sie dies tun möchten?" |
|
1095 | 1097 | text_plugin_assets_writable: Verzeichnis für Plugin-Assets beschreibbar |
|
1096 | 1098 | text_project_closed: Dieses Projekt ist geschlossen und kann nicht bearbeitet werden. |
|
1097 | 1099 | text_project_destroy_confirmation: Sind Sie sicher, dass Sie das Projekt löschen wollen? |
|
1098 | 1100 | text_project_identifier_info: 'Kleinbuchstaben (a-z), Ziffern, Binde- und Unterstriche erlaubt, muss mit einem Kleinbuchstaben beginnen.<br />Einmal gespeichert, kann die Kennung nicht mehr geändert werden.' |
|
1099 | 1101 | text_reassign_time_entries: 'Gebuchte Aufwände diesem Ticket zuweisen:' |
|
1100 | 1102 | text_regexp_info: z. B. ^[A-Z0-9]+$ |
|
1101 | 1103 | text_repository_identifier_info: 'Kleinbuchstaben (a-z), Ziffern, Binde- und Unterstriche erlaubt.<br />Einmal gespeichert, kann die Kennung nicht mehr geändert werden.' |
|
1102 | 1104 | text_repository_usernames_mapping: "Bitte legen Sie die Zuordnung der Redmine-Benutzer zu den Benutzernamen der Commit-Log-Meldungen des Projektarchivs fest.\nBenutzer mit identischen Redmine- und Projektarchiv-Benutzernamen oder -E-Mail-Adressen werden automatisch zugeordnet." |
|
1103 | 1105 | text_rmagick_available: RMagick verfügbar (optional) |
|
1104 | 1106 | text_scm_command: Kommando |
|
1105 | 1107 | text_scm_command_not_available: SCM-Kommando ist nicht verfügbar. Bitte prüfen Sie die Einstellungen im Administrationspanel. |
|
1106 | 1108 | text_scm_command_version: Version |
|
1107 | 1109 | text_scm_config: Die SCM-Kommandos können in der in config/configuration.yml konfiguriert werden. Redmine muss anschließend neu gestartet werden. |
|
1108 | 1110 | text_scm_path_encoding_note: "Standard: UTF-8" |
|
1109 | 1111 | text_select_mail_notifications: Bitte wählen Sie die Aktionen aus, für die eine Mailbenachrichtigung gesendet werden soll. |
|
1110 | 1112 | text_select_project_modules: 'Bitte wählen Sie die Module aus, die in diesem Projekt aktiviert sein sollen:' |
|
1111 | 1113 | text_session_expiration_settings: "Achtung: Änderungen können aktuelle Sitzungen beenden, Ihre eingeschlossen!" |
|
1112 | 1114 | text_status_changed_by_changeset: "Status geändert durch Changeset %{value}." |
|
1113 | 1115 | text_subprojects_destroy_warning: "Dessen Unterprojekte (%{value}) werden ebenfalls gelöscht." |
|
1114 | 1116 | text_subversion_repository_note: 'Beispiele: file:///, http://, https://, svn://, svn+[tunnelscheme]://' |
|
1115 | 1117 | text_time_entries_destroy_confirmation: Sind Sie sicher, dass Sie die ausgewählten Zeitaufwände löschen möchten? |
|
1116 | 1118 | text_time_logged_by_changeset: Angewendet in Changeset %{value}. |
|
1117 | 1119 | text_tip_issue_begin_day: Aufgabe, die an diesem Tag beginnt |
|
1118 | 1120 | text_tip_issue_begin_end_day: Aufgabe, die an diesem Tag beginnt und endet |
|
1119 | 1121 | text_tip_issue_end_day: Aufgabe, die an diesem Tag endet |
|
1120 | 1122 | text_tracker_no_workflow: Kein Workflow für diesen Tracker definiert. |
|
1121 | 1123 | text_turning_multiple_off: Wenn Sie die Mehrfachauswahl deaktivieren, werden Felder mit Mehrfachauswahl bereinigt. |
|
1122 | 1124 | Dadurch wird sichergestellt, dass lediglich ein Wert pro Feld ausgewählt ist. |
|
1123 | 1125 | text_unallowed_characters: Nicht erlaubte Zeichen |
|
1124 | 1126 | text_user_mail_option: "Für nicht ausgewählte Projekte werden Sie nur Benachrichtigungen für Dinge erhalten, die Sie beobachten oder an denen Sie beteiligt sind (z. B. Tickets, deren Autor Sie sind oder die Ihnen zugewiesen sind)." |
|
1125 | 1127 | text_user_wrote: "%{value} schrieb:" |
|
1126 | 1128 | text_warn_on_leaving_unsaved: Die aktuellen Änderungen gehen verloren, wenn Sie diese Seite verlassen. |
|
1127 | 1129 | text_wiki_destroy_confirmation: Sind Sie sicher, dass Sie dieses Wiki mit sämtlichem Inhalt löschen möchten? |
|
1128 | 1130 | text_wiki_page_destroy_children: Lösche alle Unterseiten |
|
1129 | 1131 | text_wiki_page_destroy_question: "Diese Seite hat %{descendants} Unterseite(n). Was möchten Sie tun?" |
|
1130 | 1132 | text_wiki_page_nullify_children: Verschiebe die Unterseiten auf die oberste Ebene |
|
1131 | 1133 | text_wiki_page_reassign_children: Ordne die Unterseiten dieser Seite zu |
|
1132 | 1134 | text_workflow_edit: Workflow zum Bearbeiten auswählen |
|
1133 | 1135 | text_zoom_in: Ansicht vergrößern |
|
1134 | 1136 | text_zoom_out: Ansicht verkleinern |
|
1135 | 1137 | |
|
1136 | 1138 | version_status_closed: abgeschlossen |
|
1137 | 1139 | version_status_locked: gesperrt |
|
1138 | 1140 | version_status_open: offen |
|
1139 | 1141 | |
|
1140 | 1142 | warning_attachments_not_saved: "%{count} Datei(en) konnten nicht gespeichert werden." |
|
1141 | 1143 | label_search_attachments_yes: Namen und Beschreibungen von Anhängen durchsuchen |
|
1142 | 1144 | label_search_attachments_no: Keine Anhänge suchen |
|
1143 | 1145 | label_search_attachments_only: Nur Anhänge suchen |
|
1144 | 1146 | label_search_open_issues_only: Nur offene Tickets |
|
1145 | 1147 | field_address: E-Mail |
|
1146 | 1148 | setting_max_additional_emails: Maximale Anzahl zusätzlicher E-Mailadressen |
|
1147 | 1149 | label_email_address_plural: E-Mails |
|
1148 | 1150 | label_email_address_add: E-Mailadresse hinzufügen |
|
1149 | 1151 | label_enable_notifications: Benachrichtigungen aktivieren |
|
1150 | 1152 | label_disable_notifications: Benachrichtigungen deaktivieren |
|
1151 | 1153 | setting_search_results_per_page: Suchergebnisse pro Seite |
|
1152 | 1154 | label_blank_value: leer |
|
1153 | 1155 | permission_copy_issues: Tickets kopieren |
|
1154 | 1156 | error_password_expired: Your password has expired or the administrator requires you |
|
1155 | 1157 | to change it. |
|
1156 | 1158 | field_time_entries_visibility: Time logs visibility |
|
1157 | 1159 | label_parent_task_attributes: Parent tasks attributes |
|
1158 | 1160 | label_parent_task_attributes_derived: Calculated from subtasks |
|
1159 | 1161 | label_parent_task_attributes_independent: Independent of subtasks |
|
1160 | 1162 | label_time_entries_visibility_all: All time entries |
|
1161 | 1163 | label_time_entries_visibility_own: Time entries created by the user |
|
1162 | 1164 | label_member_management: Member management |
|
1163 | 1165 | label_member_management_all_roles: All roles |
|
1164 | 1166 | label_member_management_selected_roles_only: Only these roles |
@@ -1,1144 +1,1146 | |||
|
1 | 1 | en: |
|
2 | 2 | # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) |
|
3 | 3 | direction: ltr |
|
4 | 4 | date: |
|
5 | 5 | formats: |
|
6 | 6 | # Use the strftime parameters for formats. |
|
7 | 7 | # When no format has been given, it uses default. |
|
8 | 8 | # You can provide other formats here if you like! |
|
9 | 9 | default: "%m/%d/%Y" |
|
10 | 10 | short: "%b %d" |
|
11 | 11 | long: "%B %d, %Y" |
|
12 | 12 | |
|
13 | 13 | day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] |
|
14 | 14 | abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] |
|
15 | 15 | |
|
16 | 16 | # Don't forget the nil at the beginning; there's no such thing as a 0th month |
|
17 | 17 | month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] |
|
18 | 18 | abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] |
|
19 | 19 | # Used in date_select and datime_select. |
|
20 | 20 | order: |
|
21 | 21 | - :year |
|
22 | 22 | - :month |
|
23 | 23 | - :day |
|
24 | 24 | |
|
25 | 25 | time: |
|
26 | 26 | formats: |
|
27 | 27 | default: "%m/%d/%Y %I:%M %p" |
|
28 | 28 | time: "%I:%M %p" |
|
29 | 29 | short: "%d %b %H:%M" |
|
30 | 30 | long: "%B %d, %Y %H:%M" |
|
31 | 31 | am: "am" |
|
32 | 32 | pm: "pm" |
|
33 | 33 | |
|
34 | 34 | datetime: |
|
35 | 35 | distance_in_words: |
|
36 | 36 | half_a_minute: "half a minute" |
|
37 | 37 | less_than_x_seconds: |
|
38 | 38 | one: "less than 1 second" |
|
39 | 39 | other: "less than %{count} seconds" |
|
40 | 40 | x_seconds: |
|
41 | 41 | one: "1 second" |
|
42 | 42 | other: "%{count} seconds" |
|
43 | 43 | less_than_x_minutes: |
|
44 | 44 | one: "less than a minute" |
|
45 | 45 | other: "less than %{count} minutes" |
|
46 | 46 | x_minutes: |
|
47 | 47 | one: "1 minute" |
|
48 | 48 | other: "%{count} minutes" |
|
49 | 49 | about_x_hours: |
|
50 | 50 | one: "about 1 hour" |
|
51 | 51 | other: "about %{count} hours" |
|
52 | 52 | x_hours: |
|
53 | 53 | one: "1 hour" |
|
54 | 54 | other: "%{count} hours" |
|
55 | 55 | x_days: |
|
56 | 56 | one: "1 day" |
|
57 | 57 | other: "%{count} days" |
|
58 | 58 | about_x_months: |
|
59 | 59 | one: "about 1 month" |
|
60 | 60 | other: "about %{count} months" |
|
61 | 61 | x_months: |
|
62 | 62 | one: "1 month" |
|
63 | 63 | other: "%{count} months" |
|
64 | 64 | about_x_years: |
|
65 | 65 | one: "about 1 year" |
|
66 | 66 | other: "about %{count} years" |
|
67 | 67 | over_x_years: |
|
68 | 68 | one: "over 1 year" |
|
69 | 69 | other: "over %{count} years" |
|
70 | 70 | almost_x_years: |
|
71 | 71 | one: "almost 1 year" |
|
72 | 72 | other: "almost %{count} years" |
|
73 | 73 | |
|
74 | 74 | number: |
|
75 | 75 | format: |
|
76 | 76 | separator: "." |
|
77 | 77 | delimiter: "" |
|
78 | 78 | precision: 3 |
|
79 | 79 | |
|
80 | 80 | human: |
|
81 | 81 | format: |
|
82 | 82 | delimiter: "" |
|
83 | 83 | precision: 3 |
|
84 | 84 | storage_units: |
|
85 | 85 | format: "%n %u" |
|
86 | 86 | units: |
|
87 | 87 | byte: |
|
88 | 88 | one: "Byte" |
|
89 | 89 | other: "Bytes" |
|
90 | 90 | kb: "KB" |
|
91 | 91 | mb: "MB" |
|
92 | 92 | gb: "GB" |
|
93 | 93 | tb: "TB" |
|
94 | 94 | |
|
95 | 95 | # Used in array.to_sentence. |
|
96 | 96 | support: |
|
97 | 97 | array: |
|
98 | 98 | sentence_connector: "and" |
|
99 | 99 | skip_last_comma: false |
|
100 | 100 | |
|
101 | 101 | activerecord: |
|
102 | 102 | errors: |
|
103 | 103 | template: |
|
104 | 104 | header: |
|
105 | 105 | one: "1 error prohibited this %{model} from being saved" |
|
106 | 106 | other: "%{count} errors prohibited this %{model} from being saved" |
|
107 | 107 | messages: |
|
108 | 108 | inclusion: "is not included in the list" |
|
109 | 109 | exclusion: "is reserved" |
|
110 | 110 | invalid: "is invalid" |
|
111 | 111 | confirmation: "doesn't match confirmation" |
|
112 | 112 | accepted: "must be accepted" |
|
113 | 113 | empty: "cannot be empty" |
|
114 | 114 | blank: "cannot be blank" |
|
115 | 115 | too_long: "is too long (maximum is %{count} characters)" |
|
116 | 116 | too_short: "is too short (minimum is %{count} characters)" |
|
117 | 117 | wrong_length: "is the wrong length (should be %{count} characters)" |
|
118 | 118 | taken: "has already been taken" |
|
119 | 119 | not_a_number: "is not a number" |
|
120 | 120 | not_a_date: "is not a valid date" |
|
121 | 121 | greater_than: "must be greater than %{count}" |
|
122 | 122 | greater_than_or_equal_to: "must be greater than or equal to %{count}" |
|
123 | 123 | equal_to: "must be equal to %{count}" |
|
124 | 124 | less_than: "must be less than %{count}" |
|
125 | 125 | less_than_or_equal_to: "must be less than or equal to %{count}" |
|
126 | 126 | odd: "must be odd" |
|
127 | 127 | even: "must be even" |
|
128 | 128 | greater_than_start_date: "must be greater than start date" |
|
129 | 129 | not_same_project: "doesn't belong to the same project" |
|
130 | 130 | circular_dependency: "This relation would create a circular dependency" |
|
131 | 131 | cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks" |
|
132 | 132 | earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" |
|
133 | 133 | |
|
134 | 134 | actionview_instancetag_blank_option: Please select |
|
135 | 135 | |
|
136 | 136 | general_text_No: 'No' |
|
137 | 137 | general_text_Yes: 'Yes' |
|
138 | 138 | general_text_no: 'no' |
|
139 | 139 | general_text_yes: 'yes' |
|
140 | 140 | general_lang_name: 'English' |
|
141 | 141 | general_csv_separator: ',' |
|
142 | 142 | general_csv_decimal_separator: '.' |
|
143 | 143 | general_csv_encoding: ISO-8859-1 |
|
144 | 144 | general_pdf_fontname: freesans |
|
145 | 145 | general_first_day_of_week: '7' |
|
146 | 146 | |
|
147 | 147 | notice_account_updated: Account was successfully updated. |
|
148 | 148 | notice_account_invalid_creditentials: Invalid user or password |
|
149 | 149 | notice_account_password_updated: Password was successfully updated. |
|
150 | 150 | notice_account_wrong_password: Wrong password |
|
151 | 151 | notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}. |
|
152 | 152 | notice_account_unknown_email: Unknown user. |
|
153 | 153 | notice_account_not_activated_yet: You haven't activated your account yet. If you want to receive a new activation email, please <a href="%{url}">click this link</a>. |
|
154 | 154 | notice_account_locked: Your account is locked. |
|
155 | 155 | notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password. |
|
156 | 156 | notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you. |
|
157 | 157 | notice_account_activated: Your account has been activated. You can now log in. |
|
158 | 158 | notice_successful_create: Successful creation. |
|
159 | 159 | notice_successful_update: Successful update. |
|
160 | 160 | notice_successful_delete: Successful deletion. |
|
161 | 161 | notice_successful_connection: Successful connection. |
|
162 | 162 | notice_file_not_found: The page you were trying to access doesn't exist or has been removed. |
|
163 | 163 | notice_locking_conflict: Data has been updated by another user. |
|
164 | 164 | notice_not_authorized: You are not authorized to access this page. |
|
165 | 165 | notice_not_authorized_archived_project: The project you're trying to access has been archived. |
|
166 | 166 | notice_email_sent: "An email was sent to %{value}" |
|
167 | 167 | notice_email_error: "An error occurred while sending mail (%{value})" |
|
168 | 168 | notice_feeds_access_key_reseted: Your Atom access key was reset. |
|
169 | 169 | notice_api_access_key_reseted: Your API access key was reset. |
|
170 | 170 | notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}." |
|
171 | 171 | notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." |
|
172 | 172 | notice_failed_to_save_members: "Failed to save member(s): %{errors}." |
|
173 | 173 | notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." |
|
174 | 174 | notice_account_pending: "Your account was created and is now pending administrator approval." |
|
175 | 175 | notice_default_data_loaded: Default configuration successfully loaded. |
|
176 | 176 | notice_unable_delete_version: Unable to delete version. |
|
177 | 177 | notice_unable_delete_time_entry: Unable to delete time log entry. |
|
178 | 178 | notice_issue_done_ratios_updated: Issue done ratios updated. |
|
179 | 179 | notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})" |
|
180 | 180 | notice_issue_successful_create: "Issue %{id} created." |
|
181 | 181 | notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it." |
|
182 | 182 | notice_account_deleted: "Your account has been permanently deleted." |
|
183 | 183 | notice_user_successful_create: "User %{id} created." |
|
184 | 184 | notice_new_password_must_be_different: The new password must be different from the current password |
|
185 | 185 | |
|
186 | 186 | error_can_t_load_default_data: "Default configuration could not be loaded: %{value}" |
|
187 | 187 | error_scm_not_found: "The entry or revision was not found in the repository." |
|
188 | 188 | error_scm_command_failed: "An error occurred when trying to access the repository: %{value}" |
|
189 | 189 | error_scm_annotate: "The entry does not exist or cannot be annotated." |
|
190 | 190 | error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size." |
|
191 | 191 | error_issue_not_found_in_project: 'The issue was not found or does not belong to this project' |
|
192 | 192 | error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.' |
|
193 | 193 | error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' |
|
194 | 194 | error_can_not_delete_custom_field: Unable to delete custom field |
|
195 | 195 | error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted." |
|
196 | 196 | error_can_not_remove_role: "This role is in use and cannot be deleted." |
|
197 | 197 | error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened' |
|
198 | 198 | error_can_not_archive_project: This project cannot be archived |
|
199 | 199 | error_issue_done_ratios_not_updated: "Issue done ratios not updated." |
|
200 | 200 | error_workflow_copy_source: 'Please select a source tracker or role' |
|
201 | 201 | error_workflow_copy_target: 'Please select target tracker(s) and role(s)' |
|
202 | 202 | error_unable_delete_issue_status: 'Unable to delete issue status' |
|
203 | 203 | error_unable_to_connect: "Unable to connect (%{value})" |
|
204 | 204 | error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})" |
|
205 | 205 | error_session_expired: "Your session has expired. Please login again." |
|
206 | 206 | warning_attachments_not_saved: "%{count} file(s) could not be saved." |
|
207 | 207 | error_password_expired: "Your password has expired or the administrator requires you to change it." |
|
208 | 208 | |
|
209 | 209 | mail_subject_lost_password: "Your %{value} password" |
|
210 | 210 | mail_body_lost_password: 'To change your password, click on the following link:' |
|
211 | 211 | mail_subject_register: "Your %{value} account activation" |
|
212 | 212 | mail_body_register: 'To activate your account, click on the following link:' |
|
213 | 213 | mail_body_account_information_external: "You can use your %{value} account to log in." |
|
214 | 214 | mail_body_account_information: Your account information |
|
215 | 215 | mail_subject_account_activation_request: "%{value} account activation request" |
|
216 | 216 | mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:" |
|
217 | 217 | mail_subject_reminder: "%{count} issue(s) due in the next %{days} days" |
|
218 | 218 | mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:" |
|
219 | 219 | mail_subject_wiki_content_added: "'%{id}' wiki page has been added" |
|
220 | 220 | mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}." |
|
221 | 221 | mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated" |
|
222 | 222 | mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}." |
|
223 | 223 | |
|
224 | 224 | field_name: Name |
|
225 | 225 | field_description: Description |
|
226 | 226 | field_summary: Summary |
|
227 | 227 | field_is_required: Required |
|
228 | 228 | field_firstname: First name |
|
229 | 229 | field_lastname: Last name |
|
230 | 230 | field_mail: Email |
|
231 | 231 | field_address: Email |
|
232 | 232 | field_filename: File |
|
233 | 233 | field_filesize: Size |
|
234 | 234 | field_downloads: Downloads |
|
235 | 235 | field_author: Author |
|
236 | 236 | field_created_on: Created |
|
237 | 237 | field_updated_on: Updated |
|
238 | 238 | field_closed_on: Closed |
|
239 | 239 | field_field_format: Format |
|
240 | 240 | field_is_for_all: For all projects |
|
241 | 241 | field_possible_values: Possible values |
|
242 | 242 | field_regexp: Regular expression |
|
243 | 243 | field_min_length: Minimum length |
|
244 | 244 | field_max_length: Maximum length |
|
245 | 245 | field_value: Value |
|
246 | 246 | field_category: Category |
|
247 | 247 | field_title: Title |
|
248 | 248 | field_project: Project |
|
249 | 249 | field_issue: Issue |
|
250 | 250 | field_status: Status |
|
251 | 251 | field_notes: Notes |
|
252 | 252 | field_is_closed: Issue closed |
|
253 | 253 | field_is_default: Default value |
|
254 | 254 | field_tracker: Tracker |
|
255 | 255 | field_subject: Subject |
|
256 | 256 | field_due_date: Due date |
|
257 | 257 | field_assigned_to: Assignee |
|
258 | 258 | field_priority: Priority |
|
259 | 259 | field_fixed_version: Target version |
|
260 | 260 | field_user: User |
|
261 | 261 | field_principal: Principal |
|
262 | 262 | field_role: Role |
|
263 | 263 | field_homepage: Homepage |
|
264 | 264 | field_is_public: Public |
|
265 | 265 | field_parent: Subproject of |
|
266 | 266 | field_is_in_roadmap: Issues displayed in roadmap |
|
267 | 267 | field_login: Login |
|
268 | 268 | field_mail_notification: Email notifications |
|
269 | 269 | field_admin: Administrator |
|
270 | 270 | field_last_login_on: Last connection |
|
271 | 271 | field_language: Language |
|
272 | 272 | field_effective_date: Date |
|
273 | 273 | field_password: Password |
|
274 | 274 | field_new_password: New password |
|
275 | 275 | field_password_confirmation: Confirmation |
|
276 | 276 | field_version: Version |
|
277 | 277 | field_type: Type |
|
278 | 278 | field_host: Host |
|
279 | 279 | field_port: Port |
|
280 | 280 | field_account: Account |
|
281 | 281 | field_base_dn: Base DN |
|
282 | 282 | field_attr_login: Login attribute |
|
283 | 283 | field_attr_firstname: Firstname attribute |
|
284 | 284 | field_attr_lastname: Lastname attribute |
|
285 | 285 | field_attr_mail: Email attribute |
|
286 | 286 | field_onthefly: On-the-fly user creation |
|
287 | 287 | field_start_date: Start date |
|
288 | 288 | field_done_ratio: "% Done" |
|
289 | 289 | field_auth_source: Authentication mode |
|
290 | 290 | field_hide_mail: Hide my email address |
|
291 | 291 | field_comments: Comment |
|
292 | 292 | field_url: URL |
|
293 | 293 | field_start_page: Start page |
|
294 | 294 | field_subproject: Subproject |
|
295 | 295 | field_hours: Hours |
|
296 | 296 | field_activity: Activity |
|
297 | 297 | field_spent_on: Date |
|
298 | 298 | field_identifier: Identifier |
|
299 | 299 | field_is_filter: Used as a filter |
|
300 | 300 | field_issue_to: Related issue |
|
301 | 301 | field_delay: Delay |
|
302 | 302 | field_assignable: Issues can be assigned to this role |
|
303 | 303 | field_redirect_existing_links: Redirect existing links |
|
304 | 304 | field_estimated_hours: Estimated time |
|
305 | 305 | field_column_names: Columns |
|
306 | 306 | field_time_entries: Log time |
|
307 | 307 | field_time_zone: Time zone |
|
308 | 308 | field_searchable: Searchable |
|
309 | 309 | field_default_value: Default value |
|
310 | 310 | field_comments_sorting: Display comments |
|
311 | 311 | field_parent_title: Parent page |
|
312 | 312 | field_editable: Editable |
|
313 | 313 | field_watcher: Watcher |
|
314 | 314 | field_identity_url: OpenID URL |
|
315 | 315 | field_content: Content |
|
316 | 316 | field_group_by: Group results by |
|
317 | 317 | field_sharing: Sharing |
|
318 | 318 | field_parent_issue: Parent task |
|
319 | 319 | field_member_of_group: "Assignee's group" |
|
320 | 320 | field_assigned_to_role: "Assignee's role" |
|
321 | 321 | field_text: Text field |
|
322 | 322 | field_visible: Visible |
|
323 | 323 | field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text" |
|
324 | 324 | field_issues_visibility: Issues visibility |
|
325 | 325 | field_is_private: Private |
|
326 | 326 | field_commit_logs_encoding: Commit messages encoding |
|
327 | 327 | field_scm_path_encoding: Path encoding |
|
328 | 328 | field_path_to_repository: Path to repository |
|
329 | 329 | field_root_directory: Root directory |
|
330 | 330 | field_cvsroot: CVSROOT |
|
331 | 331 | field_cvs_module: Module |
|
332 | 332 | field_repository_is_default: Main repository |
|
333 | 333 | field_multiple: Multiple values |
|
334 | 334 | field_auth_source_ldap_filter: LDAP filter |
|
335 | 335 | field_core_fields: Standard fields |
|
336 | 336 | field_timeout: "Timeout (in seconds)" |
|
337 | 337 | field_board_parent: Parent forum |
|
338 | 338 | field_private_notes: Private notes |
|
339 | 339 | field_inherit_members: Inherit members |
|
340 | 340 | field_generate_password: Generate password |
|
341 | 341 | field_must_change_passwd: Must change password at next logon |
|
342 | 342 | field_default_status: Default status |
|
343 | 343 | field_users_visibility: Users visibility |
|
344 | 344 | field_time_entries_visibility: Time logs visibility |
|
345 | 345 | |
|
346 | 346 | setting_app_title: Application title |
|
347 | 347 | setting_app_subtitle: Application subtitle |
|
348 | 348 | setting_welcome_text: Welcome text |
|
349 | 349 | setting_default_language: Default language |
|
350 | 350 | setting_login_required: Authentication required |
|
351 | 351 | setting_self_registration: Self-registration |
|
352 | 352 | setting_attachment_max_size: Maximum attachment size |
|
353 | 353 | setting_issues_export_limit: Issues export limit |
|
354 | 354 | setting_mail_from: Emission email address |
|
355 | 355 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
356 | 356 | setting_plain_text_mail: Plain text mail (no HTML) |
|
357 | 357 | setting_host_name: Host name and path |
|
358 | 358 | setting_text_formatting: Text formatting |
|
359 | 359 | setting_wiki_compression: Wiki history compression |
|
360 | 360 | setting_feeds_limit: Maximum number of items in Atom feeds |
|
361 | 361 | setting_default_projects_public: New projects are public by default |
|
362 | 362 | setting_autofetch_changesets: Fetch commits automatically |
|
363 | 363 | setting_sys_api_enabled: Enable WS for repository management |
|
364 | 364 | setting_commit_ref_keywords: Referencing keywords |
|
365 | 365 | setting_commit_fix_keywords: Fixing keywords |
|
366 | 366 | setting_autologin: Autologin |
|
367 | 367 | setting_date_format: Date format |
|
368 | 368 | setting_time_format: Time format |
|
369 | 369 | setting_cross_project_issue_relations: Allow cross-project issue relations |
|
370 | 370 | setting_cross_project_subtasks: Allow cross-project subtasks |
|
371 | 371 | setting_issue_list_default_columns: Default columns displayed on the issue list |
|
372 | 372 | setting_repositories_encodings: Attachments and repositories encodings |
|
373 | 373 | setting_emails_header: Email header |
|
374 | 374 | setting_emails_footer: Email footer |
|
375 | 375 | setting_protocol: Protocol |
|
376 | 376 | setting_per_page_options: Objects per page options |
|
377 | 377 | setting_user_format: Users display format |
|
378 | 378 | setting_activity_days_default: Days displayed on project activity |
|
379 | 379 | setting_display_subprojects_issues: Display subprojects issues on main projects by default |
|
380 | 380 | setting_enabled_scm: Enabled SCM |
|
381 | 381 | setting_mail_handler_body_delimiters: "Truncate emails after one of these lines" |
|
382 | 382 | setting_mail_handler_api_enabled: Enable WS for incoming emails |
|
383 | 383 | setting_mail_handler_api_key: API key |
|
384 | 384 | setting_sequential_project_identifiers: Generate sequential project identifiers |
|
385 | 385 | setting_gravatar_enabled: Use Gravatar user icons |
|
386 | 386 | setting_gravatar_default: Default Gravatar image |
|
387 | 387 | setting_diff_max_lines_displayed: Maximum number of diff lines displayed |
|
388 | 388 | setting_file_max_size_displayed: Maximum size of text files displayed inline |
|
389 | 389 | setting_repository_log_display_limit: Maximum number of revisions displayed on file log |
|
390 | 390 | setting_openid: Allow OpenID login and registration |
|
391 | 391 | setting_password_max_age: Require password change after |
|
392 | 392 | setting_password_min_length: Minimum password length |
|
393 | 393 | setting_new_project_user_role_id: Role given to a non-admin user who creates a project |
|
394 | 394 | setting_default_projects_modules: Default enabled modules for new projects |
|
395 | 395 | setting_issue_done_ratio: Calculate the issue done ratio with |
|
396 | 396 | setting_issue_done_ratio_issue_field: Use the issue field |
|
397 | 397 | setting_issue_done_ratio_issue_status: Use the issue status |
|
398 | 398 | setting_start_of_week: Start calendars on |
|
399 | 399 | setting_rest_api_enabled: Enable REST web service |
|
400 | 400 | setting_cache_formatted_text: Cache formatted text |
|
401 | 401 | setting_default_notification_option: Default notification option |
|
402 | 402 | setting_commit_logtime_enabled: Enable time logging |
|
403 | 403 | setting_commit_logtime_activity_id: Activity for logged time |
|
404 | 404 | setting_gantt_items_limit: Maximum number of items displayed on the gantt chart |
|
405 | 405 | setting_issue_group_assignment: Allow issue assignment to groups |
|
406 | 406 | setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues |
|
407 | 407 | setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed |
|
408 | 408 | setting_unsubscribe: Allow users to delete their own account |
|
409 | 409 | setting_session_lifetime: Session maximum lifetime |
|
410 | 410 | setting_session_timeout: Session inactivity timeout |
|
411 | 411 | setting_thumbnails_enabled: Display attachment thumbnails |
|
412 | 412 | setting_thumbnails_size: Thumbnails size (in pixels) |
|
413 | 413 | setting_non_working_week_days: Non-working days |
|
414 | 414 | setting_jsonp_enabled: Enable JSONP support |
|
415 | 415 | setting_default_projects_tracker_ids: Default trackers for new projects |
|
416 | 416 | setting_mail_handler_excluded_filenames: Exclude attachments by name |
|
417 | 417 | setting_force_default_language_for_anonymous: Force default language for anonymous users |
|
418 | 418 | setting_force_default_language_for_loggedin: Force default language for logged-in users |
|
419 | 419 | setting_link_copied_issue: Link issues on copy |
|
420 | 420 | setting_max_additional_emails: Maximum number of additional email addresses |
|
421 | 421 | setting_search_results_per_page: Search results per page |
|
422 | 422 | |
|
423 | 423 | permission_add_project: Create project |
|
424 | 424 | permission_add_subprojects: Create subprojects |
|
425 | 425 | permission_edit_project: Edit project |
|
426 | 426 | permission_close_project: Close / reopen the project |
|
427 | 427 | permission_select_project_modules: Select project modules |
|
428 | 428 | permission_manage_members: Manage members |
|
429 | 429 | permission_manage_project_activities: Manage project activities |
|
430 | 430 | permission_manage_versions: Manage versions |
|
431 | 431 | permission_manage_categories: Manage issue categories |
|
432 | 432 | permission_view_issues: View Issues |
|
433 | 433 | permission_add_issues: Add issues |
|
434 | 434 | permission_edit_issues: Edit issues |
|
435 | 435 | permission_copy_issues: Copy issues |
|
436 | 436 | permission_manage_issue_relations: Manage issue relations |
|
437 | 437 | permission_set_issues_private: Set issues public or private |
|
438 | 438 | permission_set_own_issues_private: Set own issues public or private |
|
439 | 439 | permission_add_issue_notes: Add notes |
|
440 | 440 | permission_edit_issue_notes: Edit notes |
|
441 | 441 | permission_edit_own_issue_notes: Edit own notes |
|
442 | 442 | permission_view_private_notes: View private notes |
|
443 | 443 | permission_set_notes_private: Set notes as private |
|
444 | 444 | permission_move_issues: Move issues |
|
445 | 445 | permission_delete_issues: Delete issues |
|
446 | 446 | permission_manage_public_queries: Manage public queries |
|
447 | 447 | permission_save_queries: Save queries |
|
448 | 448 | permission_view_gantt: View gantt chart |
|
449 | 449 | permission_view_calendar: View calendar |
|
450 | 450 | permission_view_issue_watchers: View watchers list |
|
451 | 451 | permission_add_issue_watchers: Add watchers |
|
452 | 452 | permission_delete_issue_watchers: Delete watchers |
|
453 | 453 | permission_log_time: Log spent time |
|
454 | 454 | permission_view_time_entries: View spent time |
|
455 | 455 | permission_edit_time_entries: Edit time logs |
|
456 | 456 | permission_edit_own_time_entries: Edit own time logs |
|
457 | 457 | permission_manage_news: Manage news |
|
458 | 458 | permission_comment_news: Comment news |
|
459 | 459 | permission_view_documents: View documents |
|
460 | 460 | permission_add_documents: Add documents |
|
461 | 461 | permission_edit_documents: Edit documents |
|
462 | 462 | permission_delete_documents: Delete documents |
|
463 | 463 | permission_manage_files: Manage files |
|
464 | 464 | permission_view_files: View files |
|
465 | 465 | permission_manage_wiki: Manage wiki |
|
466 | 466 | permission_rename_wiki_pages: Rename wiki pages |
|
467 | 467 | permission_delete_wiki_pages: Delete wiki pages |
|
468 | 468 | permission_view_wiki_pages: View wiki |
|
469 | 469 | permission_view_wiki_edits: View wiki history |
|
470 | 470 | permission_edit_wiki_pages: Edit wiki pages |
|
471 | 471 | permission_delete_wiki_pages_attachments: Delete attachments |
|
472 | 472 | permission_protect_wiki_pages: Protect wiki pages |
|
473 | 473 | permission_manage_repository: Manage repository |
|
474 | 474 | permission_browse_repository: Browse repository |
|
475 | 475 | permission_view_changesets: View changesets |
|
476 | 476 | permission_commit_access: Commit access |
|
477 | 477 | permission_manage_boards: Manage forums |
|
478 | 478 | permission_view_messages: View messages |
|
479 | 479 | permission_add_messages: Post messages |
|
480 | 480 | permission_edit_messages: Edit messages |
|
481 | 481 | permission_edit_own_messages: Edit own messages |
|
482 | 482 | permission_delete_messages: Delete messages |
|
483 | 483 | permission_delete_own_messages: Delete own messages |
|
484 | 484 | permission_export_wiki_pages: Export wiki pages |
|
485 | 485 | permission_manage_subtasks: Manage subtasks |
|
486 | 486 | permission_manage_related_issues: Manage related issues |
|
487 | 487 | |
|
488 | 488 | project_module_issue_tracking: Issue tracking |
|
489 | 489 | project_module_time_tracking: Time tracking |
|
490 | 490 | project_module_news: News |
|
491 | 491 | project_module_documents: Documents |
|
492 | 492 | project_module_files: Files |
|
493 | 493 | project_module_wiki: Wiki |
|
494 | 494 | project_module_repository: Repository |
|
495 | 495 | project_module_boards: Forums |
|
496 | 496 | project_module_calendar: Calendar |
|
497 | 497 | project_module_gantt: Gantt |
|
498 | 498 | |
|
499 | 499 | label_user: User |
|
500 | 500 | label_user_plural: Users |
|
501 | 501 | label_user_new: New user |
|
502 | 502 | label_user_anonymous: Anonymous |
|
503 | 503 | label_project: Project |
|
504 | 504 | label_project_new: New project |
|
505 | 505 | label_project_plural: Projects |
|
506 | 506 | label_x_projects: |
|
507 | 507 | zero: no projects |
|
508 | 508 | one: 1 project |
|
509 | 509 | other: "%{count} projects" |
|
510 | 510 | label_project_all: All Projects |
|
511 | 511 | label_project_latest: Latest projects |
|
512 | 512 | label_issue: Issue |
|
513 | 513 | label_issue_new: New issue |
|
514 | 514 | label_issue_plural: Issues |
|
515 | 515 | label_issue_view_all: View all issues |
|
516 | 516 | label_issues_by: "Issues by %{value}" |
|
517 | 517 | label_issue_added: Issue added |
|
518 | 518 | label_issue_updated: Issue updated |
|
519 | 519 | label_issue_note_added: Note added |
|
520 | 520 | label_issue_status_updated: Status updated |
|
521 | 521 | label_issue_assigned_to_updated: Assignee updated |
|
522 | 522 | label_issue_priority_updated: Priority updated |
|
523 | 523 | label_document: Document |
|
524 | 524 | label_document_new: New document |
|
525 | 525 | label_document_plural: Documents |
|
526 | 526 | label_document_added: Document added |
|
527 | 527 | label_role: Role |
|
528 | 528 | label_role_plural: Roles |
|
529 | 529 | label_role_new: New role |
|
530 | 530 | label_role_and_permissions: Roles and permissions |
|
531 | 531 | label_role_anonymous: Anonymous |
|
532 | 532 | label_role_non_member: Non member |
|
533 | 533 | label_member: Member |
|
534 | 534 | label_member_new: New member |
|
535 | 535 | label_member_plural: Members |
|
536 | 536 | label_tracker: Tracker |
|
537 | 537 | label_tracker_plural: Trackers |
|
538 | 538 | label_tracker_new: New tracker |
|
539 | 539 | label_workflow: Workflow |
|
540 | 540 | label_issue_status: Issue status |
|
541 | 541 | label_issue_status_plural: Issue statuses |
|
542 | 542 | label_issue_status_new: New status |
|
543 | 543 | label_issue_category: Issue category |
|
544 | 544 | label_issue_category_plural: Issue categories |
|
545 | 545 | label_issue_category_new: New category |
|
546 | 546 | label_custom_field: Custom field |
|
547 | 547 | label_custom_field_plural: Custom fields |
|
548 | 548 | label_custom_field_new: New custom field |
|
549 | 549 | label_enumerations: Enumerations |
|
550 | 550 | label_enumeration_new: New value |
|
551 | 551 | label_information: Information |
|
552 | 552 | label_information_plural: Information |
|
553 | 553 | label_please_login: Please log in |
|
554 | 554 | label_register: Register |
|
555 | 555 | label_login_with_open_id_option: or login with OpenID |
|
556 | 556 | label_password_lost: Lost password |
|
557 | label_password_required: Confirm your password to continue | |
|
557 | 558 | label_home: Home |
|
558 | 559 | label_my_page: My page |
|
559 | 560 | label_my_account: My account |
|
560 | 561 | label_my_projects: My projects |
|
561 | 562 | label_my_page_block: My page block |
|
562 | 563 | label_administration: Administration |
|
563 | 564 | label_login: Sign in |
|
564 | 565 | label_logout: Sign out |
|
565 | 566 | label_help: Help |
|
566 | 567 | label_reported_issues: Reported issues |
|
567 | 568 | label_assigned_to_me_issues: Issues assigned to me |
|
568 | 569 | label_last_login: Last connection |
|
569 | 570 | label_registered_on: Registered on |
|
570 | 571 | label_activity: Activity |
|
571 | 572 | label_overall_activity: Overall activity |
|
572 | 573 | label_user_activity: "%{value}'s activity" |
|
573 | 574 | label_new: New |
|
574 | 575 | label_logged_as: Logged in as |
|
575 | 576 | label_environment: Environment |
|
576 | 577 | label_authentication: Authentication |
|
577 | 578 | label_auth_source: Authentication mode |
|
578 | 579 | label_auth_source_new: New authentication mode |
|
579 | 580 | label_auth_source_plural: Authentication modes |
|
580 | 581 | label_subproject_plural: Subprojects |
|
581 | 582 | label_subproject_new: New subproject |
|
582 | 583 | label_and_its_subprojects: "%{value} and its subprojects" |
|
583 | 584 | label_min_max_length: Min - Max length |
|
584 | 585 | label_list: List |
|
585 | 586 | label_date: Date |
|
586 | 587 | label_integer: Integer |
|
587 | 588 | label_float: Float |
|
588 | 589 | label_boolean: Boolean |
|
589 | 590 | label_string: Text |
|
590 | 591 | label_text: Long text |
|
591 | 592 | label_attribute: Attribute |
|
592 | 593 | label_attribute_plural: Attributes |
|
593 | 594 | label_no_data: No data to display |
|
594 | 595 | label_change_status: Change status |
|
595 | 596 | label_history: History |
|
596 | 597 | label_attachment: File |
|
597 | 598 | label_attachment_new: New file |
|
598 | 599 | label_attachment_delete: Delete file |
|
599 | 600 | label_attachment_plural: Files |
|
600 | 601 | label_file_added: File added |
|
601 | 602 | label_report: Report |
|
602 | 603 | label_report_plural: Reports |
|
603 | 604 | label_news: News |
|
604 | 605 | label_news_new: Add news |
|
605 | 606 | label_news_plural: News |
|
606 | 607 | label_news_latest: Latest news |
|
607 | 608 | label_news_view_all: View all news |
|
608 | 609 | label_news_added: News added |
|
609 | 610 | label_news_comment_added: Comment added to a news |
|
610 | 611 | label_settings: Settings |
|
611 | 612 | label_overview: Overview |
|
612 | 613 | label_version: Version |
|
613 | 614 | label_version_new: New version |
|
614 | 615 | label_version_plural: Versions |
|
615 | 616 | label_close_versions: Close completed versions |
|
616 | 617 | label_confirmation: Confirmation |
|
617 | 618 | label_export_to: 'Also available in:' |
|
618 | 619 | label_read: Read... |
|
619 | 620 | label_public_projects: Public projects |
|
620 | 621 | label_open_issues: open |
|
621 | 622 | label_open_issues_plural: open |
|
622 | 623 | label_closed_issues: closed |
|
623 | 624 | label_closed_issues_plural: closed |
|
624 | 625 | label_x_open_issues_abbr_on_total: |
|
625 | 626 | zero: 0 open / %{total} |
|
626 | 627 | one: 1 open / %{total} |
|
627 | 628 | other: "%{count} open / %{total}" |
|
628 | 629 | label_x_open_issues_abbr: |
|
629 | 630 | zero: 0 open |
|
630 | 631 | one: 1 open |
|
631 | 632 | other: "%{count} open" |
|
632 | 633 | label_x_closed_issues_abbr: |
|
633 | 634 | zero: 0 closed |
|
634 | 635 | one: 1 closed |
|
635 | 636 | other: "%{count} closed" |
|
636 | 637 | label_x_issues: |
|
637 | 638 | zero: 0 issues |
|
638 | 639 | one: 1 issue |
|
639 | 640 | other: "%{count} issues" |
|
640 | 641 | label_total: Total |
|
641 | 642 | label_total_time: Total time |
|
642 | 643 | label_permissions: Permissions |
|
643 | 644 | label_current_status: Current status |
|
644 | 645 | label_new_statuses_allowed: New statuses allowed |
|
645 | 646 | label_all: all |
|
646 | 647 | label_any: any |
|
647 | 648 | label_none: none |
|
648 | 649 | label_nobody: nobody |
|
649 | 650 | label_next: Next |
|
650 | 651 | label_previous: Previous |
|
651 | 652 | label_used_by: Used by |
|
652 | 653 | label_details: Details |
|
653 | 654 | label_add_note: Add a note |
|
654 | 655 | label_calendar: Calendar |
|
655 | 656 | label_months_from: months from |
|
656 | 657 | label_gantt: Gantt |
|
657 | 658 | label_internal: Internal |
|
658 | 659 | label_last_changes: "last %{count} changes" |
|
659 | 660 | label_change_view_all: View all changes |
|
660 | 661 | label_personalize_page: Personalize this page |
|
661 | 662 | label_comment: Comment |
|
662 | 663 | label_comment_plural: Comments |
|
663 | 664 | label_x_comments: |
|
664 | 665 | zero: no comments |
|
665 | 666 | one: 1 comment |
|
666 | 667 | other: "%{count} comments" |
|
667 | 668 | label_comment_add: Add a comment |
|
668 | 669 | label_comment_added: Comment added |
|
669 | 670 | label_comment_delete: Delete comments |
|
670 | 671 | label_query: Custom query |
|
671 | 672 | label_query_plural: Custom queries |
|
672 | 673 | label_query_new: New query |
|
673 | 674 | label_my_queries: My custom queries |
|
674 | 675 | label_filter_add: Add filter |
|
675 | 676 | label_filter_plural: Filters |
|
676 | 677 | label_equals: is |
|
677 | 678 | label_not_equals: is not |
|
678 | 679 | label_in_less_than: in less than |
|
679 | 680 | label_in_more_than: in more than |
|
680 | 681 | label_in_the_next_days: in the next |
|
681 | 682 | label_in_the_past_days: in the past |
|
682 | 683 | label_greater_or_equal: '>=' |
|
683 | 684 | label_less_or_equal: '<=' |
|
684 | 685 | label_between: between |
|
685 | 686 | label_in: in |
|
686 | 687 | label_today: today |
|
687 | 688 | label_all_time: all time |
|
688 | 689 | label_yesterday: yesterday |
|
689 | 690 | label_this_week: this week |
|
690 | 691 | label_last_week: last week |
|
691 | 692 | label_last_n_weeks: "last %{count} weeks" |
|
692 | 693 | label_last_n_days: "last %{count} days" |
|
693 | 694 | label_this_month: this month |
|
694 | 695 | label_last_month: last month |
|
695 | 696 | label_this_year: this year |
|
696 | 697 | label_date_range: Date range |
|
697 | 698 | label_less_than_ago: less than days ago |
|
698 | 699 | label_more_than_ago: more than days ago |
|
699 | 700 | label_ago: days ago |
|
700 | 701 | label_contains: contains |
|
701 | 702 | label_not_contains: doesn't contain |
|
702 | 703 | label_any_issues_in_project: any issues in project |
|
703 | 704 | label_any_issues_not_in_project: any issues not in project |
|
704 | 705 | label_no_issues_in_project: no issues in project |
|
705 | 706 | label_day_plural: days |
|
706 | 707 | label_repository: Repository |
|
707 | 708 | label_repository_new: New repository |
|
708 | 709 | label_repository_plural: Repositories |
|
709 | 710 | label_browse: Browse |
|
710 | 711 | label_branch: Branch |
|
711 | 712 | label_tag: Tag |
|
712 | 713 | label_revision: Revision |
|
713 | 714 | label_revision_plural: Revisions |
|
714 | 715 | label_revision_id: "Revision %{value}" |
|
715 | 716 | label_associated_revisions: Associated revisions |
|
716 | 717 | label_added: added |
|
717 | 718 | label_modified: modified |
|
718 | 719 | label_copied: copied |
|
719 | 720 | label_renamed: renamed |
|
720 | 721 | label_deleted: deleted |
|
721 | 722 | label_latest_revision: Latest revision |
|
722 | 723 | label_latest_revision_plural: Latest revisions |
|
723 | 724 | label_view_revisions: View revisions |
|
724 | 725 | label_view_all_revisions: View all revisions |
|
725 | 726 | label_max_size: Maximum size |
|
726 | 727 | label_sort_highest: Move to top |
|
727 | 728 | label_sort_higher: Move up |
|
728 | 729 | label_sort_lower: Move down |
|
729 | 730 | label_sort_lowest: Move to bottom |
|
730 | 731 | label_roadmap: Roadmap |
|
731 | 732 | label_roadmap_due_in: "Due in %{value}" |
|
732 | 733 | label_roadmap_overdue: "%{value} late" |
|
733 | 734 | label_roadmap_no_issues: No issues for this version |
|
734 | 735 | label_search: Search |
|
735 | 736 | label_result_plural: Results |
|
736 | 737 | label_all_words: All words |
|
737 | 738 | label_wiki: Wiki |
|
738 | 739 | label_wiki_edit: Wiki edit |
|
739 | 740 | label_wiki_edit_plural: Wiki edits |
|
740 | 741 | label_wiki_page: Wiki page |
|
741 | 742 | label_wiki_page_plural: Wiki pages |
|
742 | 743 | label_index_by_title: Index by title |
|
743 | 744 | label_index_by_date: Index by date |
|
744 | 745 | label_current_version: Current version |
|
745 | 746 | label_preview: Preview |
|
746 | 747 | label_feed_plural: Feeds |
|
747 | 748 | label_changes_details: Details of all changes |
|
748 | 749 | label_issue_tracking: Issue tracking |
|
749 | 750 | label_spent_time: Spent time |
|
750 | 751 | label_overall_spent_time: Overall spent time |
|
751 | 752 | label_f_hour: "%{value} hour" |
|
752 | 753 | label_f_hour_plural: "%{value} hours" |
|
753 | 754 | label_time_tracking: Time tracking |
|
754 | 755 | label_change_plural: Changes |
|
755 | 756 | label_statistics: Statistics |
|
756 | 757 | label_commits_per_month: Commits per month |
|
757 | 758 | label_commits_per_author: Commits per author |
|
758 | 759 | label_diff: diff |
|
759 | 760 | label_view_diff: View differences |
|
760 | 761 | label_diff_inline: inline |
|
761 | 762 | label_diff_side_by_side: side by side |
|
762 | 763 | label_options: Options |
|
763 | 764 | label_copy_workflow_from: Copy workflow from |
|
764 | 765 | label_permissions_report: Permissions report |
|
765 | 766 | label_watched_issues: Watched issues |
|
766 | 767 | label_related_issues: Related issues |
|
767 | 768 | label_applied_status: Applied status |
|
768 | 769 | label_loading: Loading... |
|
769 | 770 | label_relation_new: New relation |
|
770 | 771 | label_relation_delete: Delete relation |
|
771 | 772 | label_relates_to: Related to |
|
772 | 773 | label_duplicates: Duplicates |
|
773 | 774 | label_duplicated_by: Duplicated by |
|
774 | 775 | label_blocks: Blocks |
|
775 | 776 | label_blocked_by: Blocked by |
|
776 | 777 | label_precedes: Precedes |
|
777 | 778 | label_follows: Follows |
|
778 | 779 | label_copied_to: Copied to |
|
779 | 780 | label_copied_from: Copied from |
|
780 | 781 | label_end_to_start: end to start |
|
781 | 782 | label_end_to_end: end to end |
|
782 | 783 | label_start_to_start: start to start |
|
783 | 784 | label_start_to_end: start to end |
|
784 | 785 | label_stay_logged_in: Stay logged in |
|
785 | 786 | label_disabled: disabled |
|
786 | 787 | label_show_completed_versions: Show completed versions |
|
787 | 788 | label_me: me |
|
788 | 789 | label_board: Forum |
|
789 | 790 | label_board_new: New forum |
|
790 | 791 | label_board_plural: Forums |
|
791 | 792 | label_board_locked: Locked |
|
792 | 793 | label_board_sticky: Sticky |
|
793 | 794 | label_topic_plural: Topics |
|
794 | 795 | label_message_plural: Messages |
|
795 | 796 | label_message_last: Last message |
|
796 | 797 | label_message_new: New message |
|
797 | 798 | label_message_posted: Message added |
|
798 | 799 | label_reply_plural: Replies |
|
799 | 800 | label_send_information: Send account information to the user |
|
800 | 801 | label_year: Year |
|
801 | 802 | label_month: Month |
|
802 | 803 | label_week: Week |
|
803 | 804 | label_date_from: From |
|
804 | 805 | label_date_to: To |
|
805 | 806 | label_language_based: Based on user's language |
|
806 | 807 | label_sort_by: "Sort by %{value}" |
|
807 | 808 | label_send_test_email: Send a test email |
|
808 | 809 | label_feeds_access_key: Atom access key |
|
809 | 810 | label_missing_feeds_access_key: Missing a Atom access key |
|
810 | 811 | label_feeds_access_key_created_on: "Atom access key created %{value} ago" |
|
811 | 812 | label_module_plural: Modules |
|
812 | 813 | label_added_time_by: "Added by %{author} %{age} ago" |
|
813 | 814 | label_updated_time_by: "Updated by %{author} %{age} ago" |
|
814 | 815 | label_updated_time: "Updated %{value} ago" |
|
815 | 816 | label_jump_to_a_project: Jump to a project... |
|
816 | 817 | label_file_plural: Files |
|
817 | 818 | label_changeset_plural: Changesets |
|
818 | 819 | label_default_columns: Default columns |
|
819 | 820 | label_no_change_option: (No change) |
|
820 | 821 | label_bulk_edit_selected_issues: Bulk edit selected issues |
|
821 | 822 | label_bulk_edit_selected_time_entries: Bulk edit selected time entries |
|
822 | 823 | label_theme: Theme |
|
823 | 824 | label_default: Default |
|
824 | 825 | label_search_titles_only: Search titles only |
|
825 | 826 | label_user_mail_option_all: "For any event on all my projects" |
|
826 | 827 | label_user_mail_option_selected: "For any event on the selected projects only..." |
|
827 | 828 | label_user_mail_option_none: "No events" |
|
828 | 829 | label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in" |
|
829 | 830 | label_user_mail_option_only_assigned: "Only for things I am assigned to" |
|
830 | 831 | label_user_mail_option_only_owner: "Only for things I am the owner of" |
|
831 | 832 | label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" |
|
832 | 833 | label_registration_activation_by_email: account activation by email |
|
833 | 834 | label_registration_manual_activation: manual account activation |
|
834 | 835 | label_registration_automatic_activation: automatic account activation |
|
835 | 836 | label_display_per_page: "Per page: %{value}" |
|
836 | 837 | label_age: Age |
|
837 | 838 | label_change_properties: Change properties |
|
838 | 839 | label_general: General |
|
839 | 840 | label_more: More |
|
840 | 841 | label_scm: SCM |
|
841 | 842 | label_plugins: Plugins |
|
842 | 843 | label_ldap_authentication: LDAP authentication |
|
843 | 844 | label_downloads_abbr: D/L |
|
844 | 845 | label_optional_description: Optional description |
|
845 | 846 | label_add_another_file: Add another file |
|
846 | 847 | label_preferences: Preferences |
|
847 | 848 | label_chronological_order: In chronological order |
|
848 | 849 | label_reverse_chronological_order: In reverse chronological order |
|
849 | 850 | label_planning: Planning |
|
850 | 851 | label_incoming_emails: Incoming emails |
|
851 | 852 | label_generate_key: Generate a key |
|
852 | 853 | label_issue_watchers: Watchers |
|
853 | 854 | label_example: Example |
|
854 | 855 | label_display: Display |
|
855 | 856 | label_sort: Sort |
|
856 | 857 | label_ascending: Ascending |
|
857 | 858 | label_descending: Descending |
|
858 | 859 | label_date_from_to: From %{start} to %{end} |
|
859 | 860 | label_wiki_content_added: Wiki page added |
|
860 | 861 | label_wiki_content_updated: Wiki page updated |
|
861 | 862 | label_group: Group |
|
862 | 863 | label_group_plural: Groups |
|
863 | 864 | label_group_new: New group |
|
864 | 865 | label_group_anonymous: Anonymous users |
|
865 | 866 | label_group_non_member: Non member users |
|
866 | 867 | label_time_entry_plural: Spent time |
|
867 | 868 | label_version_sharing_none: Not shared |
|
868 | 869 | label_version_sharing_descendants: With subprojects |
|
869 | 870 | label_version_sharing_hierarchy: With project hierarchy |
|
870 | 871 | label_version_sharing_tree: With project tree |
|
871 | 872 | label_version_sharing_system: With all projects |
|
872 | 873 | label_update_issue_done_ratios: Update issue done ratios |
|
873 | 874 | label_copy_source: Source |
|
874 | 875 | label_copy_target: Target |
|
875 | 876 | label_copy_same_as_target: Same as target |
|
876 | 877 | label_display_used_statuses_only: Only display statuses that are used by this tracker |
|
877 | 878 | label_api_access_key: API access key |
|
878 | 879 | label_missing_api_access_key: Missing an API access key |
|
879 | 880 | label_api_access_key_created_on: "API access key created %{value} ago" |
|
880 | 881 | label_profile: Profile |
|
881 | 882 | label_subtask_plural: Subtasks |
|
882 | 883 | label_project_copy_notifications: Send email notifications during the project copy |
|
883 | 884 | label_principal_search: "Search for user or group:" |
|
884 | 885 | label_user_search: "Search for user:" |
|
885 | 886 | label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author |
|
886 | 887 | label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee |
|
887 | 888 | label_issues_visibility_all: All issues |
|
888 | 889 | label_issues_visibility_public: All non private issues |
|
889 | 890 | label_issues_visibility_own: Issues created by or assigned to the user |
|
890 | 891 | label_git_report_last_commit: Report last commit for files and directories |
|
891 | 892 | label_parent_revision: Parent |
|
892 | 893 | label_child_revision: Child |
|
893 | 894 | label_export_options: "%{export_format} export options" |
|
894 | 895 | label_copy_attachments: Copy attachments |
|
895 | 896 | label_copy_subtasks: Copy subtasks |
|
896 | 897 | label_item_position: "%{position} of %{count}" |
|
897 | 898 | label_completed_versions: Completed versions |
|
898 | 899 | label_search_for_watchers: Search for watchers to add |
|
899 | 900 | label_session_expiration: Session expiration |
|
900 | 901 | label_show_closed_projects: View closed projects |
|
901 | 902 | label_status_transitions: Status transitions |
|
902 | 903 | label_fields_permissions: Fields permissions |
|
903 | 904 | label_readonly: Read-only |
|
904 | 905 | label_required: Required |
|
905 | 906 | label_hidden: Hidden |
|
906 | 907 | label_attribute_of_project: "Project's %{name}" |
|
907 | 908 | label_attribute_of_issue: "Issue's %{name}" |
|
908 | 909 | label_attribute_of_author: "Author's %{name}" |
|
909 | 910 | label_attribute_of_assigned_to: "Assignee's %{name}" |
|
910 | 911 | label_attribute_of_user: "User's %{name}" |
|
911 | 912 | label_attribute_of_fixed_version: "Target version's %{name}" |
|
912 | 913 | label_cross_project_descendants: With subprojects |
|
913 | 914 | label_cross_project_tree: With project tree |
|
914 | 915 | label_cross_project_hierarchy: With project hierarchy |
|
915 | 916 | label_cross_project_system: With all projects |
|
916 | 917 | label_gantt_progress_line: Progress line |
|
917 | 918 | label_visibility_private: to me only |
|
918 | 919 | label_visibility_roles: to these roles only |
|
919 | 920 | label_visibility_public: to any users |
|
920 | 921 | label_link: Link |
|
921 | 922 | label_only: only |
|
922 | 923 | label_drop_down_list: drop-down list |
|
923 | 924 | label_checkboxes: checkboxes |
|
924 | 925 | label_radio_buttons: radio buttons |
|
925 | 926 | label_link_values_to: Link values to URL |
|
926 | 927 | label_custom_field_select_type: Select the type of object to which the custom field is to be attached |
|
927 | 928 | label_check_for_updates: Check for updates |
|
928 | 929 | label_latest_compatible_version: Latest compatible version |
|
929 | 930 | label_unknown_plugin: Unknown plugin |
|
930 | 931 | label_add_projects: Add projects |
|
931 | 932 | label_users_visibility_all: All active users |
|
932 | 933 | label_users_visibility_members_of_visible_projects: Members of visible projects |
|
933 | 934 | label_edit_attachments: Edit attached files |
|
934 | 935 | label_link_copied_issue: Link copied issue |
|
935 | 936 | label_ask: Ask |
|
936 | 937 | label_search_attachments_yes: Search attachment filenames and descriptions |
|
937 | 938 | label_search_attachments_no: Do not search attachments |
|
938 | 939 | label_search_attachments_only: Search attachments only |
|
939 | 940 | label_search_open_issues_only: Open issues only |
|
940 | 941 | label_email_address_plural: Emails |
|
941 | 942 | label_email_address_add: Add email address |
|
942 | 943 | label_enable_notifications: Enable notifications |
|
943 | 944 | label_disable_notifications: Disable notifications |
|
944 | 945 | label_blank_value: blank |
|
945 | 946 | label_parent_task_attributes: Parent tasks attributes |
|
946 | 947 | label_parent_task_attributes_derived: Calculated from subtasks |
|
947 | 948 | label_parent_task_attributes_independent: Independent of subtasks |
|
948 | 949 | label_time_entries_visibility_all: All time entries |
|
949 | 950 | label_time_entries_visibility_own: Time entries created by the user |
|
950 | 951 | label_member_management: Member management |
|
951 | 952 | label_member_management_all_roles: All roles |
|
952 | 953 | label_member_management_selected_roles_only: Only these roles |
|
953 | 954 | |
|
954 | 955 | button_login: Login |
|
955 | 956 | button_submit: Submit |
|
956 | 957 | button_save: Save |
|
957 | 958 | button_check_all: Check all |
|
958 | 959 | button_uncheck_all: Uncheck all |
|
959 | 960 | button_collapse_all: Collapse all |
|
960 | 961 | button_expand_all: Expand all |
|
961 | 962 | button_delete: Delete |
|
962 | 963 | button_create: Create |
|
963 | 964 | button_create_and_continue: Create and continue |
|
964 | 965 | button_test: Test |
|
965 | 966 | button_edit: Edit |
|
966 | 967 | button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" |
|
967 | 968 | button_add: Add |
|
968 | 969 | button_change: Change |
|
969 | 970 | button_apply: Apply |
|
970 | 971 | button_clear: Clear |
|
971 | 972 | button_lock: Lock |
|
972 | 973 | button_unlock: Unlock |
|
973 | 974 | button_download: Download |
|
974 | 975 | button_list: List |
|
975 | 976 | button_view: View |
|
976 | 977 | button_move: Move |
|
977 | 978 | button_move_and_follow: Move and follow |
|
978 | 979 | button_back: Back |
|
979 | 980 | button_cancel: Cancel |
|
980 | 981 | button_activate: Activate |
|
981 | 982 | button_sort: Sort |
|
982 | 983 | button_log_time: Log time |
|
983 | 984 | button_rollback: Rollback to this version |
|
984 | 985 | button_watch: Watch |
|
985 | 986 | button_unwatch: Unwatch |
|
986 | 987 | button_reply: Reply |
|
987 | 988 | button_archive: Archive |
|
988 | 989 | button_unarchive: Unarchive |
|
989 | 990 | button_reset: Reset |
|
990 | 991 | button_rename: Rename |
|
991 | 992 | button_change_password: Change password |
|
993 | button_confirm_password: Confirm password | |
|
992 | 994 | button_copy: Copy |
|
993 | 995 | button_copy_and_follow: Copy and follow |
|
994 | 996 | button_annotate: Annotate |
|
995 | 997 | button_update: Update |
|
996 | 998 | button_configure: Configure |
|
997 | 999 | button_quote: Quote |
|
998 | 1000 | button_duplicate: Duplicate |
|
999 | 1001 | button_show: Show |
|
1000 | 1002 | button_hide: Hide |
|
1001 | 1003 | button_edit_section: Edit this section |
|
1002 | 1004 | button_export: Export |
|
1003 | 1005 | button_delete_my_account: Delete my account |
|
1004 | 1006 | button_close: Close |
|
1005 | 1007 | button_reopen: Reopen |
|
1006 | 1008 | |
|
1007 | 1009 | status_active: active |
|
1008 | 1010 | status_registered: registered |
|
1009 | 1011 | status_locked: locked |
|
1010 | 1012 | |
|
1011 | 1013 | project_status_active: active |
|
1012 | 1014 | project_status_closed: closed |
|
1013 | 1015 | project_status_archived: archived |
|
1014 | 1016 | |
|
1015 | 1017 | version_status_open: open |
|
1016 | 1018 | version_status_locked: locked |
|
1017 | 1019 | version_status_closed: closed |
|
1018 | 1020 | |
|
1019 | 1021 | field_active: Active |
|
1020 | 1022 | |
|
1021 | 1023 | text_select_mail_notifications: Select actions for which email notifications should be sent. |
|
1022 | 1024 | text_regexp_info: eg. ^[A-Z0-9]+$ |
|
1023 | 1025 | text_min_max_length_info: 0 means no restriction |
|
1024 | 1026 | text_project_destroy_confirmation: Are you sure you want to delete this project and related data? |
|
1025 | 1027 | text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted." |
|
1026 | 1028 | text_workflow_edit: Select a role and a tracker to edit the workflow |
|
1027 | 1029 | text_are_you_sure: Are you sure? |
|
1028 | 1030 | text_journal_changed: "%{label} changed from %{old} to %{new}" |
|
1029 | 1031 | text_journal_changed_no_detail: "%{label} updated" |
|
1030 | 1032 | text_journal_set_to: "%{label} set to %{value}" |
|
1031 | 1033 | text_journal_deleted: "%{label} deleted (%{old})" |
|
1032 | 1034 | text_journal_added: "%{label} %{value} added" |
|
1033 | 1035 | text_tip_issue_begin_day: issue beginning this day |
|
1034 | 1036 | text_tip_issue_end_day: issue ending this day |
|
1035 | 1037 | text_tip_issue_begin_end_day: issue beginning and ending this day |
|
1036 | 1038 | text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed.' |
|
1037 | 1039 | text_caracters_maximum: "%{count} characters maximum." |
|
1038 | 1040 | text_caracters_minimum: "Must be at least %{count} characters long." |
|
1039 | 1041 | text_length_between: "Length between %{min} and %{max} characters." |
|
1040 | 1042 | text_tracker_no_workflow: No workflow defined for this tracker |
|
1041 | 1043 | text_unallowed_characters: Unallowed characters |
|
1042 | 1044 | text_comma_separated: Multiple values allowed (comma separated). |
|
1043 | 1045 | text_line_separated: Multiple values allowed (one line for each value). |
|
1044 | 1046 | text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages |
|
1045 | 1047 | text_issue_added: "Issue %{id} has been reported by %{author}." |
|
1046 | 1048 | text_issue_updated: "Issue %{id} has been updated by %{author}." |
|
1047 | 1049 | text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content? |
|
1048 | 1050 | text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?" |
|
1049 | 1051 | text_issue_category_destroy_assignments: Remove category assignments |
|
1050 | 1052 | text_issue_category_reassign_to: Reassign issues to this category |
|
1051 | 1053 | text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)." |
|
1052 | 1054 | text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." |
|
1053 | 1055 | text_load_default_configuration: Load the default configuration |
|
1054 | 1056 | text_status_changed_by_changeset: "Applied in changeset %{value}." |
|
1055 | 1057 | text_time_logged_by_changeset: "Applied in changeset %{value}." |
|
1056 | 1058 | text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?' |
|
1057 | 1059 | text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)." |
|
1058 | 1060 | text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?' |
|
1059 | 1061 | text_select_project_modules: 'Select modules to enable for this project:' |
|
1060 | 1062 | text_default_administrator_account_changed: Default administrator account changed |
|
1061 | 1063 | text_file_repository_writable: Attachments directory writable |
|
1062 | 1064 | text_plugin_assets_writable: Plugin assets directory writable |
|
1063 | 1065 | text_rmagick_available: RMagick available (optional) |
|
1064 | 1066 | text_convert_available: ImageMagick convert available (optional) |
|
1065 | 1067 | text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?" |
|
1066 | 1068 | text_destroy_time_entries: Delete reported hours |
|
1067 | 1069 | text_assign_time_entries_to_project: Assign reported hours to the project |
|
1068 | 1070 | text_reassign_time_entries: 'Reassign reported hours to this issue:' |
|
1069 | 1071 | text_user_wrote: "%{value} wrote:" |
|
1070 | 1072 | text_enumeration_destroy_question: "%{count} objects are assigned to this value." |
|
1071 | 1073 | text_enumeration_category_reassign_to: 'Reassign them to this value:' |
|
1072 | 1074 | text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them." |
|
1073 | 1075 | text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped." |
|
1074 | 1076 | text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.' |
|
1075 | 1077 | text_custom_field_possible_values_info: 'One line for each value' |
|
1076 | 1078 | text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?" |
|
1077 | 1079 | text_wiki_page_nullify_children: "Keep child pages as root pages" |
|
1078 | 1080 | text_wiki_page_destroy_children: "Delete child pages and all their descendants" |
|
1079 | 1081 | text_wiki_page_reassign_children: "Reassign child pages to this parent page" |
|
1080 | 1082 | text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?" |
|
1081 | 1083 | text_zoom_in: Zoom in |
|
1082 | 1084 | text_zoom_out: Zoom out |
|
1083 | 1085 | text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page." |
|
1084 | 1086 | text_scm_path_encoding_note: "Default: UTF-8" |
|
1085 | 1087 | text_subversion_repository_note: "Examples: file:///, http://, https://, svn://, svn+[tunnelscheme]://" |
|
1086 | 1088 | text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) |
|
1087 | 1089 | text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) |
|
1088 | 1090 | text_scm_command: Command |
|
1089 | 1091 | text_scm_command_version: Version |
|
1090 | 1092 | text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. |
|
1091 | 1093 | text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. |
|
1092 | 1094 | text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)" |
|
1093 | 1095 | text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes" |
|
1094 | 1096 | text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}" |
|
1095 | 1097 | text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it." |
|
1096 | 1098 | text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." |
|
1097 | 1099 | text_project_closed: This project is closed and read-only. |
|
1098 | 1100 | text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item." |
|
1099 | 1101 | |
|
1100 | 1102 | default_role_manager: Manager |
|
1101 | 1103 | default_role_developer: Developer |
|
1102 | 1104 | default_role_reporter: Reporter |
|
1103 | 1105 | default_tracker_bug: Bug |
|
1104 | 1106 | default_tracker_feature: Feature |
|
1105 | 1107 | default_tracker_support: Support |
|
1106 | 1108 | default_issue_status_new: New |
|
1107 | 1109 | default_issue_status_in_progress: In Progress |
|
1108 | 1110 | default_issue_status_resolved: Resolved |
|
1109 | 1111 | default_issue_status_feedback: Feedback |
|
1110 | 1112 | default_issue_status_closed: Closed |
|
1111 | 1113 | default_issue_status_rejected: Rejected |
|
1112 | 1114 | default_doc_category_user: User documentation |
|
1113 | 1115 | default_doc_category_tech: Technical documentation |
|
1114 | 1116 | default_priority_low: Low |
|
1115 | 1117 | default_priority_normal: Normal |
|
1116 | 1118 | default_priority_high: High |
|
1117 | 1119 | default_priority_urgent: Urgent |
|
1118 | 1120 | default_priority_immediate: Immediate |
|
1119 | 1121 | default_activity_design: Design |
|
1120 | 1122 | default_activity_development: Development |
|
1121 | 1123 | |
|
1122 | 1124 | enumeration_issue_priorities: Issue priorities |
|
1123 | 1125 | enumeration_doc_categories: Document categories |
|
1124 | 1126 | enumeration_activities: Activities (time tracking) |
|
1125 | 1127 | enumeration_system_activity: System Activity |
|
1126 | 1128 | description_filter: Filter |
|
1127 | 1129 | description_search: Searchfield |
|
1128 | 1130 | description_choose_project: Projects |
|
1129 | 1131 | description_project_scope: Search scope |
|
1130 | 1132 | description_notes: Notes |
|
1131 | 1133 | description_message_content: Message content |
|
1132 | 1134 | description_query_sort_criteria_attribute: Sort attribute |
|
1133 | 1135 | description_query_sort_criteria_direction: Sort direction |
|
1134 | 1136 | description_user_mail_notification: Mail notification settings |
|
1135 | 1137 | description_available_columns: Available Columns |
|
1136 | 1138 | description_selected_columns: Selected Columns |
|
1137 | 1139 | description_all_columns: All Columns |
|
1138 | 1140 | description_issue_category_reassign: Choose issue category |
|
1139 | 1141 | description_wiki_subpages_reassign: Choose new parent page |
|
1140 | 1142 | description_date_range_list: Choose range from list |
|
1141 | 1143 | description_date_range_interval: Choose range by selecting start and end date |
|
1142 | 1144 | description_date_from: Enter start date |
|
1143 | 1145 | description_date_to: Enter end date |
|
1144 | 1146 | text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.' |
@@ -1,365 +1,366 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | Rails.application.routes.draw do |
|
19 | 19 | root :to => 'welcome#index', :as => 'home' |
|
20 | 20 | |
|
21 | 21 | match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post] |
|
22 | 22 | match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post] |
|
23 | 23 | match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register' |
|
24 | 24 | match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password' |
|
25 | 25 | match 'account/activate', :to => 'account#activate', :via => :get |
|
26 | 26 | get 'account/activation_email', :to => 'account#activation_email', :as => 'activation_email' |
|
27 | 27 | |
|
28 | 28 | match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put, :patch] |
|
29 | 29 | match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put, :patch] |
|
30 | 30 | match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put, :patch] |
|
31 | 31 | match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put, :patch] |
|
32 | 32 | |
|
33 | 33 | match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post |
|
34 | 34 | match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post] |
|
35 | 35 | |
|
36 | 36 | match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message' |
|
37 | 37 | get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message' |
|
38 | 38 | match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post] |
|
39 | 39 | get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit' |
|
40 | 40 | |
|
41 | 41 | post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message' |
|
42 | 42 | post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply' |
|
43 | 43 | post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit' |
|
44 | 44 | post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy' |
|
45 | 45 | |
|
46 | 46 | # Misc issue routes. TODO: move into resources |
|
47 | 47 | match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues' |
|
48 | 48 | match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post] |
|
49 | 49 | match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get |
|
50 | 50 | match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue' |
|
51 | 51 | |
|
52 | 52 | match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get |
|
53 | 53 | match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post] |
|
54 | 54 | |
|
55 | 55 | get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt' |
|
56 | 56 | get '/issues/gantt', :to => 'gantts#show' |
|
57 | 57 | |
|
58 | 58 | get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar' |
|
59 | 59 | get '/issues/calendar', :to => 'calendars#show' |
|
60 | 60 | |
|
61 | 61 | get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report' |
|
62 | 62 | get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details' |
|
63 | 63 | |
|
64 | 64 | match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post] |
|
65 | 65 | match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post] |
|
66 | 66 | match 'my/page', :controller => 'my', :action => 'page', :via => :get |
|
67 | 67 | match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page |
|
68 | 68 | match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post |
|
69 | 69 | match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post |
|
70 | match 'my/show_api_key', :controller => 'my', :action => 'show_api_key', :via => :get | |
|
70 | 71 | match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post] |
|
71 | 72 | match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get |
|
72 | 73 | match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post |
|
73 | 74 | match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post |
|
74 | 75 | match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post |
|
75 | 76 | |
|
76 | 77 | resources :users do |
|
77 | 78 | resources :memberships, :controller => 'principal_memberships' |
|
78 | 79 | resources :email_addresses, :only => [:index, :create, :update, :destroy] |
|
79 | 80 | end |
|
80 | 81 | |
|
81 | 82 | post 'watchers/watch', :to => 'watchers#watch', :as => 'watch' |
|
82 | 83 | delete 'watchers/watch', :to => 'watchers#unwatch' |
|
83 | 84 | get 'watchers/new', :to => 'watchers#new' |
|
84 | 85 | post 'watchers', :to => 'watchers#create' |
|
85 | 86 | post 'watchers/append', :to => 'watchers#append' |
|
86 | 87 | delete 'watchers', :to => 'watchers#destroy' |
|
87 | 88 | get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user' |
|
88 | 89 | # Specific routes for issue watchers API |
|
89 | 90 | post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue' |
|
90 | 91 | delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue' |
|
91 | 92 | |
|
92 | 93 | resources :projects do |
|
93 | 94 | member do |
|
94 | 95 | get 'settings(/:tab)', :action => 'settings', :as => 'settings' |
|
95 | 96 | post 'modules' |
|
96 | 97 | post 'archive' |
|
97 | 98 | post 'unarchive' |
|
98 | 99 | post 'close' |
|
99 | 100 | post 'reopen' |
|
100 | 101 | match 'copy', :via => [:get, :post] |
|
101 | 102 | end |
|
102 | 103 | |
|
103 | 104 | shallow do |
|
104 | 105 | resources :memberships, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do |
|
105 | 106 | collection do |
|
106 | 107 | get 'autocomplete' |
|
107 | 108 | end |
|
108 | 109 | end |
|
109 | 110 | end |
|
110 | 111 | |
|
111 | 112 | resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy] |
|
112 | 113 | |
|
113 | 114 | get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue' |
|
114 | 115 | resources :issues, :only => [:index, :new, :create] |
|
115 | 116 | # Used when updating the form of a new issue |
|
116 | 117 | post 'issues/new', :to => 'issues#new' |
|
117 | 118 | |
|
118 | 119 | resources :files, :only => [:index, :new, :create] |
|
119 | 120 | |
|
120 | 121 | resources :versions, :except => [:index, :show, :edit, :update, :destroy] do |
|
121 | 122 | collection do |
|
122 | 123 | put 'close_completed' |
|
123 | 124 | end |
|
124 | 125 | end |
|
125 | 126 | get 'versions.:format', :to => 'versions#index' |
|
126 | 127 | get 'roadmap', :to => 'versions#index', :format => false |
|
127 | 128 | get 'versions', :to => 'versions#index' |
|
128 | 129 | |
|
129 | 130 | resources :news, :except => [:show, :edit, :update, :destroy] |
|
130 | 131 | resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do |
|
131 | 132 | get 'report', :on => :collection |
|
132 | 133 | end |
|
133 | 134 | resources :queries, :only => [:new, :create] |
|
134 | 135 | shallow do |
|
135 | 136 | resources :issue_categories |
|
136 | 137 | end |
|
137 | 138 | resources :documents, :except => [:show, :edit, :update, :destroy] |
|
138 | 139 | resources :boards |
|
139 | 140 | shallow do |
|
140 | 141 | resources :repositories, :except => [:index, :show] do |
|
141 | 142 | member do |
|
142 | 143 | match 'committers', :via => [:get, :post] |
|
143 | 144 | end |
|
144 | 145 | end |
|
145 | 146 | end |
|
146 | 147 | |
|
147 | 148 | match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get |
|
148 | 149 | resources :wiki, :except => [:index, :new, :create], :as => 'wiki_page' do |
|
149 | 150 | member do |
|
150 | 151 | get 'rename' |
|
151 | 152 | post 'rename' |
|
152 | 153 | get 'history' |
|
153 | 154 | get 'diff' |
|
154 | 155 | match 'preview', :via => [:post, :put, :patch] |
|
155 | 156 | post 'protect' |
|
156 | 157 | post 'add_attachment' |
|
157 | 158 | end |
|
158 | 159 | collection do |
|
159 | 160 | get 'export' |
|
160 | 161 | get 'date_index' |
|
161 | 162 | end |
|
162 | 163 | end |
|
163 | 164 | match 'wiki', :controller => 'wiki', :action => 'show', :via => :get |
|
164 | 165 | get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/} |
|
165 | 166 | delete 'wiki/:id/:version', :to => 'wiki#destroy_version' |
|
166 | 167 | get 'wiki/:id/:version/annotate', :to => 'wiki#annotate' |
|
167 | 168 | get 'wiki/:id/:version/diff', :to => 'wiki#diff' |
|
168 | 169 | end |
|
169 | 170 | |
|
170 | 171 | resources :issues do |
|
171 | 172 | member do |
|
172 | 173 | # Used when updating the form of an existing issue |
|
173 | 174 | patch 'edit', :to => 'issues#edit' |
|
174 | 175 | end |
|
175 | 176 | collection do |
|
176 | 177 | match 'bulk_edit', :via => [:get, :post] |
|
177 | 178 | post 'bulk_update' |
|
178 | 179 | end |
|
179 | 180 | resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do |
|
180 | 181 | collection do |
|
181 | 182 | get 'report' |
|
182 | 183 | end |
|
183 | 184 | end |
|
184 | 185 | shallow do |
|
185 | 186 | resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy] |
|
186 | 187 | end |
|
187 | 188 | end |
|
188 | 189 | # Used when updating the form of a new issue outside a project |
|
189 | 190 | post '/issues/new', :to => 'issues#new' |
|
190 | 191 | match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete |
|
191 | 192 | |
|
192 | 193 | resources :queries, :except => [:show] |
|
193 | 194 | |
|
194 | 195 | resources :news, :only => [:index, :show, :edit, :update, :destroy] |
|
195 | 196 | match '/news/:id/comments', :to => 'comments#create', :via => :post |
|
196 | 197 | match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete |
|
197 | 198 | |
|
198 | 199 | resources :versions, :only => [:show, :edit, :update, :destroy] do |
|
199 | 200 | post 'status_by', :on => :member |
|
200 | 201 | end |
|
201 | 202 | |
|
202 | 203 | resources :documents, :only => [:show, :edit, :update, :destroy] do |
|
203 | 204 | post 'add_attachment', :on => :member |
|
204 | 205 | end |
|
205 | 206 | |
|
206 | 207 | match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post] |
|
207 | 208 | |
|
208 | 209 | resources :time_entries, :controller => 'timelog', :except => :destroy do |
|
209 | 210 | collection do |
|
210 | 211 | get 'report' |
|
211 | 212 | get 'bulk_edit' |
|
212 | 213 | post 'bulk_update' |
|
213 | 214 | end |
|
214 | 215 | end |
|
215 | 216 | match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/ |
|
216 | 217 | # TODO: delete /time_entries for bulk deletion |
|
217 | 218 | match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete |
|
218 | 219 | # Used to update the new time entry form |
|
219 | 220 | post '/time_entries/new', :to => 'timelog#new' |
|
220 | 221 | |
|
221 | 222 | get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity |
|
222 | 223 | get 'activity', :to => 'activities#index' |
|
223 | 224 | |
|
224 | 225 | # repositories routes |
|
225 | 226 | get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats' |
|
226 | 227 | get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph' |
|
227 | 228 | |
|
228 | 229 | get 'projects/:id/repository/:repository_id/changes(/*path)', |
|
229 | 230 | :to => 'repositories#changes', |
|
230 | 231 | :format => false |
|
231 | 232 | |
|
232 | 233 | get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision' |
|
233 | 234 | get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision' |
|
234 | 235 | post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue' |
|
235 | 236 | delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue' |
|
236 | 237 | get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions' |
|
237 | 238 | get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path)', |
|
238 | 239 | :controller => 'repositories', |
|
239 | 240 | :format => false, |
|
240 | 241 | :constraints => { |
|
241 | 242 | :action => /(browse|show|entry|raw|annotate|diff)/, |
|
242 | 243 | :rev => /[a-z0-9\.\-_]+/ |
|
243 | 244 | } |
|
244 | 245 | |
|
245 | 246 | get 'projects/:id/repository/statistics', :to => 'repositories#stats' |
|
246 | 247 | get 'projects/:id/repository/graph', :to => 'repositories#graph' |
|
247 | 248 | |
|
248 | 249 | get 'projects/:id/repository/changes(/*path)', |
|
249 | 250 | :to => 'repositories#changes', |
|
250 | 251 | :format => false |
|
251 | 252 | |
|
252 | 253 | get 'projects/:id/repository/revisions', :to => 'repositories#revisions' |
|
253 | 254 | get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision' |
|
254 | 255 | get 'projects/:id/repository/revision', :to => 'repositories#revision' |
|
255 | 256 | post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue' |
|
256 | 257 | delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue' |
|
257 | 258 | get 'projects/:id/repository/revisions/:rev/:action(/*path)', |
|
258 | 259 | :controller => 'repositories', |
|
259 | 260 | :format => false, |
|
260 | 261 | :constraints => { |
|
261 | 262 | :action => /(browse|show|entry|raw|annotate|diff)/, |
|
262 | 263 | :rev => /[a-z0-9\.\-_]+/ |
|
263 | 264 | } |
|
264 | 265 | get 'projects/:id/repository/:repository_id/:action(/*path)', |
|
265 | 266 | :controller => 'repositories', |
|
266 | 267 | :action => /(browse|show|entry|raw|changes|annotate|diff)/, |
|
267 | 268 | :format => false |
|
268 | 269 | get 'projects/:id/repository/:action(/*path)', |
|
269 | 270 | :controller => 'repositories', |
|
270 | 271 | :action => /(browse|show|entry|raw|changes|annotate|diff)/, |
|
271 | 272 | :format => false |
|
272 | 273 | |
|
273 | 274 | get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil |
|
274 | 275 | get 'projects/:id/repository', :to => 'repositories#show', :path => nil |
|
275 | 276 | |
|
276 | 277 | # additional routes for having the file name at the end of url |
|
277 | 278 | get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment' |
|
278 | 279 | get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment' |
|
279 | 280 | get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/ |
|
280 | 281 | get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail' |
|
281 | 282 | resources :attachments, :only => [:show, :destroy] |
|
282 | 283 | get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit', :as => :object_attachments_edit |
|
283 | 284 | patch 'attachments/:object_type/:object_id', :to => 'attachments#update', :as => :object_attachments |
|
284 | 285 | |
|
285 | 286 | resources :groups do |
|
286 | 287 | resources :memberships, :controller => 'principal_memberships' |
|
287 | 288 | member do |
|
288 | 289 | get 'autocomplete_for_user' |
|
289 | 290 | end |
|
290 | 291 | end |
|
291 | 292 | |
|
292 | 293 | get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users' |
|
293 | 294 | post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users' |
|
294 | 295 | delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user' |
|
295 | 296 | |
|
296 | 297 | resources :trackers, :except => :show do |
|
297 | 298 | collection do |
|
298 | 299 | match 'fields', :via => [:get, :post] |
|
299 | 300 | end |
|
300 | 301 | end |
|
301 | 302 | resources :issue_statuses, :except => :show do |
|
302 | 303 | collection do |
|
303 | 304 | post 'update_issue_done_ratio' |
|
304 | 305 | end |
|
305 | 306 | end |
|
306 | 307 | resources :custom_fields, :except => :show |
|
307 | 308 | resources :roles do |
|
308 | 309 | collection do |
|
309 | 310 | match 'permissions', :via => [:get, :post] |
|
310 | 311 | end |
|
311 | 312 | end |
|
312 | 313 | resources :enumerations, :except => :show |
|
313 | 314 | match 'enumerations/:type', :to => 'enumerations#index', :via => :get |
|
314 | 315 | |
|
315 | 316 | get 'projects/:id/search', :controller => 'search', :action => 'index' |
|
316 | 317 | get 'search', :controller => 'search', :action => 'index' |
|
317 | 318 | |
|
318 | 319 | |
|
319 | 320 | get 'mail_handler', :to => 'mail_handler#new' |
|
320 | 321 | post 'mail_handler', :to => 'mail_handler#index' |
|
321 | 322 | |
|
322 | 323 | match 'admin', :controller => 'admin', :action => 'index', :via => :get |
|
323 | 324 | match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get |
|
324 | 325 | match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get |
|
325 | 326 | match 'admin/info', :controller => 'admin', :action => 'info', :via => :get |
|
326 | 327 | match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get |
|
327 | 328 | match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post |
|
328 | 329 | |
|
329 | 330 | resources :auth_sources do |
|
330 | 331 | member do |
|
331 | 332 | get 'test_connection', :as => 'try_connection' |
|
332 | 333 | end |
|
333 | 334 | collection do |
|
334 | 335 | get 'autocomplete_for_new_user' |
|
335 | 336 | end |
|
336 | 337 | end |
|
337 | 338 | |
|
338 | 339 | match 'workflows', :controller => 'workflows', :action => 'index', :via => :get |
|
339 | 340 | match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post] |
|
340 | 341 | match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post] |
|
341 | 342 | match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post] |
|
342 | 343 | match 'settings', :controller => 'settings', :action => 'index', :via => :get |
|
343 | 344 | match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post] |
|
344 | 345 | match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings' |
|
345 | 346 | |
|
346 | 347 | match 'sys/projects', :to => 'sys#projects', :via => :get |
|
347 | 348 | match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post |
|
348 | 349 | match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post] |
|
349 | 350 | |
|
350 | 351 | match 'uploads', :to => 'attachments#upload', :via => :post |
|
351 | 352 | |
|
352 | 353 | get 'robots.txt', :to => 'welcome#robots' |
|
353 | 354 | |
|
354 | 355 | Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir| |
|
355 | 356 | file = File.join(plugin_dir, "config/routes.rb") |
|
356 | 357 | if File.exists?(file) |
|
357 | 358 | begin |
|
358 | 359 | instance_eval File.read(file) |
|
359 | 360 | rescue Exception => e |
|
360 | 361 | puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}." |
|
361 | 362 | exit 1 |
|
362 | 363 | end |
|
363 | 364 | end |
|
364 | 365 | end |
|
365 | 366 | end |
@@ -1,173 +1,174 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class AuthSourcesControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :users, :auth_sources |
|
22 | 22 | |
|
23 | 23 | def setup |
|
24 | 24 | @request.session[:user_id] = 1 |
|
25 | Redmine::SudoMode.disable! | |
|
25 | 26 | end |
|
26 | 27 | |
|
27 | 28 | def test_index |
|
28 | 29 | get :index |
|
29 | 30 | |
|
30 | 31 | assert_response :success |
|
31 | 32 | assert_template 'index' |
|
32 | 33 | assert_not_nil assigns(:auth_sources) |
|
33 | 34 | end |
|
34 | 35 | |
|
35 | 36 | def test_new |
|
36 | 37 | get :new |
|
37 | 38 | |
|
38 | 39 | assert_response :success |
|
39 | 40 | assert_template 'new' |
|
40 | 41 | |
|
41 | 42 | source = assigns(:auth_source) |
|
42 | 43 | assert_equal AuthSourceLdap, source.class |
|
43 | 44 | assert source.new_record? |
|
44 | 45 | |
|
45 | 46 | assert_select 'form#auth_source_form' do |
|
46 | 47 | assert_select 'input[name=type][value=AuthSourceLdap]' |
|
47 | 48 | assert_select 'input[name=?]', 'auth_source[host]' |
|
48 | 49 | end |
|
49 | 50 | end |
|
50 | 51 | |
|
51 | 52 | def test_new_with_invalid_type_should_respond_with_404 |
|
52 | 53 | get :new, :type => 'foo' |
|
53 | 54 | assert_response 404 |
|
54 | 55 | end |
|
55 | 56 | |
|
56 | 57 | def test_create |
|
57 | 58 | assert_difference 'AuthSourceLdap.count' do |
|
58 | 59 | post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '127.0.0.1', :port => '389', :attr_login => 'cn'} |
|
59 | 60 | assert_redirected_to '/auth_sources' |
|
60 | 61 | end |
|
61 | 62 | |
|
62 | 63 | source = AuthSourceLdap.order('id DESC').first |
|
63 | 64 | assert_equal 'Test', source.name |
|
64 | 65 | assert_equal '127.0.0.1', source.host |
|
65 | 66 | assert_equal 389, source.port |
|
66 | 67 | assert_equal 'cn', source.attr_login |
|
67 | 68 | end |
|
68 | 69 | |
|
69 | 70 | def test_create_with_failure |
|
70 | 71 | assert_no_difference 'AuthSourceLdap.count' do |
|
71 | 72 | post :create, :type => 'AuthSourceLdap', |
|
72 | 73 | :auth_source => {:name => 'Test', :host => '', |
|
73 | 74 | :port => '389', :attr_login => 'cn'} |
|
74 | 75 | assert_response :success |
|
75 | 76 | assert_template 'new' |
|
76 | 77 | end |
|
77 | 78 | assert_select_error /host cannot be blank/i |
|
78 | 79 | end |
|
79 | 80 | |
|
80 | 81 | def test_edit |
|
81 | 82 | get :edit, :id => 1 |
|
82 | 83 | |
|
83 | 84 | assert_response :success |
|
84 | 85 | assert_template 'edit' |
|
85 | 86 | |
|
86 | 87 | assert_select 'form#auth_source_form' do |
|
87 | 88 | assert_select 'input[name=?]', 'auth_source[host]' |
|
88 | 89 | end |
|
89 | 90 | end |
|
90 | 91 | |
|
91 | 92 | def test_edit_should_not_contain_password |
|
92 | 93 | AuthSource.find(1).update_column :account_password, 'secret' |
|
93 | 94 | |
|
94 | 95 | get :edit, :id => 1 |
|
95 | 96 | assert_response :success |
|
96 | 97 | assert_select 'input[value=secret]', 0 |
|
97 | 98 | assert_select 'input[name=dummy_password][value^=xxxxxx]' |
|
98 | 99 | end |
|
99 | 100 | |
|
100 | 101 | def test_edit_invalid_should_respond_with_404 |
|
101 | 102 | get :edit, :id => 99 |
|
102 | 103 | assert_response 404 |
|
103 | 104 | end |
|
104 | 105 | |
|
105 | 106 | def test_update |
|
106 | 107 | put :update, :id => 1, |
|
107 | 108 | :auth_source => {:name => 'Renamed', :host => '192.168.0.10', |
|
108 | 109 | :port => '389', :attr_login => 'uid'} |
|
109 | 110 | assert_redirected_to '/auth_sources' |
|
110 | 111 | source = AuthSourceLdap.find(1) |
|
111 | 112 | assert_equal 'Renamed', source.name |
|
112 | 113 | assert_equal '192.168.0.10', source.host |
|
113 | 114 | end |
|
114 | 115 | |
|
115 | 116 | def test_update_with_failure |
|
116 | 117 | put :update, :id => 1, |
|
117 | 118 | :auth_source => {:name => 'Renamed', :host => '', |
|
118 | 119 | :port => '389', :attr_login => 'uid'} |
|
119 | 120 | assert_response :success |
|
120 | 121 | assert_template 'edit' |
|
121 | 122 | assert_select_error /host cannot be blank/i |
|
122 | 123 | end |
|
123 | 124 | |
|
124 | 125 | def test_destroy |
|
125 | 126 | assert_difference 'AuthSourceLdap.count', -1 do |
|
126 | 127 | delete :destroy, :id => 1 |
|
127 | 128 | assert_redirected_to '/auth_sources' |
|
128 | 129 | end |
|
129 | 130 | end |
|
130 | 131 | |
|
131 | 132 | def test_destroy_auth_source_in_use |
|
132 | 133 | User.find(2).update_attribute :auth_source_id, 1 |
|
133 | 134 | |
|
134 | 135 | assert_no_difference 'AuthSourceLdap.count' do |
|
135 | 136 | delete :destroy, :id => 1 |
|
136 | 137 | assert_redirected_to '/auth_sources' |
|
137 | 138 | end |
|
138 | 139 | end |
|
139 | 140 | |
|
140 | 141 | def test_test_connection |
|
141 | 142 | AuthSourceLdap.any_instance.stubs(:test_connection).returns(true) |
|
142 | 143 | |
|
143 | 144 | get :test_connection, :id => 1 |
|
144 | 145 | assert_redirected_to '/auth_sources' |
|
145 | 146 | assert_not_nil flash[:notice] |
|
146 | 147 | assert_match /successful/i, flash[:notice] |
|
147 | 148 | end |
|
148 | 149 | |
|
149 | 150 | def test_test_connection_with_failure |
|
150 | 151 | AuthSourceLdap.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError.new("Something went wrong")) |
|
151 | 152 | |
|
152 | 153 | get :test_connection, :id => 1 |
|
153 | 154 | assert_redirected_to '/auth_sources' |
|
154 | 155 | assert_not_nil flash[:error] |
|
155 | 156 | assert_include 'Something went wrong', flash[:error] |
|
156 | 157 | end |
|
157 | 158 | |
|
158 | 159 | def test_autocomplete_for_new_user |
|
159 | 160 | AuthSource.expects(:search).with('foo').returns([ |
|
160 | 161 | {:login => 'foo1', :firstname => 'John', :lastname => 'Smith', :mail => 'foo1@example.net', :auth_source_id => 1}, |
|
161 | 162 | {:login => 'Smith', :firstname => 'John', :lastname => 'Doe', :mail => 'foo2@example.net', :auth_source_id => 1} |
|
162 | 163 | ]) |
|
163 | 164 | |
|
164 | 165 | get :autocomplete_for_new_user, :term => 'foo' |
|
165 | 166 | assert_response :success |
|
166 | 167 | assert_equal 'application/json', response.content_type |
|
167 | 168 | json = ActiveSupport::JSON.decode(response.body) |
|
168 | 169 | assert_kind_of Array, json |
|
169 | 170 | assert_equal 2, json.size |
|
170 | 171 | assert_equal 'foo1', json.first['value'] |
|
171 | 172 | assert_equal 'foo1 (John Smith)', json.first['label'] |
|
172 | 173 | end |
|
173 | 174 | end |
@@ -1,144 +1,145 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class EmailAddressesControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :users, :email_addresses |
|
22 | 22 | |
|
23 | 23 | def setup |
|
24 | 24 | User.current = nil |
|
25 | Redmine::SudoMode.disable! | |
|
25 | 26 | end |
|
26 | 27 | |
|
27 | 28 | def test_index_with_no_additional_emails |
|
28 | 29 | @request.session[:user_id] = 2 |
|
29 | 30 | get :index, :user_id => 2 |
|
30 | 31 | assert_response :success |
|
31 | 32 | assert_template 'index' |
|
32 | 33 | end |
|
33 | 34 | |
|
34 | 35 | def test_index_with_additional_emails |
|
35 | 36 | @request.session[:user_id] = 2 |
|
36 | 37 | EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
37 | 38 | |
|
38 | 39 | get :index, :user_id => 2 |
|
39 | 40 | assert_response :success |
|
40 | 41 | assert_template 'index' |
|
41 | 42 | assert_select '.email', :text => 'another@somenet.foo' |
|
42 | 43 | end |
|
43 | 44 | |
|
44 | 45 | def test_index_with_additional_emails_as_js |
|
45 | 46 | @request.session[:user_id] = 2 |
|
46 | 47 | EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
47 | 48 | |
|
48 | 49 | xhr :get, :index, :user_id => 2 |
|
49 | 50 | assert_response :success |
|
50 | 51 | assert_template 'index' |
|
51 | 52 | assert_include 'another@somenet.foo', response.body |
|
52 | 53 | end |
|
53 | 54 | |
|
54 | 55 | def test_index_by_admin_should_be_allowed |
|
55 | 56 | @request.session[:user_id] = 1 |
|
56 | 57 | get :index, :user_id => 2 |
|
57 | 58 | assert_response :success |
|
58 | 59 | assert_template 'index' |
|
59 | 60 | end |
|
60 | 61 | |
|
61 | 62 | def test_index_by_another_user_should_be_denied |
|
62 | 63 | @request.session[:user_id] = 3 |
|
63 | 64 | get :index, :user_id => 2 |
|
64 | 65 | assert_response 403 |
|
65 | 66 | end |
|
66 | 67 | |
|
67 | 68 | def test_create |
|
68 | 69 | @request.session[:user_id] = 2 |
|
69 | 70 | assert_difference 'EmailAddress.count' do |
|
70 | 71 | post :create, :user_id => 2, :email_address => {:address => 'another@somenet.foo'} |
|
71 | 72 | assert_response 302 |
|
72 | 73 | assert_redirected_to '/users/2/email_addresses' |
|
73 | 74 | end |
|
74 | 75 | email = EmailAddress.order('id DESC').first |
|
75 | 76 | assert_equal 2, email.user_id |
|
76 | 77 | assert_equal 'another@somenet.foo', email.address |
|
77 | 78 | end |
|
78 | 79 | |
|
79 | 80 | def test_create_as_js |
|
80 | 81 | @request.session[:user_id] = 2 |
|
81 | 82 | assert_difference 'EmailAddress.count' do |
|
82 | 83 | xhr :post, :create, :user_id => 2, :email_address => {:address => 'another@somenet.foo'} |
|
83 | 84 | assert_response 200 |
|
84 | 85 | end |
|
85 | 86 | end |
|
86 | 87 | |
|
87 | 88 | def test_create_with_failure |
|
88 | 89 | @request.session[:user_id] = 2 |
|
89 | 90 | assert_no_difference 'EmailAddress.count' do |
|
90 | 91 | post :create, :user_id => 2, :email_address => {:address => 'invalid'} |
|
91 | 92 | assert_response 200 |
|
92 | 93 | end |
|
93 | 94 | end |
|
94 | 95 | |
|
95 | 96 | def test_update |
|
96 | 97 | @request.session[:user_id] = 2 |
|
97 | 98 | email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
98 | 99 | |
|
99 | 100 | put :update, :user_id => 2, :id => email.id, :notify => '0' |
|
100 | 101 | assert_response 302 |
|
101 | 102 | |
|
102 | 103 | assert_equal false, email.reload.notify |
|
103 | 104 | end |
|
104 | 105 | |
|
105 | 106 | def test_update_as_js |
|
106 | 107 | @request.session[:user_id] = 2 |
|
107 | 108 | email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
108 | 109 | |
|
109 | 110 | xhr :put, :update, :user_id => 2, :id => email.id, :notify => '0' |
|
110 | 111 | assert_response 200 |
|
111 | 112 | |
|
112 | 113 | assert_equal false, email.reload.notify |
|
113 | 114 | end |
|
114 | 115 | |
|
115 | 116 | def test_destroy |
|
116 | 117 | @request.session[:user_id] = 2 |
|
117 | 118 | email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
118 | 119 | |
|
119 | 120 | assert_difference 'EmailAddress.count', -1 do |
|
120 | 121 | delete :destroy, :user_id => 2, :id => email.id |
|
121 | 122 | assert_response 302 |
|
122 | 123 | assert_redirected_to '/users/2/email_addresses' |
|
123 | 124 | end |
|
124 | 125 | end |
|
125 | 126 | |
|
126 | 127 | def test_destroy_as_js |
|
127 | 128 | @request.session[:user_id] = 2 |
|
128 | 129 | email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
129 | 130 | |
|
130 | 131 | assert_difference 'EmailAddress.count', -1 do |
|
131 | 132 | xhr :delete, :destroy, :user_id => 2, :id => email.id |
|
132 | 133 | assert_response 200 |
|
133 | 134 | end |
|
134 | 135 | end |
|
135 | 136 | |
|
136 | 137 | def test_should_not_destroy_default |
|
137 | 138 | @request.session[:user_id] = 2 |
|
138 | 139 | |
|
139 | 140 | assert_no_difference 'EmailAddress.count' do |
|
140 | 141 | delete :destroy, :user_id => 2, :id => User.find(2).email_address.id |
|
141 | 142 | assert_response 404 |
|
142 | 143 | end |
|
143 | 144 | end |
|
144 | 145 | end |
@@ -1,164 +1,165 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class GroupsControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :projects, :users, :members, :member_roles, :roles, :groups_users |
|
22 | 22 | |
|
23 | 23 | def setup |
|
24 | 24 | @request.session[:user_id] = 1 |
|
25 | Redmine::SudoMode.disable! | |
|
25 | 26 | end |
|
26 | 27 | |
|
27 | 28 | def test_index |
|
28 | 29 | get :index |
|
29 | 30 | assert_response :success |
|
30 | 31 | assert_template 'index' |
|
31 | 32 | end |
|
32 | 33 | |
|
33 | 34 | def test_index_should_show_user_count |
|
34 | 35 | get :index |
|
35 | 36 | assert_response :success |
|
36 | 37 | assert_select 'tr#group-11 td.user_count', :text => '1' |
|
37 | 38 | end |
|
38 | 39 | |
|
39 | 40 | def test_show |
|
40 | 41 | get :show, :id => 10 |
|
41 | 42 | assert_response :success |
|
42 | 43 | assert_template 'show' |
|
43 | 44 | end |
|
44 | 45 | |
|
45 | 46 | def test_show_invalid_should_return_404 |
|
46 | 47 | get :show, :id => 99 |
|
47 | 48 | assert_response 404 |
|
48 | 49 | end |
|
49 | 50 | |
|
50 | 51 | def test_new |
|
51 | 52 | get :new |
|
52 | 53 | assert_response :success |
|
53 | 54 | assert_template 'new' |
|
54 | 55 | assert_select 'input[name=?]', 'group[name]' |
|
55 | 56 | end |
|
56 | 57 | |
|
57 | 58 | def test_create |
|
58 | 59 | assert_difference 'Group.count' do |
|
59 | 60 | post :create, :group => {:name => 'New group'} |
|
60 | 61 | end |
|
61 | 62 | assert_redirected_to '/groups' |
|
62 | 63 | group = Group.order('id DESC').first |
|
63 | 64 | assert_equal 'New group', group.name |
|
64 | 65 | assert_equal [], group.users |
|
65 | 66 | end |
|
66 | 67 | |
|
67 | 68 | def test_create_and_continue |
|
68 | 69 | assert_difference 'Group.count' do |
|
69 | 70 | post :create, :group => {:name => 'New group'}, :continue => 'Create and continue' |
|
70 | 71 | end |
|
71 | 72 | assert_redirected_to '/groups/new' |
|
72 | 73 | group = Group.order('id DESC').first |
|
73 | 74 | assert_equal 'New group', group.name |
|
74 | 75 | end |
|
75 | 76 | |
|
76 | 77 | def test_create_with_failure |
|
77 | 78 | assert_no_difference 'Group.count' do |
|
78 | 79 | post :create, :group => {:name => ''} |
|
79 | 80 | end |
|
80 | 81 | assert_response :success |
|
81 | 82 | assert_template 'new' |
|
82 | 83 | end |
|
83 | 84 | |
|
84 | 85 | def test_edit |
|
85 | 86 | get :edit, :id => 10 |
|
86 | 87 | assert_response :success |
|
87 | 88 | assert_template 'edit' |
|
88 | 89 | |
|
89 | 90 | assert_select 'div#tab-content-users' |
|
90 | 91 | assert_select 'div#tab-content-memberships' do |
|
91 | 92 | assert_select 'a', :text => 'Private child of eCookbook' |
|
92 | 93 | end |
|
93 | 94 | end |
|
94 | 95 | |
|
95 | 96 | def test_update |
|
96 | 97 | new_name = 'New name' |
|
97 | 98 | put :update, :id => 10, :group => {:name => new_name} |
|
98 | 99 | assert_redirected_to '/groups' |
|
99 | 100 | group = Group.find(10) |
|
100 | 101 | assert_equal new_name, group.name |
|
101 | 102 | end |
|
102 | 103 | |
|
103 | 104 | def test_update_with_failure |
|
104 | 105 | put :update, :id => 10, :group => {:name => ''} |
|
105 | 106 | assert_response :success |
|
106 | 107 | assert_template 'edit' |
|
107 | 108 | end |
|
108 | 109 | |
|
109 | 110 | def test_destroy |
|
110 | 111 | assert_difference 'Group.count', -1 do |
|
111 | 112 | post :destroy, :id => 10 |
|
112 | 113 | end |
|
113 | 114 | assert_redirected_to '/groups' |
|
114 | 115 | end |
|
115 | 116 | |
|
116 | 117 | def test_new_users |
|
117 | 118 | get :new_users, :id => 10 |
|
118 | 119 | assert_response :success |
|
119 | 120 | assert_template 'new_users' |
|
120 | 121 | end |
|
121 | 122 | |
|
122 | 123 | def test_xhr_new_users |
|
123 | 124 | xhr :get, :new_users, :id => 10 |
|
124 | 125 | assert_response :success |
|
125 | 126 | assert_equal 'text/javascript', response.content_type |
|
126 | 127 | end |
|
127 | 128 | |
|
128 | 129 | def test_add_users |
|
129 | 130 | assert_difference 'Group.find(10).users.count', 2 do |
|
130 | 131 | post :add_users, :id => 10, :user_ids => ['2', '3'] |
|
131 | 132 | end |
|
132 | 133 | end |
|
133 | 134 | |
|
134 | 135 | def test_xhr_add_users |
|
135 | 136 | assert_difference 'Group.find(10).users.count', 2 do |
|
136 | 137 | xhr :post, :add_users, :id => 10, :user_ids => ['2', '3'] |
|
137 | 138 | assert_response :success |
|
138 | 139 | assert_template 'add_users' |
|
139 | 140 | assert_equal 'text/javascript', response.content_type |
|
140 | 141 | end |
|
141 | 142 | assert_match /John Smith/, response.body |
|
142 | 143 | end |
|
143 | 144 | |
|
144 | 145 | def test_remove_user |
|
145 | 146 | assert_difference 'Group.find(10).users.count', -1 do |
|
146 | 147 | delete :remove_user, :id => 10, :user_id => '8' |
|
147 | 148 | end |
|
148 | 149 | end |
|
149 | 150 | |
|
150 | 151 | def test_xhr_remove_user |
|
151 | 152 | assert_difference 'Group.find(10).users.count', -1 do |
|
152 | 153 | xhr :delete, :remove_user, :id => 10, :user_id => '8' |
|
153 | 154 | assert_response :success |
|
154 | 155 | assert_template 'remove_user' |
|
155 | 156 | assert_equal 'text/javascript', response.content_type |
|
156 | 157 | end |
|
157 | 158 | end |
|
158 | 159 | |
|
159 | 160 | def test_autocomplete_for_user |
|
160 | 161 | xhr :get, :autocomplete_for_user, :id => 10, :q => 'smi', :format => 'js' |
|
161 | 162 | assert_response :success |
|
162 | 163 | assert_include 'John Smith', response.body |
|
163 | 164 | end |
|
164 | 165 | end |
@@ -1,201 +1,202 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class MembersControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :projects, :members, :member_roles, :roles, :users |
|
22 | 22 | |
|
23 | 23 | def setup |
|
24 | 24 | User.current = nil |
|
25 | 25 | @request.session[:user_id] = 2 |
|
26 | Redmine::SudoMode.disable! | |
|
26 | 27 | end |
|
27 | 28 | |
|
28 | 29 | def test_new |
|
29 | 30 | get :new, :project_id => 1 |
|
30 | 31 | assert_response :success |
|
31 | 32 | end |
|
32 | 33 | |
|
33 | 34 | def test_new_should_propose_managed_roles_only |
|
34 | 35 | role = Role.find(1) |
|
35 | 36 | role.update! :all_roles_managed => false |
|
36 | 37 | role.managed_roles = Role.where(:id => [2, 3]).to_a |
|
37 | 38 | |
|
38 | 39 | get :new, :project_id => 1 |
|
39 | 40 | assert_response :success |
|
40 | 41 | assert_select 'div.roles-selection' do |
|
41 | 42 | assert_select 'label', :text => 'Manager', :count => 0 |
|
42 | 43 | assert_select 'label', :text => 'Developer' |
|
43 | 44 | assert_select 'label', :text => 'Reporter' |
|
44 | 45 | end |
|
45 | 46 | end |
|
46 | 47 | |
|
47 | 48 | def test_xhr_new |
|
48 | 49 | xhr :get, :new, :project_id => 1 |
|
49 | 50 | assert_response :success |
|
50 | 51 | assert_equal 'text/javascript', response.content_type |
|
51 | 52 | end |
|
52 | 53 | |
|
53 | 54 | def test_create |
|
54 | 55 | assert_difference 'Member.count' do |
|
55 | 56 | post :create, :project_id => 1, :membership => {:role_ids => [1], :user_id => 7} |
|
56 | 57 | end |
|
57 | 58 | assert_redirected_to '/projects/ecookbook/settings/members' |
|
58 | 59 | assert User.find(7).member_of?(Project.find(1)) |
|
59 | 60 | end |
|
60 | 61 | |
|
61 | 62 | def test_create_multiple |
|
62 | 63 | assert_difference 'Member.count', 3 do |
|
63 | 64 | post :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]} |
|
64 | 65 | end |
|
65 | 66 | assert_redirected_to '/projects/ecookbook/settings/members' |
|
66 | 67 | assert User.find(7).member_of?(Project.find(1)) |
|
67 | 68 | end |
|
68 | 69 | |
|
69 | 70 | def test_create_should_ignore_unmanaged_roles |
|
70 | 71 | role = Role.find(1) |
|
71 | 72 | role.update! :all_roles_managed => false |
|
72 | 73 | role.managed_roles = Role.where(:id => [2, 3]).to_a |
|
73 | 74 | |
|
74 | 75 | assert_difference 'Member.count' do |
|
75 | 76 | post :create, :project_id => 1, :membership => {:role_ids => [1, 2], :user_id => 7} |
|
76 | 77 | end |
|
77 | 78 | member = Member.order(:id => :desc).first |
|
78 | 79 | assert_equal [2], member.role_ids |
|
79 | 80 | end |
|
80 | 81 | |
|
81 | 82 | def test_create_should_be_allowed_for_admin_without_role |
|
82 | 83 | User.find(1).members.delete_all |
|
83 | 84 | @request.session[:user_id] = 1 |
|
84 | 85 | |
|
85 | 86 | assert_difference 'Member.count' do |
|
86 | 87 | post :create, :project_id => 1, :membership => {:role_ids => [1, 2], :user_id => 7} |
|
87 | 88 | end |
|
88 | 89 | member = Member.order(:id => :desc).first |
|
89 | 90 | assert_equal [1, 2], member.role_ids |
|
90 | 91 | end |
|
91 | 92 | |
|
92 | 93 | def test_xhr_create |
|
93 | 94 | assert_difference 'Member.count', 3 do |
|
94 | 95 | xhr :post, :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]} |
|
95 | 96 | assert_response :success |
|
96 | 97 | assert_template 'create' |
|
97 | 98 | assert_equal 'text/javascript', response.content_type |
|
98 | 99 | end |
|
99 | 100 | assert User.find(7).member_of?(Project.find(1)) |
|
100 | 101 | assert User.find(8).member_of?(Project.find(1)) |
|
101 | 102 | assert User.find(9).member_of?(Project.find(1)) |
|
102 | 103 | assert_include 'tab-content-members', response.body |
|
103 | 104 | end |
|
104 | 105 | |
|
105 | 106 | def test_xhr_create_with_failure |
|
106 | 107 | assert_no_difference 'Member.count' do |
|
107 | 108 | xhr :post, :create, :project_id => 1, :membership => {:role_ids => [], :user_ids => [7, 8, 9]} |
|
108 | 109 | assert_response :success |
|
109 | 110 | assert_template 'create' |
|
110 | 111 | assert_equal 'text/javascript', response.content_type |
|
111 | 112 | end |
|
112 | 113 | assert_match /alert/, response.body, "Alert message not sent" |
|
113 | 114 | end |
|
114 | 115 | |
|
115 | 116 | def test_update |
|
116 | 117 | assert_no_difference 'Member.count' do |
|
117 | 118 | put :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3} |
|
118 | 119 | end |
|
119 | 120 | assert_redirected_to '/projects/ecookbook/settings/members' |
|
120 | 121 | end |
|
121 | 122 | |
|
122 | 123 | def test_update_should_not_add_unmanaged_roles |
|
123 | 124 | role = Role.find(1) |
|
124 | 125 | role.update! :all_roles_managed => false |
|
125 | 126 | role.managed_roles = Role.where(:id => [2, 3]).to_a |
|
126 | 127 | member = Member.create!(:user => User.find(9), :role_ids => [3], :project_id => 1) |
|
127 | 128 | |
|
128 | 129 | put :update, :id => member.id, :membership => {:role_ids => [1, 2, 3]} |
|
129 | 130 | assert_equal [2, 3], member.reload.role_ids.sort |
|
130 | 131 | end |
|
131 | 132 | |
|
132 | 133 | def test_update_should_not_remove_unmanaged_roles |
|
133 | 134 | role = Role.find(1) |
|
134 | 135 | role.update! :all_roles_managed => false |
|
135 | 136 | role.managed_roles = Role.where(:id => [2, 3]).to_a |
|
136 | 137 | member = Member.create!(:user => User.find(9), :role_ids => [1, 3], :project_id => 1) |
|
137 | 138 | |
|
138 | 139 | put :update, :id => member.id, :membership => {:role_ids => [2]} |
|
139 | 140 | assert_equal [1, 2], member.reload.role_ids.sort |
|
140 | 141 | end |
|
141 | 142 | |
|
142 | 143 | def test_xhr_update |
|
143 | 144 | assert_no_difference 'Member.count' do |
|
144 | 145 | xhr :put, :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3} |
|
145 | 146 | assert_response :success |
|
146 | 147 | assert_template 'update' |
|
147 | 148 | assert_equal 'text/javascript', response.content_type |
|
148 | 149 | end |
|
149 | 150 | member = Member.find(2) |
|
150 | 151 | assert_equal [1], member.role_ids |
|
151 | 152 | assert_equal 3, member.user_id |
|
152 | 153 | assert_include 'tab-content-members', response.body |
|
153 | 154 | end |
|
154 | 155 | |
|
155 | 156 | def test_destroy |
|
156 | 157 | assert_difference 'Member.count', -1 do |
|
157 | 158 | delete :destroy, :id => 2 |
|
158 | 159 | end |
|
159 | 160 | assert_redirected_to '/projects/ecookbook/settings/members' |
|
160 | 161 | assert !User.find(3).member_of?(Project.find(1)) |
|
161 | 162 | end |
|
162 | 163 | |
|
163 | 164 | def test_destroy_should_fail_with_unmanaged_roles |
|
164 | 165 | role = Role.find(1) |
|
165 | 166 | role.update! :all_roles_managed => false |
|
166 | 167 | role.managed_roles = Role.where(:id => [2, 3]).to_a |
|
167 | 168 | member = Member.create!(:user => User.find(9), :role_ids => [1, 3], :project_id => 1) |
|
168 | 169 | |
|
169 | 170 | assert_no_difference 'Member.count' do |
|
170 | 171 | delete :destroy, :id => member.id |
|
171 | 172 | end |
|
172 | 173 | end |
|
173 | 174 | |
|
174 | 175 | def test_destroy_should_succeed_with_managed_roles_only |
|
175 | 176 | role = Role.find(1) |
|
176 | 177 | role.update! :all_roles_managed => false |
|
177 | 178 | role.managed_roles = Role.where(:id => [2, 3]).to_a |
|
178 | 179 | member = Member.create!(:user => User.find(9), :role_ids => [3], :project_id => 1) |
|
179 | 180 | |
|
180 | 181 | assert_difference 'Member.count', -1 do |
|
181 | 182 | delete :destroy, :id => member.id |
|
182 | 183 | end |
|
183 | 184 | end |
|
184 | 185 | |
|
185 | 186 | def test_xhr_destroy |
|
186 | 187 | assert_difference 'Member.count', -1 do |
|
187 | 188 | xhr :delete, :destroy, :id => 2 |
|
188 | 189 | assert_response :success |
|
189 | 190 | assert_template 'destroy' |
|
190 | 191 | assert_equal 'text/javascript', response.content_type |
|
191 | 192 | end |
|
192 | 193 | assert_nil Member.find_by_id(2) |
|
193 | 194 | assert_include 'tab-content-members', response.body |
|
194 | 195 | end |
|
195 | 196 | |
|
196 | 197 | def test_autocomplete |
|
197 | 198 | xhr :get, :autocomplete, :project_id => 1, :q => 'mis', :format => 'js' |
|
198 | 199 | assert_response :success |
|
199 | 200 | assert_include 'User Misc', response.body |
|
200 | 201 | end |
|
201 | 202 | end |
@@ -1,274 +1,281 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class MyControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :users, :email_addresses, :user_preferences, :roles, :projects, :members, :member_roles, |
|
22 | 22 | :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources |
|
23 | 23 | |
|
24 | 24 | def setup |
|
25 | 25 | @request.session[:user_id] = 2 |
|
26 | Redmine::SudoMode.disable! | |
|
26 | 27 | end |
|
27 | 28 | |
|
28 | 29 | def test_index |
|
29 | 30 | get :index |
|
30 | 31 | assert_response :success |
|
31 | 32 | assert_template 'page' |
|
32 | 33 | end |
|
33 | 34 | |
|
34 | 35 | def test_page |
|
35 | 36 | get :page |
|
36 | 37 | assert_response :success |
|
37 | 38 | assert_template 'page' |
|
38 | 39 | end |
|
39 | 40 | |
|
40 | 41 | def test_page_with_timelog_block |
|
41 | 42 | preferences = User.find(2).pref |
|
42 | 43 | preferences[:my_page_layout] = {'top' => ['timelog']} |
|
43 | 44 | preferences.save! |
|
44 | 45 | TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10) |
|
45 | 46 | |
|
46 | 47 | get :page |
|
47 | 48 | assert_response :success |
|
48 | 49 | assert_select 'tr.time-entry' do |
|
49 | 50 | assert_select 'td.subject a[href="/issues/1"]' |
|
50 | 51 | assert_select 'td.hours', :text => '2.50' |
|
51 | 52 | end |
|
52 | 53 | end |
|
53 | 54 | |
|
54 | 55 | def test_page_with_all_blocks |
|
55 | 56 | blocks = MyController::BLOCKS.keys |
|
56 | 57 | preferences = User.find(2).pref |
|
57 | 58 | preferences[:my_page_layout] = {'top' => blocks} |
|
58 | 59 | preferences.save! |
|
59 | 60 | |
|
60 | 61 | get :page |
|
61 | 62 | assert_response :success |
|
62 | 63 | assert_select 'div.mypage-box', blocks.size |
|
63 | 64 | end |
|
64 | 65 | |
|
65 | 66 | def test_my_account_should_show_editable_custom_fields |
|
66 | 67 | get :account |
|
67 | 68 | assert_response :success |
|
68 | 69 | assert_template 'account' |
|
69 | 70 | assert_equal User.find(2), assigns(:user) |
|
70 | 71 | |
|
71 | 72 | assert_select 'input[name=?]', 'user[custom_field_values][4]' |
|
72 | 73 | end |
|
73 | 74 | |
|
74 | 75 | def test_my_account_should_not_show_non_editable_custom_fields |
|
75 | 76 | UserCustomField.find(4).update_attribute :editable, false |
|
76 | 77 | |
|
77 | 78 | get :account |
|
78 | 79 | assert_response :success |
|
79 | 80 | assert_template 'account' |
|
80 | 81 | assert_equal User.find(2), assigns(:user) |
|
81 | 82 | |
|
82 | 83 | assert_select 'input[name=?]', 'user[custom_field_values][4]', 0 |
|
83 | 84 | end |
|
84 | 85 | |
|
85 | 86 | def test_my_account_should_show_language_select |
|
86 | 87 | get :account |
|
87 | 88 | assert_response :success |
|
88 | 89 | assert_select 'select[name=?]', 'user[language]' |
|
89 | 90 | end |
|
90 | 91 | |
|
91 | 92 | def test_my_account_should_not_show_language_select_with_force_default_language_for_loggedin |
|
92 | 93 | with_settings :force_default_language_for_loggedin => '1' do |
|
93 | 94 | get :account |
|
94 | 95 | assert_response :success |
|
95 | 96 | assert_select 'select[name=?]', 'user[language]', 0 |
|
96 | 97 | end |
|
97 | 98 | end |
|
98 | 99 | |
|
99 | 100 | def test_update_account |
|
100 | 101 | post :account, |
|
101 | 102 | :user => { |
|
102 | 103 | :firstname => "Joe", |
|
103 | 104 | :login => "root", |
|
104 | 105 | :admin => 1, |
|
105 | 106 | :group_ids => ['10'], |
|
106 | 107 | :custom_field_values => {"4" => "0100562500"} |
|
107 | 108 | } |
|
108 | 109 | |
|
109 | 110 | assert_redirected_to '/my/account' |
|
110 | 111 | user = User.find(2) |
|
111 | 112 | assert_equal user, assigns(:user) |
|
112 | 113 | assert_equal "Joe", user.firstname |
|
113 | 114 | assert_equal "jsmith", user.login |
|
114 | 115 | assert_equal "0100562500", user.custom_value_for(4).value |
|
115 | 116 | # ignored |
|
116 | 117 | assert !user.admin? |
|
117 | 118 | assert user.groups.empty? |
|
118 | 119 | end |
|
119 | 120 | |
|
120 | 121 | def test_my_account_should_show_destroy_link |
|
121 | 122 | get :account |
|
122 | 123 | assert_select 'a[href="/my/account/destroy"]' |
|
123 | 124 | end |
|
124 | 125 | |
|
125 | 126 | def test_get_destroy_should_display_the_destroy_confirmation |
|
126 | 127 | get :destroy |
|
127 | 128 | assert_response :success |
|
128 | 129 | assert_template 'destroy' |
|
129 | 130 | assert_select 'form[action="/my/account/destroy"]' do |
|
130 | 131 | assert_select 'input[name=confirm]' |
|
131 | 132 | end |
|
132 | 133 | end |
|
133 | 134 | |
|
134 | 135 | def test_post_destroy_without_confirmation_should_not_destroy_account |
|
135 | 136 | assert_no_difference 'User.count' do |
|
136 | 137 | post :destroy |
|
137 | 138 | end |
|
138 | 139 | assert_response :success |
|
139 | 140 | assert_template 'destroy' |
|
140 | 141 | end |
|
141 | 142 | |
|
142 | 143 | def test_post_destroy_without_confirmation_should_destroy_account |
|
143 | 144 | assert_difference 'User.count', -1 do |
|
144 | 145 | post :destroy, :confirm => '1' |
|
145 | 146 | end |
|
146 | 147 | assert_redirected_to '/' |
|
147 | 148 | assert_match /deleted/i, flash[:notice] |
|
148 | 149 | end |
|
149 | 150 | |
|
150 | 151 | def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account |
|
151 | 152 | User.any_instance.stubs(:own_account_deletable?).returns(false) |
|
152 | 153 | |
|
153 | 154 | assert_no_difference 'User.count' do |
|
154 | 155 | post :destroy, :confirm => '1' |
|
155 | 156 | end |
|
156 | 157 | assert_redirected_to '/my/account' |
|
157 | 158 | end |
|
158 | 159 | |
|
159 | 160 | def test_change_password |
|
160 | 161 | get :password |
|
161 | 162 | assert_response :success |
|
162 | 163 | assert_template 'password' |
|
163 | 164 | |
|
164 | 165 | # non matching password confirmation |
|
165 | 166 | post :password, :password => 'jsmith', |
|
166 | 167 | :new_password => 'secret123', |
|
167 | 168 | :new_password_confirmation => 'secret1234' |
|
168 | 169 | assert_response :success |
|
169 | 170 | assert_template 'password' |
|
170 | 171 | assert_select_error /Password doesn.*t match confirmation/ |
|
171 | 172 | |
|
172 | 173 | # wrong password |
|
173 | 174 | post :password, :password => 'wrongpassword', |
|
174 | 175 | :new_password => 'secret123', |
|
175 | 176 | :new_password_confirmation => 'secret123' |
|
176 | 177 | assert_response :success |
|
177 | 178 | assert_template 'password' |
|
178 | 179 | assert_equal 'Wrong password', flash[:error] |
|
179 | 180 | |
|
180 | 181 | # good password |
|
181 | 182 | post :password, :password => 'jsmith', |
|
182 | 183 | :new_password => 'secret123', |
|
183 | 184 | :new_password_confirmation => 'secret123' |
|
184 | 185 | assert_redirected_to '/my/account' |
|
185 | 186 | assert User.try_to_login('jsmith', 'secret123') |
|
186 | 187 | end |
|
187 | 188 | |
|
188 | 189 | def test_change_password_kills_other_sessions |
|
189 | 190 | @request.session[:ctime] = (Time.now - 30.minutes).utc.to_i |
|
190 | 191 | |
|
191 | 192 | jsmith = User.find(2) |
|
192 | 193 | jsmith.passwd_changed_on = Time.now |
|
193 | 194 | jsmith.save! |
|
194 | 195 | |
|
195 | 196 | get 'account' |
|
196 | 197 | assert_response 302 |
|
197 | 198 | assert flash[:error].match(/Your session has expired/) |
|
198 | 199 | end |
|
199 | 200 | |
|
200 | 201 | def test_change_password_should_redirect_if_user_cannot_change_its_password |
|
201 | 202 | User.find(2).update_attribute(:auth_source_id, 1) |
|
202 | 203 | |
|
203 | 204 | get :password |
|
204 | 205 | assert_not_nil flash[:error] |
|
205 | 206 | assert_redirected_to '/my/account' |
|
206 | 207 | end |
|
207 | 208 | |
|
208 | 209 | def test_page_layout |
|
209 | 210 | get :page_layout |
|
210 | 211 | assert_response :success |
|
211 | 212 | assert_template 'page_layout' |
|
212 | 213 | end |
|
213 | 214 | |
|
214 | 215 | def test_add_block |
|
215 | 216 | post :add_block, :block => 'issuesreportedbyme' |
|
216 | 217 | assert_redirected_to '/my/page_layout' |
|
217 | 218 | assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme') |
|
218 | 219 | end |
|
219 | 220 | |
|
220 | 221 | def test_add_invalid_block_should_redirect |
|
221 | 222 | post :add_block, :block => 'invalid' |
|
222 | 223 | assert_redirected_to '/my/page_layout' |
|
223 | 224 | end |
|
224 | 225 | |
|
225 | 226 | def test_remove_block |
|
226 | 227 | post :remove_block, :block => 'issuesassignedtome' |
|
227 | 228 | assert_redirected_to '/my/page_layout' |
|
228 | 229 | assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome') |
|
229 | 230 | end |
|
230 | 231 | |
|
231 | 232 | def test_order_blocks |
|
232 | 233 | xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews'] |
|
233 | 234 | assert_response :success |
|
234 | 235 | assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left'] |
|
235 | 236 | end |
|
236 | 237 | |
|
237 | 238 | def test_reset_rss_key_with_existing_key |
|
238 | 239 | @previous_token_value = User.find(2).rss_key # Will generate one if it's missing |
|
239 | 240 | post :reset_rss_key |
|
240 | 241 | |
|
241 | 242 | assert_not_equal @previous_token_value, User.find(2).rss_key |
|
242 | 243 | assert User.find(2).rss_token |
|
243 | 244 | assert_match /reset/, flash[:notice] |
|
244 | 245 | assert_redirected_to '/my/account' |
|
245 | 246 | end |
|
246 | 247 | |
|
247 | 248 | def test_reset_rss_key_without_existing_key |
|
248 | 249 | assert_nil User.find(2).rss_token |
|
249 | 250 | post :reset_rss_key |
|
250 | 251 | |
|
251 | 252 | assert User.find(2).rss_token |
|
252 | 253 | assert_match /reset/, flash[:notice] |
|
253 | 254 | assert_redirected_to '/my/account' |
|
254 | 255 | end |
|
255 | 256 | |
|
257 | def test_show_api_key | |
|
258 | get :show_api_key | |
|
259 | assert_response :success | |
|
260 | assert_select 'pre', User.find(2).api_key | |
|
261 | end | |
|
262 | ||
|
256 | 263 | def test_reset_api_key_with_existing_key |
|
257 | 264 | @previous_token_value = User.find(2).api_key # Will generate one if it's missing |
|
258 | 265 | post :reset_api_key |
|
259 | 266 | |
|
260 | 267 | assert_not_equal @previous_token_value, User.find(2).api_key |
|
261 | 268 | assert User.find(2).api_token |
|
262 | 269 | assert_match /reset/, flash[:notice] |
|
263 | 270 | assert_redirected_to '/my/account' |
|
264 | 271 | end |
|
265 | 272 | |
|
266 | 273 | def test_reset_api_key_without_existing_key |
|
267 | 274 | assert_nil User.find(2).api_token |
|
268 | 275 | post :reset_api_key |
|
269 | 276 | |
|
270 | 277 | assert User.find(2).api_token |
|
271 | 278 | assert_match /reset/, flash[:notice] |
|
272 | 279 | assert_redirected_to '/my/account' |
|
273 | 280 | end |
|
274 | 281 | end |
@@ -1,684 +1,685 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class ProjectsControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :projects, :versions, :users, :email_addresses, :roles, :members, |
|
22 | 22 | :member_roles, :issues, :journals, :journal_details, |
|
23 | 23 | :trackers, :projects_trackers, :issue_statuses, |
|
24 | 24 | :enabled_modules, :enumerations, :boards, :messages, |
|
25 | 25 | :attachments, :custom_fields, :custom_values, :time_entries, |
|
26 | 26 | :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions |
|
27 | 27 | |
|
28 | 28 | def setup |
|
29 | 29 | @request.session[:user_id] = nil |
|
30 | 30 | Setting.default_language = 'en' |
|
31 | Redmine::SudoMode.disable! | |
|
31 | 32 | end |
|
32 | 33 | |
|
33 | 34 | def test_index_by_anonymous_should_not_show_private_projects |
|
34 | 35 | get :index |
|
35 | 36 | assert_response :success |
|
36 | 37 | assert_template 'index' |
|
37 | 38 | projects = assigns(:projects) |
|
38 | 39 | assert_not_nil projects |
|
39 | 40 | assert projects.all?(&:is_public?) |
|
40 | 41 | |
|
41 | 42 | assert_select 'ul' do |
|
42 | 43 | assert_select 'li' do |
|
43 | 44 | assert_select 'a', :text => 'eCookbook' |
|
44 | 45 | assert_select 'ul' do |
|
45 | 46 | assert_select 'a', :text => 'Child of private child' |
|
46 | 47 | end |
|
47 | 48 | end |
|
48 | 49 | end |
|
49 | 50 | assert_select 'a', :text => /Private child of eCookbook/, :count => 0 |
|
50 | 51 | end |
|
51 | 52 | |
|
52 | 53 | def test_index_atom |
|
53 | 54 | get :index, :format => 'atom' |
|
54 | 55 | assert_response :success |
|
55 | 56 | assert_template 'common/feed' |
|
56 | 57 | assert_select 'feed>title', :text => 'Redmine: Latest projects' |
|
57 | 58 | assert_select 'feed>entry', :count => Project.visible(User.current).count |
|
58 | 59 | end |
|
59 | 60 | |
|
60 | 61 | test "#index by non-admin user with view_time_entries permission should show overall spent time link" do |
|
61 | 62 | @request.session[:user_id] = 3 |
|
62 | 63 | get :index |
|
63 | 64 | assert_template 'index' |
|
64 | 65 | assert_select 'a[href=?]', '/time_entries' |
|
65 | 66 | end |
|
66 | 67 | |
|
67 | 68 | test "#index by non-admin user without view_time_entries permission should not show overall spent time link" do |
|
68 | 69 | Role.find(2).remove_permission! :view_time_entries |
|
69 | 70 | Role.non_member.remove_permission! :view_time_entries |
|
70 | 71 | Role.anonymous.remove_permission! :view_time_entries |
|
71 | 72 | @request.session[:user_id] = 3 |
|
72 | 73 | |
|
73 | 74 | get :index |
|
74 | 75 | assert_template 'index' |
|
75 | 76 | assert_select 'a[href=?]', '/time_entries', 0 |
|
76 | 77 | end |
|
77 | 78 | |
|
78 | 79 | test "#index by non-admin user with permission should show add project link" do |
|
79 | 80 | Role.find(1).add_permission! :add_project |
|
80 | 81 | @request.session[:user_id] = 2 |
|
81 | 82 | get :index |
|
82 | 83 | assert_template 'index' |
|
83 | 84 | assert_select 'a[href=?]', '/projects/new' |
|
84 | 85 | end |
|
85 | 86 | |
|
86 | 87 | test "#new by admin user should accept get" do |
|
87 | 88 | @request.session[:user_id] = 1 |
|
88 | 89 | |
|
89 | 90 | get :new |
|
90 | 91 | assert_response :success |
|
91 | 92 | assert_template 'new' |
|
92 | 93 | end |
|
93 | 94 | |
|
94 | 95 | test "#new by non-admin user with add_project permission should accept get" do |
|
95 | 96 | Role.non_member.add_permission! :add_project |
|
96 | 97 | @request.session[:user_id] = 9 |
|
97 | 98 | |
|
98 | 99 | get :new |
|
99 | 100 | assert_response :success |
|
100 | 101 | assert_template 'new' |
|
101 | 102 | assert_select 'select[name=?]', 'project[parent_id]', 0 |
|
102 | 103 | end |
|
103 | 104 | |
|
104 | 105 | test "#new by non-admin user with add_subprojects permission should accept get" do |
|
105 | 106 | Role.find(1).remove_permission! :add_project |
|
106 | 107 | Role.find(1).add_permission! :add_subprojects |
|
107 | 108 | @request.session[:user_id] = 2 |
|
108 | 109 | |
|
109 | 110 | get :new, :parent_id => 'ecookbook' |
|
110 | 111 | assert_response :success |
|
111 | 112 | assert_template 'new' |
|
112 | 113 | |
|
113 | 114 | assert_select 'select[name=?]', 'project[parent_id]' do |
|
114 | 115 | # parent project selected |
|
115 | 116 | assert_select 'option[value="1"][selected=selected]' |
|
116 | 117 | # no empty value |
|
117 | 118 | assert_select 'option[value=""]', 0 |
|
118 | 119 | end |
|
119 | 120 | end |
|
120 | 121 | |
|
121 | 122 | test "#create by admin user should create a new project" do |
|
122 | 123 | @request.session[:user_id] = 1 |
|
123 | 124 | |
|
124 | 125 | post :create, |
|
125 | 126 | :project => { |
|
126 | 127 | :name => "blog", |
|
127 | 128 | :description => "weblog", |
|
128 | 129 | :homepage => 'http://weblog', |
|
129 | 130 | :identifier => "blog", |
|
130 | 131 | :is_public => 1, |
|
131 | 132 | :custom_field_values => { '3' => 'Beta' }, |
|
132 | 133 | :tracker_ids => ['1', '3'], |
|
133 | 134 | # an issue custom field that is not for all project |
|
134 | 135 | :issue_custom_field_ids => ['9'], |
|
135 | 136 | :enabled_module_names => ['issue_tracking', 'news', 'repository'] |
|
136 | 137 | } |
|
137 | 138 | assert_redirected_to '/projects/blog/settings' |
|
138 | 139 | |
|
139 | 140 | project = Project.find_by_name('blog') |
|
140 | 141 | assert_kind_of Project, project |
|
141 | 142 | assert project.active? |
|
142 | 143 | assert_equal 'weblog', project.description |
|
143 | 144 | assert_equal 'http://weblog', project.homepage |
|
144 | 145 | assert_equal true, project.is_public? |
|
145 | 146 | assert_nil project.parent |
|
146 | 147 | assert_equal 'Beta', project.custom_value_for(3).value |
|
147 | 148 | assert_equal [1, 3], project.trackers.map(&:id).sort |
|
148 | 149 | assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort |
|
149 | 150 | assert project.issue_custom_fields.include?(IssueCustomField.find(9)) |
|
150 | 151 | end |
|
151 | 152 | |
|
152 | 153 | test "#create by admin user should create a new subproject" do |
|
153 | 154 | @request.session[:user_id] = 1 |
|
154 | 155 | |
|
155 | 156 | assert_difference 'Project.count' do |
|
156 | 157 | post :create, :project => { :name => "blog", |
|
157 | 158 | :description => "weblog", |
|
158 | 159 | :identifier => "blog", |
|
159 | 160 | :is_public => 1, |
|
160 | 161 | :custom_field_values => { '3' => 'Beta' }, |
|
161 | 162 | :parent_id => 1 |
|
162 | 163 | } |
|
163 | 164 | assert_redirected_to '/projects/blog/settings' |
|
164 | 165 | end |
|
165 | 166 | |
|
166 | 167 | project = Project.find_by_name('blog') |
|
167 | 168 | assert_kind_of Project, project |
|
168 | 169 | assert_equal Project.find(1), project.parent |
|
169 | 170 | end |
|
170 | 171 | |
|
171 | 172 | test "#create by admin user should continue" do |
|
172 | 173 | @request.session[:user_id] = 1 |
|
173 | 174 | |
|
174 | 175 | assert_difference 'Project.count' do |
|
175 | 176 | post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue' |
|
176 | 177 | end |
|
177 | 178 | assert_redirected_to '/projects/new' |
|
178 | 179 | end |
|
179 | 180 | |
|
180 | 181 | test "#create by non-admin user with add_project permission should create a new project" do |
|
181 | 182 | Role.non_member.add_permission! :add_project |
|
182 | 183 | @request.session[:user_id] = 9 |
|
183 | 184 | |
|
184 | 185 | post :create, :project => { :name => "blog", |
|
185 | 186 | :description => "weblog", |
|
186 | 187 | :identifier => "blog", |
|
187 | 188 | :is_public => 1, |
|
188 | 189 | :custom_field_values => { '3' => 'Beta' }, |
|
189 | 190 | :tracker_ids => ['1', '3'], |
|
190 | 191 | :enabled_module_names => ['issue_tracking', 'news', 'repository'] |
|
191 | 192 | } |
|
192 | 193 | |
|
193 | 194 | assert_redirected_to '/projects/blog/settings' |
|
194 | 195 | |
|
195 | 196 | project = Project.find_by_name('blog') |
|
196 | 197 | assert_kind_of Project, project |
|
197 | 198 | assert_equal 'weblog', project.description |
|
198 | 199 | assert_equal true, project.is_public? |
|
199 | 200 | assert_equal [1, 3], project.trackers.map(&:id).sort |
|
200 | 201 | assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort |
|
201 | 202 | |
|
202 | 203 | # User should be added as a project member |
|
203 | 204 | assert User.find(9).member_of?(project) |
|
204 | 205 | assert_equal 1, project.members.size |
|
205 | 206 | end |
|
206 | 207 | |
|
207 | 208 | test "#create by non-admin user with add_project permission should fail with parent_id" do |
|
208 | 209 | Role.non_member.add_permission! :add_project |
|
209 | 210 | @request.session[:user_id] = 9 |
|
210 | 211 | |
|
211 | 212 | assert_no_difference 'Project.count' do |
|
212 | 213 | post :create, :project => { :name => "blog", |
|
213 | 214 | :description => "weblog", |
|
214 | 215 | :identifier => "blog", |
|
215 | 216 | :is_public => 1, |
|
216 | 217 | :custom_field_values => { '3' => 'Beta' }, |
|
217 | 218 | :parent_id => 1 |
|
218 | 219 | } |
|
219 | 220 | end |
|
220 | 221 | assert_response :success |
|
221 | 222 | project = assigns(:project) |
|
222 | 223 | assert_kind_of Project, project |
|
223 | 224 | assert_not_equal [], project.errors[:parent_id] |
|
224 | 225 | end |
|
225 | 226 | |
|
226 | 227 | test "#create by non-admin user with add_subprojects permission should create a project with a parent_id" do |
|
227 | 228 | Role.find(1).remove_permission! :add_project |
|
228 | 229 | Role.find(1).add_permission! :add_subprojects |
|
229 | 230 | @request.session[:user_id] = 2 |
|
230 | 231 | |
|
231 | 232 | post :create, :project => { :name => "blog", |
|
232 | 233 | :description => "weblog", |
|
233 | 234 | :identifier => "blog", |
|
234 | 235 | :is_public => 1, |
|
235 | 236 | :custom_field_values => { '3' => 'Beta' }, |
|
236 | 237 | :parent_id => 1 |
|
237 | 238 | } |
|
238 | 239 | assert_redirected_to '/projects/blog/settings' |
|
239 | 240 | project = Project.find_by_name('blog') |
|
240 | 241 | end |
|
241 | 242 | |
|
242 | 243 | test "#create by non-admin user with add_subprojects permission should fail without parent_id" do |
|
243 | 244 | Role.find(1).remove_permission! :add_project |
|
244 | 245 | Role.find(1).add_permission! :add_subprojects |
|
245 | 246 | @request.session[:user_id] = 2 |
|
246 | 247 | |
|
247 | 248 | assert_no_difference 'Project.count' do |
|
248 | 249 | post :create, :project => { :name => "blog", |
|
249 | 250 | :description => "weblog", |
|
250 | 251 | :identifier => "blog", |
|
251 | 252 | :is_public => 1, |
|
252 | 253 | :custom_field_values => { '3' => 'Beta' } |
|
253 | 254 | } |
|
254 | 255 | end |
|
255 | 256 | assert_response :success |
|
256 | 257 | project = assigns(:project) |
|
257 | 258 | assert_kind_of Project, project |
|
258 | 259 | assert_not_equal [], project.errors[:parent_id] |
|
259 | 260 | end |
|
260 | 261 | |
|
261 | 262 | test "#create by non-admin user with add_subprojects permission should fail with unauthorized parent_id" do |
|
262 | 263 | Role.find(1).remove_permission! :add_project |
|
263 | 264 | Role.find(1).add_permission! :add_subprojects |
|
264 | 265 | @request.session[:user_id] = 2 |
|
265 | 266 | |
|
266 | 267 | assert !User.find(2).member_of?(Project.find(6)) |
|
267 | 268 | assert_no_difference 'Project.count' do |
|
268 | 269 | post :create, :project => { :name => "blog", |
|
269 | 270 | :description => "weblog", |
|
270 | 271 | :identifier => "blog", |
|
271 | 272 | :is_public => 1, |
|
272 | 273 | :custom_field_values => { '3' => 'Beta' }, |
|
273 | 274 | :parent_id => 6 |
|
274 | 275 | } |
|
275 | 276 | end |
|
276 | 277 | assert_response :success |
|
277 | 278 | project = assigns(:project) |
|
278 | 279 | assert_kind_of Project, project |
|
279 | 280 | assert_not_equal [], project.errors[:parent_id] |
|
280 | 281 | end |
|
281 | 282 | |
|
282 | 283 | def test_create_subproject_with_inherit_members_should_inherit_members |
|
283 | 284 | Role.find_by_name('Manager').add_permission! :add_subprojects |
|
284 | 285 | parent = Project.find(1) |
|
285 | 286 | @request.session[:user_id] = 2 |
|
286 | 287 | |
|
287 | 288 | assert_difference 'Project.count' do |
|
288 | 289 | post :create, :project => { |
|
289 | 290 | :name => 'inherited', :identifier => 'inherited', :parent_id => parent.id, :inherit_members => '1' |
|
290 | 291 | } |
|
291 | 292 | assert_response 302 |
|
292 | 293 | end |
|
293 | 294 | |
|
294 | 295 | project = Project.order('id desc').first |
|
295 | 296 | assert_equal 'inherited', project.name |
|
296 | 297 | assert_equal parent, project.parent |
|
297 | 298 | assert project.memberships.count > 0 |
|
298 | 299 | assert_equal parent.memberships.count, project.memberships.count |
|
299 | 300 | end |
|
300 | 301 | |
|
301 | 302 | def test_create_should_preserve_modules_on_validation_failure |
|
302 | 303 | with_settings :default_projects_modules => ['issue_tracking', 'repository'] do |
|
303 | 304 | @request.session[:user_id] = 1 |
|
304 | 305 | assert_no_difference 'Project.count' do |
|
305 | 306 | post :create, :project => { |
|
306 | 307 | :name => "blog", |
|
307 | 308 | :identifier => "", |
|
308 | 309 | :enabled_module_names => %w(issue_tracking news) |
|
309 | 310 | } |
|
310 | 311 | end |
|
311 | 312 | assert_response :success |
|
312 | 313 | project = assigns(:project) |
|
313 | 314 | assert_equal %w(issue_tracking news), project.enabled_module_names.sort |
|
314 | 315 | end |
|
315 | 316 | end |
|
316 | 317 | |
|
317 | 318 | def test_show_by_id |
|
318 | 319 | get :show, :id => 1 |
|
319 | 320 | assert_response :success |
|
320 | 321 | assert_template 'show' |
|
321 | 322 | assert_not_nil assigns(:project) |
|
322 | 323 | end |
|
323 | 324 | |
|
324 | 325 | def test_show_by_identifier |
|
325 | 326 | get :show, :id => 'ecookbook' |
|
326 | 327 | assert_response :success |
|
327 | 328 | assert_template 'show' |
|
328 | 329 | assert_not_nil assigns(:project) |
|
329 | 330 | assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) |
|
330 | 331 | |
|
331 | 332 | assert_select 'li', :text => /Development status/ |
|
332 | 333 | end |
|
333 | 334 | |
|
334 | 335 | def test_show_should_not_display_empty_sidebar |
|
335 | 336 | p = Project.find(1) |
|
336 | 337 | p.enabled_module_names = [] |
|
337 | 338 | p.save! |
|
338 | 339 | |
|
339 | 340 | get :show, :id => 'ecookbook' |
|
340 | 341 | assert_response :success |
|
341 | 342 | assert_select '#main.nosidebar' |
|
342 | 343 | end |
|
343 | 344 | |
|
344 | 345 | def test_show_should_not_display_hidden_custom_fields |
|
345 | 346 | ProjectCustomField.find_by_name('Development status').update_attribute :visible, false |
|
346 | 347 | get :show, :id => 'ecookbook' |
|
347 | 348 | assert_response :success |
|
348 | 349 | assert_template 'show' |
|
349 | 350 | assert_not_nil assigns(:project) |
|
350 | 351 | |
|
351 | 352 | assert_select 'li', :text => /Development status/, :count => 0 |
|
352 | 353 | end |
|
353 | 354 | |
|
354 | 355 | def test_show_should_not_display_blank_custom_fields_with_multiple_values |
|
355 | 356 | f1 = ProjectCustomField.generate! :field_format => 'list', :possible_values => %w(Foo Bar), :multiple => true |
|
356 | 357 | f2 = ProjectCustomField.generate! :field_format => 'list', :possible_values => %w(Baz Qux), :multiple => true |
|
357 | 358 | project = Project.generate!(:custom_field_values => {f2.id.to_s => %w(Qux)}) |
|
358 | 359 | |
|
359 | 360 | get :show, :id => project.id |
|
360 | 361 | assert_response :success |
|
361 | 362 | |
|
362 | 363 | assert_select 'li', :text => /#{f1.name}/, :count => 0 |
|
363 | 364 | assert_select 'li', :text => /#{f2.name}/ |
|
364 | 365 | end |
|
365 | 366 | |
|
366 | 367 | def test_show_should_not_display_blank_text_custom_fields |
|
367 | 368 | f1 = ProjectCustomField.generate! :field_format => 'text' |
|
368 | 369 | |
|
369 | 370 | get :show, :id => 1 |
|
370 | 371 | assert_response :success |
|
371 | 372 | |
|
372 | 373 | assert_select 'li', :text => /#{f1.name}/, :count => 0 |
|
373 | 374 | end |
|
374 | 375 | |
|
375 | 376 | def test_show_should_not_fail_when_custom_values_are_nil |
|
376 | 377 | project = Project.find_by_identifier('ecookbook') |
|
377 | 378 | project.custom_values.first.update_attribute(:value, nil) |
|
378 | 379 | get :show, :id => 'ecookbook' |
|
379 | 380 | assert_response :success |
|
380 | 381 | assert_template 'show' |
|
381 | 382 | assert_not_nil assigns(:project) |
|
382 | 383 | assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) |
|
383 | 384 | end |
|
384 | 385 | |
|
385 | 386 | def show_archived_project_should_be_denied |
|
386 | 387 | project = Project.find_by_identifier('ecookbook') |
|
387 | 388 | project.archive! |
|
388 | 389 | |
|
389 | 390 | get :show, :id => 'ecookbook' |
|
390 | 391 | assert_response 403 |
|
391 | 392 | assert_nil assigns(:project) |
|
392 | 393 | assert_select 'p', :text => /archived/ |
|
393 | 394 | end |
|
394 | 395 | |
|
395 | 396 | def test_show_should_not_show_private_subprojects_that_are_not_visible |
|
396 | 397 | get :show, :id => 'ecookbook' |
|
397 | 398 | assert_response :success |
|
398 | 399 | assert_template 'show' |
|
399 | 400 | assert_select 'a', :text => /Private child/, :count => 0 |
|
400 | 401 | end |
|
401 | 402 | |
|
402 | 403 | def test_show_should_show_private_subprojects_that_are_visible |
|
403 | 404 | @request.session[:user_id] = 2 # manager who is a member of the private subproject |
|
404 | 405 | get :show, :id => 'ecookbook' |
|
405 | 406 | assert_response :success |
|
406 | 407 | assert_template 'show' |
|
407 | 408 | assert_select 'a', :text => /Private child/ |
|
408 | 409 | end |
|
409 | 410 | |
|
410 | 411 | def test_settings |
|
411 | 412 | @request.session[:user_id] = 2 # manager |
|
412 | 413 | get :settings, :id => 1 |
|
413 | 414 | assert_response :success |
|
414 | 415 | assert_template 'settings' |
|
415 | 416 | end |
|
416 | 417 | |
|
417 | 418 | def test_settings_of_subproject |
|
418 | 419 | @request.session[:user_id] = 2 |
|
419 | 420 | get :settings, :id => 'private-child' |
|
420 | 421 | assert_response :success |
|
421 | 422 | assert_template 'settings' |
|
422 | 423 | |
|
423 | 424 | assert_select 'input[type=checkbox][name=?]', 'project[inherit_members]' |
|
424 | 425 | end |
|
425 | 426 | |
|
426 | 427 | def test_settings_should_be_denied_for_member_on_closed_project |
|
427 | 428 | Project.find(1).close |
|
428 | 429 | @request.session[:user_id] = 2 # manager |
|
429 | 430 | |
|
430 | 431 | get :settings, :id => 1 |
|
431 | 432 | assert_response 403 |
|
432 | 433 | end |
|
433 | 434 | |
|
434 | 435 | def test_settings_should_be_denied_for_anonymous_on_closed_project |
|
435 | 436 | Project.find(1).close |
|
436 | 437 | |
|
437 | 438 | get :settings, :id => 1 |
|
438 | 439 | assert_response 302 |
|
439 | 440 | end |
|
440 | 441 | |
|
441 | 442 | def test_setting_with_wiki_module_and_no_wiki |
|
442 | 443 | Project.find(1).wiki.destroy |
|
443 | 444 | Role.find(1).add_permission! :manage_wiki |
|
444 | 445 | @request.session[:user_id] = 2 |
|
445 | 446 | |
|
446 | 447 | get :settings, :id => 1 |
|
447 | 448 | assert_response :success |
|
448 | 449 | assert_template 'settings' |
|
449 | 450 | |
|
450 | 451 | assert_select 'form[action=?]', '/projects/ecookbook/wiki' do |
|
451 | 452 | assert_select 'input[name=?]', 'wiki[start_page]' |
|
452 | 453 | end |
|
453 | 454 | end |
|
454 | 455 | |
|
455 | 456 | def test_update |
|
456 | 457 | @request.session[:user_id] = 2 # manager |
|
457 | 458 | post :update, :id => 1, :project => {:name => 'Test changed name', |
|
458 | 459 | :issue_custom_field_ids => ['']} |
|
459 | 460 | assert_redirected_to '/projects/ecookbook/settings' |
|
460 | 461 | project = Project.find(1) |
|
461 | 462 | assert_equal 'Test changed name', project.name |
|
462 | 463 | end |
|
463 | 464 | |
|
464 | 465 | def test_update_with_failure |
|
465 | 466 | @request.session[:user_id] = 2 # manager |
|
466 | 467 | post :update, :id => 1, :project => {:name => ''} |
|
467 | 468 | assert_response :success |
|
468 | 469 | assert_template 'settings' |
|
469 | 470 | assert_select_error /name cannot be blank/i |
|
470 | 471 | end |
|
471 | 472 | |
|
472 | 473 | def test_update_should_be_denied_for_member_on_closed_project |
|
473 | 474 | Project.find(1).close |
|
474 | 475 | @request.session[:user_id] = 2 # manager |
|
475 | 476 | |
|
476 | 477 | post :update, :id => 1, :project => {:name => 'Closed'} |
|
477 | 478 | assert_response 403 |
|
478 | 479 | assert_equal 'eCookbook', Project.find(1).name |
|
479 | 480 | end |
|
480 | 481 | |
|
481 | 482 | def test_update_should_be_denied_for_anonymous_on_closed_project |
|
482 | 483 | Project.find(1).close |
|
483 | 484 | |
|
484 | 485 | post :update, :id => 1, :project => {:name => 'Closed'} |
|
485 | 486 | assert_response 302 |
|
486 | 487 | assert_equal 'eCookbook', Project.find(1).name |
|
487 | 488 | end |
|
488 | 489 | |
|
489 | 490 | def test_modules |
|
490 | 491 | @request.session[:user_id] = 2 |
|
491 | 492 | Project.find(1).enabled_module_names = ['issue_tracking', 'news'] |
|
492 | 493 | |
|
493 | 494 | post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents'] |
|
494 | 495 | assert_redirected_to '/projects/ecookbook/settings/modules' |
|
495 | 496 | assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort |
|
496 | 497 | end |
|
497 | 498 | |
|
498 | 499 | def test_destroy_leaf_project_without_confirmation_should_show_confirmation |
|
499 | 500 | @request.session[:user_id] = 1 # admin |
|
500 | 501 | |
|
501 | 502 | assert_no_difference 'Project.count' do |
|
502 | 503 | delete :destroy, :id => 2 |
|
503 | 504 | assert_response :success |
|
504 | 505 | assert_template 'destroy' |
|
505 | 506 | end |
|
506 | 507 | end |
|
507 | 508 | |
|
508 | 509 | def test_destroy_without_confirmation_should_show_confirmation_with_subprojects |
|
509 | 510 | @request.session[:user_id] = 1 # admin |
|
510 | 511 | |
|
511 | 512 | assert_no_difference 'Project.count' do |
|
512 | 513 | delete :destroy, :id => 1 |
|
513 | 514 | assert_response :success |
|
514 | 515 | assert_template 'destroy' |
|
515 | 516 | end |
|
516 | 517 | assert_select 'strong', |
|
517 | 518 | :text => ['Private child of eCookbook', |
|
518 | 519 | 'Child of private child, eCookbook Subproject 1', |
|
519 | 520 | 'eCookbook Subproject 2'].join(', ') |
|
520 | 521 | end |
|
521 | 522 | |
|
522 | 523 | def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects |
|
523 | 524 | @request.session[:user_id] = 1 # admin |
|
524 | 525 | |
|
525 | 526 | assert_difference 'Project.count', -5 do |
|
526 | 527 | delete :destroy, :id => 1, :confirm => 1 |
|
527 | 528 | assert_redirected_to '/admin/projects' |
|
528 | 529 | end |
|
529 | 530 | assert_nil Project.find_by_id(1) |
|
530 | 531 | end |
|
531 | 532 | |
|
532 | 533 | def test_archive |
|
533 | 534 | @request.session[:user_id] = 1 # admin |
|
534 | 535 | post :archive, :id => 1 |
|
535 | 536 | assert_redirected_to '/admin/projects' |
|
536 | 537 | assert !Project.find(1).active? |
|
537 | 538 | end |
|
538 | 539 | |
|
539 | 540 | def test_archive_with_failure |
|
540 | 541 | @request.session[:user_id] = 1 |
|
541 | 542 | Project.any_instance.stubs(:archive).returns(false) |
|
542 | 543 | post :archive, :id => 1 |
|
543 | 544 | assert_redirected_to '/admin/projects' |
|
544 | 545 | assert_match /project cannot be archived/i, flash[:error] |
|
545 | 546 | end |
|
546 | 547 | |
|
547 | 548 | def test_unarchive |
|
548 | 549 | @request.session[:user_id] = 1 # admin |
|
549 | 550 | Project.find(1).archive |
|
550 | 551 | post :unarchive, :id => 1 |
|
551 | 552 | assert_redirected_to '/admin/projects' |
|
552 | 553 | assert Project.find(1).active? |
|
553 | 554 | end |
|
554 | 555 | |
|
555 | 556 | def test_close |
|
556 | 557 | @request.session[:user_id] = 2 |
|
557 | 558 | post :close, :id => 1 |
|
558 | 559 | assert_redirected_to '/projects/ecookbook' |
|
559 | 560 | assert_equal Project::STATUS_CLOSED, Project.find(1).status |
|
560 | 561 | end |
|
561 | 562 | |
|
562 | 563 | def test_reopen |
|
563 | 564 | Project.find(1).close |
|
564 | 565 | @request.session[:user_id] = 2 |
|
565 | 566 | post :reopen, :id => 1 |
|
566 | 567 | assert_redirected_to '/projects/ecookbook' |
|
567 | 568 | assert Project.find(1).active? |
|
568 | 569 | end |
|
569 | 570 | |
|
570 | 571 | def test_project_breadcrumbs_should_be_limited_to_3_ancestors |
|
571 | 572 | CustomField.delete_all |
|
572 | 573 | parent = nil |
|
573 | 574 | 6.times do |i| |
|
574 | 575 | p = Project.generate_with_parent!(parent) |
|
575 | 576 | get :show, :id => p |
|
576 | 577 | assert_select '#header h1' do |
|
577 | 578 | assert_select 'a', :count => [i, 3].min |
|
578 | 579 | end |
|
579 | 580 | |
|
580 | 581 | parent = p |
|
581 | 582 | end |
|
582 | 583 | end |
|
583 | 584 | |
|
584 | 585 | def test_get_copy |
|
585 | 586 | @request.session[:user_id] = 1 # admin |
|
586 | 587 | get :copy, :id => 1 |
|
587 | 588 | assert_response :success |
|
588 | 589 | assert_template 'copy' |
|
589 | 590 | assert assigns(:project) |
|
590 | 591 | assert_equal Project.find(1).description, assigns(:project).description |
|
591 | 592 | assert_nil assigns(:project).id |
|
592 | 593 | |
|
593 | 594 | assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1 |
|
594 | 595 | end |
|
595 | 596 | |
|
596 | 597 | def test_get_copy_with_invalid_source_should_respond_with_404 |
|
597 | 598 | @request.session[:user_id] = 1 |
|
598 | 599 | get :copy, :id => 99 |
|
599 | 600 | assert_response 404 |
|
600 | 601 | end |
|
601 | 602 | |
|
602 | 603 | def test_post_copy_should_copy_requested_items |
|
603 | 604 | @request.session[:user_id] = 1 # admin |
|
604 | 605 | CustomField.delete_all |
|
605 | 606 | |
|
606 | 607 | assert_difference 'Project.count' do |
|
607 | 608 | post :copy, :id => 1, |
|
608 | 609 | :project => { |
|
609 | 610 | :name => 'Copy', |
|
610 | 611 | :identifier => 'unique-copy', |
|
611 | 612 | :tracker_ids => ['1', '2', '3', ''], |
|
612 | 613 | :enabled_module_names => %w(issue_tracking time_tracking) |
|
613 | 614 | }, |
|
614 | 615 | :only => %w(issues versions) |
|
615 | 616 | end |
|
616 | 617 | project = Project.find('unique-copy') |
|
617 | 618 | source = Project.find(1) |
|
618 | 619 | assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort |
|
619 | 620 | |
|
620 | 621 | assert_equal source.versions.count, project.versions.count, "All versions were not copied" |
|
621 | 622 | assert_equal source.issues.count, project.issues.count, "All issues were not copied" |
|
622 | 623 | assert_equal 0, project.members.count |
|
623 | 624 | end |
|
624 | 625 | |
|
625 | 626 | def test_post_copy_should_redirect_to_settings_when_successful |
|
626 | 627 | @request.session[:user_id] = 1 # admin |
|
627 | 628 | post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'} |
|
628 | 629 | assert_response :redirect |
|
629 | 630 | assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy' |
|
630 | 631 | end |
|
631 | 632 | |
|
632 | 633 | def test_post_copy_with_failure |
|
633 | 634 | @request.session[:user_id] = 1 |
|
634 | 635 | post :copy, :id => 1, :project => {:name => 'Copy', :identifier => ''} |
|
635 | 636 | assert_response :success |
|
636 | 637 | assert_template 'copy' |
|
637 | 638 | end |
|
638 | 639 | |
|
639 | 640 | def test_jump_should_redirect_to_active_tab |
|
640 | 641 | get :show, :id => 1, :jump => 'issues' |
|
641 | 642 | assert_redirected_to '/projects/ecookbook/issues' |
|
642 | 643 | end |
|
643 | 644 | |
|
644 | 645 | def test_jump_should_not_redirect_to_inactive_tab |
|
645 | 646 | get :show, :id => 3, :jump => 'documents' |
|
646 | 647 | assert_response :success |
|
647 | 648 | assert_template 'show' |
|
648 | 649 | end |
|
649 | 650 | |
|
650 | 651 | def test_jump_should_not_redirect_to_unknown_tab |
|
651 | 652 | get :show, :id => 3, :jump => 'foobar' |
|
652 | 653 | assert_response :success |
|
653 | 654 | assert_template 'show' |
|
654 | 655 | end |
|
655 | 656 | |
|
656 | 657 | def test_body_should_have_project_css_class |
|
657 | 658 | get :show, :id => 1 |
|
658 | 659 | assert_select 'body.project-ecookbook' |
|
659 | 660 | end |
|
660 | 661 | |
|
661 | 662 | def test_project_menu_should_include_new_issue_link |
|
662 | 663 | @request.session[:user_id] = 2 |
|
663 | 664 | get :show, :id => 1 |
|
664 | 665 | assert_select '#main-menu a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue' |
|
665 | 666 | end |
|
666 | 667 | |
|
667 | 668 | def test_project_menu_should_not_include_new_issue_link_for_project_without_trackers |
|
668 | 669 | Project.find(1).trackers.clear |
|
669 | 670 | |
|
670 | 671 | @request.session[:user_id] = 2 |
|
671 | 672 | get :show, :id => 1 |
|
672 | 673 | assert_select '#main-menu a.new-issue', 0 |
|
673 | 674 | end |
|
674 | 675 | |
|
675 | 676 | def test_project_menu_should_not_include_new_issue_link_for_users_with_copy_issues_permission_only |
|
676 | 677 | role = Role.find(1) |
|
677 | 678 | role.remove_permission! :add_issues |
|
678 | 679 | role.add_permission! :copy_issues |
|
679 | 680 | |
|
680 | 681 | @request.session[:user_id] = 2 |
|
681 | 682 | get :show, :id => 1 |
|
682 | 683 | assert_select '#main-menu a.new-issue', 0 |
|
683 | 684 | end |
|
684 | 685 | end |
@@ -1,208 +1,209 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class RolesControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :roles, :users, :members, :member_roles, :workflows, :trackers |
|
22 | 22 | |
|
23 | 23 | def setup |
|
24 | 24 | User.current = nil |
|
25 | 25 | @request.session[:user_id] = 1 # admin |
|
26 | Redmine::SudoMode.disable! | |
|
26 | 27 | end |
|
27 | 28 | |
|
28 | 29 | def test_index |
|
29 | 30 | get :index |
|
30 | 31 | assert_response :success |
|
31 | 32 | assert_template 'index' |
|
32 | 33 | |
|
33 | 34 | assert_not_nil assigns(:roles) |
|
34 | 35 | assert_equal Role.order('builtin, position').to_a, assigns(:roles) |
|
35 | 36 | |
|
36 | 37 | assert_select 'a[href="/roles/1/edit"]', :text => 'Manager' |
|
37 | 38 | end |
|
38 | 39 | |
|
39 | 40 | def test_new |
|
40 | 41 | get :new |
|
41 | 42 | assert_response :success |
|
42 | 43 | assert_template 'new' |
|
43 | 44 | end |
|
44 | 45 | |
|
45 | 46 | def test_new_with_copy |
|
46 | 47 | copy_from = Role.find(2) |
|
47 | 48 | |
|
48 | 49 | get :new, :copy => copy_from.id.to_s |
|
49 | 50 | assert_response :success |
|
50 | 51 | assert_template 'new' |
|
51 | 52 | |
|
52 | 53 | role = assigns(:role) |
|
53 | 54 | assert_equal copy_from.permissions, role.permissions |
|
54 | 55 | |
|
55 | 56 | assert_select 'form' do |
|
56 | 57 | # blank name |
|
57 | 58 | assert_select 'input[name=?][value=""]', 'role[name]' |
|
58 | 59 | # edit_project permission checked |
|
59 | 60 | assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]' |
|
60 | 61 | # add_project permission not checked |
|
61 | 62 | assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]' |
|
62 | 63 | assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0 |
|
63 | 64 | # workflow copy selected |
|
64 | 65 | assert_select 'select[name=?]', 'copy_workflow_from' do |
|
65 | 66 | assert_select 'option[value="2"][selected=selected]' |
|
66 | 67 | end |
|
67 | 68 | end |
|
68 | 69 | end |
|
69 | 70 | |
|
70 | 71 | def test_create_with_validaton_failure |
|
71 | 72 | post :create, :role => {:name => '', |
|
72 | 73 | :permissions => ['add_issues', 'edit_issues', 'log_time', ''], |
|
73 | 74 | :assignable => '0'} |
|
74 | 75 | |
|
75 | 76 | assert_response :success |
|
76 | 77 | assert_template 'new' |
|
77 | 78 | assert_select 'div#errorExplanation' |
|
78 | 79 | end |
|
79 | 80 | |
|
80 | 81 | def test_create_without_workflow_copy |
|
81 | 82 | post :create, :role => {:name => 'RoleWithoutWorkflowCopy', |
|
82 | 83 | :permissions => ['add_issues', 'edit_issues', 'log_time', ''], |
|
83 | 84 | :assignable => '0'} |
|
84 | 85 | |
|
85 | 86 | assert_redirected_to '/roles' |
|
86 | 87 | role = Role.find_by_name('RoleWithoutWorkflowCopy') |
|
87 | 88 | assert_not_nil role |
|
88 | 89 | assert_equal [:add_issues, :edit_issues, :log_time], role.permissions |
|
89 | 90 | assert !role.assignable? |
|
90 | 91 | end |
|
91 | 92 | |
|
92 | 93 | def test_create_with_workflow_copy |
|
93 | 94 | post :create, :role => {:name => 'RoleWithWorkflowCopy', |
|
94 | 95 | :permissions => ['add_issues', 'edit_issues', 'log_time', ''], |
|
95 | 96 | :assignable => '0'}, |
|
96 | 97 | :copy_workflow_from => '1' |
|
97 | 98 | |
|
98 | 99 | assert_redirected_to '/roles' |
|
99 | 100 | role = Role.find_by_name('RoleWithWorkflowCopy') |
|
100 | 101 | assert_not_nil role |
|
101 | 102 | assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size |
|
102 | 103 | end |
|
103 | 104 | |
|
104 | 105 | def test_edit |
|
105 | 106 | get :edit, :id => 1 |
|
106 | 107 | assert_response :success |
|
107 | 108 | assert_template 'edit' |
|
108 | 109 | assert_equal Role.find(1), assigns(:role) |
|
109 | 110 | assert_select 'select[name=?]', 'role[issues_visibility]' |
|
110 | 111 | end |
|
111 | 112 | |
|
112 | 113 | def test_edit_anonymous |
|
113 | 114 | get :edit, :id => Role.anonymous.id |
|
114 | 115 | assert_response :success |
|
115 | 116 | assert_template 'edit' |
|
116 | 117 | assert_select 'select[name=?]', 'role[issues_visibility]', 0 |
|
117 | 118 | end |
|
118 | 119 | |
|
119 | 120 | def test_edit_invalid_should_respond_with_404 |
|
120 | 121 | get :edit, :id => 999 |
|
121 | 122 | assert_response 404 |
|
122 | 123 | end |
|
123 | 124 | |
|
124 | 125 | def test_update |
|
125 | 126 | put :update, :id => 1, |
|
126 | 127 | :role => {:name => 'Manager', |
|
127 | 128 | :permissions => ['edit_project', ''], |
|
128 | 129 | :assignable => '0'} |
|
129 | 130 | |
|
130 | 131 | assert_redirected_to '/roles' |
|
131 | 132 | role = Role.find(1) |
|
132 | 133 | assert_equal [:edit_project], role.permissions |
|
133 | 134 | end |
|
134 | 135 | |
|
135 | 136 | def test_update_with_failure |
|
136 | 137 | put :update, :id => 1, :role => {:name => ''} |
|
137 | 138 | assert_response :success |
|
138 | 139 | assert_template 'edit' |
|
139 | 140 | end |
|
140 | 141 | |
|
141 | 142 | def test_destroy |
|
142 | 143 | r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages]) |
|
143 | 144 | |
|
144 | 145 | delete :destroy, :id => r |
|
145 | 146 | assert_redirected_to '/roles' |
|
146 | 147 | assert_nil Role.find_by_id(r.id) |
|
147 | 148 | end |
|
148 | 149 | |
|
149 | 150 | def test_destroy_role_in_use |
|
150 | 151 | delete :destroy, :id => 1 |
|
151 | 152 | assert_redirected_to '/roles' |
|
152 | 153 | assert_equal 'This role is in use and cannot be deleted.', flash[:error] |
|
153 | 154 | assert_not_nil Role.find_by_id(1) |
|
154 | 155 | end |
|
155 | 156 | |
|
156 | 157 | def test_get_permissions |
|
157 | 158 | get :permissions |
|
158 | 159 | assert_response :success |
|
159 | 160 | assert_template 'permissions' |
|
160 | 161 | |
|
161 | 162 | assert_not_nil assigns(:roles) |
|
162 | 163 | assert_equal Role.order('builtin, position').to_a, assigns(:roles) |
|
163 | 164 | |
|
164 | 165 | assert_select 'input[name=?][type=checkbox][value=add_issues][checked=checked]', 'permissions[3][]' |
|
165 | 166 | assert_select 'input[name=?][type=checkbox][value=delete_issues]:not([checked])', 'permissions[3][]' |
|
166 | 167 | end |
|
167 | 168 | |
|
168 | 169 | def test_post_permissions |
|
169 | 170 | post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']} |
|
170 | 171 | assert_redirected_to '/roles' |
|
171 | 172 | |
|
172 | 173 | assert_equal [:edit_issues], Role.find(1).permissions |
|
173 | 174 | assert_equal [:add_issues, :delete_issues], Role.find(3).permissions |
|
174 | 175 | assert Role.find(2).permissions.empty? |
|
175 | 176 | end |
|
176 | 177 | |
|
177 | 178 | def test_clear_all_permissions |
|
178 | 179 | post :permissions, :permissions => { '0' => '' } |
|
179 | 180 | assert_redirected_to '/roles' |
|
180 | 181 | assert Role.find(1).permissions.empty? |
|
181 | 182 | end |
|
182 | 183 | |
|
183 | 184 | def test_move_highest |
|
184 | 185 | put :update, :id => 3, :role => {:move_to => 'highest'} |
|
185 | 186 | assert_redirected_to '/roles' |
|
186 | 187 | assert_equal 1, Role.find(3).position |
|
187 | 188 | end |
|
188 | 189 | |
|
189 | 190 | def test_move_higher |
|
190 | 191 | position = Role.find(3).position |
|
191 | 192 | put :update, :id => 3, :role => {:move_to => 'higher'} |
|
192 | 193 | assert_redirected_to '/roles' |
|
193 | 194 | assert_equal position - 1, Role.find(3).position |
|
194 | 195 | end |
|
195 | 196 | |
|
196 | 197 | def test_move_lower |
|
197 | 198 | position = Role.find(2).position |
|
198 | 199 | put :update, :id => 2, :role => {:move_to => 'lower'} |
|
199 | 200 | assert_redirected_to '/roles' |
|
200 | 201 | assert_equal position + 1, Role.find(2).position |
|
201 | 202 | end |
|
202 | 203 | |
|
203 | 204 | def test_move_lowest |
|
204 | 205 | put :update, :id => 2, :role => {:move_to => 'lowest'} |
|
205 | 206 | assert_redirected_to '/roles' |
|
206 | 207 | assert_equal Role.count, Role.find(2).position |
|
207 | 208 | end |
|
208 | 209 | end |
@@ -1,192 +1,193 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class SettingsControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :projects, :trackers, :issue_statuses, :issues, |
|
22 | 22 | :users |
|
23 | 23 | |
|
24 | 24 | def setup |
|
25 | 25 | User.current = nil |
|
26 | 26 | @request.session[:user_id] = 1 # admin |
|
27 | Redmine::SudoMode.disable! | |
|
27 | 28 | end |
|
28 | 29 | |
|
29 | 30 | def test_index |
|
30 | 31 | get :index |
|
31 | 32 | assert_response :success |
|
32 | 33 | assert_template 'edit' |
|
33 | 34 | end |
|
34 | 35 | |
|
35 | 36 | def test_get_edit |
|
36 | 37 | get :edit |
|
37 | 38 | assert_response :success |
|
38 | 39 | assert_template 'edit' |
|
39 | 40 | |
|
40 | 41 | assert_select 'input[name=?][value=""]', 'settings[enabled_scm][]' |
|
41 | 42 | end |
|
42 | 43 | |
|
43 | 44 | def test_get_edit_should_preselect_default_issue_list_columns |
|
44 | 45 | with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do |
|
45 | 46 | get :edit |
|
46 | 47 | assert_response :success |
|
47 | 48 | end |
|
48 | 49 | |
|
49 | 50 | assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do |
|
50 | 51 | assert_select 'option', 4 |
|
51 | 52 | assert_select 'option[value=tracker]', :text => 'Tracker' |
|
52 | 53 | assert_select 'option[value=subject]', :text => 'Subject' |
|
53 | 54 | assert_select 'option[value=status]', :text => 'Status' |
|
54 | 55 | assert_select 'option[value=updated_on]', :text => 'Updated' |
|
55 | 56 | end |
|
56 | 57 | |
|
57 | 58 | assert_select 'select[id=available_columns]' do |
|
58 | 59 | assert_select 'option[value=tracker]', 0 |
|
59 | 60 | assert_select 'option[value=priority]', :text => 'Priority' |
|
60 | 61 | end |
|
61 | 62 | end |
|
62 | 63 | |
|
63 | 64 | def test_get_edit_without_trackers_should_succeed |
|
64 | 65 | Tracker.delete_all |
|
65 | 66 | |
|
66 | 67 | get :edit |
|
67 | 68 | assert_response :success |
|
68 | 69 | end |
|
69 | 70 | |
|
70 | 71 | def test_post_edit_notifications |
|
71 | 72 | post :edit, :settings => {:mail_from => 'functional@test.foo', |
|
72 | 73 | :bcc_recipients => '0', |
|
73 | 74 | :notified_events => %w(issue_added issue_updated news_added), |
|
74 | 75 | :emails_footer => 'Test footer' |
|
75 | 76 | } |
|
76 | 77 | assert_redirected_to '/settings' |
|
77 | 78 | assert_equal 'functional@test.foo', Setting.mail_from |
|
78 | 79 | assert !Setting.bcc_recipients? |
|
79 | 80 | assert_equal %w(issue_added issue_updated news_added), Setting.notified_events |
|
80 | 81 | assert_equal 'Test footer', Setting.emails_footer |
|
81 | 82 | Setting.clear_cache |
|
82 | 83 | end |
|
83 | 84 | |
|
84 | 85 | def test_edit_commit_update_keywords |
|
85 | 86 | with_settings :commit_update_keywords => [ |
|
86 | 87 | {"keywords" => "fixes, resolves", "status_id" => "3"}, |
|
87 | 88 | {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"} |
|
88 | 89 | ] do |
|
89 | 90 | get :edit |
|
90 | 91 | end |
|
91 | 92 | assert_response :success |
|
92 | 93 | assert_select 'tr.commit-keywords', 2 |
|
93 | 94 | assert_select 'tr.commit-keywords:nth-child(1)' do |
|
94 | 95 | assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves' |
|
95 | 96 | assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do |
|
96 | 97 | assert_select 'option[value="3"][selected=selected]' |
|
97 | 98 | end |
|
98 | 99 | end |
|
99 | 100 | assert_select 'tr.commit-keywords:nth-child(2)' do |
|
100 | 101 | assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes' |
|
101 | 102 | assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do |
|
102 | 103 | assert_select 'option[value="5"][selected=selected]', :text => 'Closed' |
|
103 | 104 | end |
|
104 | 105 | assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do |
|
105 | 106 | assert_select 'option[value="100"][selected=selected]', :text => '100 %' |
|
106 | 107 | end |
|
107 | 108 | assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do |
|
108 | 109 | assert_select 'option[value="2"][selected=selected]', :text => 'Feature request' |
|
109 | 110 | end |
|
110 | 111 | end |
|
111 | 112 | end |
|
112 | 113 | |
|
113 | 114 | def test_edit_without_commit_update_keywords_should_show_blank_line |
|
114 | 115 | with_settings :commit_update_keywords => [] do |
|
115 | 116 | get :edit |
|
116 | 117 | end |
|
117 | 118 | assert_response :success |
|
118 | 119 | assert_select 'tr.commit-keywords', 1 do |
|
119 | 120 | assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]' |
|
120 | 121 | end |
|
121 | 122 | end |
|
122 | 123 | |
|
123 | 124 | def test_post_edit_commit_update_keywords |
|
124 | 125 | post :edit, :settings => { |
|
125 | 126 | :commit_update_keywords => { |
|
126 | 127 | :keywords => ["resolves", "closes"], |
|
127 | 128 | :status_id => ["3", "5"], |
|
128 | 129 | :done_ratio => ["", "100"], |
|
129 | 130 | :if_tracker_id => ["", "2"] |
|
130 | 131 | } |
|
131 | 132 | } |
|
132 | 133 | assert_redirected_to '/settings' |
|
133 | 134 | assert_equal([ |
|
134 | 135 | {"keywords" => "resolves", "status_id" => "3"}, |
|
135 | 136 | {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"} |
|
136 | 137 | ], Setting.commit_update_keywords) |
|
137 | 138 | end |
|
138 | 139 | |
|
139 | 140 | def test_get_plugin_settings |
|
140 | 141 | ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins")) |
|
141 | 142 | Redmine::Plugin.register :foo do |
|
142 | 143 | settings :partial => "foo_plugin/foo_plugin_settings" |
|
143 | 144 | end |
|
144 | 145 | Setting.plugin_foo = {'sample_setting' => 'Plugin setting value'} |
|
145 | 146 | |
|
146 | 147 | get :plugin, :id => 'foo' |
|
147 | 148 | assert_response :success |
|
148 | 149 | assert_template 'plugin' |
|
149 | 150 | assert_select 'form[action="/settings/plugin/foo"]' do |
|
150 | 151 | assert_select 'input[name=?][value=?]', 'settings[sample_setting]', 'Plugin setting value' |
|
151 | 152 | end |
|
152 | 153 | ensure |
|
153 | 154 | Redmine::Plugin.unregister(:foo) |
|
154 | 155 | end |
|
155 | 156 | |
|
156 | 157 | def test_get_invalid_plugin_settings |
|
157 | 158 | get :plugin, :id => 'none' |
|
158 | 159 | assert_response 404 |
|
159 | 160 | end |
|
160 | 161 | |
|
161 | 162 | def test_get_non_configurable_plugin_settings |
|
162 | 163 | Redmine::Plugin.register(:foo) {} |
|
163 | 164 | |
|
164 | 165 | get :plugin, :id => 'foo' |
|
165 | 166 | assert_response 404 |
|
166 | 167 | |
|
167 | 168 | ensure |
|
168 | 169 | Redmine::Plugin.unregister(:foo) |
|
169 | 170 | end |
|
170 | 171 | |
|
171 | 172 | def test_post_plugin_settings |
|
172 | 173 | Redmine::Plugin.register(:foo) do |
|
173 | 174 | settings :partial => 'not blank', # so that configurable? is true |
|
174 | 175 | :default => {'sample_setting' => 'Plugin setting value'} |
|
175 | 176 | end |
|
176 | 177 | |
|
177 | 178 | post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'} |
|
178 | 179 | assert_redirected_to '/settings/plugin/foo' |
|
179 | 180 | |
|
180 | 181 | assert_equal({'sample_setting' => 'Value'}, Setting.plugin_foo) |
|
181 | 182 | end |
|
182 | 183 | |
|
183 | 184 | def test_post_non_configurable_plugin_settings |
|
184 | 185 | Redmine::Plugin.register(:foo) {} |
|
185 | 186 | |
|
186 | 187 | post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'} |
|
187 | 188 | assert_response 404 |
|
188 | 189 | |
|
189 | 190 | ensure |
|
190 | 191 | Redmine::Plugin.unregister(:foo) |
|
191 | 192 | end |
|
192 | 193 | end |
@@ -1,452 +1,453 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class UsersControllerTest < ActionController::TestCase |
|
21 | 21 | include Redmine::I18n |
|
22 | 22 | |
|
23 | 23 | fixtures :users, :email_addresses, :projects, :members, :member_roles, :roles, |
|
24 | 24 | :custom_fields, :custom_values, :groups_users, |
|
25 | 25 | :auth_sources, |
|
26 | 26 | :enabled_modules, |
|
27 | 27 | :issues, :issue_statuses, |
|
28 | 28 | :trackers |
|
29 | 29 | |
|
30 | 30 | def setup |
|
31 | 31 | User.current = nil |
|
32 | 32 | @request.session[:user_id] = 1 # admin |
|
33 | Redmine::SudoMode.disable! | |
|
33 | 34 | end |
|
34 | 35 | |
|
35 | 36 | def test_index |
|
36 | 37 | get :index |
|
37 | 38 | assert_response :success |
|
38 | 39 | assert_template 'index' |
|
39 | 40 | assert_not_nil assigns(:users) |
|
40 | 41 | # active users only |
|
41 | 42 | assert_nil assigns(:users).detect {|u| !u.active?} |
|
42 | 43 | end |
|
43 | 44 | |
|
44 | 45 | def test_index_with_status_filter |
|
45 | 46 | get :index, :status => 3 |
|
46 | 47 | assert_response :success |
|
47 | 48 | assert_template 'index' |
|
48 | 49 | assert_not_nil assigns(:users) |
|
49 | 50 | assert_equal [3], assigns(:users).map(&:status).uniq |
|
50 | 51 | end |
|
51 | 52 | |
|
52 | 53 | def test_index_with_name_filter |
|
53 | 54 | get :index, :name => 'john' |
|
54 | 55 | assert_response :success |
|
55 | 56 | assert_template 'index' |
|
56 | 57 | users = assigns(:users) |
|
57 | 58 | assert_not_nil users |
|
58 | 59 | assert_equal 1, users.size |
|
59 | 60 | assert_equal 'John', users.first.firstname |
|
60 | 61 | end |
|
61 | 62 | |
|
62 | 63 | def test_index_with_group_filter |
|
63 | 64 | get :index, :group_id => '10' |
|
64 | 65 | assert_response :success |
|
65 | 66 | assert_template 'index' |
|
66 | 67 | users = assigns(:users) |
|
67 | 68 | assert users.any? |
|
68 | 69 | assert_equal([], (users - Group.find(10).users)) |
|
69 | 70 | assert_select 'select[name=group_id]' do |
|
70 | 71 | assert_select 'option[value="10"][selected=selected]' |
|
71 | 72 | end |
|
72 | 73 | end |
|
73 | 74 | |
|
74 | 75 | def test_show |
|
75 | 76 | @request.session[:user_id] = nil |
|
76 | 77 | get :show, :id => 2 |
|
77 | 78 | assert_response :success |
|
78 | 79 | assert_template 'show' |
|
79 | 80 | assert_not_nil assigns(:user) |
|
80 | 81 | |
|
81 | 82 | assert_select 'li', :text => /Phone number/ |
|
82 | 83 | end |
|
83 | 84 | |
|
84 | 85 | def test_show_should_not_display_hidden_custom_fields |
|
85 | 86 | @request.session[:user_id] = nil |
|
86 | 87 | UserCustomField.find_by_name('Phone number').update_attribute :visible, false |
|
87 | 88 | get :show, :id => 2 |
|
88 | 89 | assert_response :success |
|
89 | 90 | assert_template 'show' |
|
90 | 91 | assert_not_nil assigns(:user) |
|
91 | 92 | |
|
92 | 93 | assert_select 'li', :text => /Phone number/, :count => 0 |
|
93 | 94 | end |
|
94 | 95 | |
|
95 | 96 | def test_show_should_not_fail_when_custom_values_are_nil |
|
96 | 97 | user = User.find(2) |
|
97 | 98 | |
|
98 | 99 | # Create a custom field to illustrate the issue |
|
99 | 100 | custom_field = CustomField.create!(:name => 'Testing', :field_format => 'text') |
|
100 | 101 | custom_value = user.custom_values.build(:custom_field => custom_field).save! |
|
101 | 102 | |
|
102 | 103 | get :show, :id => 2 |
|
103 | 104 | assert_response :success |
|
104 | 105 | end |
|
105 | 106 | |
|
106 | 107 | def test_show_inactive |
|
107 | 108 | @request.session[:user_id] = nil |
|
108 | 109 | get :show, :id => 5 |
|
109 | 110 | assert_response 404 |
|
110 | 111 | end |
|
111 | 112 | |
|
112 | 113 | def test_show_inactive_by_admin |
|
113 | 114 | @request.session[:user_id] = 1 |
|
114 | 115 | get :show, :id => 5 |
|
115 | 116 | assert_response 200 |
|
116 | 117 | assert_not_nil assigns(:user) |
|
117 | 118 | end |
|
118 | 119 | |
|
119 | 120 | def test_show_user_who_is_not_visible_should_return_404 |
|
120 | 121 | Role.anonymous.update! :users_visibility => 'members_of_visible_projects' |
|
121 | 122 | user = User.generate! |
|
122 | 123 | |
|
123 | 124 | @request.session[:user_id] = nil |
|
124 | 125 | get :show, :id => user.id |
|
125 | 126 | assert_response 404 |
|
126 | 127 | end |
|
127 | 128 | |
|
128 | 129 | def test_show_displays_memberships_based_on_project_visibility |
|
129 | 130 | @request.session[:user_id] = 1 |
|
130 | 131 | get :show, :id => 2 |
|
131 | 132 | assert_response :success |
|
132 | 133 | memberships = assigns(:memberships) |
|
133 | 134 | assert_not_nil memberships |
|
134 | 135 | project_ids = memberships.map(&:project_id) |
|
135 | 136 | assert project_ids.include?(2) #private project admin can see |
|
136 | 137 | end |
|
137 | 138 | |
|
138 | 139 | def test_show_current_should_require_authentication |
|
139 | 140 | @request.session[:user_id] = nil |
|
140 | 141 | get :show, :id => 'current' |
|
141 | 142 | assert_response 302 |
|
142 | 143 | end |
|
143 | 144 | |
|
144 | 145 | def test_show_current |
|
145 | 146 | @request.session[:user_id] = 2 |
|
146 | 147 | get :show, :id => 'current' |
|
147 | 148 | assert_response :success |
|
148 | 149 | assert_template 'show' |
|
149 | 150 | assert_equal User.find(2), assigns(:user) |
|
150 | 151 | end |
|
151 | 152 | |
|
152 | 153 | def test_new |
|
153 | 154 | get :new |
|
154 | 155 | assert_response :success |
|
155 | 156 | assert_template :new |
|
156 | 157 | assert assigns(:user) |
|
157 | 158 | end |
|
158 | 159 | |
|
159 | 160 | def test_create |
|
160 | 161 | Setting.bcc_recipients = '1' |
|
161 | 162 | |
|
162 | 163 | assert_difference 'User.count' do |
|
163 | 164 | assert_difference 'ActionMailer::Base.deliveries.size' do |
|
164 | 165 | post :create, |
|
165 | 166 | :user => { |
|
166 | 167 | :firstname => 'John', |
|
167 | 168 | :lastname => 'Doe', |
|
168 | 169 | :login => 'jdoe', |
|
169 | 170 | :password => 'secret123', |
|
170 | 171 | :password_confirmation => 'secret123', |
|
171 | 172 | :mail => 'jdoe@gmail.com', |
|
172 | 173 | :mail_notification => 'none' |
|
173 | 174 | }, |
|
174 | 175 | :send_information => '1' |
|
175 | 176 | end |
|
176 | 177 | end |
|
177 | 178 | |
|
178 | 179 | user = User.order('id DESC').first |
|
179 | 180 | assert_redirected_to :controller => 'users', :action => 'edit', :id => user.id |
|
180 | 181 | |
|
181 | 182 | assert_equal 'John', user.firstname |
|
182 | 183 | assert_equal 'Doe', user.lastname |
|
183 | 184 | assert_equal 'jdoe', user.login |
|
184 | 185 | assert_equal 'jdoe@gmail.com', user.mail |
|
185 | 186 | assert_equal 'none', user.mail_notification |
|
186 | 187 | assert user.check_password?('secret123') |
|
187 | 188 | |
|
188 | 189 | mail = ActionMailer::Base.deliveries.last |
|
189 | 190 | assert_not_nil mail |
|
190 | 191 | assert_equal [user.mail], mail.bcc |
|
191 | 192 | assert_mail_body_match 'secret', mail |
|
192 | 193 | end |
|
193 | 194 | |
|
194 | 195 | def test_create_with_preferences |
|
195 | 196 | assert_difference 'User.count' do |
|
196 | 197 | post :create, |
|
197 | 198 | :user => { |
|
198 | 199 | :firstname => 'John', |
|
199 | 200 | :lastname => 'Doe', |
|
200 | 201 | :login => 'jdoe', |
|
201 | 202 | :password => 'secret123', |
|
202 | 203 | :password_confirmation => 'secret123', |
|
203 | 204 | :mail => 'jdoe@gmail.com', |
|
204 | 205 | :mail_notification => 'none' |
|
205 | 206 | }, |
|
206 | 207 | :pref => { |
|
207 | 208 | 'hide_mail' => '1', |
|
208 | 209 | 'time_zone' => 'Paris', |
|
209 | 210 | 'comments_sorting' => 'desc', |
|
210 | 211 | 'warn_on_leaving_unsaved' => '0' |
|
211 | 212 | } |
|
212 | 213 | end |
|
213 | 214 | user = User.order('id DESC').first |
|
214 | 215 | assert_equal 'jdoe', user.login |
|
215 | 216 | assert_equal true, user.pref.hide_mail |
|
216 | 217 | assert_equal 'Paris', user.pref.time_zone |
|
217 | 218 | assert_equal 'desc', user.pref[:comments_sorting] |
|
218 | 219 | assert_equal '0', user.pref[:warn_on_leaving_unsaved] |
|
219 | 220 | end |
|
220 | 221 | |
|
221 | 222 | def test_create_with_generate_password_should_email_the_password |
|
222 | 223 | assert_difference 'User.count' do |
|
223 | 224 | post :create, :user => { |
|
224 | 225 | :login => 'randompass', |
|
225 | 226 | :firstname => 'Random', |
|
226 | 227 | :lastname => 'Pass', |
|
227 | 228 | :mail => 'randompass@example.net', |
|
228 | 229 | :language => 'en', |
|
229 | 230 | :generate_password => '1', |
|
230 | 231 | :password => '', |
|
231 | 232 | :password_confirmation => '' |
|
232 | 233 | }, :send_information => 1 |
|
233 | 234 | end |
|
234 | 235 | user = User.order('id DESC').first |
|
235 | 236 | assert_equal 'randompass', user.login |
|
236 | 237 | |
|
237 | 238 | mail = ActionMailer::Base.deliveries.last |
|
238 | 239 | assert_not_nil mail |
|
239 | 240 | m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/) |
|
240 | 241 | assert m |
|
241 | 242 | password = m[1] |
|
242 | 243 | assert user.check_password?(password) |
|
243 | 244 | end |
|
244 | 245 | |
|
245 | 246 | def test_create_and_continue |
|
246 | 247 | post :create, :user => { |
|
247 | 248 | :login => 'randompass', |
|
248 | 249 | :firstname => 'Random', |
|
249 | 250 | :lastname => 'Pass', |
|
250 | 251 | :mail => 'randompass@example.net', |
|
251 | 252 | :generate_password => '1' |
|
252 | 253 | }, :continue => '1' |
|
253 | 254 | assert_redirected_to '/users/new?user%5Bgenerate_password%5D=1' |
|
254 | 255 | end |
|
255 | 256 | |
|
256 | 257 | def test_create_with_failure |
|
257 | 258 | assert_no_difference 'User.count' do |
|
258 | 259 | post :create, :user => {} |
|
259 | 260 | end |
|
260 | 261 | assert_response :success |
|
261 | 262 | assert_template 'new' |
|
262 | 263 | end |
|
263 | 264 | |
|
264 | 265 | def test_create_with_failure_sould_preserve_preference |
|
265 | 266 | assert_no_difference 'User.count' do |
|
266 | 267 | post :create, |
|
267 | 268 | :user => {}, |
|
268 | 269 | :pref => { |
|
269 | 270 | 'no_self_notified' => '1', |
|
270 | 271 | 'hide_mail' => '1', |
|
271 | 272 | 'time_zone' => 'Paris', |
|
272 | 273 | 'comments_sorting' => 'desc', |
|
273 | 274 | 'warn_on_leaving_unsaved' => '0' |
|
274 | 275 | } |
|
275 | 276 | end |
|
276 | 277 | assert_response :success |
|
277 | 278 | assert_template 'new' |
|
278 | 279 | |
|
279 | 280 | assert_select 'select#pref_time_zone option[selected=selected]', :text => /Paris/ |
|
280 | 281 | assert_select 'input#pref_no_self_notified[value="1"][checked=checked]' |
|
281 | 282 | end |
|
282 | 283 | |
|
283 | 284 | def test_edit |
|
284 | 285 | get :edit, :id => 2 |
|
285 | 286 | assert_response :success |
|
286 | 287 | assert_template 'edit' |
|
287 | 288 | assert_equal User.find(2), assigns(:user) |
|
288 | 289 | end |
|
289 | 290 | |
|
290 | 291 | def test_edit_registered_user |
|
291 | 292 | assert User.find(2).register! |
|
292 | 293 | |
|
293 | 294 | get :edit, :id => 2 |
|
294 | 295 | assert_response :success |
|
295 | 296 | assert_select 'a', :text => 'Activate' |
|
296 | 297 | end |
|
297 | 298 | |
|
298 | 299 | def test_update |
|
299 | 300 | ActionMailer::Base.deliveries.clear |
|
300 | 301 | put :update, :id => 2, |
|
301 | 302 | :user => {:firstname => 'Changed', :mail_notification => 'only_assigned'}, |
|
302 | 303 | :pref => {:hide_mail => '1', :comments_sorting => 'desc'} |
|
303 | 304 | user = User.find(2) |
|
304 | 305 | assert_equal 'Changed', user.firstname |
|
305 | 306 | assert_equal 'only_assigned', user.mail_notification |
|
306 | 307 | assert_equal true, user.pref[:hide_mail] |
|
307 | 308 | assert_equal 'desc', user.pref[:comments_sorting] |
|
308 | 309 | assert ActionMailer::Base.deliveries.empty? |
|
309 | 310 | end |
|
310 | 311 | |
|
311 | 312 | def test_update_with_failure |
|
312 | 313 | assert_no_difference 'User.count' do |
|
313 | 314 | put :update, :id => 2, :user => {:firstname => ''} |
|
314 | 315 | end |
|
315 | 316 | assert_response :success |
|
316 | 317 | assert_template 'edit' |
|
317 | 318 | end |
|
318 | 319 | |
|
319 | 320 | def test_update_with_group_ids_should_assign_groups |
|
320 | 321 | put :update, :id => 2, :user => {:group_ids => ['10']} |
|
321 | 322 | user = User.find(2) |
|
322 | 323 | assert_equal [10], user.group_ids |
|
323 | 324 | end |
|
324 | 325 | |
|
325 | 326 | def test_update_with_activation_should_send_a_notification |
|
326 | 327 | u = User.new(:firstname => 'Foo', :lastname => 'Bar', :mail => 'foo.bar@somenet.foo', :language => 'fr') |
|
327 | 328 | u.login = 'foo' |
|
328 | 329 | u.status = User::STATUS_REGISTERED |
|
329 | 330 | u.save! |
|
330 | 331 | ActionMailer::Base.deliveries.clear |
|
331 | 332 | Setting.bcc_recipients = '1' |
|
332 | 333 | |
|
333 | 334 | put :update, :id => u.id, :user => {:status => User::STATUS_ACTIVE} |
|
334 | 335 | assert u.reload.active? |
|
335 | 336 | mail = ActionMailer::Base.deliveries.last |
|
336 | 337 | assert_not_nil mail |
|
337 | 338 | assert_equal ['foo.bar@somenet.foo'], mail.bcc |
|
338 | 339 | assert_mail_body_match ll('fr', :notice_account_activated), mail |
|
339 | 340 | end |
|
340 | 341 | |
|
341 | 342 | def test_update_with_password_change_should_send_a_notification |
|
342 | 343 | ActionMailer::Base.deliveries.clear |
|
343 | 344 | Setting.bcc_recipients = '1' |
|
344 | 345 | |
|
345 | 346 | put :update, :id => 2, :user => {:password => 'newpass123', :password_confirmation => 'newpass123'}, :send_information => '1' |
|
346 | 347 | u = User.find(2) |
|
347 | 348 | assert u.check_password?('newpass123') |
|
348 | 349 | |
|
349 | 350 | mail = ActionMailer::Base.deliveries.last |
|
350 | 351 | assert_not_nil mail |
|
351 | 352 | assert_equal [u.mail], mail.bcc |
|
352 | 353 | assert_mail_body_match 'newpass123', mail |
|
353 | 354 | end |
|
354 | 355 | |
|
355 | 356 | def test_update_with_generate_password_should_email_the_password |
|
356 | 357 | ActionMailer::Base.deliveries.clear |
|
357 | 358 | Setting.bcc_recipients = '1' |
|
358 | 359 | |
|
359 | 360 | put :update, :id => 2, :user => { |
|
360 | 361 | :generate_password => '1', |
|
361 | 362 | :password => '', |
|
362 | 363 | :password_confirmation => '' |
|
363 | 364 | }, :send_information => '1' |
|
364 | 365 | |
|
365 | 366 | mail = ActionMailer::Base.deliveries.last |
|
366 | 367 | assert_not_nil mail |
|
367 | 368 | m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/) |
|
368 | 369 | assert m |
|
369 | 370 | password = m[1] |
|
370 | 371 | assert User.find(2).check_password?(password) |
|
371 | 372 | end |
|
372 | 373 | |
|
373 | 374 | def test_update_without_generate_password_should_not_change_password |
|
374 | 375 | put :update, :id => 2, :user => { |
|
375 | 376 | :firstname => 'changed', |
|
376 | 377 | :generate_password => '0', |
|
377 | 378 | :password => '', |
|
378 | 379 | :password_confirmation => '' |
|
379 | 380 | }, :send_information => '1' |
|
380 | 381 | |
|
381 | 382 | user = User.find(2) |
|
382 | 383 | assert_equal 'changed', user.firstname |
|
383 | 384 | assert user.check_password?('jsmith') |
|
384 | 385 | end |
|
385 | 386 | |
|
386 | 387 | def test_update_user_switchin_from_auth_source_to_password_authentication |
|
387 | 388 | # Configure as auth source |
|
388 | 389 | u = User.find(2) |
|
389 | 390 | u.auth_source = AuthSource.find(1) |
|
390 | 391 | u.save! |
|
391 | 392 | |
|
392 | 393 | put :update, :id => u.id, :user => {:auth_source_id => '', :password => 'newpass123', :password_confirmation => 'newpass123'} |
|
393 | 394 | |
|
394 | 395 | assert_equal nil, u.reload.auth_source |
|
395 | 396 | assert u.check_password?('newpass123') |
|
396 | 397 | end |
|
397 | 398 | |
|
398 | 399 | def test_update_notified_project |
|
399 | 400 | get :edit, :id => 2 |
|
400 | 401 | assert_response :success |
|
401 | 402 | assert_template 'edit' |
|
402 | 403 | u = User.find(2) |
|
403 | 404 | assert_equal [1, 2, 5], u.projects.collect{|p| p.id}.sort |
|
404 | 405 | assert_equal [1, 2, 5], u.notified_projects_ids.sort |
|
405 | 406 | assert_select 'input[name=?][value=?]', 'user[notified_project_ids][]', '1' |
|
406 | 407 | assert_equal 'all', u.mail_notification |
|
407 | 408 | put :update, :id => 2, |
|
408 | 409 | :user => { |
|
409 | 410 | :mail_notification => 'selected', |
|
410 | 411 | :notified_project_ids => [1, 2] |
|
411 | 412 | } |
|
412 | 413 | u = User.find(2) |
|
413 | 414 | assert_equal 'selected', u.mail_notification |
|
414 | 415 | assert_equal [1, 2], u.notified_projects_ids.sort |
|
415 | 416 | end |
|
416 | 417 | |
|
417 | 418 | def test_update_status_should_not_update_attributes |
|
418 | 419 | user = User.find(2) |
|
419 | 420 | user.pref[:no_self_notified] = '1' |
|
420 | 421 | user.pref.save |
|
421 | 422 | |
|
422 | 423 | put :update, :id => 2, :user => {:status => 3} |
|
423 | 424 | assert_response 302 |
|
424 | 425 | user = User.find(2) |
|
425 | 426 | assert_equal 3, user.status |
|
426 | 427 | assert_equal '1', user.pref[:no_self_notified] |
|
427 | 428 | end |
|
428 | 429 | |
|
429 | 430 | def test_destroy |
|
430 | 431 | assert_difference 'User.count', -1 do |
|
431 | 432 | delete :destroy, :id => 2 |
|
432 | 433 | end |
|
433 | 434 | assert_redirected_to '/users' |
|
434 | 435 | assert_nil User.find_by_id(2) |
|
435 | 436 | end |
|
436 | 437 | |
|
437 | 438 | def test_destroy_should_be_denied_for_non_admin_users |
|
438 | 439 | @request.session[:user_id] = 3 |
|
439 | 440 | |
|
440 | 441 | assert_no_difference 'User.count' do |
|
441 | 442 | get :destroy, :id => 2 |
|
442 | 443 | end |
|
443 | 444 | assert_response 403 |
|
444 | 445 | end |
|
445 | 446 | |
|
446 | 447 | def test_destroy_should_redirect_to_back_url_param |
|
447 | 448 | assert_difference 'User.count', -1 do |
|
448 | 449 | delete :destroy, :id => 2, :back_url => '/users?name=foo' |
|
449 | 450 | end |
|
450 | 451 | assert_redirected_to '/users?name=foo' |
|
451 | 452 | end |
|
452 | 453 | end |
@@ -1,61 +1,78 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class AdminTest < Redmine::IntegrationTest |
|
21 | 21 | fixtures :projects, :trackers, :issue_statuses, :issues, |
|
22 | 22 | :enumerations, :users, :issue_categories, |
|
23 | 23 | :projects_trackers, |
|
24 | 24 | :roles, |
|
25 | 25 | :member_roles, |
|
26 | 26 | :members, |
|
27 | 27 | :enabled_modules |
|
28 | 28 | |
|
29 | def setup | |
|
30 | Redmine::SudoMode.enable! | |
|
31 | end | |
|
32 | ||
|
33 | def teardown | |
|
34 | Redmine::SudoMode.disable! | |
|
35 | end | |
|
36 | ||
|
29 | 37 | def test_add_user |
|
30 | 38 | log_user("admin", "admin") |
|
31 | 39 | get "/users/new" |
|
32 | 40 | assert_response :success |
|
33 | 41 | assert_template "users/new" |
|
34 | 42 | post "/users", |
|
35 | 43 | :user => { :login => "psmith", :firstname => "Paul", |
|
36 | 44 | :lastname => "Smith", :mail => "psmith@somenet.foo", |
|
37 | 45 | :language => "en", :password => "psmith09", |
|
38 | 46 | :password_confirmation => "psmith09" } |
|
47 | assert_response :success | |
|
48 | assert_nil User.find_by_login("psmith") | |
|
49 | ||
|
50 | post "/users", | |
|
51 | :user => { :login => "psmith", :firstname => "Paul", | |
|
52 | :lastname => "Smith", :mail => "psmith@somenet.foo", | |
|
53 | :language => "en", :password => "psmith09", | |
|
54 | :password_confirmation => "psmith09" }, | |
|
55 | :sudo_password => 'admin' | |
|
39 | 56 | |
|
40 | 57 | user = User.find_by_login("psmith") |
|
41 | 58 | assert_kind_of User, user |
|
42 | 59 | assert_redirected_to "/users/#{ user.id }/edit" |
|
43 | 60 | |
|
44 | 61 | logged_user = User.try_to_login("psmith", "psmith09") |
|
45 | 62 | assert_kind_of User, logged_user |
|
46 | 63 | assert_equal "Paul", logged_user.firstname |
|
47 | 64 | |
|
48 | 65 | put "/users/#{user.id}", :id => user.id, :user => { :status => User::STATUS_LOCKED } |
|
49 | 66 | assert_redirected_to "/users/#{ user.id }/edit" |
|
50 | 67 | locked_user = User.try_to_login("psmith", "psmith09") |
|
51 | 68 | assert_equal nil, locked_user |
|
52 | 69 | end |
|
53 | 70 | |
|
54 | 71 | test "Add a user as an anonymous user should fail" do |
|
55 | 72 | post '/users', |
|
56 | 73 | :user => { :login => 'psmith', :firstname => 'Paul'}, |
|
57 | 74 | :password => "psmith09", :password_confirmation => "psmith09" |
|
58 | 75 | assert_response :redirect |
|
59 | 76 | assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fusers" |
|
60 | 77 | end |
|
61 | 78 | end |
General Comments 0
You need to be logged in to leave comments.
Login now