##// END OF EJS Templates
Add support for multiple email addresses per user (#4244)....
Jean-Philippe Lang -
r13504:e3618bdbecd9
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,105
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class EmailAddressesController < ApplicationController
19 before_filter :find_user, :require_admin_or_current_user
20 before_filter :find_email_address, :only => [:update, :destroy]
21
22 def index
23 @addresses = @user.email_addresses.order(:id).where(:is_default => false).to_a
24 @address ||= EmailAddress.new
25 end
26
27 def create
28 saved = false
29 if @user.email_addresses.count <= Setting.max_additional_emails.to_i
30 @address = EmailAddress.new(:user => @user, :is_default => false)
31 attrs = params[:email_address]
32 if attrs.is_a?(Hash)
33 @address.address = attrs[:address].to_s
34 end
35 saved = @address.save
36 end
37
38 respond_to do |format|
39 format.html {
40 if saved
41 redirect_to user_email_addresses_path(@user)
42 else
43 index
44 render :action => 'index'
45 end
46 }
47 format.js {
48 @address = nil if saved
49 index
50 render :action => 'index'
51 }
52 end
53 end
54
55 def update
56 if params[:notify].present?
57 @address.notify = params[:notify].to_s
58 end
59 @address.save
60
61 respond_to do |format|
62 format.html {
63 redirect_to user_email_addresses_path(@user)
64 }
65 format.js {
66 @address = nil
67 index
68 render :action => 'index'
69 }
70 end
71 end
72
73 def destroy
74 @address.destroy
75
76 respond_to do |format|
77 format.html {
78 redirect_to user_email_addresses_path(@user)
79 }
80 format.js {
81 @address = nil
82 index
83 render :action => 'index'
84 }
85 end
86 end
87
88 private
89
90 def find_user
91 @user = User.find(params[:user_id])
92 end
93
94 def find_email_address
95 @address = @user.email_addresses.where(:is_default => false).find(params[:id])
96 rescue ActiveRecord::RecordNotFound
97 render_404
98 end
99
100 def require_admin_or_current_user
101 unless @user == User.current
102 require_admin
103 end
104 end
105 end
@@ -0,0 +1,38
1 # encoding: utf-8
2 #
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
20 module EmailAddressesHelper
21
22 # Returns a link to enable or disable notifications for the address
23 def toggle_email_address_notify_link(address)
24 if address.notify?
25 link_to image_tag('email.png'),
26 user_email_address_path(address.user, address, :notify => '0'),
27 :method => :put,
28 :title => l(:label_disable_notifications),
29 :remote => true
30 else
31 link_to image_tag('email_disabled.png'),
32 user_email_address_path(address.user, address, :notify => '1'),
33 :method => :put,
34 :title => l(:label_enable_notifications),
35 :remote => true
36 end
37 end
38 end
@@ -0,0 +1,54
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class EmailAddress < ActiveRecord::Base
19 belongs_to :user
20 attr_protected :id
21
22 after_update :destroy_tokens
23 after_destroy :destroy_tokens
24
25 validates_presence_of :address
26 validates_format_of :address, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
27 validates_length_of :address, :maximum => User::MAIL_LENGTH_LIMIT, :allow_nil => true
28 validates_uniqueness_of :address, :case_sensitive => false,
29 :if => Proc.new {|email| email.address_changed? && email.address.present?}
30
31 def address=(arg)
32 write_attribute(:address, arg.to_s.strip)
33 end
34
35 def destroy
36 if is_default?
37 false
38 else
39 super
40 end
41 end
42
43 private
44
45 # Delete all outstanding password reset tokens on email change.
46 # This helps to keep the account secure in case the associated email account
47 # was compromised.
48 def destroy_tokens
49 if address_changed? || destroyed?
50 tokens = ['recovery']
51 Token.where(:user_id => user_id, :action => tokens).delete_all
52 end
53 end
54 end
@@ -0,0 +1,26
1 <% if @addresses.present? %>
2 <table class="list email_addresses">
3 <% @addresses.each do |address| %>
4 <tr class="<%= cycle("odd", "even") %>">
5 <td class="email"><%= address.address %></td>
6 <td class="buttons">
7 <%= toggle_email_address_notify_link(address) %>
8 <%= delete_link user_email_address_path(@user, address), :remote => true %>
9 </td>
10 </tr>
11 <% end %>
12 </table>
13 <% end %>
14
15 <% unless @addresses.size >= Setting.max_additional_emails.to_i %>
16 <div>
17 <%= form_for @address, :url => user_email_addresses_path(@user), :remote => true do |f| %>
18 <p><%= l(:label_email_address_add) %></p>
19 <%= error_messages_for @address %>
20 <p>
21 <%= f.text_field :address, :size => 40 %>
22 <%= submit_tag l(:button_add) %>
23 </p>
24 <% end %>
25 </div>
26 <% end %>
@@ -0,0 +1,2
1 <h2><%= @user.name %></h2>
2 <%= render :partial => 'email_addresses/index' %>
@@ -0,0 +1,3
1 $('#ajax-modal').html('<%= escape_javascript(render :partial => 'email_addresses/index') %>');
2 showModal('ajax-modal', '600px', '<%= escape_javascript l(:label_email_address_plural) %>');
3 $('#email_address_address').focus();
@@ -0,0 +1,12
1 class CreateEmailAddresses < ActiveRecord::Migration
2 def change
3 create_table :email_addresses do |t|
4 t.column :user_id, :integer, :null => false
5 t.column :address, :string, :null => false
6 t.column :is_default, :boolean, :null => false, :default => false
7 t.column :notify, :boolean, :null => false, :default => true
8 t.column :created_on, :timestamp, :null => false
9 t.column :updated_on, :timestamp, :null => false
10 end
11 end
12 end
@@ -0,0 +1,14
1 class PopulateEmailAddresses < ActiveRecord::Migration
2 def self.up
3 t = EmailAddress.connection.quoted_true
4 n = EmailAddress.connection.quoted_date(Time.now)
5
6 sql = "INSERT INTO #{EmailAddress.table_name} (user_id, address, is_default, notify, created_on, updated_on)" +
7 " SELECT id, mail, #{t}, #{t}, '#{n}', '#{n}' FROM #{User.table_name} WHERE type = 'User' ORDER BY id"
8 EmailAddress.connection.execute(sql)
9 end
10
11 def self.down
12 EmailAddress.delete_all
13 end
14 end
@@ -0,0 +1,9
1 class RemoveUsersMail < ActiveRecord::Migration
2 def self.up
3 remove_column :users, :mail
4 end
5
6 def self.down
7 raise IrreversibleMigration
8 end
9 end
@@ -0,0 +1,9
1 class AddEmailAddressesUserIdIndex < ActiveRecord::Migration
2 def up
3 add_index :email_addresses, :user_id
4 end
5
6 def down
7 remove_index :email_addresses, :user_id
8 end
9 end
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,57
1 ---
2 email_address_001:
3 id: 1
4 user_id: 1
5 address: admin@somenet.foo
6 is_default: true
7 created_on: 2006-07-19 19:34:07 +02:00
8 updated_on: 2006-07-19 19:34:07 +02:00
9 email_address_002:
10 id: 2
11 user_id: 2
12 address: jsmith@somenet.foo
13 is_default: true
14 created_on: 2006-07-19 19:34:07 +02:00
15 updated_on: 2006-07-19 19:34:07 +02:00
16 email_address_003:
17 id: 3
18 user_id: 3
19 address: dlopper@somenet.foo
20 is_default: true
21 created_on: 2006-07-19 19:34:07 +02:00
22 updated_on: 2006-07-19 19:34:07 +02:00
23 email_address_004:
24 id: 4
25 user_id: 4
26 address: rhill@somenet.foo
27 is_default: true
28 created_on: 2006-07-19 19:34:07 +02:00
29 updated_on: 2006-07-19 19:34:07 +02:00
30 email_address_005:
31 id: 5
32 user_id: 5
33 address: dlopper2@somenet.foo
34 is_default: true
35 created_on: 2006-07-19 19:34:07 +02:00
36 updated_on: 2006-07-19 19:34:07 +02:00
37 email_address_007:
38 id: 7
39 user_id: 7
40 address: someone@foo.bar
41 is_default: true
42 created_on: 2006-07-19 19:34:07 +02:00
43 updated_on: 2006-07-19 19:34:07 +02:00
44 email_address_008:
45 id: 8
46 user_id: 8
47 address: miscuser8@foo.bar
48 is_default: true
49 created_on: 2006-07-19 19:34:07 +02:00
50 updated_on: 2006-07-19 19:34:07 +02:00
51 email_address_009:
52 id: 9
53 user_id: 9
54 address: miscuser9@foo.bar
55 is_default: true
56 created_on: 2006-07-19 19:34:07 +02:00
57 updated_on: 2006-07-19 19:34:07 +02:00
@@ -0,0 +1,144
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../test_helper', __FILE__)
19
20 class EmailAddressesControllerTest < ActionController::TestCase
21 fixtures :users, :email_addresses
22
23 def setup
24 User.current = nil
25 end
26
27 def test_index_with_no_additional_emails
28 @request.session[:user_id] = 2
29 get :index, :user_id => 2
30 assert_response :success
31 assert_template 'index'
32 end
33
34 def test_index_with_additional_emails
35 @request.session[:user_id] = 2
36 EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
37
38 get :index, :user_id => 2
39 assert_response :success
40 assert_template 'index'
41 assert_select '.email', :text => 'another@somenet.foo'
42 end
43
44 def test_index_with_additional_emails_as_js
45 @request.session[:user_id] = 2
46 EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
47
48 xhr :get, :index, :user_id => 2
49 assert_response :success
50 assert_template 'index'
51 assert_include 'another@somenet.foo', response.body
52 end
53
54 def test_index_by_admin_should_be_allowed
55 @request.session[:user_id] = 1
56 get :index, :user_id => 2
57 assert_response :success
58 assert_template 'index'
59 end
60
61 def test_index_by_another_user_should_be_denied
62 @request.session[:user_id] = 3
63 get :index, :user_id => 2
64 assert_response 403
65 end
66
67 def test_create
68 @request.session[:user_id] = 2
69 assert_difference 'EmailAddress.count' do
70 post :create, :user_id => 2, :email_address => {:address => 'another@somenet.foo'}
71 assert_response 302
72 assert_redirected_to '/users/2/email_addresses'
73 end
74 email = EmailAddress.order('id DESC').first
75 assert_equal 2, email.user_id
76 assert_equal 'another@somenet.foo', email.address
77 end
78
79 def test_create_as_js
80 @request.session[:user_id] = 2
81 assert_difference 'EmailAddress.count' do
82 xhr :post, :create, :user_id => 2, :email_address => {:address => 'another@somenet.foo'}
83 assert_response 200
84 end
85 end
86
87 def test_create_with_failure
88 @request.session[:user_id] = 2
89 assert_no_difference 'EmailAddress.count' do
90 post :create, :user_id => 2, :email_address => {:address => 'invalid'}
91 assert_response 200
92 end
93 end
94
95 def test_update
96 @request.session[:user_id] = 2
97 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
98
99 put :update, :user_id => 2, :id => email.id, :notify => '0'
100 assert_response 302
101
102 assert_equal false, email.reload.notify
103 end
104
105 def test_update_as_js
106 @request.session[:user_id] = 2
107 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
108
109 xhr :put, :update, :user_id => 2, :id => email.id, :notify => '0'
110 assert_response 200
111
112 assert_equal false, email.reload.notify
113 end
114
115 def test_destroy
116 @request.session[:user_id] = 2
117 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
118
119 assert_difference 'EmailAddress.count', -1 do
120 delete :destroy, :user_id => 2, :id => email.id
121 assert_response 302
122 assert_redirected_to '/users/2/email_addresses'
123 end
124 end
125
126 def test_destroy_as_js
127 @request.session[:user_id] = 2
128 email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo')
129
130 assert_difference 'EmailAddress.count', -1 do
131 xhr :delete, :destroy, :user_id => 2, :id => email.id
132 assert_response 200
133 end
134 end
135
136 def test_should_not_destroy_default
137 @request.session[:user_id] = 2
138
139 assert_no_difference 'EmailAddress.count' do
140 delete :destroy, :user_id => 2, :id => User.find(2).email_address.id
141 assert_response 404
142 end
143 end
144 end
@@ -1,188 +1,188
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 31 def index
32 32 sort_init 'login', 'asc'
33 33 sort_update %w(login firstname lastname mail admin created_on last_login_on)
34 34
35 35 case params[:format]
36 36 when 'xml', 'json'
37 37 @offset, @limit = api_offset_and_limit
38 38 else
39 39 @limit = per_page_option
40 40 end
41 41
42 42 @status = params[:status] || 1
43 43
44 scope = User.logged.status(@status)
44 scope = User.logged.status(@status).preload(:email_address)
45 45 scope = scope.like(params[:name]) if params[:name].present?
46 46 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
47 47
48 48 @user_count = scope.count
49 49 @user_pages = Paginator.new @user_count, @limit, params['page']
50 50 @offset ||= @user_pages.offset
51 51 @users = scope.order(sort_clause).limit(@limit).offset(@offset).to_a
52 52
53 53 respond_to do |format|
54 54 format.html {
55 55 @groups = Group.all.sort
56 56 render :layout => !request.xhr?
57 57 }
58 58 format.api
59 59 end
60 60 end
61 61
62 62 def show
63 63 unless @user.visible?
64 64 render_404
65 65 return
66 66 end
67 67
68 68 # show projects based on current user visibility
69 69 @memberships = @user.memberships.where(Project.visible_condition(User.current)).to_a
70 70
71 71 respond_to do |format|
72 72 format.html {
73 73 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
74 74 @events_by_day = events.group_by(&:event_date)
75 75 render :layout => 'base'
76 76 }
77 77 format.api
78 78 end
79 79 end
80 80
81 81 def new
82 82 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
83 83 @user.safe_attributes = params[:user]
84 84 @auth_sources = AuthSource.all
85 85 end
86 86
87 87 def create
88 88 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
89 89 @user.safe_attributes = params[:user]
90 90 @user.admin = params[:user][:admin] || false
91 91 @user.login = params[:user][:login]
92 92 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
93 93 @user.pref.attributes = params[:pref] if params[:pref]
94 94
95 95 if @user.save
96 96 Mailer.account_information(@user, @user.password).deliver if params[:send_information]
97 97
98 98 respond_to do |format|
99 99 format.html {
100 100 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
101 101 if params[:continue]
102 102 attrs = params[:user].slice(:generate_password)
103 103 redirect_to new_user_path(:user => attrs)
104 104 else
105 105 redirect_to edit_user_path(@user)
106 106 end
107 107 }
108 108 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
109 109 end
110 110 else
111 111 @auth_sources = AuthSource.all
112 112 # Clear password input
113 113 @user.password = @user.password_confirmation = nil
114 114
115 115 respond_to do |format|
116 116 format.html { render :action => 'new' }
117 117 format.api { render_validation_errors(@user) }
118 118 end
119 119 end
120 120 end
121 121
122 122 def edit
123 123 @auth_sources = AuthSource.all
124 124 @membership ||= Member.new
125 125 end
126 126
127 127 def update
128 128 @user.admin = params[:user][:admin] if params[:user][:admin]
129 129 @user.login = params[:user][:login] if params[:user][:login]
130 130 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
131 131 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
132 132 end
133 133 @user.safe_attributes = params[:user]
134 134 # Was the account actived ? (do it before User#save clears the change)
135 135 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
136 136 # TODO: Similar to My#account
137 137 @user.pref.attributes = params[:pref] if params[:pref]
138 138
139 139 if @user.save
140 140 @user.pref.save
141 141
142 142 if was_activated
143 143 Mailer.account_activated(@user).deliver
144 144 elsif @user.active? && params[:send_information] && @user.password.present? && @user.auth_source_id.nil?
145 145 Mailer.account_information(@user, @user.password).deliver
146 146 end
147 147
148 148 respond_to do |format|
149 149 format.html {
150 150 flash[:notice] = l(:notice_successful_update)
151 151 redirect_to_referer_or edit_user_path(@user)
152 152 }
153 153 format.api { render_api_ok }
154 154 end
155 155 else
156 156 @auth_sources = AuthSource.all
157 157 @membership ||= Member.new
158 158 # Clear password input
159 159 @user.password = @user.password_confirmation = nil
160 160
161 161 respond_to do |format|
162 162 format.html { render :action => :edit }
163 163 format.api { render_validation_errors(@user) }
164 164 end
165 165 end
166 166 end
167 167
168 168 def destroy
169 169 @user.destroy
170 170 respond_to do |format|
171 171 format.html { redirect_back_or_default(users_path) }
172 172 format.api { render_api_ok }
173 173 end
174 174 end
175 175
176 176 private
177 177
178 178 def find_user
179 179 if params[:id] == 'current'
180 180 require_login || return
181 181 @user = User.current
182 182 else
183 183 @user = User.find(params[:id])
184 184 end
185 185 rescue ActiveRecord::RecordNotFound
186 186 render_404
187 187 end
188 188 end
@@ -1,54 +1,60
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 module UsersHelper
21 21 def users_status_options_for_select(selected)
22 22 user_count_by_status = User.group('status').count.to_hash
23 23 options_for_select([[l(:label_all), ''],
24 24 ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'],
25 25 ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'],
26 26 ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
27 27 end
28 28
29 29 def user_mail_notification_options(user)
30 30 user.valid_notification_options.collect {|o| [l(o.last), o.first]}
31 31 end
32 32
33 33 def change_status_link(user)
34 34 url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
35 35
36 36 if user.locked?
37 37 link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
38 38 elsif user.registered?
39 39 link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
40 40 elsif user != User.current
41 41 link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
42 42 end
43 43 end
44 44
45 def additional_emails_link(user)
46 if user.email_addresses.count > 1 || Setting.max_additional_emails.to_i > 0
47 link_to l(:label_email_address_plural), user_email_addresses_path(@user), :class => 'icon icon-email-add', :remote => true
48 end
49 end
50
45 51 def user_settings_tabs
46 52 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
47 53 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
48 54 ]
49 55 if Group.givable.any?
50 56 tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
51 57 end
52 58 tabs
53 59 end
54 60 end
@@ -1,70 +1,74
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 Document < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 belongs_to :category, :class_name => "DocumentCategory"
22 22 acts_as_attachable :delete_permission => :delete_documents
23 23
24 24 acts_as_searchable :columns => ['title', "#{table_name}.description"],
25 25 :preload => :project
26 26 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
27 27 :author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) },
28 28 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
29 29 acts_as_activity_provider :scope => preload(:project)
30 30
31 31 validates_presence_of :project, :title, :category
32 32 validates_length_of :title, :maximum => 60
33 33 attr_protected :id
34 34
35 35 after_create :send_notification
36 36
37 37 scope :visible, lambda {|*args|
38 38 joins(:project).
39 39 where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
40 40 }
41 41
42 42 safe_attributes 'category_id', 'title', 'description'
43 43
44 44 def visible?(user=User.current)
45 45 !user.nil? && user.allowed_to?(:view_documents, project)
46 46 end
47 47
48 48 def initialize(attributes=nil, *args)
49 49 super
50 50 if new_record?
51 51 self.category ||= DocumentCategory.default
52 52 end
53 53 end
54 54
55 55 def updated_on
56 56 unless @updated_on
57 57 a = attachments.last
58 58 @updated_on = (a && a.created_on) || created_on
59 59 end
60 60 @updated_on
61 61 end
62 62
63 def notified_users
64 project.notified_users.reject {|user| !visible?(user)}
65 end
66
63 67 private
64 68
65 69 def send_notification
66 70 if Setting.notified_events.include?('document_added')
67 71 Mailer.document_added(self).deliver
68 72 end
69 73 end
70 74 end
@@ -1,551 +1,551
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 MailHandler < ActionMailer::Base
19 19 include ActionView::Helpers::SanitizeHelper
20 20 include Redmine::I18n
21 21
22 22 class UnauthorizedAction < StandardError; end
23 23 class MissingInformation < StandardError; end
24 24
25 25 attr_reader :email, :user
26 26
27 27 def self.receive(email, options={})
28 28 @@handler_options = options.deep_dup
29 29
30 30 @@handler_options[:issue] ||= {}
31 31
32 32 if @@handler_options[:allow_override].is_a?(String)
33 33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 34 end
35 35 @@handler_options[:allow_override] ||= []
36 36 # Project needs to be overridable if not specified
37 37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 38 # Status overridable by default
39 39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40 40
41 41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
42 42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
43 43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
44 44
45 45 email.force_encoding('ASCII-8BIT')
46 46 super(email)
47 47 end
48 48
49 49 # Receives an email and rescues any exception
50 50 def self.safe_receive(*args)
51 51 receive(*args)
52 52 rescue Exception => e
53 53 logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger
54 54 return false
55 55 end
56 56
57 57 # Extracts MailHandler options from environment variables
58 58 # Use when receiving emails with rake tasks
59 59 def self.extract_options_from_env(env)
60 60 options = {:issue => {}}
61 61 %w(project status tracker category priority).each do |option|
62 62 options[:issue][option.to_sym] = env[option] if env[option]
63 63 end
64 64 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
65 65 options[option.to_sym] = env[option] if env[option]
66 66 end
67 67 options
68 68 end
69 69
70 70 def logger
71 71 Rails.logger
72 72 end
73 73
74 74 cattr_accessor :ignored_emails_headers
75 75 @@ignored_emails_headers = {
76 76 'X-Auto-Response-Suppress' => 'oof',
77 77 'Auto-Submitted' => /\Aauto-(replied|generated)/,
78 78 'X-Autoreply' => 'yes'
79 79 }
80 80
81 81 # Processes incoming emails
82 82 # Returns the created object (eg. an issue, a message) or false
83 83 def receive(email)
84 84 @email = email
85 85 sender_email = email.from.to_a.first.to_s.strip
86 86 # Ignore emails received from the application emission address to avoid hell cycles
87 87 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
88 88 if logger
89 89 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
90 90 end
91 91 return false
92 92 end
93 93 # Ignore auto generated emails
94 94 self.class.ignored_emails_headers.each do |key, ignored_value|
95 95 value = email.header[key]
96 96 if value
97 97 value = value.to_s.downcase
98 98 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
99 99 if logger
100 100 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
101 101 end
102 102 return false
103 103 end
104 104 end
105 105 end
106 106 @user = User.find_by_mail(sender_email) if sender_email.present?
107 107 if @user && !@user.active?
108 108 if logger
109 109 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
110 110 end
111 111 return false
112 112 end
113 113 if @user.nil?
114 114 # Email was submitted by an unknown user
115 115 case @@handler_options[:unknown_user]
116 116 when 'accept'
117 117 @user = User.anonymous
118 118 when 'create'
119 119 @user = create_user_from_email
120 120 if @user
121 121 if logger
122 122 logger.info "MailHandler: [#{@user.login}] account created"
123 123 end
124 124 add_user_to_group(@@handler_options[:default_group])
125 125 unless @@handler_options[:no_account_notice]
126 126 Mailer.account_information(@user, @user.password).deliver
127 127 end
128 128 else
129 129 if logger
130 130 logger.error "MailHandler: could not create account for [#{sender_email}]"
131 131 end
132 132 return false
133 133 end
134 134 else
135 135 # Default behaviour, emails from unknown users are ignored
136 136 if logger
137 137 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
138 138 end
139 139 return false
140 140 end
141 141 end
142 142 User.current = @user
143 143 dispatch
144 144 end
145 145
146 146 private
147 147
148 148 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
149 149 ISSUE_REPLY_SUBJECT_RE = %r{\[(?:[^\]]*\s+)?#(\d+)\]}
150 150 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
151 151
152 152 def dispatch
153 153 headers = [email.in_reply_to, email.references].flatten.compact
154 154 subject = email.subject.to_s
155 155 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
156 156 klass, object_id = $1, $2.to_i
157 157 method_name = "receive_#{klass}_reply"
158 158 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
159 159 send method_name, object_id
160 160 else
161 161 # ignoring it
162 162 end
163 163 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
164 164 receive_issue_reply(m[1].to_i)
165 165 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
166 166 receive_message_reply(m[1].to_i)
167 167 else
168 168 dispatch_to_default
169 169 end
170 170 rescue ActiveRecord::RecordInvalid => e
171 171 # TODO: send a email to the user
172 172 logger.error e.message if logger
173 173 false
174 174 rescue MissingInformation => e
175 175 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
176 176 false
177 177 rescue UnauthorizedAction => e
178 178 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
179 179 false
180 180 end
181 181
182 182 def dispatch_to_default
183 183 receive_issue
184 184 end
185 185
186 186 # Creates a new issue
187 187 def receive_issue
188 188 project = target_project
189 189 # check permission
190 190 unless @@handler_options[:no_permission_check]
191 191 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
192 192 end
193 193
194 194 issue = Issue.new(:author => user, :project => project)
195 195 issue.safe_attributes = issue_attributes_from_keywords(issue)
196 196 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
197 197 issue.subject = cleaned_up_subject
198 198 if issue.subject.blank?
199 199 issue.subject = '(no subject)'
200 200 end
201 201 issue.description = cleaned_up_text_body
202 202 issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
203 203
204 204 # add To and Cc as watchers before saving so the watchers can reply to Redmine
205 205 add_watchers(issue)
206 206 issue.save!
207 207 add_attachments(issue)
208 208 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
209 209 issue
210 210 end
211 211
212 212 # Adds a note to an existing issue
213 213 def receive_issue_reply(issue_id, from_journal=nil)
214 214 issue = Issue.find_by_id(issue_id)
215 215 return unless issue
216 216 # check permission
217 217 unless @@handler_options[:no_permission_check]
218 218 unless user.allowed_to?(:add_issue_notes, issue.project) ||
219 219 user.allowed_to?(:edit_issues, issue.project)
220 220 raise UnauthorizedAction
221 221 end
222 222 end
223 223
224 224 # ignore CLI-supplied defaults for new issues
225 225 @@handler_options[:issue].clear
226 226
227 227 journal = issue.init_journal(user)
228 228 if from_journal && from_journal.private_notes?
229 229 # If the received email was a reply to a private note, make the added note private
230 230 issue.private_notes = true
231 231 end
232 232 issue.safe_attributes = issue_attributes_from_keywords(issue)
233 233 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
234 234 journal.notes = cleaned_up_text_body
235 235 add_attachments(issue)
236 236 issue.save!
237 237 if logger
238 238 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
239 239 end
240 240 journal
241 241 end
242 242
243 243 # Reply will be added to the issue
244 244 def receive_journal_reply(journal_id)
245 245 journal = Journal.find_by_id(journal_id)
246 246 if journal && journal.journalized_type == 'Issue'
247 247 receive_issue_reply(journal.journalized_id, journal)
248 248 end
249 249 end
250 250
251 251 # Receives a reply to a forum message
252 252 def receive_message_reply(message_id)
253 253 message = Message.find_by_id(message_id)
254 254 if message
255 255 message = message.root
256 256
257 257 unless @@handler_options[:no_permission_check]
258 258 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
259 259 end
260 260
261 261 if !message.locked?
262 262 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
263 263 :content => cleaned_up_text_body)
264 264 reply.author = user
265 265 reply.board = message.board
266 266 message.children << reply
267 267 add_attachments(reply)
268 268 reply
269 269 else
270 270 if logger
271 271 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
272 272 end
273 273 end
274 274 end
275 275 end
276 276
277 277 def add_attachments(obj)
278 278 if email.attachments && email.attachments.any?
279 279 email.attachments.each do |attachment|
280 280 next unless accept_attachment?(attachment)
281 281 obj.attachments << Attachment.create(:container => obj,
282 282 :file => attachment.decoded,
283 283 :filename => attachment.filename,
284 284 :author => user,
285 285 :content_type => attachment.mime_type)
286 286 end
287 287 end
288 288 end
289 289
290 290 # Returns false if the +attachment+ of the incoming email should be ignored
291 291 def accept_attachment?(attachment)
292 292 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
293 293 @excluded.each do |pattern|
294 294 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
295 295 if attachment.filename.to_s =~ regexp
296 296 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
297 297 return false
298 298 end
299 299 end
300 300 true
301 301 end
302 302
303 303 # Adds To and Cc as watchers of the given object if the sender has the
304 304 # appropriate permission
305 305 def add_watchers(obj)
306 306 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
307 307 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
308 308 unless addresses.empty?
309 User.active.where('LOWER(mail) IN (?)', addresses).each do |w|
309 User.active.having_mail(addresses).each do |w|
310 310 obj.add_watcher(w)
311 311 end
312 312 end
313 313 end
314 314 end
315 315
316 316 def get_keyword(attr, options={})
317 317 @keywords ||= {}
318 318 if @keywords.has_key?(attr)
319 319 @keywords[attr]
320 320 else
321 321 @keywords[attr] = begin
322 322 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
323 323 (v = extract_keyword!(cleaned_up_text_body, attr, options[:format]))
324 324 v
325 325 elsif !@@handler_options[:issue][attr].blank?
326 326 @@handler_options[:issue][attr]
327 327 end
328 328 end
329 329 end
330 330 end
331 331
332 332 # Destructively extracts the value for +attr+ in +text+
333 333 # Returns nil if no matching keyword found
334 334 def extract_keyword!(text, attr, format=nil)
335 335 keys = [attr.to_s.humanize]
336 336 if attr.is_a?(Symbol)
337 337 if user && user.language.present?
338 338 keys << l("field_#{attr}", :default => '', :locale => user.language)
339 339 end
340 340 if Setting.default_language.present?
341 341 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
342 342 end
343 343 end
344 344 keys.reject! {|k| k.blank?}
345 345 keys.collect! {|k| Regexp.escape(k)}
346 346 format ||= '.+'
347 347 keyword = nil
348 348 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
349 349 if m = text.match(regexp)
350 350 keyword = m[2].strip
351 351 text.sub!(regexp, '')
352 352 end
353 353 keyword
354 354 end
355 355
356 356 def target_project
357 357 # TODO: other ways to specify project:
358 358 # * parse the email To field
359 359 # * specific project (eg. Setting.mail_handler_target_project)
360 360 target = Project.find_by_identifier(get_keyword(:project))
361 361 if target.nil?
362 362 # Invalid project keyword, use the project specified as the default one
363 363 default_project = @@handler_options[:issue][:project]
364 364 if default_project.present?
365 365 target = Project.find_by_identifier(default_project)
366 366 end
367 367 end
368 368 raise MissingInformation.new('Unable to determine target project') if target.nil?
369 369 target
370 370 end
371 371
372 372 # Returns a Hash of issue attributes extracted from keywords in the email body
373 373 def issue_attributes_from_keywords(issue)
374 374 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
375 375
376 376 attrs = {
377 377 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
378 378 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
379 379 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
380 380 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
381 381 'assigned_to_id' => assigned_to.try(:id),
382 382 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
383 383 issue.project.shared_versions.named(k).first.try(:id),
384 384 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
385 385 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
386 386 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
387 387 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
388 388 }.delete_if {|k, v| v.blank? }
389 389
390 390 if issue.new_record? && attrs['tracker_id'].nil?
391 391 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
392 392 end
393 393
394 394 attrs
395 395 end
396 396
397 397 # Returns a Hash of issue custom field values extracted from keywords in the email body
398 398 def custom_field_values_from_keywords(customized)
399 399 customized.custom_field_values.inject({}) do |h, v|
400 400 if keyword = get_keyword(v.custom_field.name, :override => true)
401 401 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
402 402 end
403 403 h
404 404 end
405 405 end
406 406
407 407 # Returns the text/plain part of the email
408 408 # If not found (eg. HTML-only email), returns the body with tags removed
409 409 def plain_text_body
410 410 return @plain_text_body unless @plain_text_body.nil?
411 411
412 412 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
413 413 text_parts
414 414 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
415 415 html_parts
416 416 else
417 417 [email]
418 418 end
419 419
420 420 parts.reject! do |part|
421 421 part.attachment?
422 422 end
423 423
424 424 @plain_text_body = parts.map do |p|
425 425 body_charset = Mail::RubyVer.respond_to?(:pick_encoding) ?
426 426 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
427 427 Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
428 428 end.join("\r\n")
429 429
430 430 # strip html tags and remove doctype directive
431 431 if parts.any? {|p| p.mime_type == 'text/html'}
432 432 @plain_text_body = strip_tags(@plain_text_body.strip)
433 433 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
434 434 end
435 435
436 436 @plain_text_body
437 437 end
438 438
439 439 def cleaned_up_text_body
440 440 @cleaned_up_text_body ||= cleanup_body(plain_text_body)
441 441 end
442 442
443 443 def cleaned_up_subject
444 444 subject = email.subject.to_s
445 445 subject.strip[0,255]
446 446 end
447 447
448 448 def self.full_sanitizer
449 449 @full_sanitizer ||= HTML::FullSanitizer.new
450 450 end
451 451
452 452 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
453 453 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
454 454 value = value.to_s.slice(0, limit)
455 455 object.send("#{attribute}=", value)
456 456 end
457 457
458 458 # Returns a User from an email address and a full name
459 459 def self.new_user_from_attributes(email_address, fullname=nil)
460 460 user = User.new
461 461
462 462 # Truncating the email address would result in an invalid format
463 463 user.mail = email_address
464 464 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
465 465
466 466 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
467 467 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
468 468 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
469 469 user.lastname = '-' if user.lastname.blank?
470 470 user.language = Setting.default_language
471 471 user.generate_password = true
472 472 user.mail_notification = 'only_my_events'
473 473
474 474 unless user.valid?
475 475 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
476 476 user.firstname = "-" unless user.errors[:firstname].blank?
477 477 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
478 478 end
479 479
480 480 user
481 481 end
482 482
483 483 # Creates a User for the +email+ sender
484 484 # Returns the user or nil if it could not be created
485 485 def create_user_from_email
486 486 from = email.header['from'].to_s
487 487 addr, name = from, nil
488 488 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
489 489 addr, name = m[2], m[1]
490 490 end
491 491 if addr.present?
492 492 user = self.class.new_user_from_attributes(addr, name)
493 493 if @@handler_options[:no_notification]
494 494 user.mail_notification = 'none'
495 495 end
496 496 if user.save
497 497 user
498 498 else
499 499 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
500 500 nil
501 501 end
502 502 else
503 503 logger.error "MailHandler: failed to create User: no FROM address found" if logger
504 504 nil
505 505 end
506 506 end
507 507
508 508 # Adds the newly created user to default group
509 509 def add_user_to_group(default_group)
510 510 if default_group.present?
511 511 default_group.split(',').each do |group_name|
512 512 if group = Group.named(group_name).first
513 513 group.users << @user
514 514 elsif logger
515 515 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
516 516 end
517 517 end
518 518 end
519 519 end
520 520
521 521 # Removes the email body of text after the truncation configurations.
522 522 def cleanup_body(body)
523 523 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
524 524 unless delimiters.empty?
525 525 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
526 526 body = body.gsub(regex, '')
527 527 end
528 528 body.strip
529 529 end
530 530
531 531 def find_assignee_from_keyword(keyword, issue)
532 532 keyword = keyword.to_s.downcase
533 533 assignable = issue.assignable_users
534 534 assignee = nil
535 535 assignee ||= assignable.detect {|a|
536 536 a.mail.to_s.downcase == keyword ||
537 537 a.login.to_s.downcase == keyword
538 538 }
539 539 if assignee.nil? && keyword.match(/ /)
540 540 firstname, lastname = *(keyword.split) # "First Last Throwaway"
541 541 assignee ||= assignable.detect {|a|
542 542 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
543 543 a.lastname.to_s.downcase == lastname
544 544 }
545 545 end
546 546 if assignee.nil?
547 547 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
548 548 end
549 549 assignee
550 550 end
551 551 end
@@ -1,495 +1,522
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 Mailer < ActionMailer::Base
19 19 layout 'mailer'
20 20 helper :application
21 21 helper :issues
22 22 helper :custom_fields
23 23
24 24 include Redmine::I18n
25 25
26 26 def self.default_url_options
27 27 { :host => Setting.host_name, :protocol => Setting.protocol }
28 28 end
29 29
30 30 # Builds a mail for notifying to_users and cc_users about a new issue
31 31 def issue_add(issue, to_users, cc_users)
32 32 redmine_headers 'Project' => issue.project.identifier,
33 33 'Issue-Id' => issue.id,
34 34 'Issue-Author' => issue.author.login
35 35 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
36 36 message_id issue
37 37 references issue
38 38 @author = issue.author
39 39 @issue = issue
40 40 @users = to_users + cc_users
41 41 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
42 mail :to => to_users.map(&:mail),
43 :cc => cc_users.map(&:mail),
42 mail :to => to_users,
43 :cc => cc_users,
44 44 :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
45 45 end
46 46
47 47 # Notifies users about a new issue
48 48 def self.deliver_issue_add(issue)
49 49 to = issue.notified_users
50 50 cc = issue.notified_watchers - to
51 51 issue.each_notification(to + cc) do |users|
52 52 Mailer.issue_add(issue, to & users, cc & users).deliver
53 53 end
54 54 end
55 55
56 56 # Builds a mail for notifying to_users and cc_users about an issue update
57 57 def issue_edit(journal, to_users, cc_users)
58 58 issue = journal.journalized
59 59 redmine_headers 'Project' => issue.project.identifier,
60 60 'Issue-Id' => issue.id,
61 61 'Issue-Author' => issue.author.login
62 62 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
63 63 message_id journal
64 64 references issue
65 65 @author = journal.user
66 66 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
67 67 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
68 68 s << issue.subject
69 69 @issue = issue
70 70 @users = to_users + cc_users
71 71 @journal = journal
72 72 @journal_details = journal.visible_details(@users.first)
73 73 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
74 mail :to => to_users.map(&:mail),
75 :cc => cc_users.map(&:mail),
74 mail :to => to_users,
75 :cc => cc_users,
76 76 :subject => s
77 77 end
78 78
79 79 # Notifies users about an issue update
80 80 def self.deliver_issue_edit(journal)
81 81 issue = journal.journalized.reload
82 82 to = journal.notified_users
83 83 cc = journal.notified_watchers - to
84 84 journal.each_notification(to + cc) do |users|
85 85 issue.each_notification(users) do |users2|
86 86 Mailer.issue_edit(journal, to & users2, cc & users2).deliver
87 87 end
88 88 end
89 89 end
90 90
91 91 def reminder(user, issues, days)
92 92 set_language_if_valid user.language
93 93 @issues = issues
94 94 @days = days
95 95 @issues_url = url_for(:controller => 'issues', :action => 'index',
96 96 :set_filter => 1, :assigned_to_id => user.id,
97 97 :sort => 'due_date:asc')
98 mail :to => user.mail,
98 mail :to => user,
99 99 :subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
100 100 end
101 101
102 102 # Builds a Mail::Message object used to email users belonging to the added document's project.
103 103 #
104 104 # Example:
105 105 # document_added(document) => Mail::Message object
106 106 # Mailer.document_added(document).deliver => sends an email to the document's project recipients
107 107 def document_added(document)
108 108 redmine_headers 'Project' => document.project.identifier
109 109 @author = User.current
110 110 @document = document
111 111 @document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
112 mail :to => document.recipients,
112 mail :to => document.notified_users,
113 113 :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
114 114 end
115 115
116 116 # Builds a Mail::Message object used to email recipients of a project when an attachements are added.
117 117 #
118 118 # Example:
119 119 # attachments_added(attachments) => Mail::Message object
120 120 # Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients
121 121 def attachments_added(attachments)
122 122 container = attachments.first.container
123 123 added_to = ''
124 124 added_to_url = ''
125 125 @author = attachments.first.author
126 126 case container.class.name
127 127 when 'Project'
128 128 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
129 129 added_to = "#{l(:label_project)}: #{container}"
130 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
130 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
131 131 when 'Version'
132 132 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
133 133 added_to = "#{l(:label_version)}: #{container.name}"
134 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
134 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
135 135 when 'Document'
136 136 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
137 137 added_to = "#{l(:label_document)}: #{container.title}"
138 recipients = container.recipients
138 recipients = container.notified_users
139 139 end
140 140 redmine_headers 'Project' => container.project.identifier
141 141 @attachments = attachments
142 142 @added_to = added_to
143 143 @added_to_url = added_to_url
144 144 mail :to => recipients,
145 145 :subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
146 146 end
147 147
148 148 # Builds a Mail::Message object used to email recipients of a news' project when a news item is added.
149 149 #
150 150 # Example:
151 151 # news_added(news) => Mail::Message object
152 152 # Mailer.news_added(news).deliver => sends an email to the news' project recipients
153 153 def news_added(news)
154 154 redmine_headers 'Project' => news.project.identifier
155 155 @author = news.author
156 156 message_id news
157 157 references news
158 158 @news = news
159 159 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
160 mail :to => news.recipients,
161 :cc => news.cc_for_added_news,
160 mail :to => news.notified_users,
161 :cc => news.notified_watchers_for_added_news,
162 162 :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
163 163 end
164 164
165 165 # Builds a Mail::Message object used to email recipients of a news' project when a news comment is added.
166 166 #
167 167 # Example:
168 168 # news_comment_added(comment) => Mail::Message object
169 169 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
170 170 def news_comment_added(comment)
171 171 news = comment.commented
172 172 redmine_headers 'Project' => news.project.identifier
173 173 @author = comment.author
174 174 message_id comment
175 175 references news
176 176 @news = news
177 177 @comment = comment
178 178 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
179 mail :to => news.recipients,
180 :cc => news.watcher_recipients,
179 mail :to => news.notified_users,
180 :cc => news.notified_watchers,
181 181 :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
182 182 end
183 183
184 184 # Builds a Mail::Message object used to email the recipients of the specified message that was posted.
185 185 #
186 186 # Example:
187 187 # message_posted(message) => Mail::Message object
188 188 # Mailer.message_posted(message).deliver => sends an email to the recipients
189 189 def message_posted(message)
190 190 redmine_headers 'Project' => message.project.identifier,
191 191 'Topic-Id' => (message.parent_id || message.id)
192 192 @author = message.author
193 193 message_id message
194 194 references message.root
195 recipients = message.recipients
196 cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
195 recipients = message.notified_users
196 cc = ((message.root.notified_watchers + message.board.notified_watchers).uniq - recipients)
197 197 @message = message
198 198 @message_url = url_for(message.event_url)
199 199 mail :to => recipients,
200 200 :cc => cc,
201 201 :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
202 202 end
203 203
204 204 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added.
205 205 #
206 206 # Example:
207 207 # wiki_content_added(wiki_content) => Mail::Message object
208 208 # Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients
209 209 def wiki_content_added(wiki_content)
210 210 redmine_headers 'Project' => wiki_content.project.identifier,
211 211 'Wiki-Page-Id' => wiki_content.page.id
212 212 @author = wiki_content.author
213 213 message_id wiki_content
214 recipients = wiki_content.recipients
215 cc = wiki_content.page.wiki.watcher_recipients - recipients
214 recipients = wiki_content.notified_users
215 cc = wiki_content.page.wiki.notified_watchers - recipients
216 216 @wiki_content = wiki_content
217 217 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
218 218 :project_id => wiki_content.project,
219 219 :id => wiki_content.page.title)
220 220 mail :to => recipients,
221 221 :cc => cc,
222 222 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
223 223 end
224 224
225 225 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated.
226 226 #
227 227 # Example:
228 228 # wiki_content_updated(wiki_content) => Mail::Message object
229 229 # Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients
230 230 def wiki_content_updated(wiki_content)
231 231 redmine_headers 'Project' => wiki_content.project.identifier,
232 232 'Wiki-Page-Id' => wiki_content.page.id
233 233 @author = wiki_content.author
234 234 message_id wiki_content
235 recipients = wiki_content.recipients
236 cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
235 recipients = wiki_content.notified_users
236 cc = wiki_content.page.wiki.notified_watchers + wiki_content.page.notified_watchers - recipients
237 237 @wiki_content = wiki_content
238 238 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
239 239 :project_id => wiki_content.project,
240 240 :id => wiki_content.page.title)
241 241 @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
242 242 :project_id => wiki_content.project, :id => wiki_content.page.title,
243 243 :version => wiki_content.version)
244 244 mail :to => recipients,
245 245 :cc => cc,
246 246 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
247 247 end
248 248
249 249 # Builds a Mail::Message object used to email the specified user their account information.
250 250 #
251 251 # Example:
252 252 # account_information(user, password) => Mail::Message object
253 253 # Mailer.account_information(user, password).deliver => sends account information to the user
254 254 def account_information(user, password)
255 255 set_language_if_valid user.language
256 256 @user = user
257 257 @password = password
258 258 @login_url = url_for(:controller => 'account', :action => 'login')
259 259 mail :to => user.mail,
260 260 :subject => l(:mail_subject_register, Setting.app_title)
261 261 end
262 262
263 263 # Builds a Mail::Message object used to email all active administrators of an account activation request.
264 264 #
265 265 # Example:
266 266 # account_activation_request(user) => Mail::Message object
267 267 # Mailer.account_activation_request(user).deliver => sends an email to all active administrators
268 268 def account_activation_request(user)
269 269 # Send the email to all active administrators
270 recipients = User.active.where(:admin => true).collect { |u| u.mail }.compact
270 recipients = User.active.where(:admin => true)
271 271 @user = user
272 272 @url = url_for(:controller => 'users', :action => 'index',
273 273 :status => User::STATUS_REGISTERED,
274 274 :sort_key => 'created_on', :sort_order => 'desc')
275 275 mail :to => recipients,
276 276 :subject => l(:mail_subject_account_activation_request, Setting.app_title)
277 277 end
278 278
279 279 # Builds a Mail::Message object used to email the specified user that their account was activated by an administrator.
280 280 #
281 281 # Example:
282 282 # account_activated(user) => Mail::Message object
283 283 # Mailer.account_activated(user).deliver => sends an email to the registered user
284 284 def account_activated(user)
285 285 set_language_if_valid user.language
286 286 @user = user
287 287 @login_url = url_for(:controller => 'account', :action => 'login')
288 288 mail :to => user.mail,
289 289 :subject => l(:mail_subject_register, Setting.app_title)
290 290 end
291 291
292 292 def lost_password(token)
293 293 set_language_if_valid(token.user.language)
294 294 @token = token
295 295 @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
296 296 mail :to => token.user.mail,
297 297 :subject => l(:mail_subject_lost_password, Setting.app_title)
298 298 end
299 299
300 300 def register(token)
301 301 set_language_if_valid(token.user.language)
302 302 @token = token
303 303 @url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
304 304 mail :to => token.user.mail,
305 305 :subject => l(:mail_subject_register, Setting.app_title)
306 306 end
307 307
308 308 def test_email(user)
309 309 set_language_if_valid(user.language)
310 310 @url = url_for(:controller => 'welcome')
311 311 mail :to => user.mail,
312 312 :subject => 'Redmine test'
313 313 end
314 314
315 315 # Sends reminders to issue assignees
316 316 # Available options:
317 317 # * :days => how many days in the future to remind about (defaults to 7)
318 318 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
319 319 # * :project => id or identifier of project to process (defaults to all projects)
320 320 # * :users => array of user/group ids who should be reminded
321 321 def self.reminders(options={})
322 322 days = options[:days] || 7
323 323 project = options[:project] ? Project.find(options[:project]) : nil
324 324 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
325 325 user_ids = options[:users]
326 326
327 327 scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" +
328 328 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
329 329 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date
330 330 )
331 331 scope = scope.where(:assigned_to_id => user_ids) if user_ids.present?
332 332 scope = scope.where(:project_id => project.id) if project
333 333 scope = scope.where(:tracker_id => tracker.id) if tracker
334 334 issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).
335 335 group_by(&:assigned_to)
336 336 issues_by_assignee.keys.each do |assignee|
337 337 if assignee.is_a?(Group)
338 338 assignee.users.each do |user|
339 339 issues_by_assignee[user] ||= []
340 340 issues_by_assignee[user] += issues_by_assignee[assignee]
341 341 end
342 342 end
343 343 end
344 344
345 345 issues_by_assignee.each do |assignee, issues|
346 346 reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
347 347 end
348 348 end
349 349
350 350 # Activates/desactivates email deliveries during +block+
351 351 def self.with_deliveries(enabled = true, &block)
352 352 was_enabled = ActionMailer::Base.perform_deliveries
353 353 ActionMailer::Base.perform_deliveries = !!enabled
354 354 yield
355 355 ensure
356 356 ActionMailer::Base.perform_deliveries = was_enabled
357 357 end
358 358
359 359 # Sends emails synchronously in the given block
360 360 def self.with_synched_deliveries(&block)
361 361 saved_method = ActionMailer::Base.delivery_method
362 362 if m = saved_method.to_s.match(%r{^async_(.+)$})
363 363 synched_method = m[1]
364 364 ActionMailer::Base.delivery_method = synched_method.to_sym
365 365 ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings")
366 366 end
367 367 yield
368 368 ensure
369 369 ActionMailer::Base.delivery_method = saved_method
370 370 end
371 371
372 372 def mail(headers={}, &block)
373 373 headers.reverse_merge! 'X-Mailer' => 'Redmine',
374 374 'X-Redmine-Host' => Setting.host_name,
375 375 'X-Redmine-Site' => Setting.app_title,
376 376 'X-Auto-Response-Suppress' => 'OOF',
377 377 'Auto-Submitted' => 'auto-generated',
378 378 'From' => Setting.mail_from,
379 379 'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
380 380
381 # Replaces users with their email addresses
382 [:to, :cc, :bcc].each do |key|
383 if headers[key].present?
384 headers[key] = self.class.email_addresses(headers[key])
385 end
386 end
387
381 388 # Removes the author from the recipients and cc
382 389 # if the author does not want to receive notifications
383 390 # about what the author do
384 391 if @author && @author.logged? && @author.pref.no_self_notified
385 headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
386 headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
392 addresses = @author.mails
393 headers[:to] -= addresses if headers[:to].is_a?(Array)
394 headers[:cc] -= addresses if headers[:cc].is_a?(Array)
387 395 end
388 396
389 397 if @author && @author.logged?
390 398 redmine_headers 'Sender' => @author.login
391 399 end
392 400
393 401 # Blind carbon copy recipients
394 402 if Setting.bcc_recipients?
395 403 headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
396 404 headers[:to] = nil
397 405 headers[:cc] = nil
398 406 end
399 407
400 408 if @message_id_object
401 409 headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
402 410 end
403 411 if @references_objects
404 412 headers[:references] = @references_objects.collect {|o| "<#{self.class.references_for(o)}>"}.join(' ')
405 413 end
406 414
407 415 m = if block_given?
408 416 super headers, &block
409 417 else
410 418 super headers do |format|
411 419 format.text
412 420 format.html unless Setting.plain_text_mail?
413 421 end
414 422 end
415 423 set_language_if_valid @initial_language
416 424
417 425 m
418 426 end
419 427
420 428 def initialize(*args)
421 429 @initial_language = current_language
422 430 set_language_if_valid Setting.default_language
423 431 super
424 432 end
425 433
426 434 def self.deliver_mail(mail)
427 435 return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
428 436 begin
429 437 # Log errors when raise_delivery_errors is set to false, Rails does not
430 438 mail.raise_delivery_errors = true
431 439 super
432 440 rescue Exception => e
433 441 if ActionMailer::Base.raise_delivery_errors
434 442 raise e
435 443 else
436 444 Rails.logger.error "Email delivery error: #{e.message}"
437 445 end
438 446 end
439 447 end
440 448
441 449 def self.method_missing(method, *args, &block)
442 450 if m = method.to_s.match(%r{^deliver_(.+)$})
443 451 ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead."
444 452 send(m[1], *args).deliver
445 453 else
446 454 super
447 455 end
448 456 end
449 457
458 # Returns an array of email addresses to notify by
459 # replacing users in arg with their notified email addresses
460 #
461 # Example:
462 # Mailer.email_addresses(users)
463 # => ["foo@example.net", "bar@example.net"]
464 def self.email_addresses(arg)
465 arr = Array.wrap(arg)
466 mails = arr.reject {|a| a.is_a? Principal}
467 users = arr - mails
468 if users.any?
469 mails += EmailAddress.
470 where(:user_id => users.map(&:id)).
471 where("is_default = ? OR notify = ?", true, true).
472 pluck(:address)
473 end
474 mails
475 end
476
450 477 private
451 478
452 479 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
453 480 def redmine_headers(h)
454 481 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
455 482 end
456 483
457 484 def self.token_for(object, rand=true)
458 485 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
459 486 hash = [
460 487 "redmine",
461 488 "#{object.class.name.demodulize.underscore}-#{object.id}",
462 489 timestamp.strftime("%Y%m%d%H%M%S")
463 490 ]
464 491 if rand
465 492 hash << Redmine::Utils.random_hex(8)
466 493 end
467 494 host = Setting.mail_from.to_s.strip.gsub(%r{^.*@|>}, '')
468 495 host = "#{::Socket.gethostname}.redmine" if host.empty?
469 496 "#{hash.join('.')}@#{host}"
470 497 end
471 498
472 499 # Returns a Message-Id for the given object
473 500 def self.message_id_for(object)
474 501 token_for(object, true)
475 502 end
476 503
477 504 # Returns a uniq token for a given object referenced by all notifications
478 505 # related to this object
479 506 def self.references_for(object)
480 507 token_for(object, false)
481 508 end
482 509
483 510 def message_id(object)
484 511 @message_id_object = object
485 512 end
486 513
487 514 def references(object)
488 515 @references_objects ||= []
489 516 @references_objects << object
490 517 end
491 518
492 519 def mylogger
493 520 Rails.logger
494 521 end
495 522 end
@@ -1,117 +1,121
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 Message < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :board
21 21 belongs_to :author, :class_name => 'User'
22 22 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
23 23 acts_as_attachable
24 24 belongs_to :last_reply, :class_name => 'Message'
25 25 attr_protected :id
26 26
27 27 acts_as_searchable :columns => ['subject', 'content'],
28 28 :preload => {:board => :project},
29 29 :project_key => "#{Board.table_name}.project_id"
30 30
31 31 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
32 32 :description => :content,
33 33 :group => :parent,
34 34 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
35 35 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
36 36 {:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})}
37 37
38 38 acts_as_activity_provider :scope => preload({:board => :project}, :author),
39 39 :author_key => :author_id
40 40 acts_as_watchable
41 41
42 42 validates_presence_of :board, :subject, :content
43 43 validates_length_of :subject, :maximum => 255
44 44 validate :cannot_reply_to_locked_topic, :on => :create
45 45
46 46 after_create :add_author_as_watcher, :reset_counters!
47 47 after_update :update_messages_board
48 48 after_destroy :reset_counters!
49 49 after_create :send_notification
50 50
51 51 scope :visible, lambda {|*args|
52 52 joins(:board => :project).
53 53 where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args))
54 54 }
55 55
56 56 safe_attributes 'subject', 'content'
57 57 safe_attributes 'locked', 'sticky', 'board_id',
58 58 :if => lambda {|message, user|
59 59 user.allowed_to?(:edit_messages, message.project)
60 60 }
61 61
62 62 def visible?(user=User.current)
63 63 !user.nil? && user.allowed_to?(:view_messages, project)
64 64 end
65 65
66 66 def cannot_reply_to_locked_topic
67 67 # Can not reply to a locked topic
68 68 errors.add :base, 'Topic is locked' if root.locked? && self != root
69 69 end
70 70
71 71 def update_messages_board
72 72 if board_id_changed?
73 73 Message.where(["id = ? OR parent_id = ?", root.id, root.id]).update_all({:board_id => board_id})
74 74 Board.reset_counters!(board_id_was)
75 75 Board.reset_counters!(board_id)
76 76 end
77 77 end
78 78
79 79 def reset_counters!
80 80 if parent && parent.id
81 81 Message.where({:id => parent.id}).update_all({:last_reply_id => parent.children.maximum(:id)})
82 82 end
83 83 board.reset_counters!
84 84 end
85 85
86 86 def sticky=(arg)
87 87 write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
88 88 end
89 89
90 90 def sticky?
91 91 sticky == 1
92 92 end
93 93
94 94 def project
95 95 board.project
96 96 end
97 97
98 98 def editable_by?(usr)
99 99 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
100 100 end
101 101
102 102 def destroyable_by?(usr)
103 103 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
104 104 end
105 105
106 def notified_users
107 project.notified_users.reject {|user| !visible?(user)}
108 end
109
106 110 private
107 111
108 112 def add_author_as_watcher
109 113 Watcher.create(:watchable => self.root, :user => author)
110 114 end
111 115
112 116 def send_notification
113 117 if Setting.notified_events.include?('message_posted')
114 118 Mailer.message_posted(self).deliver
115 119 end
116 120 end
117 121 end
@@ -1,89 +1,98
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 News < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 belongs_to :author, :class_name => 'User'
22 22 has_many :comments, lambda {order("created_on")}, :as => :commented, :dependent => :delete_all
23 23
24 24 validates_presence_of :title, :description
25 25 validates_length_of :title, :maximum => 60
26 26 validates_length_of :summary, :maximum => 255
27 27 attr_protected :id
28 28
29 29 acts_as_attachable :edit_permission => :manage_news,
30 30 :delete_permission => :manage_news
31 31 acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"],
32 32 :preload => :project
33 33 acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
34 34 acts_as_activity_provider :scope => preload(:project, :author),
35 35 :author_key => :author_id
36 36 acts_as_watchable
37 37
38 38 after_create :add_author_as_watcher
39 39 after_create :send_notification
40 40
41 41 scope :visible, lambda {|*args|
42 42 joins(:project).
43 43 where(Project.allowed_to_condition(args.shift || User.current, :view_news, *args))
44 44 }
45 45
46 46 safe_attributes 'title', 'summary', 'description'
47 47
48 48 def visible?(user=User.current)
49 49 !user.nil? && user.allowed_to?(:view_news, project)
50 50 end
51 51
52 52 # Returns true if the news can be commented by user
53 53 def commentable?(user=User.current)
54 54 user.allowed_to?(:comment_news, project)
55 55 end
56 56
57 def notified_users
58 project.users.select {|user| user.notify_about?(self) && user.allowed_to?(:view_news, project)}
59 end
60
57 61 def recipients
58 project.users.select {|user| user.notify_about?(self) && user.allowed_to?(:view_news, project)}.map(&:mail)
62 notified_users.map(&:mail)
59 63 end
60 64
61 # Returns the email addresses that should be cc'd when a new news is added
62 def cc_for_added_news
63 cc = []
65 # Returns the users that should be cc'd when a new news is added
66 def notified_watchers_for_added_news
67 watchers = []
64 68 if m = project.enabled_module('news')
65 cc = m.notified_watchers
69 watchers = m.notified_watchers
66 70 unless project.is_public?
67 cc = cc.select {|user| project.users.include?(user)}
71 watchers = watchers.select {|user| project.users.include?(user)}
68 72 end
69 73 end
70 cc.map(&:mail)
74 watchers
75 end
76
77 # Returns the email addresses that should be cc'd when a new news is added
78 def cc_for_added_news
79 notified_watchers_for_added_news.map(&:mail)
71 80 end
72 81
73 82 # returns latest news for projects visible by user
74 83 def self.latest(user = User.current, count = 5)
75 84 visible(user).preload(:author, :project).order("#{News.table_name}.created_on DESC").limit(count).to_a
76 85 end
77 86
78 87 private
79 88
80 89 def add_author_as_watcher
81 90 Watcher.create(:watchable => self, :user => author)
82 91 end
83 92
84 93 def send_notification
85 94 if Setting.notified_events.include?('news_added')
86 95 Mailer.news_added(self).deliver
87 96 end
88 97 end
89 98 end
@@ -1,154 +1,162
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 Principal < ActiveRecord::Base
19 19 self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
20 20
21 21 # Account statuses
22 22 STATUS_ANONYMOUS = 0
23 23 STATUS_ACTIVE = 1
24 24 STATUS_REGISTERED = 2
25 25 STATUS_LOCKED = 3
26 26
27 27 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
28 28 has_many :memberships,
29 29 lambda {preload(:project, :roles).
30 30 joins(:project).
31 31 where("#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}").
32 32 order("#{Project.table_name}.name")},
33 33 :class_name => 'Member',
34 34 :foreign_key => 'user_id'
35 35 has_many :projects, :through => :memberships
36 36 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
37 37
38 38 # Groups and active users
39 39 scope :active, lambda { where(:status => STATUS_ACTIVE) }
40 40
41 41 scope :visible, lambda {|*args|
42 42 user = args.first || User.current
43 43
44 44 if user.admin?
45 45 all
46 46 else
47 47 view_all_active = false
48 48 if user.memberships.to_a.any?
49 49 view_all_active = user.memberships.any? {|m| m.roles.any? {|r| r.users_visibility == 'all'}}
50 50 else
51 51 view_all_active = user.builtin_role.users_visibility == 'all'
52 52 end
53 53
54 54 if view_all_active
55 55 active
56 56 else
57 57 # self and members of visible projects
58 58 active.where("#{table_name}.id = ? OR #{table_name}.id IN (SELECT user_id FROM #{Member.table_name} WHERE project_id IN (?))",
59 59 user.id, user.visible_project_ids
60 60 )
61 61 end
62 62 end
63 63 }
64 64
65 65 scope :like, lambda {|q|
66 66 q = q.to_s
67 67 if q.blank?
68 68 where({})
69 69 else
70 70 pattern = "%#{q}%"
71 sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
71 sql = %w(login firstname lastname).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
72 sql << " OR #{table_name}.id IN (SELECT user_id FROM #{EmailAddress.table_name} WHERE LOWER(address) LIKE LOWER(:p))"
72 73 params = {:p => pattern}
73 74 if q =~ /^(.+)\s+(.+)$/
74 75 a, b = "#{$1}%", "#{$2}%"
75 76 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))"
76 77 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))"
77 78 params.merge!(:a => a, :b => b)
78 79 end
79 80 where(sql, params)
80 81 end
81 82 }
82 83
83 84 # Principals that are members of a collection of projects
84 85 scope :member_of, lambda {|projects|
85 86 projects = [projects] if projects.is_a?(Project)
86 87 if projects.blank?
87 88 where("1=0")
88 89 else
89 90 ids = projects.map(&:id)
90 91 active.where("#{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
91 92 end
92 93 }
93 94 # Principals that are not members of projects
94 95 scope :not_member_of, lambda {|projects|
95 96 projects = [projects] unless projects.is_a?(Array)
96 97 if projects.empty?
97 98 where("1=0")
98 99 else
99 100 ids = projects.map(&:id)
100 101 where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
101 102 end
102 103 }
103 104 scope :sorted, lambda { order(*Principal.fields_for_order_statement)}
104 105
105 106 before_create :set_default_empty_values
106 107
107 108 def name(formatter = nil)
108 109 to_s
109 110 end
110 111
112 def mail=(*args)
113 nil
114 end
115
116 def mail
117 nil
118 end
119
111 120 def visible?(user=User.current)
112 121 Principal.visible(user).where(:id => id).first == self
113 122 end
114 123
115 124 # Return true if the principal is a member of project
116 125 def member_of?(project)
117 126 projects.to_a.include?(project)
118 127 end
119 128
120 129 def <=>(principal)
121 130 if principal.nil?
122 131 -1
123 132 elsif self.class.name == principal.class.name
124 133 self.to_s.downcase <=> principal.to_s.downcase
125 134 else
126 135 # groups after users
127 136 principal.class.name <=> self.class.name
128 137 end
129 138 end
130 139
131 140 # Returns an array of fields names than can be used to make an order statement for principals.
132 141 # Users are sorted before Groups.
133 142 # Examples:
134 143 def self.fields_for_order_statement(table=nil)
135 144 table ||= table_name
136 145 columns = ['type DESC'] + (User.name_formatter[:order] - ['id']) + ['lastname', 'id']
137 146 columns.uniq.map {|field| "#{table}.#{field}"}
138 147 end
139 148
140 149 protected
141 150
142 151 # Make sure we don't try to insert NULL values (see #4632)
143 152 def set_default_empty_values
144 153 self.login ||= ''
145 154 self.hashed_password ||= ''
146 155 self.firstname ||= ''
147 156 self.lastname ||= ''
148 self.mail ||= ''
149 157 true
150 158 end
151 159 end
152 160
153 161 require_dependency "user"
154 162 require_dependency "group"
@@ -1,807 +1,835
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < Principal
21 21 include Redmine::SafeAttributes
22 22
23 23 # Different ways of displaying/sorting users
24 24 USER_FORMATS = {
25 25 :firstname_lastname => {
26 26 :string => '#{firstname} #{lastname}',
27 27 :order => %w(firstname lastname id),
28 28 :setting_order => 1
29 29 },
30 30 :firstname_lastinitial => {
31 31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 32 :order => %w(firstname lastname id),
33 33 :setting_order => 2
34 34 },
35 35 :firstinitial_lastname => {
36 36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 37 :order => %w(firstname lastname id),
38 38 :setting_order => 2
39 39 },
40 40 :firstname => {
41 41 :string => '#{firstname}',
42 42 :order => %w(firstname id),
43 43 :setting_order => 3
44 44 },
45 45 :lastname_firstname => {
46 46 :string => '#{lastname} #{firstname}',
47 47 :order => %w(lastname firstname id),
48 48 :setting_order => 4
49 49 },
50 50 :lastname_coma_firstname => {
51 51 :string => '#{lastname}, #{firstname}',
52 52 :order => %w(lastname firstname id),
53 53 :setting_order => 5
54 54 },
55 55 :lastname => {
56 56 :string => '#{lastname}',
57 57 :order => %w(lastname id),
58 58 :setting_order => 6
59 59 },
60 60 :username => {
61 61 :string => '#{login}',
62 62 :order => %w(login id),
63 63 :setting_order => 7
64 64 },
65 65 }
66 66
67 67 MAIL_NOTIFICATION_OPTIONS = [
68 68 ['all', :label_user_mail_option_all],
69 69 ['selected', :label_user_mail_option_selected],
70 70 ['only_my_events', :label_user_mail_option_only_my_events],
71 71 ['only_assigned', :label_user_mail_option_only_assigned],
72 72 ['only_owner', :label_user_mail_option_only_owner],
73 73 ['none', :label_user_mail_option_none]
74 74 ]
75 75
76 76 has_and_belongs_to_many :groups,
77 77 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
78 78 :after_add => Proc.new {|user, group| group.user_added(user)},
79 79 :after_remove => Proc.new {|user, group| group.user_removed(user)}
80 80 has_many :changesets, :dependent => :nullify
81 81 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
82 82 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
83 83 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
84 has_one :email_address, lambda {where :is_default => true}, :autosave => true
85 has_many :email_addresses, :dependent => :delete_all
84 86 belongs_to :auth_source
85 87
86 88 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
87 89 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
88 90
89 91 acts_as_customizable
90 92
91 93 attr_accessor :password, :password_confirmation, :generate_password
92 94 attr_accessor :last_before_login_on
93 95 # Prevents unauthorized assignments
94 96 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
95 97
96 98 LOGIN_LENGTH_LIMIT = 60
97 99 MAIL_LENGTH_LIMIT = 60
98 100
99 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
101 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
100 102 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
101 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
102 103 # Login must contain letters, numbers, underscores only
103 104 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
104 105 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
105 106 validates_length_of :firstname, :lastname, :maximum => 30
106 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
107 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
108 107 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
109 108 validate :validate_password_length
110 109 validate do
111 110 if password_confirmation && password != password_confirmation
112 111 errors.add(:password, :confirmation)
113 112 end
114 113 end
115 114
115 before_validation :instantiate_email_address
116 116 before_create :set_mail_notification
117 117 before_save :generate_password_if_needed, :update_hashed_password
118 118 before_destroy :remove_references_before_destroy
119 119 after_save :update_notified_project_ids, :destroy_tokens
120 120
121 121 scope :in_group, lambda {|group|
122 122 group_id = group.is_a?(Group) ? group.id : group.to_i
123 123 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
124 124 }
125 125 scope :not_in_group, lambda {|group|
126 126 group_id = group.is_a?(Group) ? group.id : group.to_i
127 127 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
128 128 }
129 129 scope :sorted, lambda { order(*User.fields_for_order_statement)}
130 scope :having_mail, lambda {|arg|
131 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
132 if addresses.any?
133 joins(:email_addresses).where("LOWER(address) IN (?)", addresses).uniq
134 else
135 none
136 end
137 }
130 138
131 139 def set_mail_notification
132 140 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
133 141 true
134 142 end
135 143
136 144 def update_hashed_password
137 145 # update hashed_password if password was set
138 146 if self.password && self.auth_source_id.blank?
139 147 salt_password(password)
140 148 end
141 149 end
142 150
143 151 alias :base_reload :reload
144 152 def reload(*args)
145 153 @name = nil
146 154 @projects_by_role = nil
147 155 @membership_by_project_id = nil
148 156 @notified_projects_ids = nil
149 157 @notified_projects_ids_changed = false
150 158 @builtin_role = nil
151 159 @visible_project_ids = nil
152 160 base_reload(*args)
153 161 end
154 162
163 def mail
164 email_address.try(:address)
165 end
166
155 167 def mail=(arg)
156 write_attribute(:mail, arg.to_s.strip)
168 email = email_address || build_email_address
169 email.address = arg
170 end
171
172 def mail_changed?
173 email_address.try(:address_changed?)
174 end
175
176 def mails
177 email_addresses.pluck(:address)
157 178 end
158 179
159 180 def self.find_or_initialize_by_identity_url(url)
160 181 user = where(:identity_url => url).first
161 182 unless user
162 183 user = User.new
163 184 user.identity_url = url
164 185 end
165 186 user
166 187 end
167 188
168 189 def identity_url=(url)
169 190 if url.blank?
170 191 write_attribute(:identity_url, '')
171 192 else
172 193 begin
173 194 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
174 195 rescue OpenIdAuthentication::InvalidOpenId
175 196 # Invalid url, don't save
176 197 end
177 198 end
178 199 self.read_attribute(:identity_url)
179 200 end
180 201
181 202 # Returns the user that matches provided login and password, or nil
182 203 def self.try_to_login(login, password, active_only=true)
183 204 login = login.to_s
184 205 password = password.to_s
185 206
186 207 # Make sure no one can sign in with an empty login or password
187 208 return nil if login.empty? || password.empty?
188 209 user = find_by_login(login)
189 210 if user
190 211 # user is already in local database
191 212 return nil unless user.check_password?(password)
192 213 return nil if !user.active? && active_only
193 214 else
194 215 # user is not yet registered, try to authenticate with available sources
195 216 attrs = AuthSource.authenticate(login, password)
196 217 if attrs
197 218 user = new(attrs)
198 219 user.login = login
199 220 user.language = Setting.default_language
200 221 if user.save
201 222 user.reload
202 223 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
203 224 end
204 225 end
205 226 end
206 227 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
207 228 user
208 229 rescue => text
209 230 raise text
210 231 end
211 232
212 233 # Returns the user who matches the given autologin +key+ or nil
213 234 def self.try_to_autologin(key)
214 235 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
215 236 if user
216 237 user.update_column(:last_login_on, Time.now)
217 238 user
218 239 end
219 240 end
220 241
221 242 def self.name_formatter(formatter = nil)
222 243 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
223 244 end
224 245
225 246 # Returns an array of fields names than can be used to make an order statement for users
226 247 # according to how user names are displayed
227 248 # Examples:
228 249 #
229 250 # User.fields_for_order_statement => ['users.login', 'users.id']
230 251 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
231 252 def self.fields_for_order_statement(table=nil)
232 253 table ||= table_name
233 254 name_formatter[:order].map {|field| "#{table}.#{field}"}
234 255 end
235 256
236 257 # Return user's full name for display
237 258 def name(formatter = nil)
238 259 f = self.class.name_formatter(formatter)
239 260 if formatter
240 261 eval('"' + f[:string] + '"')
241 262 else
242 263 @name ||= eval('"' + f[:string] + '"')
243 264 end
244 265 end
245 266
246 267 def active?
247 268 self.status == STATUS_ACTIVE
248 269 end
249 270
250 271 def registered?
251 272 self.status == STATUS_REGISTERED
252 273 end
253 274
254 275 def locked?
255 276 self.status == STATUS_LOCKED
256 277 end
257 278
258 279 def activate
259 280 self.status = STATUS_ACTIVE
260 281 end
261 282
262 283 def register
263 284 self.status = STATUS_REGISTERED
264 285 end
265 286
266 287 def lock
267 288 self.status = STATUS_LOCKED
268 289 end
269 290
270 291 def activate!
271 292 update_attribute(:status, STATUS_ACTIVE)
272 293 end
273 294
274 295 def register!
275 296 update_attribute(:status, STATUS_REGISTERED)
276 297 end
277 298
278 299 def lock!
279 300 update_attribute(:status, STATUS_LOCKED)
280 301 end
281 302
282 303 # Returns true if +clear_password+ is the correct user's password, otherwise false
283 304 def check_password?(clear_password)
284 305 if auth_source_id.present?
285 306 auth_source.authenticate(self.login, clear_password)
286 307 else
287 308 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
288 309 end
289 310 end
290 311
291 312 # Generates a random salt and computes hashed_password for +clear_password+
292 313 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
293 314 def salt_password(clear_password)
294 315 self.salt = User.generate_salt
295 316 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
296 317 self.passwd_changed_on = Time.now
297 318 end
298 319
299 320 # Does the backend storage allow this user to change their password?
300 321 def change_password_allowed?
301 322 return true if auth_source.nil?
302 323 return auth_source.allow_password_changes?
303 324 end
304 325
305 326 def must_change_password?
306 327 must_change_passwd? && change_password_allowed?
307 328 end
308 329
309 330 def generate_password?
310 331 generate_password == '1' || generate_password == true
311 332 end
312 333
313 334 # Generate and set a random password on given length
314 335 def random_password(length=40)
315 336 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
316 337 chars -= %w(0 O 1 l)
317 338 password = ''
318 339 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
319 340 self.password = password
320 341 self.password_confirmation = password
321 342 self
322 343 end
323 344
324 345 def pref
325 346 self.preference ||= UserPreference.new(:user => self)
326 347 end
327 348
328 349 def time_zone
329 350 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
330 351 end
331 352
332 353 def force_default_language?
333 354 Setting.force_default_language_for_loggedin?
334 355 end
335 356
336 357 def language
337 358 if force_default_language?
338 359 Setting.default_language
339 360 else
340 361 super
341 362 end
342 363 end
343 364
344 365 def wants_comments_in_reverse_order?
345 366 self.pref[:comments_sorting] == 'desc'
346 367 end
347 368
348 369 # Return user's RSS key (a 40 chars long string), used to access feeds
349 370 def rss_key
350 371 if rss_token.nil?
351 372 create_rss_token(:action => 'feeds')
352 373 end
353 374 rss_token.value
354 375 end
355 376
356 377 # Return user's API key (a 40 chars long string), used to access the API
357 378 def api_key
358 379 if api_token.nil?
359 380 create_api_token(:action => 'api')
360 381 end
361 382 api_token.value
362 383 end
363 384
364 385 # Return an array of project ids for which the user has explicitly turned mail notifications on
365 386 def notified_projects_ids
366 387 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
367 388 end
368 389
369 390 def notified_project_ids=(ids)
370 391 @notified_projects_ids_changed = true
371 392 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
372 393 end
373 394
374 395 # Updates per project notifications (after_save callback)
375 396 def update_notified_project_ids
376 397 if @notified_projects_ids_changed
377 398 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
378 399 members.update_all(:mail_notification => false)
379 400 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
380 401 end
381 402 end
382 403 private :update_notified_project_ids
383 404
384 405 def valid_notification_options
385 406 self.class.valid_notification_options(self)
386 407 end
387 408
388 409 # Only users that belong to more than 1 project can select projects for which they are notified
389 410 def self.valid_notification_options(user=nil)
390 411 # Note that @user.membership.size would fail since AR ignores
391 412 # :include association option when doing a count
392 413 if user.nil? || user.memberships.length < 1
393 414 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
394 415 else
395 416 MAIL_NOTIFICATION_OPTIONS
396 417 end
397 418 end
398 419
399 420 # Find a user account by matching the exact login and then a case-insensitive
400 421 # version. Exact matches will be given priority.
401 422 def self.find_by_login(login)
402 423 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
403 424 if login.present?
404 425 # First look for an exact match
405 426 user = where(:login => login).detect {|u| u.login == login}
406 427 unless user
407 428 # Fail over to case-insensitive if none was found
408 429 user = where("LOWER(login) = ?", login.downcase).first
409 430 end
410 431 user
411 432 end
412 433 end
413 434
414 435 def self.find_by_rss_key(key)
415 436 Token.find_active_user('feeds', key)
416 437 end
417 438
418 439 def self.find_by_api_key(key)
419 440 Token.find_active_user('api', key)
420 441 end
421 442
422 443 # Makes find_by_mail case-insensitive
423 444 def self.find_by_mail(mail)
424 where("LOWER(mail) = ?", mail.to_s.downcase).first
445 having_mail(mail).first
425 446 end
426 447
427 448 # Returns true if the default admin account can no longer be used
428 449 def self.default_admin_account_changed?
429 450 !User.active.find_by_login("admin").try(:check_password?, "admin")
430 451 end
431 452
432 453 def to_s
433 454 name
434 455 end
435 456
436 457 CSS_CLASS_BY_STATUS = {
437 458 STATUS_ANONYMOUS => 'anon',
438 459 STATUS_ACTIVE => 'active',
439 460 STATUS_REGISTERED => 'registered',
440 461 STATUS_LOCKED => 'locked'
441 462 }
442 463
443 464 def css_classes
444 465 "user #{CSS_CLASS_BY_STATUS[status]}"
445 466 end
446 467
447 468 # Returns the current day according to user's time zone
448 469 def today
449 470 if time_zone.nil?
450 471 Date.today
451 472 else
452 473 Time.now.in_time_zone(time_zone).to_date
453 474 end
454 475 end
455 476
456 477 # Returns the day of +time+ according to user's time zone
457 478 def time_to_date(time)
458 479 if time_zone.nil?
459 480 time.to_date
460 481 else
461 482 time.in_time_zone(time_zone).to_date
462 483 end
463 484 end
464 485
465 486 def logged?
466 487 true
467 488 end
468 489
469 490 def anonymous?
470 491 !logged?
471 492 end
472 493
473 494 # Returns user's membership for the given project
474 495 # or nil if the user is not a member of project
475 496 def membership(project)
476 497 project_id = project.is_a?(Project) ? project.id : project
477 498
478 499 @membership_by_project_id ||= Hash.new {|h, project_id|
479 500 h[project_id] = memberships.where(:project_id => project_id).first
480 501 }
481 502 @membership_by_project_id[project_id]
482 503 end
483 504
484 505 # Returns the user's bult-in role
485 506 def builtin_role
486 507 @builtin_role ||= Role.non_member
487 508 end
488 509
489 510 # Return user's roles for project
490 511 def roles_for_project(project)
491 512 # No role on archived projects
492 513 return [] if project.nil? || project.archived?
493 514 if membership = membership(project)
494 515 membership.roles.dup
495 516 elsif project.is_public?
496 517 project.override_roles(builtin_role)
497 518 else
498 519 []
499 520 end
500 521 end
501 522
502 523 # Returns a hash of user's projects grouped by roles
503 524 def projects_by_role
504 525 return @projects_by_role if @projects_by_role
505 526
506 527 hash = Hash.new([])
507 528
508 529 group_class = anonymous? ? GroupAnonymous : GroupNonMember
509 530 members = Member.joins(:project, :principal).
510 531 where("#{Project.table_name}.status <> 9").
511 532 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
512 533 preload(:project, :roles).
513 534 to_a
514 535
515 536 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
516 537 members.each do |member|
517 538 if member.project
518 539 member.roles.each do |role|
519 540 hash[role] = [] unless hash.key?(role)
520 541 hash[role] << member.project
521 542 end
522 543 end
523 544 end
524 545
525 546 hash.each do |role, projects|
526 547 projects.uniq!
527 548 end
528 549
529 550 @projects_by_role = hash
530 551 end
531 552
532 553 # Returns the ids of visible projects
533 554 def visible_project_ids
534 555 @visible_project_ids ||= Project.visible(self).pluck(:id)
535 556 end
536 557
537 558 # Returns true if user is arg or belongs to arg
538 559 def is_or_belongs_to?(arg)
539 560 if arg.is_a?(User)
540 561 self == arg
541 562 elsif arg.is_a?(Group)
542 563 arg.users.include?(self)
543 564 else
544 565 false
545 566 end
546 567 end
547 568
548 569 # Return true if the user is allowed to do the specified action on a specific context
549 570 # Action can be:
550 571 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
551 572 # * a permission Symbol (eg. :edit_project)
552 573 # Context can be:
553 574 # * a project : returns true if user is allowed to do the specified action on this project
554 575 # * an array of projects : returns true if user is allowed on every project
555 576 # * nil with options[:global] set : check if user has at least one role allowed for this action,
556 577 # or falls back to Non Member / Anonymous permissions depending if the user is logged
557 578 def allowed_to?(action, context, options={}, &block)
558 579 if context && context.is_a?(Project)
559 580 return false unless context.allows_to?(action)
560 581 # Admin users are authorized for anything else
561 582 return true if admin?
562 583
563 584 roles = roles_for_project(context)
564 585 return false unless roles
565 586 roles.any? {|role|
566 587 (context.is_public? || role.member?) &&
567 588 role.allowed_to?(action) &&
568 589 (block_given? ? yield(role, self) : true)
569 590 }
570 591 elsif context && context.is_a?(Array)
571 592 if context.empty?
572 593 false
573 594 else
574 595 # Authorize if user is authorized on every element of the array
575 596 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
576 597 end
577 598 elsif context
578 599 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
579 600 elsif options[:global]
580 601 # Admin users are always authorized
581 602 return true if admin?
582 603
583 604 # authorize if user has at least one role that has this permission
584 605 roles = memberships.collect {|m| m.roles}.flatten.uniq
585 606 roles << (self.logged? ? Role.non_member : Role.anonymous)
586 607 roles.any? {|role|
587 608 role.allowed_to?(action) &&
588 609 (block_given? ? yield(role, self) : true)
589 610 }
590 611 else
591 612 false
592 613 end
593 614 end
594 615
595 616 # Is the user allowed to do the specified action on any project?
596 617 # See allowed_to? for the actions and valid options.
597 618 #
598 619 # NB: this method is not used anywhere in the core codebase as of
599 620 # 2.5.2, but it's used by many plugins so if we ever want to remove
600 621 # it it has to be carefully deprecated for a version or two.
601 622 def allowed_to_globally?(action, options={}, &block)
602 623 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
603 624 end
604 625
605 626 # Returns true if the user is allowed to delete the user's own account
606 627 def own_account_deletable?
607 628 Setting.unsubscribe? &&
608 629 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
609 630 end
610 631
611 632 safe_attributes 'login',
612 633 'firstname',
613 634 'lastname',
614 635 'mail',
615 636 'mail_notification',
616 637 'notified_project_ids',
617 638 'language',
618 639 'custom_field_values',
619 640 'custom_fields',
620 641 'identity_url'
621 642
622 643 safe_attributes 'status',
623 644 'auth_source_id',
624 645 'generate_password',
625 646 'must_change_passwd',
626 647 :if => lambda {|user, current_user| current_user.admin?}
627 648
628 649 safe_attributes 'group_ids',
629 650 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
630 651
631 652 # Utility method to help check if a user should be notified about an
632 653 # event.
633 654 #
634 655 # TODO: only supports Issue events currently
635 656 def notify_about?(object)
636 657 if mail_notification == 'all'
637 658 true
638 659 elsif mail_notification.blank? || mail_notification == 'none'
639 660 false
640 661 else
641 662 case object
642 663 when Issue
643 664 case mail_notification
644 665 when 'selected', 'only_my_events'
645 666 # user receives notifications for created/assigned issues on unselected projects
646 667 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
647 668 when 'only_assigned'
648 669 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
649 670 when 'only_owner'
650 671 object.author == self
651 672 end
652 673 when News
653 674 # always send to project members except when mail_notification is set to 'none'
654 675 true
655 676 end
656 677 end
657 678 end
658 679
659 680 def self.current=(user)
660 681 RequestStore.store[:current_user] = user
661 682 end
662 683
663 684 def self.current
664 685 RequestStore.store[:current_user] ||= User.anonymous
665 686 end
666 687
667 688 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
668 689 # one anonymous user per database.
669 690 def self.anonymous
670 691 anonymous_user = AnonymousUser.first
671 692 if anonymous_user.nil?
672 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
693 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
673 694 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
674 695 end
675 696 anonymous_user
676 697 end
677 698
678 699 # Salts all existing unsalted passwords
679 700 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
680 701 # This method is used in the SaltPasswords migration and is to be kept as is
681 702 def self.salt_unsalted_passwords!
682 703 transaction do
683 704 User.where("salt IS NULL OR salt = ''").find_each do |user|
684 705 next if user.hashed_password.blank?
685 706 salt = User.generate_salt
686 707 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
687 708 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
688 709 end
689 710 end
690 711 end
691 712
692 713 protected
693 714
694 715 def validate_password_length
695 716 return if password.blank? && generate_password?
696 717 # Password length validation based on setting
697 718 if !password.nil? && password.size < Setting.password_min_length.to_i
698 719 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
699 720 end
700 721 end
701 722
723 def instantiate_email_address
724 email_address || build_email_address
725 end
726
702 727 private
703 728
704 729 def generate_password_if_needed
705 730 if generate_password? && auth_source.nil?
706 731 length = [Setting.password_min_length.to_i + 2, 10].max
707 732 random_password(length)
708 733 end
709 734 end
710 735
711 # Delete all outstanding password reset tokens on password or email change.
736 # Delete all outstanding password reset tokens on password change.
712 737 # Delete the autologin tokens on password change to prohibit session leakage.
713 738 # This helps to keep the account secure in case the associated email account
714 739 # was compromised.
715 740 def destroy_tokens
716 tokens = []
717 tokens |= ['recovery', 'autologin'] if hashed_password_changed?
718 tokens |= ['recovery'] if mail_changed?
719
720 if tokens.any?
741 if hashed_password_changed?
742 tokens = ['recovery', 'autologin']
721 743 Token.where(:user_id => id, :action => tokens).delete_all
722 744 end
723 745 end
724 746
725 747 # Removes references that are not handled by associations
726 748 # Things that are not deleted are reassociated with the anonymous user
727 749 def remove_references_before_destroy
728 750 return if self.id.nil?
729 751
730 752 substitute = User.anonymous
731 753 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
732 754 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
733 755 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
734 756 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
735 757 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
736 758 JournalDetail.
737 759 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
738 760 update_all(['old_value = ?', substitute.id.to_s])
739 761 JournalDetail.
740 762 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
741 763 update_all(['value = ?', substitute.id.to_s])
742 764 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
743 765 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
744 766 # Remove private queries and keep public ones
745 767 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
746 768 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
747 769 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
748 770 Token.delete_all ['user_id = ?', id]
749 771 Watcher.delete_all ['user_id = ?', id]
750 772 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
751 773 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
752 774 end
753 775
754 776 # Return password digest
755 777 def self.hash_password(clear_password)
756 778 Digest::SHA1.hexdigest(clear_password || "")
757 779 end
758 780
759 781 # Returns a 128bits random salt as a hex string (32 chars long)
760 782 def self.generate_salt
761 783 Redmine::Utils.random_hex(16)
762 784 end
763 785
764 786 end
765 787
766 788 class AnonymousUser < User
767 789 validate :validate_anonymous_uniqueness, :on => :create
768 790
769 791 def validate_anonymous_uniqueness
770 792 # There should be only one AnonymousUser in the database
771 793 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
772 794 end
773 795
774 796 def available_custom_fields
775 797 []
776 798 end
777 799
778 800 # Overrides a few properties
779 801 def logged?; false end
780 802 def admin; false end
781 803 def name(*args); I18n.t(:label_user_anonymous) end
804 def mail=(*args); nil end
782 805 def mail; nil end
783 806 def time_zone; nil end
784 807 def rss_key; nil end
785 808
786 809 def pref
787 810 UserPreference.new(:user => self)
788 811 end
789 812
790 813 # Returns the user's bult-in role
791 814 def builtin_role
792 815 @builtin_role ||= Role.anonymous
793 816 end
794 817
795 818 def membership(*args)
796 819 nil
797 820 end
798 821
799 822 def member_of?(*args)
800 823 false
801 824 end
802 825
803 826 # Anonymous user can not be destroyed
804 827 def destroy
805 828 false
806 829 end
830
831 protected
832
833 def instantiate_email_address
834 end
807 835 end
@@ -1,166 +1,168
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 'zlib'
19 19
20 20 class WikiContent < ActiveRecord::Base
21 21 self.locking_column = 'version'
22 22 belongs_to :page, :class_name => 'WikiPage'
23 23 belongs_to :author, :class_name => 'User'
24 24 validates_presence_of :text
25 25 validates_length_of :comments, :maximum => 255, :allow_nil => true
26 26 attr_protected :id
27 27
28 28 acts_as_versioned
29 29
30 30 after_save :send_notification
31 31
32 32 def visible?(user=User.current)
33 33 page.visible?(user)
34 34 end
35 35
36 36 def project
37 37 page.project
38 38 end
39 39
40 40 def attachments
41 41 page.nil? ? [] : page.attachments
42 42 end
43 43
44 def notified_users
45 project.notified_users.reject {|user| !visible?(user)}
46 end
47
44 48 # Returns the mail addresses of users that should be notified
45 49 def recipients
46 notified = project.notified_users
47 notified.reject! {|user| !visible?(user)}
48 notified.collect(&:mail)
50 notified_users.collect(&:mail)
49 51 end
50 52
51 53 # Return true if the content is the current page content
52 54 def current_version?
53 55 true
54 56 end
55 57
56 58 class Version
57 59 belongs_to :page, :class_name => '::WikiPage'
58 60 belongs_to :author, :class_name => '::User'
59 61 attr_protected :data
60 62
61 63 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
62 64 :description => :comments,
63 65 :datetime => :updated_on,
64 66 :type => 'wiki-page',
65 67 :group => :page,
66 68 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}}
67 69
68 70 acts_as_activity_provider :type => 'wiki_edits',
69 71 :timestamp => "#{WikiContent.versioned_table_name}.updated_on",
70 72 :author_key => "#{WikiContent.versioned_table_name}.author_id",
71 73 :permission => :view_wiki_edits,
72 74 :scope => select("#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
73 75 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
74 76 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
75 77 "#{WikiContent.versioned_table_name}.id").
76 78 joins("LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
77 79 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
78 80 "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id")
79 81
80 82 after_destroy :page_update_after_destroy
81 83
82 84 def text=(plain)
83 85 case Setting.wiki_compression
84 86 when 'gzip'
85 87 begin
86 88 self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
87 89 self.compression = 'gzip'
88 90 rescue
89 91 self.data = plain
90 92 self.compression = ''
91 93 end
92 94 else
93 95 self.data = plain
94 96 self.compression = ''
95 97 end
96 98 plain
97 99 end
98 100
99 101 def text
100 102 @text ||= begin
101 103 str = case compression
102 104 when 'gzip'
103 105 Zlib::Inflate.inflate(data)
104 106 else
105 107 # uncompressed data
106 108 data
107 109 end
108 110 str.force_encoding("UTF-8")
109 111 str
110 112 end
111 113 end
112 114
113 115 def project
114 116 page.project
115 117 end
116 118
117 119 # Return true if the content is the current page content
118 120 def current_version?
119 121 page.content.version == self.version
120 122 end
121 123
122 124 # Returns the previous version or nil
123 125 def previous
124 126 @previous ||= WikiContent::Version.
125 127 reorder('version DESC').
126 128 includes(:author).
127 129 where("wiki_content_id = ? AND version < ?", wiki_content_id, version).first
128 130 end
129 131
130 132 # Returns the next version or nil
131 133 def next
132 134 @next ||= WikiContent::Version.
133 135 reorder('version ASC').
134 136 includes(:author).
135 137 where("wiki_content_id = ? AND version > ?", wiki_content_id, version).first
136 138 end
137 139
138 140 private
139 141
140 142 # Updates page's content if the latest version is removed
141 143 # or destroys the page if it was the only version
142 144 def page_update_after_destroy
143 145 latest = page.content.versions.reorder("#{self.class.table_name}.version DESC").first
144 146 if latest && page.content.version != latest.version
145 147 raise ActiveRecord::Rollback unless page.content.revert_to!(latest)
146 148 elsif latest.nil?
147 149 raise ActiveRecord::Rollback unless page.destroy
148 150 end
149 151 end
150 152 end
151 153
152 154 private
153 155
154 156 def send_notification
155 157 # new_record? returns false in after_save callbacks
156 158 if id_changed?
157 159 if Setting.notified_events.include?('wiki_content_added')
158 160 Mailer.wiki_content_added(self).deliver
159 161 end
160 162 elsif text_changed?
161 163 if Setting.notified_events.include?('wiki_content_updated')
162 164 Mailer.wiki_content_updated(self).deliver
163 165 end
164 166 end
165 167 end
166 168 end
@@ -1,54 +1,55
1 1 <div class="contextual">
2 <%= additional_emails_link(@user) %>
2 3 <%= link_to(l(:button_change_password), {:action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
3 4 <%= call_hook(:view_my_account_contextual, :user => @user)%>
4 5 </div>
5 6
6 7 <h2><%=l(:label_my_account)%></h2>
7 8 <%= error_messages_for 'user' %>
8 9
9 10 <%= labelled_form_for :user, @user,
10 11 :url => { :action => "account" },
11 12 :html => { :id => 'my_account_form',
12 13 :method => :post } do |f| %>
13 14 <div class="splitcontentleft">
14 15 <fieldset class="box tabular">
15 16 <legend><%=l(:label_information_plural)%></legend>
16 17 <p><%= f.text_field :firstname, :required => true %></p>
17 18 <p><%= f.text_field :lastname, :required => true %></p>
18 19 <p><%= f.text_field :mail, :required => true %></p>
19 20 <% unless @user.force_default_language? %>
20 21 <p><%= f.select :language, lang_options_for_select %></p>
21 22 <% end %>
22 23 <% if Setting.openid? %>
23 24 <p><%= f.text_field :identity_url %></p>
24 25 <% end %>
25 26
26 27 <% @user.custom_field_values.select(&:editable?).each do |value| %>
27 28 <p><%= custom_field_tag_with_label :user, value %></p>
28 29 <% end %>
29 30 <%= call_hook(:view_my_account, :user => @user, :form => f) %>
30 31 </fieldset>
31 32
32 33 <%= submit_tag l(:button_save) %>
33 34 </div>
34 35
35 36 <div class="splitcontentright">
36 37 <fieldset class="box">
37 38 <legend><%=l(:field_mail_notification)%></legend>
38 39 <%= render :partial => 'users/mail_notifications' %>
39 40 </fieldset>
40 41
41 42 <fieldset class="box tabular">
42 43 <legend><%=l(:label_preferences)%></legend>
43 44 <%= render :partial => 'users/preferences' %>
44 45 <%= call_hook(:view_my_account_preferences, :user => @user, :form => f) %>
45 46 </fieldset>
46 47
47 48 </div>
48 49 <% end %>
49 50
50 51 <% content_for :sidebar do %>
51 52 <%= render :partial => 'sidebar' %>
52 53 <% end %>
53 54
54 55 <% html_title(l(:label_my_account)) -%>
@@ -1,38 +1,40
1 1 <%= form_tag({:action => 'edit', :tab => 'authentication'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_check_box :login_required %></p>
5 5
6 6 <p><%= setting_select :autologin, [[l(:label_disabled), 0]] + [1, 7, 30, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), days.to_s]} %></p>
7 7
8 8 <p><%= setting_select :self_registration, [[l(:label_disabled), "0"],
9 9 [l(:label_registration_activation_by_email), "1"],
10 10 [l(:label_registration_manual_activation), "2"],
11 11 [l(:label_registration_automatic_activation), "3"]] %></p>
12 12
13 13 <p><%= setting_check_box :unsubscribe %></p>
14 14
15 15 <p><%= setting_text_field :password_min_length, :size => 6 %></p>
16 16
17 17 <p><%= setting_check_box :lost_password, :label => :label_password_lost %></p>
18 18
19 <p><%= setting_text_field :max_additional_emails, :size => 6 %></p>
20
19 21 <p><%= setting_check_box :openid, :disabled => !Object.const_defined?(:OpenID) %></p>
20 22
21 23 <p><%= setting_check_box :rest_api_enabled %></p>
22 24
23 25 <p><%= setting_check_box :jsonp_enabled %></p>
24 26 </div>
25 27
26 28 <fieldset class="box">
27 29 <legend><%= l(:label_session_expiration) %></legend>
28 30
29 31 <div class="tabular settings">
30 32 <p><%= setting_select :session_lifetime, [[l(:label_disabled), 0]] + [1, 7, 30, 60, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), (days * 60 * 24).to_s]} %></p>
31 33 <p><%= setting_select :session_timeout, [[l(:label_disabled), 0]] + [1, 2, 4, 8, 12, 24, 48].collect{|hours| [l('datetime.distance_in_words.x_hours', :count => hours), (hours * 60).to_s]} %></p>
32 34 </div>
33 35
34 36 <p><em class="info"><%= l(:text_session_expiration_settings) %></em></p>
35 37 </fieldset>
36 38
37 39 <%= submit_tag l(:button_save) %>
38 40 <% end %>
@@ -1,9 +1,10
1 1 <div class="contextual">
2 2 <%= link_to l(:label_profile), user_path(@user), :class => 'icon icon-user' %>
3 <%= additional_emails_link(@user) %>
3 4 <%= change_status_link(@user) %>
4 5 <%= delete_link user_path(@user) if User.current != @user %>
5 6 </div>
6 7
7 8 <%= title [l(:label_user_plural), users_path], @user.login %>
8 9
9 10 <%= render_tabs user_settings_tabs %>
@@ -1,195 +1,194
1 1 require 'active_record'
2 2
3 3 module ActiveRecord
4 4 class Base
5 5 include Redmine::I18n
6 6 # Translate attribute names for validation errors display
7 7 def self.human_attribute_name(attr, *args)
8 attr = attr.to_s.sub(/_id$/, '')
9
8 attr = attr.to_s.sub(/_id$/, '').sub(/^.+\./, '')
10 9 l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr])
11 10 end
12 11 end
13 12
14 13 # Undefines private Kernel#open method to allow using `open` scopes in models.
15 14 # See Defect #11545 (http://www.redmine.org/issues/11545) for details.
16 15 class Base
17 16 class << self
18 17 undef open
19 18 end
20 19 end
21 20 class Relation ; undef open ; end
22 21 end
23 22
24 23 module ActionView
25 24 module Helpers
26 25 module DateHelper
27 26 # distance_of_time_in_words breaks when difference is greater than 30 years
28 27 def distance_of_date_in_words(from_date, to_date = 0, options = {})
29 28 from_date = from_date.to_date if from_date.respond_to?(:to_date)
30 29 to_date = to_date.to_date if to_date.respond_to?(:to_date)
31 30 distance_in_days = (to_date - from_date).abs
32 31
33 32 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
34 33 case distance_in_days
35 34 when 0..60 then locale.t :x_days, :count => distance_in_days.round
36 35 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
37 36 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
38 37 end
39 38 end
40 39 end
41 40 end
42 41 end
43 42
44 43 class Resolver
45 44 def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
46 45 cached(key, [name, prefix, partial], details, locals) do
47 46 if details[:formats] & [:xml, :json]
48 47 details = details.dup
49 48 details[:formats] = details[:formats].dup + [:api]
50 49 end
51 50 find_templates(name, prefix, partial, details)
52 51 end
53 52 end
54 53 end
55 54 end
56 55
57 56 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| html_tag || ''.html_safe }
58 57
59 58 # HTML5: <option value=""></option> is invalid, use <option value="">&nbsp;</option> instead
60 59 module ActionView
61 60 module Helpers
62 61 module Tags
63 62 class Base
64 63 private
65 64 def add_options_with_non_empty_blank_option(option_tags, options, value = nil)
66 65 if options[:include_blank] == true
67 66 options = options.dup
68 67 options[:include_blank] = '&nbsp;'.html_safe
69 68 end
70 69 add_options_without_non_empty_blank_option(option_tags, options, value)
71 70 end
72 71 alias_method_chain :add_options, :non_empty_blank_option
73 72 end
74 73 end
75 74
76 75 module FormTagHelper
77 76 def select_tag_with_non_empty_blank_option(name, option_tags = nil, options = {})
78 77 if options.delete(:include_blank)
79 78 options[:prompt] = '&nbsp;'.html_safe
80 79 end
81 80 select_tag_without_non_empty_blank_option(name, option_tags, options)
82 81 end
83 82 alias_method_chain :select_tag, :non_empty_blank_option
84 83 end
85 84
86 85 module FormOptionsHelper
87 86 def options_for_select_with_non_empty_blank_option(container, selected = nil)
88 87 if container.is_a?(Array)
89 88 container = container.map {|element| element.blank? ? ["&nbsp;".html_safe, ""] : element}
90 89 end
91 90 options_for_select_without_non_empty_blank_option(container, selected)
92 91 end
93 92 alias_method_chain :options_for_select, :non_empty_blank_option
94 93 end
95 94 end
96 95 end
97 96
98 97 require 'mail'
99 98
100 99 module DeliveryMethods
101 100 class AsyncSMTP < ::Mail::SMTP
102 101 def deliver!(*args)
103 102 Thread.start do
104 103 super *args
105 104 end
106 105 end
107 106 end
108 107
109 108 class AsyncSendmail < ::Mail::Sendmail
110 109 def deliver!(*args)
111 110 Thread.start do
112 111 super *args
113 112 end
114 113 end
115 114 end
116 115
117 116 class TmpFile
118 117 def initialize(*args); end
119 118
120 119 def deliver!(mail)
121 120 dest_dir = File.join(Rails.root, 'tmp', 'emails')
122 121 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
123 122 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
124 123 end
125 124 end
126 125 end
127 126
128 127 ActionMailer::Base.add_delivery_method :async_smtp, DeliveryMethods::AsyncSMTP
129 128 ActionMailer::Base.add_delivery_method :async_sendmail, DeliveryMethods::AsyncSendmail
130 129 ActionMailer::Base.add_delivery_method :tmp_file, DeliveryMethods::TmpFile
131 130
132 131 # Changes how sent emails are logged
133 132 # Rails doesn't log cc and bcc which is misleading when using bcc only (#12090)
134 133 module ActionMailer
135 134 class LogSubscriber < ActiveSupport::LogSubscriber
136 135 def deliver(event)
137 136 recipients = [:to, :cc, :bcc].inject("") do |s, header|
138 137 r = Array.wrap(event.payload[header])
139 138 if r.any?
140 139 s << "\n #{header}: #{r.join(', ')}"
141 140 end
142 141 s
143 142 end
144 143 info("\nSent email \"#{event.payload[:subject]}\" (%1.fms)#{recipients}" % event.duration)
145 144 debug(event.payload[:mail])
146 145 end
147 146 end
148 147 end
149 148
150 149 module ActionController
151 150 module MimeResponds
152 151 class Collector
153 152 def api(&block)
154 153 any(:xml, :json, &block)
155 154 end
156 155 end
157 156 end
158 157 end
159 158
160 159 module ActionController
161 160 class Base
162 161 # Displays an explicit message instead of a NoMethodError exception
163 162 # when trying to start Redmine with an old session_store.rb
164 163 # TODO: remove it in a later version
165 164 def self.session=(*args)
166 165 $stderr.puts "Please remove config/initializers/session_store.rb and run `rake generate_secret_token`.\n" +
167 166 "Setting the session secret with ActionController.session= is no longer supported in Rails 3."
168 167 exit 1
169 168 end
170 169 end
171 170 end
172 171
173 172 if Rails::VERSION::MAJOR < 4 && RUBY_VERSION >= "2.1"
174 173 module ActiveSupport
175 174 class HashWithIndifferentAccess
176 175 def select(*args, &block)
177 176 dup.tap { |hash| hash.select!(*args, &block) }
178 177 end
179 178
180 179 def reject(*args, &block)
181 180 dup.tap { |hash| hash.reject!(*args, &block) }
182 181 end
183 182 end
184 183
185 184 class OrderedHash
186 185 def select(*args, &block)
187 186 dup.tap { |hash| hash.select!(*args, &block) }
188 187 end
189 188
190 189 def reject(*args, &block)
191 190 dup.tap { |hash| hash.reject!(*args, &block) }
192 191 end
193 192 end
194 193 end
195 194 end
@@ -1,1125 +1,1131
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
208 208 mail_subject_lost_password: "Your %{value} password"
209 209 mail_body_lost_password: 'To change your password, click on the following link:'
210 210 mail_subject_register: "Your %{value} account activation"
211 211 mail_body_register: 'To activate your account, click on the following link:'
212 212 mail_body_account_information_external: "You can use your %{value} account to log in."
213 213 mail_body_account_information: Your account information
214 214 mail_subject_account_activation_request: "%{value} account activation request"
215 215 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
216 216 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
217 217 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
218 218 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
219 219 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
220 220 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
221 221 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
222 222
223 223 field_name: Name
224 224 field_description: Description
225 225 field_summary: Summary
226 226 field_is_required: Required
227 227 field_firstname: First name
228 228 field_lastname: Last name
229 229 field_mail: Email
230 field_address: Email
230 231 field_filename: File
231 232 field_filesize: Size
232 233 field_downloads: Downloads
233 234 field_author: Author
234 235 field_created_on: Created
235 236 field_updated_on: Updated
236 237 field_closed_on: Closed
237 238 field_field_format: Format
238 239 field_is_for_all: For all projects
239 240 field_possible_values: Possible values
240 241 field_regexp: Regular expression
241 242 field_min_length: Minimum length
242 243 field_max_length: Maximum length
243 244 field_value: Value
244 245 field_category: Category
245 246 field_title: Title
246 247 field_project: Project
247 248 field_issue: Issue
248 249 field_status: Status
249 250 field_notes: Notes
250 251 field_is_closed: Issue closed
251 252 field_is_default: Default value
252 253 field_tracker: Tracker
253 254 field_subject: Subject
254 255 field_due_date: Due date
255 256 field_assigned_to: Assignee
256 257 field_priority: Priority
257 258 field_fixed_version: Target version
258 259 field_user: User
259 260 field_principal: Principal
260 261 field_role: Role
261 262 field_homepage: Homepage
262 263 field_is_public: Public
263 264 field_parent: Subproject of
264 265 field_is_in_roadmap: Issues displayed in roadmap
265 266 field_login: Login
266 267 field_mail_notification: Email notifications
267 268 field_admin: Administrator
268 269 field_last_login_on: Last connection
269 270 field_language: Language
270 271 field_effective_date: Date
271 272 field_password: Password
272 273 field_new_password: New password
273 274 field_password_confirmation: Confirmation
274 275 field_version: Version
275 276 field_type: Type
276 277 field_host: Host
277 278 field_port: Port
278 279 field_account: Account
279 280 field_base_dn: Base DN
280 281 field_attr_login: Login attribute
281 282 field_attr_firstname: Firstname attribute
282 283 field_attr_lastname: Lastname attribute
283 284 field_attr_mail: Email attribute
284 285 field_onthefly: On-the-fly user creation
285 286 field_start_date: Start date
286 287 field_done_ratio: "% Done"
287 288 field_auth_source: Authentication mode
288 289 field_hide_mail: Hide my email address
289 290 field_comments: Comment
290 291 field_url: URL
291 292 field_start_page: Start page
292 293 field_subproject: Subproject
293 294 field_hours: Hours
294 295 field_activity: Activity
295 296 field_spent_on: Date
296 297 field_identifier: Identifier
297 298 field_is_filter: Used as a filter
298 299 field_issue_to: Related issue
299 300 field_delay: Delay
300 301 field_assignable: Issues can be assigned to this role
301 302 field_redirect_existing_links: Redirect existing links
302 303 field_estimated_hours: Estimated time
303 304 field_column_names: Columns
304 305 field_time_entries: Log time
305 306 field_time_zone: Time zone
306 307 field_searchable: Searchable
307 308 field_default_value: Default value
308 309 field_comments_sorting: Display comments
309 310 field_parent_title: Parent page
310 311 field_editable: Editable
311 312 field_watcher: Watcher
312 313 field_identity_url: OpenID URL
313 314 field_content: Content
314 315 field_group_by: Group results by
315 316 field_sharing: Sharing
316 317 field_parent_issue: Parent task
317 318 field_member_of_group: "Assignee's group"
318 319 field_assigned_to_role: "Assignee's role"
319 320 field_text: Text field
320 321 field_visible: Visible
321 322 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
322 323 field_issues_visibility: Issues visibility
323 324 field_is_private: Private
324 325 field_commit_logs_encoding: Commit messages encoding
325 326 field_scm_path_encoding: Path encoding
326 327 field_path_to_repository: Path to repository
327 328 field_root_directory: Root directory
328 329 field_cvsroot: CVSROOT
329 330 field_cvs_module: Module
330 331 field_repository_is_default: Main repository
331 332 field_multiple: Multiple values
332 333 field_auth_source_ldap_filter: LDAP filter
333 334 field_core_fields: Standard fields
334 335 field_timeout: "Timeout (in seconds)"
335 336 field_board_parent: Parent forum
336 337 field_private_notes: Private notes
337 338 field_inherit_members: Inherit members
338 339 field_generate_password: Generate password
339 340 field_must_change_passwd: Must change password at next logon
340 341 field_default_status: Default status
341 342 field_users_visibility: Users visibility
342 343
343 344 setting_app_title: Application title
344 345 setting_app_subtitle: Application subtitle
345 346 setting_welcome_text: Welcome text
346 347 setting_default_language: Default language
347 348 setting_login_required: Authentication required
348 349 setting_self_registration: Self-registration
349 350 setting_attachment_max_size: Maximum attachment size
350 351 setting_issues_export_limit: Issues export limit
351 352 setting_mail_from: Emission email address
352 353 setting_bcc_recipients: Blind carbon copy recipients (bcc)
353 354 setting_plain_text_mail: Plain text mail (no HTML)
354 355 setting_host_name: Host name and path
355 356 setting_text_formatting: Text formatting
356 357 setting_wiki_compression: Wiki history compression
357 358 setting_feeds_limit: Maximum number of items in Atom feeds
358 359 setting_default_projects_public: New projects are public by default
359 360 setting_autofetch_changesets: Fetch commits automatically
360 361 setting_sys_api_enabled: Enable WS for repository management
361 362 setting_commit_ref_keywords: Referencing keywords
362 363 setting_commit_fix_keywords: Fixing keywords
363 364 setting_autologin: Autologin
364 365 setting_date_format: Date format
365 366 setting_time_format: Time format
366 367 setting_cross_project_issue_relations: Allow cross-project issue relations
367 368 setting_cross_project_subtasks: Allow cross-project subtasks
368 369 setting_issue_list_default_columns: Default columns displayed on the issue list
369 370 setting_repositories_encodings: Attachments and repositories encodings
370 371 setting_emails_header: Email header
371 372 setting_emails_footer: Email footer
372 373 setting_protocol: Protocol
373 374 setting_per_page_options: Objects per page options
374 375 setting_user_format: Users display format
375 376 setting_activity_days_default: Days displayed on project activity
376 377 setting_display_subprojects_issues: Display subprojects issues on main projects by default
377 378 setting_enabled_scm: Enabled SCM
378 379 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
379 380 setting_mail_handler_api_enabled: Enable WS for incoming emails
380 381 setting_mail_handler_api_key: API key
381 382 setting_sequential_project_identifiers: Generate sequential project identifiers
382 383 setting_gravatar_enabled: Use Gravatar user icons
383 384 setting_gravatar_default: Default Gravatar image
384 385 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
385 386 setting_file_max_size_displayed: Maximum size of text files displayed inline
386 387 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
387 388 setting_openid: Allow OpenID login and registration
388 389 setting_password_min_length: Minimum password length
389 390 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
390 391 setting_default_projects_modules: Default enabled modules for new projects
391 392 setting_issue_done_ratio: Calculate the issue done ratio with
392 393 setting_issue_done_ratio_issue_field: Use the issue field
393 394 setting_issue_done_ratio_issue_status: Use the issue status
394 395 setting_start_of_week: Start calendars on
395 396 setting_rest_api_enabled: Enable REST web service
396 397 setting_cache_formatted_text: Cache formatted text
397 398 setting_default_notification_option: Default notification option
398 399 setting_commit_logtime_enabled: Enable time logging
399 400 setting_commit_logtime_activity_id: Activity for logged time
400 401 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
401 402 setting_issue_group_assignment: Allow issue assignment to groups
402 403 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
403 404 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
404 405 setting_unsubscribe: Allow users to delete their own account
405 406 setting_session_lifetime: Session maximum lifetime
406 407 setting_session_timeout: Session inactivity timeout
407 408 setting_thumbnails_enabled: Display attachment thumbnails
408 409 setting_thumbnails_size: Thumbnails size (in pixels)
409 410 setting_non_working_week_days: Non-working days
410 411 setting_jsonp_enabled: Enable JSONP support
411 412 setting_default_projects_tracker_ids: Default trackers for new projects
412 413 setting_mail_handler_excluded_filenames: Exclude attachments by name
413 414 setting_force_default_language_for_anonymous: Force default language for anonymous users
414 415 setting_force_default_language_for_loggedin: Force default language for logged-in users
415 416 setting_link_copied_issue: Link issues on copy
417 setting_max_additional_emails: Maximum number of additional email addresses
416 418
417 419 permission_add_project: Create project
418 420 permission_add_subprojects: Create subprojects
419 421 permission_edit_project: Edit project
420 422 permission_close_project: Close / reopen the project
421 423 permission_select_project_modules: Select project modules
422 424 permission_manage_members: Manage members
423 425 permission_manage_project_activities: Manage project activities
424 426 permission_manage_versions: Manage versions
425 427 permission_manage_categories: Manage issue categories
426 428 permission_view_issues: View Issues
427 429 permission_add_issues: Add issues
428 430 permission_edit_issues: Edit issues
429 431 permission_manage_issue_relations: Manage issue relations
430 432 permission_set_issues_private: Set issues public or private
431 433 permission_set_own_issues_private: Set own issues public or private
432 434 permission_add_issue_notes: Add notes
433 435 permission_edit_issue_notes: Edit notes
434 436 permission_edit_own_issue_notes: Edit own notes
435 437 permission_view_private_notes: View private notes
436 438 permission_set_notes_private: Set notes as private
437 439 permission_move_issues: Move issues
438 440 permission_delete_issues: Delete issues
439 441 permission_manage_public_queries: Manage public queries
440 442 permission_save_queries: Save queries
441 443 permission_view_gantt: View gantt chart
442 444 permission_view_calendar: View calendar
443 445 permission_view_issue_watchers: View watchers list
444 446 permission_add_issue_watchers: Add watchers
445 447 permission_delete_issue_watchers: Delete watchers
446 448 permission_log_time: Log spent time
447 449 permission_view_time_entries: View spent time
448 450 permission_edit_time_entries: Edit time logs
449 451 permission_edit_own_time_entries: Edit own time logs
450 452 permission_manage_news: Manage news
451 453 permission_comment_news: Comment news
452 454 permission_view_documents: View documents
453 455 permission_add_documents: Add documents
454 456 permission_edit_documents: Edit documents
455 457 permission_delete_documents: Delete documents
456 458 permission_manage_files: Manage files
457 459 permission_view_files: View files
458 460 permission_manage_wiki: Manage wiki
459 461 permission_rename_wiki_pages: Rename wiki pages
460 462 permission_delete_wiki_pages: Delete wiki pages
461 463 permission_view_wiki_pages: View wiki
462 464 permission_view_wiki_edits: View wiki history
463 465 permission_edit_wiki_pages: Edit wiki pages
464 466 permission_delete_wiki_pages_attachments: Delete attachments
465 467 permission_protect_wiki_pages: Protect wiki pages
466 468 permission_manage_repository: Manage repository
467 469 permission_browse_repository: Browse repository
468 470 permission_view_changesets: View changesets
469 471 permission_commit_access: Commit access
470 472 permission_manage_boards: Manage forums
471 473 permission_view_messages: View messages
472 474 permission_add_messages: Post messages
473 475 permission_edit_messages: Edit messages
474 476 permission_edit_own_messages: Edit own messages
475 477 permission_delete_messages: Delete messages
476 478 permission_delete_own_messages: Delete own messages
477 479 permission_export_wiki_pages: Export wiki pages
478 480 permission_manage_subtasks: Manage subtasks
479 481 permission_manage_related_issues: Manage related issues
480 482
481 483 project_module_issue_tracking: Issue tracking
482 484 project_module_time_tracking: Time tracking
483 485 project_module_news: News
484 486 project_module_documents: Documents
485 487 project_module_files: Files
486 488 project_module_wiki: Wiki
487 489 project_module_repository: Repository
488 490 project_module_boards: Forums
489 491 project_module_calendar: Calendar
490 492 project_module_gantt: Gantt
491 493
492 494 label_user: User
493 495 label_user_plural: Users
494 496 label_user_new: New user
495 497 label_user_anonymous: Anonymous
496 498 label_project: Project
497 499 label_project_new: New project
498 500 label_project_plural: Projects
499 501 label_x_projects:
500 502 zero: no projects
501 503 one: 1 project
502 504 other: "%{count} projects"
503 505 label_project_all: All Projects
504 506 label_project_latest: Latest projects
505 507 label_issue: Issue
506 508 label_issue_new: New issue
507 509 label_issue_plural: Issues
508 510 label_issue_view_all: View all issues
509 511 label_issues_by: "Issues by %{value}"
510 512 label_issue_added: Issue added
511 513 label_issue_updated: Issue updated
512 514 label_issue_note_added: Note added
513 515 label_issue_status_updated: Status updated
514 516 label_issue_assigned_to_updated: Assignee updated
515 517 label_issue_priority_updated: Priority updated
516 518 label_document: Document
517 519 label_document_new: New document
518 520 label_document_plural: Documents
519 521 label_document_added: Document added
520 522 label_role: Role
521 523 label_role_plural: Roles
522 524 label_role_new: New role
523 525 label_role_and_permissions: Roles and permissions
524 526 label_role_anonymous: Anonymous
525 527 label_role_non_member: Non member
526 528 label_member: Member
527 529 label_member_new: New member
528 530 label_member_plural: Members
529 531 label_tracker: Tracker
530 532 label_tracker_plural: Trackers
531 533 label_tracker_new: New tracker
532 534 label_workflow: Workflow
533 535 label_issue_status: Issue status
534 536 label_issue_status_plural: Issue statuses
535 537 label_issue_status_new: New status
536 538 label_issue_category: Issue category
537 539 label_issue_category_plural: Issue categories
538 540 label_issue_category_new: New category
539 541 label_custom_field: Custom field
540 542 label_custom_field_plural: Custom fields
541 543 label_custom_field_new: New custom field
542 544 label_enumerations: Enumerations
543 545 label_enumeration_new: New value
544 546 label_information: Information
545 547 label_information_plural: Information
546 548 label_please_login: Please log in
547 549 label_register: Register
548 550 label_login_with_open_id_option: or login with OpenID
549 551 label_password_lost: Lost password
550 552 label_home: Home
551 553 label_my_page: My page
552 554 label_my_account: My account
553 555 label_my_projects: My projects
554 556 label_my_page_block: My page block
555 557 label_administration: Administration
556 558 label_login: Sign in
557 559 label_logout: Sign out
558 560 label_help: Help
559 561 label_reported_issues: Reported issues
560 562 label_assigned_to_me_issues: Issues assigned to me
561 563 label_last_login: Last connection
562 564 label_registered_on: Registered on
563 565 label_activity: Activity
564 566 label_overall_activity: Overall activity
565 567 label_user_activity: "%{value}'s activity"
566 568 label_new: New
567 569 label_logged_as: Logged in as
568 570 label_environment: Environment
569 571 label_authentication: Authentication
570 572 label_auth_source: Authentication mode
571 573 label_auth_source_new: New authentication mode
572 574 label_auth_source_plural: Authentication modes
573 575 label_subproject_plural: Subprojects
574 576 label_subproject_new: New subproject
575 577 label_and_its_subprojects: "%{value} and its subprojects"
576 578 label_min_max_length: Min - Max length
577 579 label_list: List
578 580 label_date: Date
579 581 label_integer: Integer
580 582 label_float: Float
581 583 label_boolean: Boolean
582 584 label_string: Text
583 585 label_text: Long text
584 586 label_attribute: Attribute
585 587 label_attribute_plural: Attributes
586 588 label_no_data: No data to display
587 589 label_change_status: Change status
588 590 label_history: History
589 591 label_attachment: File
590 592 label_attachment_new: New file
591 593 label_attachment_delete: Delete file
592 594 label_attachment_plural: Files
593 595 label_file_added: File added
594 596 label_report: Report
595 597 label_report_plural: Reports
596 598 label_news: News
597 599 label_news_new: Add news
598 600 label_news_plural: News
599 601 label_news_latest: Latest news
600 602 label_news_view_all: View all news
601 603 label_news_added: News added
602 604 label_news_comment_added: Comment added to a news
603 605 label_settings: Settings
604 606 label_overview: Overview
605 607 label_version: Version
606 608 label_version_new: New version
607 609 label_version_plural: Versions
608 610 label_close_versions: Close completed versions
609 611 label_confirmation: Confirmation
610 612 label_export_to: 'Also available in:'
611 613 label_read: Read...
612 614 label_public_projects: Public projects
613 615 label_open_issues: open
614 616 label_open_issues_plural: open
615 617 label_closed_issues: closed
616 618 label_closed_issues_plural: closed
617 619 label_x_open_issues_abbr_on_total:
618 620 zero: 0 open / %{total}
619 621 one: 1 open / %{total}
620 622 other: "%{count} open / %{total}"
621 623 label_x_open_issues_abbr:
622 624 zero: 0 open
623 625 one: 1 open
624 626 other: "%{count} open"
625 627 label_x_closed_issues_abbr:
626 628 zero: 0 closed
627 629 one: 1 closed
628 630 other: "%{count} closed"
629 631 label_x_issues:
630 632 zero: 0 issues
631 633 one: 1 issue
632 634 other: "%{count} issues"
633 635 label_total: Total
634 636 label_total_time: Total time
635 637 label_permissions: Permissions
636 638 label_current_status: Current status
637 639 label_new_statuses_allowed: New statuses allowed
638 640 label_all: all
639 641 label_any: any
640 642 label_none: none
641 643 label_nobody: nobody
642 644 label_next: Next
643 645 label_previous: Previous
644 646 label_used_by: Used by
645 647 label_details: Details
646 648 label_add_note: Add a note
647 649 label_per_page: Per page
648 650 label_calendar: Calendar
649 651 label_months_from: months from
650 652 label_gantt: Gantt
651 653 label_internal: Internal
652 654 label_last_changes: "last %{count} changes"
653 655 label_change_view_all: View all changes
654 656 label_personalize_page: Personalize this page
655 657 label_comment: Comment
656 658 label_comment_plural: Comments
657 659 label_x_comments:
658 660 zero: no comments
659 661 one: 1 comment
660 662 other: "%{count} comments"
661 663 label_comment_add: Add a comment
662 664 label_comment_added: Comment added
663 665 label_comment_delete: Delete comments
664 666 label_query: Custom query
665 667 label_query_plural: Custom queries
666 668 label_query_new: New query
667 669 label_my_queries: My custom queries
668 670 label_filter_add: Add filter
669 671 label_filter_plural: Filters
670 672 label_equals: is
671 673 label_not_equals: is not
672 674 label_in_less_than: in less than
673 675 label_in_more_than: in more than
674 676 label_in_the_next_days: in the next
675 677 label_in_the_past_days: in the past
676 678 label_greater_or_equal: '>='
677 679 label_less_or_equal: '<='
678 680 label_between: between
679 681 label_in: in
680 682 label_today: today
681 683 label_all_time: all time
682 684 label_yesterday: yesterday
683 685 label_this_week: this week
684 686 label_last_week: last week
685 687 label_last_n_weeks: "last %{count} weeks"
686 688 label_last_n_days: "last %{count} days"
687 689 label_this_month: this month
688 690 label_last_month: last month
689 691 label_this_year: this year
690 692 label_date_range: Date range
691 693 label_less_than_ago: less than days ago
692 694 label_more_than_ago: more than days ago
693 695 label_ago: days ago
694 696 label_contains: contains
695 697 label_not_contains: doesn't contain
696 698 label_any_issues_in_project: any issues in project
697 699 label_any_issues_not_in_project: any issues not in project
698 700 label_no_issues_in_project: no issues in project
699 701 label_day_plural: days
700 702 label_repository: Repository
701 703 label_repository_new: New repository
702 704 label_repository_plural: Repositories
703 705 label_browse: Browse
704 706 label_branch: Branch
705 707 label_tag: Tag
706 708 label_revision: Revision
707 709 label_revision_plural: Revisions
708 710 label_revision_id: "Revision %{value}"
709 711 label_associated_revisions: Associated revisions
710 712 label_added: added
711 713 label_modified: modified
712 714 label_copied: copied
713 715 label_renamed: renamed
714 716 label_deleted: deleted
715 717 label_latest_revision: Latest revision
716 718 label_latest_revision_plural: Latest revisions
717 719 label_view_revisions: View revisions
718 720 label_view_all_revisions: View all revisions
719 721 label_max_size: Maximum size
720 722 label_sort_highest: Move to top
721 723 label_sort_higher: Move up
722 724 label_sort_lower: Move down
723 725 label_sort_lowest: Move to bottom
724 726 label_roadmap: Roadmap
725 727 label_roadmap_due_in: "Due in %{value}"
726 728 label_roadmap_overdue: "%{value} late"
727 729 label_roadmap_no_issues: No issues for this version
728 730 label_search: Search
729 731 label_result_plural: Results
730 732 label_all_words: All words
731 733 label_wiki: Wiki
732 734 label_wiki_edit: Wiki edit
733 735 label_wiki_edit_plural: Wiki edits
734 736 label_wiki_page: Wiki page
735 737 label_wiki_page_plural: Wiki pages
736 738 label_index_by_title: Index by title
737 739 label_index_by_date: Index by date
738 740 label_current_version: Current version
739 741 label_preview: Preview
740 742 label_feed_plural: Feeds
741 743 label_changes_details: Details of all changes
742 744 label_issue_tracking: Issue tracking
743 745 label_spent_time: Spent time
744 746 label_overall_spent_time: Overall spent time
745 747 label_f_hour: "%{value} hour"
746 748 label_f_hour_plural: "%{value} hours"
747 749 label_time_tracking: Time tracking
748 750 label_change_plural: Changes
749 751 label_statistics: Statistics
750 752 label_commits_per_month: Commits per month
751 753 label_commits_per_author: Commits per author
752 754 label_diff: diff
753 755 label_view_diff: View differences
754 756 label_diff_inline: inline
755 757 label_diff_side_by_side: side by side
756 758 label_options: Options
757 759 label_copy_workflow_from: Copy workflow from
758 760 label_permissions_report: Permissions report
759 761 label_watched_issues: Watched issues
760 762 label_related_issues: Related issues
761 763 label_applied_status: Applied status
762 764 label_loading: Loading...
763 765 label_relation_new: New relation
764 766 label_relation_delete: Delete relation
765 767 label_relates_to: Related to
766 768 label_duplicates: Duplicates
767 769 label_duplicated_by: Duplicated by
768 770 label_blocks: Blocks
769 771 label_blocked_by: Blocked by
770 772 label_precedes: Precedes
771 773 label_follows: Follows
772 774 label_copied_to: Copied to
773 775 label_copied_from: Copied from
774 776 label_end_to_start: end to start
775 777 label_end_to_end: end to end
776 778 label_start_to_start: start to start
777 779 label_start_to_end: start to end
778 780 label_stay_logged_in: Stay logged in
779 781 label_disabled: disabled
780 782 label_show_completed_versions: Show completed versions
781 783 label_me: me
782 784 label_board: Forum
783 785 label_board_new: New forum
784 786 label_board_plural: Forums
785 787 label_board_locked: Locked
786 788 label_board_sticky: Sticky
787 789 label_topic_plural: Topics
788 790 label_message_plural: Messages
789 791 label_message_last: Last message
790 792 label_message_new: New message
791 793 label_message_posted: Message added
792 794 label_reply_plural: Replies
793 795 label_send_information: Send account information to the user
794 796 label_year: Year
795 797 label_month: Month
796 798 label_week: Week
797 799 label_date_from: From
798 800 label_date_to: To
799 801 label_language_based: Based on user's language
800 802 label_sort_by: "Sort by %{value}"
801 803 label_send_test_email: Send a test email
802 804 label_feeds_access_key: Atom access key
803 805 label_missing_feeds_access_key: Missing a Atom access key
804 806 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
805 807 label_module_plural: Modules
806 808 label_added_time_by: "Added by %{author} %{age} ago"
807 809 label_updated_time_by: "Updated by %{author} %{age} ago"
808 810 label_updated_time: "Updated %{value} ago"
809 811 label_jump_to_a_project: Jump to a project...
810 812 label_file_plural: Files
811 813 label_changeset_plural: Changesets
812 814 label_default_columns: Default columns
813 815 label_no_change_option: (No change)
814 816 label_bulk_edit_selected_issues: Bulk edit selected issues
815 817 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
816 818 label_theme: Theme
817 819 label_default: Default
818 820 label_search_titles_only: Search titles only
819 821 label_user_mail_option_all: "For any event on all my projects"
820 822 label_user_mail_option_selected: "For any event on the selected projects only..."
821 823 label_user_mail_option_none: "No events"
822 824 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
823 825 label_user_mail_option_only_assigned: "Only for things I am assigned to"
824 826 label_user_mail_option_only_owner: "Only for things I am the owner of"
825 827 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
826 828 label_registration_activation_by_email: account activation by email
827 829 label_registration_manual_activation: manual account activation
828 830 label_registration_automatic_activation: automatic account activation
829 831 label_display_per_page: "Per page: %{value}"
830 832 label_age: Age
831 833 label_change_properties: Change properties
832 834 label_general: General
833 835 label_more: More
834 836 label_scm: SCM
835 837 label_plugins: Plugins
836 838 label_ldap_authentication: LDAP authentication
837 839 label_downloads_abbr: D/L
838 840 label_optional_description: Optional description
839 841 label_add_another_file: Add another file
840 842 label_preferences: Preferences
841 843 label_chronological_order: In chronological order
842 844 label_reverse_chronological_order: In reverse chronological order
843 845 label_planning: Planning
844 846 label_incoming_emails: Incoming emails
845 847 label_generate_key: Generate a key
846 848 label_issue_watchers: Watchers
847 849 label_example: Example
848 850 label_display: Display
849 851 label_sort: Sort
850 852 label_ascending: Ascending
851 853 label_descending: Descending
852 854 label_date_from_to: From %{start} to %{end}
853 855 label_wiki_content_added: Wiki page added
854 856 label_wiki_content_updated: Wiki page updated
855 857 label_group: Group
856 858 label_group_plural: Groups
857 859 label_group_new: New group
858 860 label_group_anonymous: Anonymous users
859 861 label_group_non_member: Non member users
860 862 label_time_entry_plural: Spent time
861 863 label_version_sharing_none: Not shared
862 864 label_version_sharing_descendants: With subprojects
863 865 label_version_sharing_hierarchy: With project hierarchy
864 866 label_version_sharing_tree: With project tree
865 867 label_version_sharing_system: With all projects
866 868 label_update_issue_done_ratios: Update issue done ratios
867 869 label_copy_source: Source
868 870 label_copy_target: Target
869 871 label_copy_same_as_target: Same as target
870 872 label_display_used_statuses_only: Only display statuses that are used by this tracker
871 873 label_api_access_key: API access key
872 874 label_missing_api_access_key: Missing an API access key
873 875 label_api_access_key_created_on: "API access key created %{value} ago"
874 876 label_profile: Profile
875 877 label_subtask_plural: Subtasks
876 878 label_project_copy_notifications: Send email notifications during the project copy
877 879 label_principal_search: "Search for user or group:"
878 880 label_user_search: "Search for user:"
879 881 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
880 882 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
881 883 label_issues_visibility_all: All issues
882 884 label_issues_visibility_public: All non private issues
883 885 label_issues_visibility_own: Issues created by or assigned to the user
884 886 label_git_report_last_commit: Report last commit for files and directories
885 887 label_parent_revision: Parent
886 888 label_child_revision: Child
887 889 label_export_options: "%{export_format} export options"
888 890 label_copy_attachments: Copy attachments
889 891 label_copy_subtasks: Copy subtasks
890 892 label_item_position: "%{position} of %{count}"
891 893 label_completed_versions: Completed versions
892 894 label_search_for_watchers: Search for watchers to add
893 895 label_session_expiration: Session expiration
894 896 label_show_closed_projects: View closed projects
895 897 label_status_transitions: Status transitions
896 898 label_fields_permissions: Fields permissions
897 899 label_readonly: Read-only
898 900 label_required: Required
899 901 label_hidden: Hidden
900 902 label_attribute_of_project: "Project's %{name}"
901 903 label_attribute_of_issue: "Issue's %{name}"
902 904 label_attribute_of_author: "Author's %{name}"
903 905 label_attribute_of_assigned_to: "Assignee's %{name}"
904 906 label_attribute_of_user: "User's %{name}"
905 907 label_attribute_of_fixed_version: "Target version's %{name}"
906 908 label_cross_project_descendants: With subprojects
907 909 label_cross_project_tree: With project tree
908 910 label_cross_project_hierarchy: With project hierarchy
909 911 label_cross_project_system: With all projects
910 912 label_gantt_progress_line: Progress line
911 913 label_visibility_private: to me only
912 914 label_visibility_roles: to these roles only
913 915 label_visibility_public: to any users
914 916 label_link: Link
915 917 label_only: only
916 918 label_drop_down_list: drop-down list
917 919 label_checkboxes: checkboxes
918 920 label_radio_buttons: radio buttons
919 921 label_link_values_to: Link values to URL
920 922 label_custom_field_select_type: Select the type of object to which the custom field is to be attached
921 923 label_check_for_updates: Check for updates
922 924 label_latest_compatible_version: Latest compatible version
923 925 label_unknown_plugin: Unknown plugin
924 926 label_add_projects: Add projects
925 927 label_users_visibility_all: All active users
926 928 label_users_visibility_members_of_visible_projects: Members of visible projects
927 929 label_edit_attachments: Edit attached files
928 930 label_link_copied_issue: Link copied issue
929 931 label_ask: Ask
930 932 label_search_attachments_yes: Search attachment filenames and descriptions
931 933 label_search_attachments_no: Do not search attachments
932 934 label_search_attachments_only: Search attachments only
933 935 label_search_open_issues_only: Open issues only
936 label_email_address_plural: Emails
937 label_email_address_add: Add email address
938 label_enable_notifications: Enable notifications
939 label_disable_notifications: Disable notifications
934 940
935 941 button_login: Login
936 942 button_submit: Submit
937 943 button_save: Save
938 944 button_check_all: Check all
939 945 button_uncheck_all: Uncheck all
940 946 button_collapse_all: Collapse all
941 947 button_expand_all: Expand all
942 948 button_delete: Delete
943 949 button_create: Create
944 950 button_create_and_continue: Create and continue
945 951 button_test: Test
946 952 button_edit: Edit
947 953 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
948 954 button_add: Add
949 955 button_change: Change
950 956 button_apply: Apply
951 957 button_clear: Clear
952 958 button_lock: Lock
953 959 button_unlock: Unlock
954 960 button_download: Download
955 961 button_list: List
956 962 button_view: View
957 963 button_move: Move
958 964 button_move_and_follow: Move and follow
959 965 button_back: Back
960 966 button_cancel: Cancel
961 967 button_activate: Activate
962 968 button_sort: Sort
963 969 button_log_time: Log time
964 970 button_rollback: Rollback to this version
965 971 button_watch: Watch
966 972 button_unwatch: Unwatch
967 973 button_reply: Reply
968 974 button_archive: Archive
969 975 button_unarchive: Unarchive
970 976 button_reset: Reset
971 977 button_rename: Rename
972 978 button_change_password: Change password
973 979 button_copy: Copy
974 980 button_copy_and_follow: Copy and follow
975 981 button_annotate: Annotate
976 982 button_update: Update
977 983 button_configure: Configure
978 984 button_quote: Quote
979 985 button_duplicate: Duplicate
980 986 button_show: Show
981 987 button_hide: Hide
982 988 button_edit_section: Edit this section
983 989 button_export: Export
984 990 button_delete_my_account: Delete my account
985 991 button_close: Close
986 992 button_reopen: Reopen
987 993
988 994 status_active: active
989 995 status_registered: registered
990 996 status_locked: locked
991 997
992 998 project_status_active: active
993 999 project_status_closed: closed
994 1000 project_status_archived: archived
995 1001
996 1002 version_status_open: open
997 1003 version_status_locked: locked
998 1004 version_status_closed: closed
999 1005
1000 1006 field_active: Active
1001 1007
1002 1008 text_select_mail_notifications: Select actions for which email notifications should be sent.
1003 1009 text_regexp_info: eg. ^[A-Z0-9]+$
1004 1010 text_min_max_length_info: 0 means no restriction
1005 1011 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
1006 1012 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
1007 1013 text_workflow_edit: Select a role and a tracker to edit the workflow
1008 1014 text_are_you_sure: Are you sure?
1009 1015 text_journal_changed: "%{label} changed from %{old} to %{new}"
1010 1016 text_journal_changed_no_detail: "%{label} updated"
1011 1017 text_journal_set_to: "%{label} set to %{value}"
1012 1018 text_journal_deleted: "%{label} deleted (%{old})"
1013 1019 text_journal_added: "%{label} %{value} added"
1014 1020 text_tip_issue_begin_day: issue beginning this day
1015 1021 text_tip_issue_end_day: issue ending this day
1016 1022 text_tip_issue_begin_end_day: issue beginning and ending this day
1017 1023 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.'
1018 1024 text_caracters_maximum: "%{count} characters maximum."
1019 1025 text_caracters_minimum: "Must be at least %{count} characters long."
1020 1026 text_length_between: "Length between %{min} and %{max} characters."
1021 1027 text_tracker_no_workflow: No workflow defined for this tracker
1022 1028 text_unallowed_characters: Unallowed characters
1023 1029 text_comma_separated: Multiple values allowed (comma separated).
1024 1030 text_line_separated: Multiple values allowed (one line for each value).
1025 1031 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
1026 1032 text_issue_added: "Issue %{id} has been reported by %{author}."
1027 1033 text_issue_updated: "Issue %{id} has been updated by %{author}."
1028 1034 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
1029 1035 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
1030 1036 text_issue_category_destroy_assignments: Remove category assignments
1031 1037 text_issue_category_reassign_to: Reassign issues to this category
1032 1038 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)."
1033 1039 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."
1034 1040 text_load_default_configuration: Load the default configuration
1035 1041 text_status_changed_by_changeset: "Applied in changeset %{value}."
1036 1042 text_time_logged_by_changeset: "Applied in changeset %{value}."
1037 1043 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1038 1044 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1039 1045 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1040 1046 text_select_project_modules: 'Select modules to enable for this project:'
1041 1047 text_default_administrator_account_changed: Default administrator account changed
1042 1048 text_file_repository_writable: Attachments directory writable
1043 1049 text_plugin_assets_writable: Plugin assets directory writable
1044 1050 text_rmagick_available: RMagick available (optional)
1045 1051 text_convert_available: ImageMagick convert available (optional)
1046 1052 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
1047 1053 text_destroy_time_entries: Delete reported hours
1048 1054 text_assign_time_entries_to_project: Assign reported hours to the project
1049 1055 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1050 1056 text_user_wrote: "%{value} wrote:"
1051 1057 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1052 1058 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1053 1059 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."
1054 1060 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."
1055 1061 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1056 1062 text_custom_field_possible_values_info: 'One line for each value'
1057 1063 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1058 1064 text_wiki_page_nullify_children: "Keep child pages as root pages"
1059 1065 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1060 1066 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1061 1067 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?"
1062 1068 text_zoom_in: Zoom in
1063 1069 text_zoom_out: Zoom out
1064 1070 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1065 1071 text_scm_path_encoding_note: "Default: UTF-8"
1066 1072 text_subversion_repository_note: "Examples: file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1067 1073 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1068 1074 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1069 1075 text_scm_command: Command
1070 1076 text_scm_command_version: Version
1071 1077 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1072 1078 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1073 1079 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1074 1080 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1075 1081 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1076 1082 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1077 1083 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1078 1084 text_project_closed: This project is closed and read-only.
1079 1085 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
1080 1086
1081 1087 default_role_manager: Manager
1082 1088 default_role_developer: Developer
1083 1089 default_role_reporter: Reporter
1084 1090 default_tracker_bug: Bug
1085 1091 default_tracker_feature: Feature
1086 1092 default_tracker_support: Support
1087 1093 default_issue_status_new: New
1088 1094 default_issue_status_in_progress: In Progress
1089 1095 default_issue_status_resolved: Resolved
1090 1096 default_issue_status_feedback: Feedback
1091 1097 default_issue_status_closed: Closed
1092 1098 default_issue_status_rejected: Rejected
1093 1099 default_doc_category_user: User documentation
1094 1100 default_doc_category_tech: Technical documentation
1095 1101 default_priority_low: Low
1096 1102 default_priority_normal: Normal
1097 1103 default_priority_high: High
1098 1104 default_priority_urgent: Urgent
1099 1105 default_priority_immediate: Immediate
1100 1106 default_activity_design: Design
1101 1107 default_activity_development: Development
1102 1108
1103 1109 enumeration_issue_priorities: Issue priorities
1104 1110 enumeration_doc_categories: Document categories
1105 1111 enumeration_activities: Activities (time tracking)
1106 1112 enumeration_system_activity: System Activity
1107 1113 description_filter: Filter
1108 1114 description_search: Searchfield
1109 1115 description_choose_project: Projects
1110 1116 description_project_scope: Search scope
1111 1117 description_notes: Notes
1112 1118 description_message_content: Message content
1113 1119 description_query_sort_criteria_attribute: Sort attribute
1114 1120 description_query_sort_criteria_direction: Sort direction
1115 1121 description_user_mail_notification: Mail notification settings
1116 1122 description_available_columns: Available Columns
1117 1123 description_selected_columns: Selected Columns
1118 1124 description_all_columns: All Columns
1119 1125 description_issue_category_reassign: Choose issue category
1120 1126 description_wiki_subpages_reassign: Choose new parent page
1121 1127 description_date_range_list: Choose range from list
1122 1128 description_date_range_interval: Choose range by selecting start and end date
1123 1129 description_date_from: Enter start date
1124 1130 description_date_to: Enter end date
1125 1131 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,1145 +1,1151
1 1 # French translations for Ruby on Rails
2 2 # by Christian Lescuyer (christian@flyingcoders.com)
3 3 # contributor: Sebastien Grosjean - ZenCocoon.com
4 4 # contributor: Thibaut Cuvelier - Developpez.com
5 5
6 6 fr:
7 7 direction: ltr
8 8 date:
9 9 formats:
10 10 default: "%d/%m/%Y"
11 11 short: "%e %b"
12 12 long: "%e %B %Y"
13 13 long_ordinal: "%e %B %Y"
14 14 only_day: "%e"
15 15
16 16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
17 17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
18 18
19 19 # Don't forget the nil at the beginning; there's no such thing as a 0th month
20 20 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
21 21 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
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: "%d %b %H:%M"
33 33 long: "%A %d %B %Y %H:%M:%S %Z"
34 34 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
35 35 only_second: "%S"
36 36 am: 'am'
37 37 pm: 'pm'
38 38
39 39 datetime:
40 40 distance_in_words:
41 41 half_a_minute: "30 secondes"
42 42 less_than_x_seconds:
43 43 zero: "moins d'une seconde"
44 44 one: "moins d'une seconde"
45 45 other: "moins de %{count} secondes"
46 46 x_seconds:
47 47 one: "1 seconde"
48 48 other: "%{count} secondes"
49 49 less_than_x_minutes:
50 50 zero: "moins d'une minute"
51 51 one: "moins d'une minute"
52 52 other: "moins de %{count} minutes"
53 53 x_minutes:
54 54 one: "1 minute"
55 55 other: "%{count} minutes"
56 56 about_x_hours:
57 57 one: "environ une heure"
58 58 other: "environ %{count} heures"
59 59 x_hours:
60 60 one: "une heure"
61 61 other: "%{count} heures"
62 62 x_days:
63 63 one: "un jour"
64 64 other: "%{count} jours"
65 65 about_x_months:
66 66 one: "environ un mois"
67 67 other: "environ %{count} mois"
68 68 x_months:
69 69 one: "un mois"
70 70 other: "%{count} mois"
71 71 about_x_years:
72 72 one: "environ un an"
73 73 other: "environ %{count} ans"
74 74 over_x_years:
75 75 one: "plus d'un an"
76 76 other: "plus de %{count} ans"
77 77 almost_x_years:
78 78 one: "presqu'un an"
79 79 other: "presque %{count} ans"
80 80 prompts:
81 81 year: "Année"
82 82 month: "Mois"
83 83 day: "Jour"
84 84 hour: "Heure"
85 85 minute: "Minute"
86 86 second: "Seconde"
87 87
88 88 number:
89 89 format:
90 90 precision: 3
91 91 separator: ','
92 92 delimiter: ' '
93 93 currency:
94 94 format:
95 95 unit: '€'
96 96 precision: 2
97 97 format: '%n %u'
98 98 human:
99 99 format:
100 100 precision: 3
101 101 storage_units:
102 102 format: "%n %u"
103 103 units:
104 104 byte:
105 105 one: "octet"
106 106 other: "octets"
107 107 kb: "ko"
108 108 mb: "Mo"
109 109 gb: "Go"
110 110 tb: "To"
111 111
112 112 support:
113 113 array:
114 114 sentence_connector: 'et'
115 115 skip_last_comma: true
116 116 word_connector: ", "
117 117 two_words_connector: " et "
118 118 last_word_connector: " et "
119 119
120 120 activerecord:
121 121 errors:
122 122 template:
123 123 header:
124 124 one: "Impossible d'enregistrer %{model} : une erreur"
125 125 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
126 126 body: "Veuillez vérifier les champs suivants :"
127 127 messages:
128 128 inclusion: "n'est pas inclus(e) dans la liste"
129 129 exclusion: "n'est pas disponible"
130 130 invalid: "n'est pas valide"
131 131 confirmation: "ne concorde pas avec la confirmation"
132 132 accepted: "doit être accepté(e)"
133 133 empty: "doit être renseigné(e)"
134 134 blank: "doit être renseigné(e)"
135 135 too_long: "est trop long (pas plus de %{count} caractères)"
136 136 too_short: "est trop court (au moins %{count} caractères)"
137 137 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
138 138 taken: "est déjà utilisé"
139 139 not_a_number: "n'est pas un nombre"
140 140 not_a_date: "n'est pas une date valide"
141 141 greater_than: "doit être supérieur à %{count}"
142 142 greater_than_or_equal_to: "doit être supérieur ou égal à %{count}"
143 143 equal_to: "doit être égal à %{count}"
144 144 less_than: "doit être inférieur à %{count}"
145 145 less_than_or_equal_to: "doit être inférieur ou égal à %{count}"
146 146 odd: "doit être impair"
147 147 even: "doit être pair"
148 148 greater_than_start_date: "doit être postérieure à la date de début"
149 149 not_same_project: "n'appartient pas au même projet"
150 150 circular_dependency: "Cette relation créerait une dépendance circulaire"
151 151 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
152 152 earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précèdent"
153 153
154 154 actionview_instancetag_blank_option: Choisir
155 155
156 156 general_text_No: 'Non'
157 157 general_text_Yes: 'Oui'
158 158 general_text_no: 'non'
159 159 general_text_yes: 'oui'
160 160 general_lang_name: 'Français'
161 161 general_csv_separator: ';'
162 162 general_csv_decimal_separator: ','
163 163 general_csv_encoding: ISO-8859-1
164 164 general_pdf_fontname: freesans
165 165 general_first_day_of_week: '1'
166 166
167 167 notice_account_updated: Le compte a été mis à jour avec succès.
168 168 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
169 169 notice_account_password_updated: Mot de passe mis à jour avec succès.
170 170 notice_account_wrong_password: Mot de passe incorrect
171 171 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}.
172 172 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
173 173 notice_account_not_activated_yet: Vous n'avez pas encore activé votre compte. Si vous voulez recevoir un nouveau message d'activation, veuillez <a href="%{url}">cliquer sur ce lien</a>.
174 174 notice_account_locked: Votre compte est verrouillé.
175 175 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
176 176 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
177 177 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
178 178 notice_successful_create: Création effectuée avec succès.
179 179 notice_successful_update: Mise à jour effectuée avec succès.
180 180 notice_successful_delete: Suppression effectuée avec succès.
181 181 notice_successful_connection: Connexion réussie.
182 182 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
183 183 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
184 184 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
185 185 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
186 186 notice_email_sent: "Un email a été envoyé à %{value}"
187 187 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
188 188 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée."
189 189 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
190 190 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
191 191 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
192 192 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
193 193 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
194 194 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
195 195 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
196 196 notice_unable_delete_version: Impossible de supprimer cette version.
197 197 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
198 198 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
199 199 notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})"
200 200 notice_issue_successful_create: "Demande %{id} créée."
201 201 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
202 202 notice_account_deleted: "Votre compte a été définitivement supprimé."
203 203 notice_user_successful_create: "Utilisateur %{id} créé."
204 204 notice_new_password_must_be_different: Votre nouveau mot de passe doit être différent de votre mot de passe actuel
205 205
206 206 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
207 207 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
208 208 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
209 209 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
210 210 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
211 211 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
212 212 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
213 213 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
214 214 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
215 215 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
216 216 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
217 217 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
218 218 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
219 219 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
220 220 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
221 221 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
222 222 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
223 223 error_unable_to_connect: Connexion impossible (%{value})
224 224 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
225 225 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
226 226 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
227 227
228 228 mail_subject_lost_password: "Votre mot de passe %{value}"
229 229 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
230 230 mail_subject_register: "Activation de votre compte %{value}"
231 231 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
232 232 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
233 233 mail_body_account_information: Paramètres de connexion de votre compte
234 234 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
235 235 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
236 236 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
237 237 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
238 238 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
239 239 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
240 240 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
241 241 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
242 242
243 243 field_name: Nom
244 244 field_description: Description
245 245 field_summary: Résumé
246 246 field_is_required: Obligatoire
247 247 field_firstname: Prénom
248 248 field_lastname: Nom
249 249 field_mail: Email
250 field_address: Email
250 251 field_filename: Fichier
251 252 field_filesize: Taille
252 253 field_downloads: Téléchargements
253 254 field_author: Auteur
254 255 field_created_on: Créé
255 256 field_updated_on: Mis-à-jour
256 257 field_closed_on: Fermé
257 258 field_field_format: Format
258 259 field_is_for_all: Pour tous les projets
259 260 field_possible_values: Valeurs possibles
260 261 field_regexp: Expression régulière
261 262 field_min_length: Longueur minimum
262 263 field_max_length: Longueur maximum
263 264 field_value: Valeur
264 265 field_category: Catégorie
265 266 field_title: Titre
266 267 field_project: Projet
267 268 field_issue: Demande
268 269 field_status: Statut
269 270 field_notes: Notes
270 271 field_is_closed: Demande fermée
271 272 field_is_default: Valeur par défaut
272 273 field_tracker: Tracker
273 274 field_subject: Sujet
274 275 field_due_date: Echéance
275 276 field_assigned_to: Assigné à
276 277 field_priority: Priorité
277 278 field_fixed_version: Version cible
278 279 field_user: Utilisateur
279 280 field_principal: Principal
280 281 field_role: Rôle
281 282 field_homepage: Site web
282 283 field_is_public: Public
283 284 field_parent: Sous-projet de
284 285 field_is_in_roadmap: Demandes affichées dans la roadmap
285 286 field_login: Identifiant
286 287 field_mail_notification: Notifications par mail
287 288 field_admin: Administrateur
288 289 field_last_login_on: Dernière connexion
289 290 field_language: Langue
290 291 field_effective_date: Date
291 292 field_password: Mot de passe
292 293 field_new_password: Nouveau mot de passe
293 294 field_password_confirmation: Confirmation
294 295 field_version: Version
295 296 field_type: Type
296 297 field_host: Hôte
297 298 field_port: Port
298 299 field_account: Compte
299 300 field_base_dn: Base DN
300 301 field_attr_login: Attribut Identifiant
301 302 field_attr_firstname: Attribut Prénom
302 303 field_attr_lastname: Attribut Nom
303 304 field_attr_mail: Attribut Email
304 305 field_onthefly: Création des utilisateurs à la volée
305 306 field_start_date: Début
306 307 field_done_ratio: "% réalisé"
307 308 field_auth_source: Mode d'authentification
308 309 field_hide_mail: Cacher mon adresse mail
309 310 field_comments: Commentaire
310 311 field_url: URL
311 312 field_start_page: Page de démarrage
312 313 field_subproject: Sous-projet
313 314 field_hours: Heures
314 315 field_activity: Activité
315 316 field_spent_on: Date
316 317 field_identifier: Identifiant
317 318 field_is_filter: Utilisé comme filtre
318 319 field_issue_to: Demande liée
319 320 field_delay: Retard
320 321 field_assignable: Demandes assignables à ce rôle
321 322 field_redirect_existing_links: Rediriger les liens existants
322 323 field_estimated_hours: Temps estimé
323 324 field_column_names: Colonnes
324 325 field_time_entries: Temps passé
325 326 field_time_zone: Fuseau horaire
326 327 field_searchable: Utilisé pour les recherches
327 328 field_default_value: Valeur par défaut
328 329 field_comments_sorting: Afficher les commentaires
329 330 field_parent_title: Page parent
330 331 field_editable: Modifiable
331 332 field_watcher: Observateur
332 333 field_identity_url: URL OpenID
333 334 field_content: Contenu
334 335 field_group_by: Grouper par
335 336 field_sharing: Partage
336 337 field_parent_issue: Tâche parente
337 338 field_member_of_group: Groupe de l'assigné
338 339 field_assigned_to_role: Rôle de l'assigné
339 340 field_text: Champ texte
340 341 field_visible: Visible
341 342 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
342 343 field_issues_visibility: Visibilité des demandes
343 344 field_is_private: Privée
344 345 field_commit_logs_encoding: Encodage des messages de commit
345 346 field_scm_path_encoding: Encodage des chemins
346 347 field_path_to_repository: Chemin du dépôt
347 348 field_root_directory: Répertoire racine
348 349 field_cvsroot: CVSROOT
349 350 field_cvs_module: Module
350 351 field_repository_is_default: Dépôt principal
351 352 field_multiple: Valeurs multiples
352 353 field_auth_source_ldap_filter: Filtre LDAP
353 354 field_core_fields: Champs standards
354 355 field_timeout: "Timeout (en secondes)"
355 356 field_board_parent: Forum parent
356 357 field_private_notes: Notes privées
357 358 field_inherit_members: Hériter les membres
358 359 field_generate_password: Générer un mot de passe
359 360 field_must_change_passwd: Doit changer de mot de passe à la prochaine connexion
360 361 field_default_status: Statut par défaut
361 362 field_users_visibility: Visibilité des utilisateurs
362 363
363 364 setting_app_title: Titre de l'application
364 365 setting_app_subtitle: Sous-titre de l'application
365 366 setting_welcome_text: Texte d'accueil
366 367 setting_default_language: Langue par défaut
367 368 setting_login_required: Authentification obligatoire
368 369 setting_self_registration: Inscription des nouveaux utilisateurs
369 370 setting_attachment_max_size: Taille maximale des fichiers
370 371 setting_issues_export_limit: Limite d'exportation des demandes
371 372 setting_mail_from: Adresse d'émission
372 373 setting_bcc_recipients: Destinataires en copie cachée (cci)
373 374 setting_plain_text_mail: Mail en texte brut (non HTML)
374 375 setting_host_name: Nom d'hôte et chemin
375 376 setting_text_formatting: Formatage du texte
376 377 setting_wiki_compression: Compression de l'historique des pages wiki
377 378 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
378 379 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
379 380 setting_autofetch_changesets: Récupération automatique des commits
380 381 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
381 382 setting_commit_ref_keywords: Mots-clés de référencement
382 383 setting_commit_fix_keywords: Mots-clés de résolution
383 384 setting_autologin: Durée maximale de connexion automatique
384 385 setting_date_format: Format de date
385 386 setting_time_format: Format d'heure
386 387 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
387 388 setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
388 389 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
389 390 setting_repositories_encodings: Encodages des fichiers et des dépôts
390 391 setting_emails_header: En-tête des emails
391 392 setting_emails_footer: Pied-de-page des emails
392 393 setting_protocol: Protocole
393 394 setting_per_page_options: Options d'objets affichés par page
394 395 setting_user_format: Format d'affichage des utilisateurs
395 396 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
396 397 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
397 398 setting_enabled_scm: SCM activés
398 399 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
399 400 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
400 401 setting_mail_handler_api_key: Clé de protection de l'API
401 402 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
402 403 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
403 404 setting_gravatar_default: Image Gravatar par défaut
404 405 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
405 406 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
406 407 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
407 408 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
408 409 setting_password_min_length: Longueur minimum des mots de passe
409 410 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
410 411 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
411 412 setting_issue_done_ratio: Calcul de l'avancement des demandes
412 413 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
413 414 setting_issue_done_ratio_issue_status: Utiliser le statut
414 415 setting_start_of_week: Jour de début des calendriers
415 416 setting_rest_api_enabled: Activer l'API REST
416 417 setting_cache_formatted_text: Mettre en cache le texte formaté
417 418 setting_default_notification_option: Option de notification par défaut
418 419 setting_commit_logtime_enabled: Permettre la saisie de temps
419 420 setting_commit_logtime_activity_id: Activité pour le temps saisi
420 421 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
421 422 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
422 423 setting_default_issue_start_date_to_creation_date: Donner à la date de début d'une nouvelle demande la valeur de la date du jour
423 424 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
424 425 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
425 426 setting_session_lifetime: Durée de vie maximale des sessions
426 427 setting_session_timeout: Durée maximale d'inactivité
427 428 setting_thumbnails_enabled: Afficher les vignettes des images
428 429 setting_thumbnails_size: Taille des vignettes (en pixels)
429 430 setting_non_working_week_days: Jours non travaillés
430 431 setting_jsonp_enabled: Activer le support JSONP
431 432 setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets
432 433 setting_mail_handler_excluded_filenames: Exclure les fichiers attachés par leur nom
433 434 setting_force_default_language_for_anonymous: Forcer la langue par défault pour les utilisateurs anonymes
434 435 setting_force_default_language_for_loggedin: Forcer la langue par défault pour les utilisateurs identifiés
435 436 setting_link_copied_issue: Lier les demandes lors de la copie
437 setting_max_additional_emails: Nombre maximal d'adresses email additionnelles
436 438
437 439 permission_add_project: Créer un projet
438 440 permission_add_subprojects: Créer des sous-projets
439 441 permission_edit_project: Modifier le projet
440 442 permission_close_project: Fermer / réouvrir le projet
441 443 permission_select_project_modules: Choisir les modules
442 444 permission_manage_members: Gérer les membres
443 445 permission_manage_project_activities: Gérer les activités
444 446 permission_manage_versions: Gérer les versions
445 447 permission_manage_categories: Gérer les catégories de demandes
446 448 permission_view_issues: Voir les demandes
447 449 permission_add_issues: Créer des demandes
448 450 permission_edit_issues: Modifier les demandes
449 451 permission_manage_issue_relations: Gérer les relations
450 452 permission_set_issues_private: Rendre les demandes publiques ou privées
451 453 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
452 454 permission_add_issue_notes: Ajouter des notes
453 455 permission_edit_issue_notes: Modifier les notes
454 456 permission_edit_own_issue_notes: Modifier ses propres notes
455 457 permission_view_private_notes: Voir les notes privées
456 458 permission_set_notes_private: Rendre les notes privées
457 459 permission_move_issues: Déplacer les demandes
458 460 permission_delete_issues: Supprimer les demandes
459 461 permission_manage_public_queries: Gérer les requêtes publiques
460 462 permission_save_queries: Sauvegarder les requêtes
461 463 permission_view_gantt: Voir le gantt
462 464 permission_view_calendar: Voir le calendrier
463 465 permission_view_issue_watchers: Voir la liste des observateurs
464 466 permission_add_issue_watchers: Ajouter des observateurs
465 467 permission_delete_issue_watchers: Supprimer des observateurs
466 468 permission_log_time: Saisir le temps passé
467 469 permission_view_time_entries: Voir le temps passé
468 470 permission_edit_time_entries: Modifier les temps passés
469 471 permission_edit_own_time_entries: Modifier son propre temps passé
470 472 permission_manage_news: Gérer les annonces
471 473 permission_comment_news: Commenter les annonces
472 474 permission_view_documents: Voir les documents
473 475 permission_add_documents: Ajouter des documents
474 476 permission_edit_documents: Modifier les documents
475 477 permission_delete_documents: Supprimer les documents
476 478 permission_manage_files: Gérer les fichiers
477 479 permission_view_files: Voir les fichiers
478 480 permission_manage_wiki: Gérer le wiki
479 481 permission_rename_wiki_pages: Renommer les pages
480 482 permission_delete_wiki_pages: Supprimer les pages
481 483 permission_view_wiki_pages: Voir le wiki
482 484 permission_view_wiki_edits: "Voir l'historique des modifications"
483 485 permission_edit_wiki_pages: Modifier les pages
484 486 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
485 487 permission_protect_wiki_pages: Protéger les pages
486 488 permission_manage_repository: Gérer le dépôt de sources
487 489 permission_browse_repository: Parcourir les sources
488 490 permission_view_changesets: Voir les révisions
489 491 permission_commit_access: Droit de commit
490 492 permission_manage_boards: Gérer les forums
491 493 permission_view_messages: Voir les messages
492 494 permission_add_messages: Poster un message
493 495 permission_edit_messages: Modifier les messages
494 496 permission_edit_own_messages: Modifier ses propres messages
495 497 permission_delete_messages: Supprimer les messages
496 498 permission_delete_own_messages: Supprimer ses propres messages
497 499 permission_export_wiki_pages: Exporter les pages
498 500 permission_manage_subtasks: Gérer les sous-tâches
499 501 permission_manage_related_issues: Gérer les demandes associées
500 502
501 503 project_module_issue_tracking: Suivi des demandes
502 504 project_module_time_tracking: Suivi du temps passé
503 505 project_module_news: Publication d'annonces
504 506 project_module_documents: Publication de documents
505 507 project_module_files: Publication de fichiers
506 508 project_module_wiki: Wiki
507 509 project_module_repository: Dépôt de sources
508 510 project_module_boards: Forums de discussion
509 511 project_module_calendar: Calendrier
510 512 project_module_gantt: Gantt
511 513
512 514 label_user: Utilisateur
513 515 label_user_plural: Utilisateurs
514 516 label_user_new: Nouvel utilisateur
515 517 label_user_anonymous: Anonyme
516 518 label_project: Projet
517 519 label_project_new: Nouveau projet
518 520 label_project_plural: Projets
519 521 label_x_projects:
520 522 zero: aucun projet
521 523 one: un projet
522 524 other: "%{count} projets"
523 525 label_project_all: Tous les projets
524 526 label_project_latest: Derniers projets
525 527 label_issue: Demande
526 528 label_issue_new: Nouvelle demande
527 529 label_issue_plural: Demandes
528 530 label_issue_view_all: Voir toutes les demandes
529 531 label_issues_by: "Demandes par %{value}"
530 532 label_issue_added: Demande ajoutée
531 533 label_issue_updated: Demande mise à jour
532 534 label_issue_note_added: Note ajoutée
533 535 label_issue_status_updated: Statut changé
534 536 label_issue_assigned_to_updated: Assigné changé
535 537 label_issue_priority_updated: Priorité changée
536 538 label_document: Document
537 539 label_document_new: Nouveau document
538 540 label_document_plural: Documents
539 541 label_document_added: Document ajouté
540 542 label_role: Rôle
541 543 label_role_plural: Rôles
542 544 label_role_new: Nouveau rôle
543 545 label_role_and_permissions: Rôles et permissions
544 546 label_role_anonymous: Anonyme
545 547 label_role_non_member: Non membre
546 548 label_member: Membre
547 549 label_member_new: Nouveau membre
548 550 label_member_plural: Membres
549 551 label_tracker: Tracker
550 552 label_tracker_plural: Trackers
551 553 label_tracker_new: Nouveau tracker
552 554 label_workflow: Workflow
553 555 label_issue_status: Statut de demandes
554 556 label_issue_status_plural: Statuts de demandes
555 557 label_issue_status_new: Nouveau statut
556 558 label_issue_category: Catégorie de demandes
557 559 label_issue_category_plural: Catégories de demandes
558 560 label_issue_category_new: Nouvelle catégorie
559 561 label_custom_field: Champ personnalisé
560 562 label_custom_field_plural: Champs personnalisés
561 563 label_custom_field_new: Nouveau champ personnalisé
562 564 label_enumerations: Listes de valeurs
563 565 label_enumeration_new: Nouvelle valeur
564 566 label_information: Information
565 567 label_information_plural: Informations
566 568 label_please_login: Identification
567 569 label_register: S'enregistrer
568 570 label_login_with_open_id_option: S'authentifier avec OpenID
569 571 label_password_lost: Mot de passe perdu
570 572 label_home: Accueil
571 573 label_my_page: Ma page
572 574 label_my_account: Mon compte
573 575 label_my_projects: Mes projets
574 576 label_my_page_block: Blocs disponibles
575 577 label_administration: Administration
576 578 label_login: Connexion
577 579 label_logout: Déconnexion
578 580 label_help: Aide
579 581 label_reported_issues: Demandes soumises
580 582 label_assigned_to_me_issues: Demandes qui me sont assignées
581 583 label_last_login: Dernière connexion
582 584 label_registered_on: Inscrit le
583 585 label_activity: Activité
584 586 label_overall_activity: Activité globale
585 587 label_user_activity: "Activité de %{value}"
586 588 label_new: Nouveau
587 589 label_logged_as: Connecté en tant que
588 590 label_environment: Environnement
589 591 label_authentication: Authentification
590 592 label_auth_source: Mode d'authentification
591 593 label_auth_source_new: Nouveau mode d'authentification
592 594 label_auth_source_plural: Modes d'authentification
593 595 label_subproject_plural: Sous-projets
594 596 label_subproject_new: Nouveau sous-projet
595 597 label_and_its_subprojects: "%{value} et ses sous-projets"
596 598 label_min_max_length: Longueurs mini - maxi
597 599 label_list: Liste
598 600 label_date: Date
599 601 label_integer: Entier
600 602 label_float: Nombre décimal
601 603 label_boolean: Booléen
602 604 label_string: Texte
603 605 label_text: Texte long
604 606 label_attribute: Attribut
605 607 label_attribute_plural: Attributs
606 608 label_no_data: Aucune donnée à afficher
607 609 label_change_status: Changer le statut
608 610 label_history: Historique
609 611 label_attachment: Fichier
610 612 label_attachment_new: Nouveau fichier
611 613 label_attachment_delete: Supprimer le fichier
612 614 label_attachment_plural: Fichiers
613 615 label_file_added: Fichier ajouté
614 616 label_report: Rapport
615 617 label_report_plural: Rapports
616 618 label_news: Annonce
617 619 label_news_new: Nouvelle annonce
618 620 label_news_plural: Annonces
619 621 label_news_latest: Dernières annonces
620 622 label_news_view_all: Voir toutes les annonces
621 623 label_news_added: Annonce ajoutée
622 624 label_news_comment_added: Commentaire ajouté à une annonce
623 625 label_settings: Configuration
624 626 label_overview: Aperçu
625 627 label_version: Version
626 628 label_version_new: Nouvelle version
627 629 label_version_plural: Versions
628 630 label_close_versions: Fermer les versions terminées
629 631 label_confirmation: Confirmation
630 632 label_export_to: 'Formats disponibles :'
631 633 label_read: Lire...
632 634 label_public_projects: Projets publics
633 635 label_open_issues: ouvert
634 636 label_open_issues_plural: ouverts
635 637 label_closed_issues: fermé
636 638 label_closed_issues_plural: fermés
637 639 label_x_open_issues_abbr_on_total:
638 640 zero: 0 ouverte sur %{total}
639 641 one: 1 ouverte sur %{total}
640 642 other: "%{count} ouvertes sur %{total}"
641 643 label_x_open_issues_abbr:
642 644 zero: 0 ouverte
643 645 one: 1 ouverte
644 646 other: "%{count} ouvertes"
645 647 label_x_closed_issues_abbr:
646 648 zero: 0 fermée
647 649 one: 1 fermée
648 650 other: "%{count} fermées"
649 651 label_x_issues:
650 652 zero: 0 demande
651 653 one: 1 demande
652 654 other: "%{count} demandes"
653 655 label_total: Total
654 656 label_total_time: Temps total
655 657 label_permissions: Permissions
656 658 label_current_status: Statut actuel
657 659 label_new_statuses_allowed: Nouveaux statuts autorisés
658 660 label_all: tous
659 661 label_any: tous
660 662 label_none: aucun
661 663 label_nobody: personne
662 664 label_next: Suivant
663 665 label_previous: Précédent
664 666 label_used_by: Utilisé par
665 667 label_details: Détails
666 668 label_add_note: Ajouter une note
667 669 label_per_page: Par page
668 670 label_calendar: Calendrier
669 671 label_months_from: mois depuis
670 672 label_gantt: Gantt
671 673 label_internal: Interne
672 674 label_last_changes: "%{count} derniers changements"
673 675 label_change_view_all: Voir tous les changements
674 676 label_personalize_page: Personnaliser cette page
675 677 label_comment: Commentaire
676 678 label_comment_plural: Commentaires
677 679 label_x_comments:
678 680 zero: aucun commentaire
679 681 one: un commentaire
680 682 other: "%{count} commentaires"
681 683 label_comment_add: Ajouter un commentaire
682 684 label_comment_added: Commentaire ajouté
683 685 label_comment_delete: Supprimer les commentaires
684 686 label_query: Rapport personnalisé
685 687 label_query_plural: Rapports personnalisés
686 688 label_query_new: Nouveau rapport
687 689 label_my_queries: Mes rapports personnalisés
688 690 label_filter_add: Ajouter le filtre
689 691 label_filter_plural: Filtres
690 692 label_equals: égal
691 693 label_not_equals: différent
692 694 label_in_less_than: dans moins de
693 695 label_in_more_than: dans plus de
694 696 label_in_the_next_days: dans les prochains jours
695 697 label_in_the_past_days: dans les derniers jours
696 698 label_greater_or_equal: '>='
697 699 label_less_or_equal: '<='
698 700 label_between: entre
699 701 label_in: dans
700 702 label_today: aujourd'hui
701 703 label_all_time: toute la période
702 704 label_yesterday: hier
703 705 label_this_week: cette semaine
704 706 label_last_week: la semaine dernière
705 707 label_last_n_weeks: "les %{count} dernières semaines"
706 708 label_last_n_days: "les %{count} derniers jours"
707 709 label_this_month: ce mois-ci
708 710 label_last_month: le mois dernier
709 711 label_this_year: cette année
710 712 label_date_range: Période
711 713 label_less_than_ago: il y a moins de
712 714 label_more_than_ago: il y a plus de
713 715 label_ago: il y a
714 716 label_contains: contient
715 717 label_not_contains: ne contient pas
716 718 label_any_issues_in_project: une demande du projet
717 719 label_any_issues_not_in_project: une demande hors du projet
718 720 label_no_issues_in_project: aucune demande du projet
719 721 label_day_plural: jours
720 722 label_repository: Dépôt
721 723 label_repository_new: Nouveau dépôt
722 724 label_repository_plural: Dépôts
723 725 label_browse: Parcourir
724 726 label_branch: Branche
725 727 label_tag: Tag
726 728 label_revision: Révision
727 729 label_revision_plural: Révisions
728 730 label_revision_id: "Révision %{value}"
729 731 label_associated_revisions: Révisions associées
730 732 label_added: ajouté
731 733 label_modified: modifié
732 734 label_copied: copié
733 735 label_renamed: renommé
734 736 label_deleted: supprimé
735 737 label_latest_revision: Dernière révision
736 738 label_latest_revision_plural: Dernières révisions
737 739 label_view_revisions: Voir les révisions
738 740 label_view_all_revisions: Voir toutes les révisions
739 741 label_max_size: Taille maximale
740 742 label_sort_highest: Remonter en premier
741 743 label_sort_higher: Remonter
742 744 label_sort_lower: Descendre
743 745 label_sort_lowest: Descendre en dernier
744 746 label_roadmap: Roadmap
745 747 label_roadmap_due_in: "Échéance dans %{value}"
746 748 label_roadmap_overdue: "En retard de %{value}"
747 749 label_roadmap_no_issues: Aucune demande pour cette version
748 750 label_search: Recherche
749 751 label_result_plural: Résultats
750 752 label_all_words: Tous les mots
751 753 label_wiki: Wiki
752 754 label_wiki_edit: Révision wiki
753 755 label_wiki_edit_plural: Révisions wiki
754 756 label_wiki_page: Page wiki
755 757 label_wiki_page_plural: Pages wiki
756 758 label_index_by_title: Index par titre
757 759 label_index_by_date: Index par date
758 760 label_current_version: Version actuelle
759 761 label_preview: Prévisualisation
760 762 label_feed_plural: Flux Atom
761 763 label_changes_details: Détails de tous les changements
762 764 label_issue_tracking: Suivi des demandes
763 765 label_spent_time: Temps passé
764 766 label_overall_spent_time: Temps passé global
765 767 label_f_hour: "%{value} heure"
766 768 label_f_hour_plural: "%{value} heures"
767 769 label_time_tracking: Suivi du temps
768 770 label_change_plural: Changements
769 771 label_statistics: Statistiques
770 772 label_commits_per_month: Commits par mois
771 773 label_commits_per_author: Commits par auteur
772 774 label_diff: diff
773 775 label_view_diff: Voir les différences
774 776 label_diff_inline: en ligne
775 777 label_diff_side_by_side: côte à côte
776 778 label_options: Options
777 779 label_copy_workflow_from: Copier le workflow de
778 780 label_permissions_report: Synthèse des permissions
779 781 label_watched_issues: Demandes surveillées
780 782 label_related_issues: Demandes liées
781 783 label_applied_status: Statut appliqué
782 784 label_loading: Chargement...
783 785 label_relation_new: Nouvelle relation
784 786 label_relation_delete: Supprimer la relation
785 787 label_relates_to: Lié à
786 788 label_duplicates: Duplique
787 789 label_duplicated_by: Dupliqué par
788 790 label_blocks: Bloque
789 791 label_blocked_by: Bloqué par
790 792 label_precedes: Précède
791 793 label_follows: Suit
792 794 label_copied_to: Copié vers
793 795 label_copied_from: Copié depuis
794 796 label_end_to_start: fin à début
795 797 label_end_to_end: fin à fin
796 798 label_start_to_start: début à début
797 799 label_start_to_end: début à fin
798 800 label_stay_logged_in: Rester connecté
799 801 label_disabled: désactivé
800 802 label_show_completed_versions: Voir les versions passées
801 803 label_me: moi
802 804 label_board: Forum
803 805 label_board_new: Nouveau forum
804 806 label_board_plural: Forums
805 807 label_board_locked: Verrouillé
806 808 label_board_sticky: Sticky
807 809 label_topic_plural: Discussions
808 810 label_message_plural: Messages
809 811 label_message_last: Dernier message
810 812 label_message_new: Nouveau message
811 813 label_message_posted: Message ajouté
812 814 label_reply_plural: Réponses
813 815 label_send_information: Envoyer les informations à l'utilisateur
814 816 label_year: Année
815 817 label_month: Mois
816 818 label_week: Semaine
817 819 label_date_from: Du
818 820 label_date_to: Au
819 821 label_language_based: Basé sur la langue de l'utilisateur
820 822 label_sort_by: "Trier par %{value}"
821 823 label_send_test_email: Envoyer un email de test
822 824 label_feeds_access_key: Clé d'accès Atom
823 825 label_missing_feeds_access_key: Clé d'accès Atom manquante
824 826 label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}"
825 827 label_module_plural: Modules
826 828 label_added_time_by: "Ajouté par %{author} il y a %{age}"
827 829 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
828 830 label_updated_time: "Mis à jour il y a %{value}"
829 831 label_jump_to_a_project: Aller à un projet...
830 832 label_file_plural: Fichiers
831 833 label_changeset_plural: Révisions
832 834 label_default_columns: Colonnes par défaut
833 835 label_no_change_option: (Pas de changement)
834 836 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
835 837 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
836 838 label_theme: Thème
837 839 label_default: Défaut
838 840 label_search_titles_only: Uniquement dans les titres
839 841 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
840 842 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
841 843 label_user_mail_option_none: Aucune notification
842 844 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
843 845 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
844 846 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
845 847 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
846 848 label_registration_activation_by_email: activation du compte par email
847 849 label_registration_manual_activation: activation manuelle du compte
848 850 label_registration_automatic_activation: activation automatique du compte
849 851 label_display_per_page: "Par page : %{value}"
850 852 label_age: Âge
851 853 label_change_properties: Changer les propriétés
852 854 label_general: Général
853 855 label_more: Plus
854 856 label_scm: SCM
855 857 label_plugins: Plugins
856 858 label_ldap_authentication: Authentification LDAP
857 859 label_downloads_abbr: D/L
858 860 label_optional_description: Description facultative
859 861 label_add_another_file: Ajouter un autre fichier
860 862 label_preferences: Préférences
861 863 label_chronological_order: Dans l'ordre chronologique
862 864 label_reverse_chronological_order: Dans l'ordre chronologique inverse
863 865 label_planning: Planning
864 866 label_incoming_emails: Emails entrants
865 867 label_generate_key: Générer une clé
866 868 label_issue_watchers: Observateurs
867 869 label_example: Exemple
868 870 label_display: Affichage
869 871 label_sort: Tri
870 872 label_ascending: Croissant
871 873 label_descending: Décroissant
872 874 label_date_from_to: Du %{start} au %{end}
873 875 label_wiki_content_added: Page wiki ajoutée
874 876 label_wiki_content_updated: Page wiki mise à jour
875 877 label_group: Groupe
876 878 label_group_plural: Groupes
877 879 label_group_new: Nouveau groupe
878 880 label_group_anonymous: Utilisateurs anonymes
879 881 label_group_non_member: Utilisateurs non membres
880 882 label_time_entry_plural: Temps passé
881 883 label_version_sharing_none: Non partagé
882 884 label_version_sharing_descendants: Avec les sous-projets
883 885 label_version_sharing_hierarchy: Avec toute la hiérarchie
884 886 label_version_sharing_tree: Avec tout l'arbre
885 887 label_version_sharing_system: Avec tous les projets
886 888 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
887 889 label_copy_source: Source
888 890 label_copy_target: Cible
889 891 label_copy_same_as_target: Comme la cible
890 892 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
891 893 label_api_access_key: Clé d'accès API
892 894 label_missing_api_access_key: Clé d'accès API manquante
893 895 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
894 896 label_profile: Profil
895 897 label_subtask_plural: Sous-tâches
896 898 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
897 899 label_principal_search: "Rechercher un utilisateur ou un groupe :"
898 900 label_user_search: "Rechercher un utilisateur :"
899 901 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
900 902 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
901 903 label_issues_visibility_all: Toutes les demandes
902 904 label_issues_visibility_public: Toutes les demandes non privées
903 905 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
904 906 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
905 907 label_parent_revision: Parent
906 908 label_child_revision: Enfant
907 909 label_export_options: Options d'exportation %{export_format}
908 910 label_copy_attachments: Copier les fichiers
909 911 label_copy_subtasks: Copier les sous-tâches
910 912 label_item_position: "%{position} sur %{count}"
911 913 label_completed_versions: Versions passées
912 914 label_search_for_watchers: Rechercher des observateurs
913 915 label_session_expiration: Expiration des sessions
914 916 label_show_closed_projects: Voir les projets fermés
915 917 label_status_transitions: Changements de statut
916 918 label_fields_permissions: Permissions sur les champs
917 919 label_readonly: Lecture
918 920 label_required: Obligatoire
919 921 label_hidden: Caché
920 922 label_attribute_of_project: "%{name} du projet"
921 923 label_attribute_of_issue: "%{name} de la demande"
922 924 label_attribute_of_author: "%{name} de l'auteur"
923 925 label_attribute_of_assigned_to: "%{name} de l'assigné"
924 926 label_attribute_of_user: "%{name} de l'utilisateur"
925 927 label_attribute_of_fixed_version: "%{name} de la version cible"
926 928 label_cross_project_descendants: Avec les sous-projets
927 929 label_cross_project_tree: Avec tout l'arbre
928 930 label_cross_project_hierarchy: Avec toute la hiérarchie
929 931 label_cross_project_system: Avec tous les projets
930 932 label_gantt_progress_line: Ligne de progression
931 933 label_visibility_private: par moi uniquement
932 934 label_visibility_roles: par ces rôles uniquement
933 935 label_visibility_public: par tout le monde
934 936 label_link: Lien
935 937 label_only: seulement
936 938 label_drop_down_list: liste déroulante
937 939 label_checkboxes: cases à cocher
938 940 label_radio_buttons: boutons radio
939 941 label_link_values_to: Lier les valeurs vers l'URL
940 942 label_custom_field_select_type: Selectionner le type d'objet auquel attacher le champ personnalisé
941 943 label_check_for_updates: Vérifier les mises à jour
942 944 label_latest_compatible_version: Dernière version compatible
943 945 label_unknown_plugin: Plugin inconnu
944 946 label_add_projects: Ajouter des projets
945 947 label_users_visibility_all: Tous les utilisateurs actifs
946 948 label_users_visibility_members_of_visible_projects: Membres des projets visibles
947 949 label_edit_attachments: Modifier les fichiers attachés
948 950 label_link_copied_issue: Lier la demande copiée
949 951 label_ask: Demander
950 952 label_search_attachments_yes: Rechercher les noms et descriptions de fichiers
951 953 label_search_attachments_no: Ne pas rechercher les fichiers
952 954 label_search_attachments_only: Rechercher les fichiers uniquement
953 955 label_search_open_issues_only: Demandes ouvertes uniquement
956 label_email_address_plural: Emails
957 label_email_address_add: Ajouter une adresse email
958 label_enable_notifications: Activer les notifications
959 label_disable_notifications: Désactiver les notifications
954 960
955 961 button_login: Connexion
956 962 button_submit: Soumettre
957 963 button_save: Sauvegarder
958 964 button_check_all: Tout cocher
959 965 button_uncheck_all: Tout décocher
960 966 button_collapse_all: Plier tout
961 967 button_expand_all: Déplier tout
962 968 button_delete: Supprimer
963 969 button_create: Créer
964 970 button_create_and_continue: Créer et continuer
965 971 button_test: Tester
966 972 button_edit: Modifier
967 973 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
968 974 button_add: Ajouter
969 975 button_change: Changer
970 976 button_apply: Appliquer
971 977 button_clear: Effacer
972 978 button_lock: Verrouiller
973 979 button_unlock: Déverrouiller
974 980 button_download: Télécharger
975 981 button_list: Lister
976 982 button_view: Voir
977 983 button_move: Déplacer
978 984 button_move_and_follow: Déplacer et suivre
979 985 button_back: Retour
980 986 button_cancel: Annuler
981 987 button_activate: Activer
982 988 button_sort: Trier
983 989 button_log_time: Saisir temps
984 990 button_rollback: Revenir à cette version
985 991 button_watch: Surveiller
986 992 button_unwatch: Ne plus surveiller
987 993 button_reply: Répondre
988 994 button_archive: Archiver
989 995 button_unarchive: Désarchiver
990 996 button_reset: Réinitialiser
991 997 button_rename: Renommer
992 998 button_change_password: Changer de mot de passe
993 999 button_copy: Copier
994 1000 button_copy_and_follow: Copier et suivre
995 1001 button_annotate: Annoter
996 1002 button_update: Mettre à jour
997 1003 button_configure: Configurer
998 1004 button_quote: Citer
999 1005 button_duplicate: Dupliquer
1000 1006 button_show: Afficher
1001 1007 button_hide: Cacher
1002 1008 button_edit_section: Modifier cette section
1003 1009 button_export: Exporter
1004 1010 button_delete_my_account: Supprimer mon compte
1005 1011 button_close: Fermer
1006 1012 button_reopen: Réouvrir
1007 1013
1008 1014 status_active: actif
1009 1015 status_registered: enregistré
1010 1016 status_locked: verrouillé
1011 1017
1012 1018 project_status_active: actif
1013 1019 project_status_closed: fermé
1014 1020 project_status_archived: archivé
1015 1021
1016 1022 version_status_open: ouvert
1017 1023 version_status_locked: verrouillé
1018 1024 version_status_closed: fermé
1019 1025
1020 1026 field_active: Actif
1021 1027
1022 1028 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
1023 1029 text_regexp_info: ex. ^[A-Z0-9]+$
1024 1030 text_min_max_length_info: 0 pour aucune restriction
1025 1031 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
1026 1032 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
1027 1033 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
1028 1034 text_are_you_sure: Êtes-vous sûr ?
1029 1035 text_journal_changed: "%{label} changé de %{old} à %{new}"
1030 1036 text_journal_changed_no_detail: "%{label} mis à jour"
1031 1037 text_journal_set_to: "%{label} mis à %{value}"
1032 1038 text_journal_deleted: "%{label} %{old} supprimé"
1033 1039 text_journal_added: "%{label} %{value} ajouté"
1034 1040 text_tip_issue_begin_day: tâche commençant ce jour
1035 1041 text_tip_issue_end_day: tâche finissant ce jour
1036 1042 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
1037 1043 text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés, doit commencer par une minuscule.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
1038 1044 text_caracters_maximum: "%{count} caractères maximum."
1039 1045 text_caracters_minimum: "%{count} caractères minimum."
1040 1046 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
1041 1047 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
1042 1048 text_unallowed_characters: Caractères non autorisés
1043 1049 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
1044 1050 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
1045 1051 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
1046 1052 text_issue_added: "La demande %{id} a été soumise par %{author}."
1047 1053 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
1048 1054 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
1049 1055 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
1050 1056 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
1051 1057 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
1052 1058 text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)."
1053 1059 text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé."
1054 1060 text_load_default_configuration: Charger le paramétrage par défaut
1055 1061 text_status_changed_by_changeset: "Appliqué par commit %{value}."
1056 1062 text_time_logged_by_changeset: "Appliqué par commit %{value}"
1057 1063 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
1058 1064 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
1059 1065 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1060 1066 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
1061 1067 text_default_administrator_account_changed: Compte administrateur par défaut changé
1062 1068 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
1063 1069 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
1064 1070 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
1065 1071 text_convert_available: Binaire convert de ImageMagick présent (optionel)
1066 1072 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
1067 1073 text_destroy_time_entries: Supprimer les heures
1068 1074 text_assign_time_entries_to_project: Reporter les heures sur le projet
1069 1075 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
1070 1076 text_user_wrote: "%{value} a écrit :"
1071 1077 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
1072 1078 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
1073 1079 text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/configuration.yml et redémarrez l'application pour les activer."
1074 1080 text_repository_usernames_mapping: "Vous pouvez sélectionner ou modifier l'utilisateur Redmine associé à chaque nom d'utilisateur figurant dans l'historique du dépôt.\nLes utilisateurs avec le même identifiant ou la même adresse mail seront automatiquement associés."
1075 1081 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
1076 1082 text_custom_field_possible_values_info: 'Une ligne par valeur'
1077 1083 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
1078 1084 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
1079 1085 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
1080 1086 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
1081 1087 text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?"
1082 1088 text_zoom_in: Zoom avant
1083 1089 text_zoom_out: Zoom arrière
1084 1090 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
1085 1091 text_scm_path_encoding_note: "Défaut : UTF-8"
1086 1092 text_subversion_repository_note: "Exemples (en fonction des protocoles supportés) : file:///, http://, https://, svn://, svn+[tunnelscheme]://"
1087 1093 text_git_repository_note: "Chemin vers un dépôt vide et local (exemples : /gitrepo, c:\\gitrepo)"
1088 1094 text_mercurial_repository_note: "Chemin vers un dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1089 1095 text_scm_command: Commande
1090 1096 text_scm_command_version: Version
1091 1097 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1092 1098 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1093 1099 text_issue_conflict_resolution_overwrite: "Appliquer quand même ma mise à jour (les notes précédentes seront conservées mais des changements pourront être écrasés)"
1094 1100 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
1095 1101 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
1096 1102 text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
1097 1103 text_session_expiration_settings: "Attention : le changement de ces paramètres peut entrainer l'expiration des sessions utilisateurs en cours, y compris la vôtre."
1098 1104 text_project_closed: Ce projet est fermé et accessible en lecture seule.
1099 1105 text_turning_multiple_off: "Si vous désactivez les valeurs multiples, les valeurs multiples seront supprimées pour n'en conserver qu'une par objet."
1100 1106
1101 1107 default_role_manager: Manager
1102 1108 default_role_developer: Développeur
1103 1109 default_role_reporter: Rapporteur
1104 1110 default_tracker_bug: Anomalie
1105 1111 default_tracker_feature: Evolution
1106 1112 default_tracker_support: Assistance
1107 1113 default_issue_status_new: Nouveau
1108 1114 default_issue_status_in_progress: En cours
1109 1115 default_issue_status_resolved: Résolu
1110 1116 default_issue_status_feedback: Commentaire
1111 1117 default_issue_status_closed: Fermé
1112 1118 default_issue_status_rejected: Rejeté
1113 1119 default_doc_category_user: Documentation utilisateur
1114 1120 default_doc_category_tech: Documentation technique
1115 1121 default_priority_low: Bas
1116 1122 default_priority_normal: Normal
1117 1123 default_priority_high: Haut
1118 1124 default_priority_urgent: Urgent
1119 1125 default_priority_immediate: Immédiat
1120 1126 default_activity_design: Conception
1121 1127 default_activity_development: Développement
1122 1128
1123 1129 enumeration_issue_priorities: Priorités des demandes
1124 1130 enumeration_doc_categories: Catégories des documents
1125 1131 enumeration_activities: Activités (suivi du temps)
1126 1132 enumeration_system_activity: Activité système
1127 1133 description_filter: Filtre
1128 1134 description_search: Champ de recherche
1129 1135 description_choose_project: Projets
1130 1136 description_project_scope: Périmètre de recherche
1131 1137 description_notes: Notes
1132 1138 description_message_content: Contenu du message
1133 1139 description_query_sort_criteria_attribute: Critère de tri
1134 1140 description_query_sort_criteria_direction: Ordre de tri
1135 1141 description_user_mail_notification: Option de notification
1136 1142 description_available_columns: Colonnes disponibles
1137 1143 description_selected_columns: Colonnes sélectionnées
1138 1144 description_all_columns: Toutes les colonnes
1139 1145 description_issue_category_reassign: Choisir une catégorie
1140 1146 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1141 1147 description_date_range_list: Choisir une période prédéfinie
1142 1148 description_date_range_interval: Choisir une période
1143 1149 description_date_from: Date de début
1144 1150 description_date_to: Date de fin
1145 1151 text_repository_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
@@ -1,350 +1,351
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 70 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
71 71 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
72 72 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
73 73 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
74 74 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
75 75
76 76 resources :users do
77 77 resources :memberships, :controller => 'principal_memberships'
78 resources :email_addresses, :only => [:index, :create, :update, :destroy]
78 79 end
79 80
80 81 post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
81 82 delete 'watchers/watch', :to => 'watchers#unwatch'
82 83 get 'watchers/new', :to => 'watchers#new'
83 84 post 'watchers', :to => 'watchers#create'
84 85 post 'watchers/append', :to => 'watchers#append'
85 86 delete 'watchers', :to => 'watchers#destroy'
86 87 get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
87 88 # Specific routes for issue watchers API
88 89 post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
89 90 delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue'
90 91
91 92 resources :projects do
92 93 member do
93 94 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
94 95 post 'modules'
95 96 post 'archive'
96 97 post 'unarchive'
97 98 post 'close'
98 99 post 'reopen'
99 100 match 'copy', :via => [:get, :post]
100 101 end
101 102
102 103 shallow do
103 104 resources :memberships, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
104 105 collection do
105 106 get 'autocomplete'
106 107 end
107 108 end
108 109 end
109 110
110 111 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
111 112
112 113 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
113 114 resources :issues, :only => [:index, :new, :create]
114 115 # issue form update
115 116 match 'issues/update_form', :controller => 'issues', :action => 'update_form', :via => [:put, :patch, :post], :as => 'issue_form'
116 117
117 118 resources :files, :only => [:index, :new, :create]
118 119
119 120 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
120 121 collection do
121 122 put 'close_completed'
122 123 end
123 124 end
124 125 get 'versions.:format', :to => 'versions#index'
125 126 get 'roadmap', :to => 'versions#index', :format => false
126 127 get 'versions', :to => 'versions#index'
127 128
128 129 resources :news, :except => [:show, :edit, :update, :destroy]
129 130 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
130 131 get 'report', :on => :collection
131 132 end
132 133 resources :queries, :only => [:new, :create]
133 134 shallow do
134 135 resources :issue_categories
135 136 end
136 137 resources :documents, :except => [:show, :edit, :update, :destroy]
137 138 resources :boards
138 139 shallow do
139 140 resources :repositories, :except => [:index, :show] do
140 141 member do
141 142 match 'committers', :via => [:get, :post]
142 143 end
143 144 end
144 145 end
145 146
146 147 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
147 148 resources :wiki, :except => [:index, :new, :create], :as => 'wiki_page' do
148 149 member do
149 150 get 'rename'
150 151 post 'rename'
151 152 get 'history'
152 153 get 'diff'
153 154 match 'preview', :via => [:post, :put, :patch]
154 155 post 'protect'
155 156 post 'add_attachment'
156 157 end
157 158 collection do
158 159 get 'export'
159 160 get 'date_index'
160 161 end
161 162 end
162 163 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
163 164 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
164 165 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
165 166 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
166 167 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
167 168 end
168 169
169 170 resources :issues do
170 171 collection do
171 172 match 'bulk_edit', :via => [:get, :post]
172 173 post 'bulk_update'
173 174 end
174 175 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
175 176 collection do
176 177 get 'report'
177 178 end
178 179 end
179 180 shallow do
180 181 resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
181 182 end
182 183 end
183 184 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
184 185
185 186 resources :queries, :except => [:show]
186 187
187 188 resources :news, :only => [:index, :show, :edit, :update, :destroy]
188 189 match '/news/:id/comments', :to => 'comments#create', :via => :post
189 190 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
190 191
191 192 resources :versions, :only => [:show, :edit, :update, :destroy] do
192 193 post 'status_by', :on => :member
193 194 end
194 195
195 196 resources :documents, :only => [:show, :edit, :update, :destroy] do
196 197 post 'add_attachment', :on => :member
197 198 end
198 199
199 200 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
200 201
201 202 resources :time_entries, :controller => 'timelog', :except => :destroy do
202 203 collection do
203 204 get 'report'
204 205 get 'bulk_edit'
205 206 post 'bulk_update'
206 207 end
207 208 end
208 209 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
209 210 # TODO: delete /time_entries for bulk deletion
210 211 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
211 212
212 213 get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity
213 214 get 'activity', :to => 'activities#index'
214 215
215 216 # repositories routes
216 217 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
217 218 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
218 219
219 220 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
220 221 :to => 'repositories#changes'
221 222
222 223 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
223 224 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
224 225 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
225 226 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
226 227 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
227 228 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
228 229 :controller => 'repositories',
229 230 :format => false,
230 231 :constraints => {
231 232 :action => /(browse|show|entry|raw|annotate|diff)/,
232 233 :rev => /[a-z0-9\.\-_]+/
233 234 }
234 235
235 236 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
236 237 get 'projects/:id/repository/graph', :to => 'repositories#graph'
237 238
238 239 get 'projects/:id/repository/changes(/*path(.:ext))',
239 240 :to => 'repositories#changes'
240 241
241 242 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
242 243 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
243 244 get 'projects/:id/repository/revision', :to => 'repositories#revision'
244 245 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
245 246 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
246 247 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
247 248 :controller => 'repositories',
248 249 :format => false,
249 250 :constraints => {
250 251 :action => /(browse|show|entry|raw|annotate|diff)/,
251 252 :rev => /[a-z0-9\.\-_]+/
252 253 }
253 254 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
254 255 :controller => 'repositories',
255 256 :action => /(browse|show|entry|raw|changes|annotate|diff)/
256 257 get 'projects/:id/repository/:action(/*path(.:ext))',
257 258 :controller => 'repositories',
258 259 :action => /(browse|show|entry|raw|changes|annotate|diff)/
259 260
260 261 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
261 262 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
262 263
263 264 # additional routes for having the file name at the end of url
264 265 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
265 266 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
266 267 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
267 268 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
268 269 resources :attachments, :only => [:show, :destroy]
269 270 get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit', :as => :object_attachments_edit
270 271 patch 'attachments/:object_type/:object_id', :to => 'attachments#update', :as => :object_attachments
271 272
272 273 resources :groups do
273 274 resources :memberships, :controller => 'principal_memberships'
274 275 member do
275 276 get 'autocomplete_for_user'
276 277 end
277 278 end
278 279
279 280 get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users'
280 281 post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users'
281 282 delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
282 283
283 284 resources :trackers, :except => :show do
284 285 collection do
285 286 match 'fields', :via => [:get, :post]
286 287 end
287 288 end
288 289 resources :issue_statuses, :except => :show do
289 290 collection do
290 291 post 'update_issue_done_ratio'
291 292 end
292 293 end
293 294 resources :custom_fields, :except => :show
294 295 resources :roles do
295 296 collection do
296 297 match 'permissions', :via => [:get, :post]
297 298 end
298 299 end
299 300 resources :enumerations, :except => :show
300 301 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
301 302
302 303 get 'projects/:id/search', :controller => 'search', :action => 'index'
303 304 get 'search', :controller => 'search', :action => 'index'
304 305
305 306 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
306 307
307 308 match 'admin', :controller => 'admin', :action => 'index', :via => :get
308 309 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
309 310 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
310 311 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
311 312 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
312 313 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
313 314
314 315 resources :auth_sources do
315 316 member do
316 317 get 'test_connection', :as => 'try_connection'
317 318 end
318 319 collection do
319 320 get 'autocomplete_for_new_user'
320 321 end
321 322 end
322 323
323 324 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
324 325 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
325 326 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
326 327 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
327 328 match 'settings', :controller => 'settings', :action => 'index', :via => :get
328 329 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
329 330 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
330 331
331 332 match 'sys/projects', :to => 'sys#projects', :via => :get
332 333 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
333 334 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post]
334 335
335 336 match 'uploads', :to => 'attachments#upload', :via => :post
336 337
337 338 get 'robots.txt', :to => 'welcome#robots'
338 339
339 340 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
340 341 file = File.join(plugin_dir, "config/routes.rb")
341 342 if File.exists?(file)
342 343 begin
343 344 instance_eval File.read(file)
344 345 rescue Exception => e
345 346 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
346 347 exit 1
347 348 end
348 349 end
349 350 end
350 351 end
@@ -1,236 +1,240
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
19 19 # DO NOT MODIFY THIS FILE !!!
20 20 # Settings can be defined through the application in Admin -> Settings
21 21
22 22 app_title:
23 23 default: Redmine
24 24 app_subtitle:
25 25 default: Project management
26 26 welcome_text:
27 27 default:
28 28 login_required:
29 29 default: 0
30 30 self_registration:
31 31 default: '2'
32 32 lost_password:
33 33 default: 1
34 34 unsubscribe:
35 35 default: 1
36 36 password_min_length:
37 37 format: int
38 38 default: 8
39 # Maximum number of additional email addresses per user
40 max_additional_emails:
41 format: int
42 default: 5
39 43 # Maximum lifetime of user sessions in minutes
40 44 session_lifetime:
41 45 format: int
42 46 default: 0
43 47 # User session timeout in minutes
44 48 session_timeout:
45 49 format: int
46 50 default: 0
47 51 attachment_max_size:
48 52 format: int
49 53 default: 5120
50 54 issues_export_limit:
51 55 format: int
52 56 default: 500
53 57 activity_days_default:
54 58 format: int
55 59 default: 30
56 60 per_page_options:
57 61 default: '25,50,100'
58 62 mail_from:
59 63 default: redmine@example.net
60 64 bcc_recipients:
61 65 default: 1
62 66 plain_text_mail:
63 67 default: 0
64 68 text_formatting:
65 69 default: textile
66 70 cache_formatted_text:
67 71 default: 0
68 72 wiki_compression:
69 73 default: ""
70 74 default_language:
71 75 default: en
72 76 force_default_language_for_anonymous:
73 77 default: 0
74 78 force_default_language_for_loggedin:
75 79 default: 0
76 80 host_name:
77 81 default: localhost:3000
78 82 protocol:
79 83 default: http
80 84 feeds_limit:
81 85 format: int
82 86 default: 15
83 87 gantt_items_limit:
84 88 format: int
85 89 default: 500
86 90 # Maximum size of files that can be displayed
87 91 # inline through the file viewer (in KB)
88 92 file_max_size_displayed:
89 93 format: int
90 94 default: 512
91 95 diff_max_lines_displayed:
92 96 format: int
93 97 default: 1500
94 98 enabled_scm:
95 99 serialized: true
96 100 default:
97 101 - Subversion
98 102 - Darcs
99 103 - Mercurial
100 104 - Cvs
101 105 - Bazaar
102 106 - Git
103 107 autofetch_changesets:
104 108 default: 1
105 109 sys_api_enabled:
106 110 default: 0
107 111 sys_api_key:
108 112 default: ''
109 113 commit_cross_project_ref:
110 114 default: 0
111 115 commit_ref_keywords:
112 116 default: 'refs,references,IssueID'
113 117 commit_update_keywords:
114 118 serialized: true
115 119 default: []
116 120 commit_logtime_enabled:
117 121 default: 0
118 122 commit_logtime_activity_id:
119 123 format: int
120 124 default: 0
121 125 # autologin duration in days
122 126 # 0 means autologin is disabled
123 127 autologin:
124 128 format: int
125 129 default: 0
126 130 # date format
127 131 date_format:
128 132 default: ''
129 133 time_format:
130 134 default: ''
131 135 user_format:
132 136 default: :firstname_lastname
133 137 format: symbol
134 138 cross_project_issue_relations:
135 139 default: 0
136 140 # Enables subtasks to be in other projects
137 141 cross_project_subtasks:
138 142 default: 'tree'
139 143 link_copied_issue:
140 144 default: 'ask'
141 145 issue_group_assignment:
142 146 default: 0
143 147 default_issue_start_date_to_creation_date:
144 148 default: 1
145 149 notified_events:
146 150 serialized: true
147 151 default:
148 152 - issue_added
149 153 - issue_updated
150 154 mail_handler_body_delimiters:
151 155 default: ''
152 156 mail_handler_excluded_filenames:
153 157 default: ''
154 158 mail_handler_api_enabled:
155 159 default: 0
156 160 mail_handler_api_key:
157 161 default:
158 162 issue_list_default_columns:
159 163 serialized: true
160 164 default:
161 165 - tracker
162 166 - status
163 167 - priority
164 168 - subject
165 169 - assigned_to
166 170 - updated_on
167 171 display_subprojects_issues:
168 172 default: 1
169 173 issue_done_ratio:
170 174 default: 'issue_field'
171 175 default_projects_public:
172 176 default: 1
173 177 default_projects_modules:
174 178 serialized: true
175 179 default:
176 180 - issue_tracking
177 181 - time_tracking
178 182 - news
179 183 - documents
180 184 - files
181 185 - wiki
182 186 - repository
183 187 - boards
184 188 - calendar
185 189 - gantt
186 190 default_projects_tracker_ids:
187 191 serialized: true
188 192 default:
189 193 # Role given to a non-admin user who creates a project
190 194 new_project_user_role_id:
191 195 format: int
192 196 default: ''
193 197 sequential_project_identifiers:
194 198 default: 0
195 199 # encodings used to convert repository files content to UTF-8
196 200 # multiple values accepted, comma separated
197 201 repositories_encodings:
198 202 default: ''
199 203 # encoding used to convert commit logs to UTF-8
200 204 commit_logs_encoding:
201 205 default: 'UTF-8'
202 206 repository_log_display_limit:
203 207 format: int
204 208 default: 100
205 209 ui_theme:
206 210 default: ''
207 211 emails_footer:
208 212 default: |-
209 213 You have received this notification because you have either subscribed to it, or are involved in it.
210 214 To change your notification preferences, please click here: http://hostname/my/account
211 215 gravatar_enabled:
212 216 default: 0
213 217 openid:
214 218 default: 0
215 219 gravatar_default:
216 220 default: ''
217 221 start_of_week:
218 222 default: ''
219 223 rest_api_enabled:
220 224 default: 0
221 225 jsonp_enabled:
222 226 default: 0
223 227 default_notification_option:
224 228 default: 'only_my_events'
225 229 emails_header:
226 230 default: ''
227 231 thumbnails_enabled:
228 232 default: 0
229 233 thumbnails_size:
230 234 format: int
231 235 default: 100
232 236 non_working_week_days:
233 237 serialized: true
234 238 default:
235 239 - '6'
236 240 - '7'
@@ -1,652 +1,652
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2015 Jean-Philippe Lang */
3 3
4 4 function checkAll(id, checked) {
5 5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
6 6 }
7 7
8 8 function toggleCheckboxesBySelector(selector) {
9 9 var all_checked = true;
10 10 $(selector).each(function(index) {
11 11 if (!$(this).is(':checked')) { all_checked = false; }
12 12 });
13 13 $(selector).prop('checked', !all_checked);
14 14 }
15 15
16 16 function showAndScrollTo(id, focus) {
17 17 $('#'+id).show();
18 18 if (focus !== null) {
19 19 $('#'+focus).focus();
20 20 }
21 21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
22 22 }
23 23
24 24 function toggleRowGroup(el) {
25 25 var tr = $(el).parents('tr').first();
26 26 var n = tr.next();
27 27 tr.toggleClass('open');
28 28 while (n.length && !n.hasClass('group')) {
29 29 n.toggle();
30 30 n = n.next('tr');
31 31 }
32 32 }
33 33
34 34 function collapseAllRowGroups(el) {
35 35 var tbody = $(el).parents('tbody').first();
36 36 tbody.children('tr').each(function(index) {
37 37 if ($(this).hasClass('group')) {
38 38 $(this).removeClass('open');
39 39 } else {
40 40 $(this).hide();
41 41 }
42 42 });
43 43 }
44 44
45 45 function expandAllRowGroups(el) {
46 46 var tbody = $(el).parents('tbody').first();
47 47 tbody.children('tr').each(function(index) {
48 48 if ($(this).hasClass('group')) {
49 49 $(this).addClass('open');
50 50 } else {
51 51 $(this).show();
52 52 }
53 53 });
54 54 }
55 55
56 56 function toggleAllRowGroups(el) {
57 57 var tr = $(el).parents('tr').first();
58 58 if (tr.hasClass('open')) {
59 59 collapseAllRowGroups(el);
60 60 } else {
61 61 expandAllRowGroups(el);
62 62 }
63 63 }
64 64
65 65 function toggleFieldset(el) {
66 66 var fieldset = $(el).parents('fieldset').first();
67 67 fieldset.toggleClass('collapsed');
68 68 fieldset.children('div').toggle();
69 69 }
70 70
71 71 function hideFieldset(el) {
72 72 var fieldset = $(el).parents('fieldset').first();
73 73 fieldset.toggleClass('collapsed');
74 74 fieldset.children('div').hide();
75 75 }
76 76
77 77 // columns selection
78 78 function moveOptions(theSelFrom, theSelTo) {
79 79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
80 80 }
81 81
82 82 function moveOptionUp(theSel) {
83 83 $(theSel).find('option:selected').each(function(){
84 84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
85 85 });
86 86 }
87 87
88 88 function moveOptionTop(theSel) {
89 89 $(theSel).find('option:selected').detach().prependTo($(theSel));
90 90 }
91 91
92 92 function moveOptionDown(theSel) {
93 93 $($(theSel).find('option:selected').get().reverse()).each(function(){
94 94 $(this).next(':not(:selected)').detach().insertBefore($(this));
95 95 });
96 96 }
97 97
98 98 function moveOptionBottom(theSel) {
99 99 $(theSel).find('option:selected').detach().appendTo($(theSel));
100 100 }
101 101
102 102 function initFilters() {
103 103 $('#add_filter_select').change(function() {
104 104 addFilter($(this).val(), '', []);
105 105 });
106 106 $('#filters-table td.field input[type=checkbox]').each(function() {
107 107 toggleFilter($(this).val());
108 108 });
109 109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
110 110 toggleFilter($(this).val());
111 111 });
112 112 $('#filters-table').on('click', '.toggle-multiselect', function() {
113 113 toggleMultiSelect($(this).siblings('select'));
114 114 });
115 115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
116 116 if (e.keyCode == 13) $(this).closest('form').submit();
117 117 });
118 118 }
119 119
120 120 function addFilter(field, operator, values) {
121 121 var fieldId = field.replace('.', '_');
122 122 var tr = $('#tr_'+fieldId);
123 123 if (tr.length > 0) {
124 124 tr.show();
125 125 } else {
126 126 buildFilterRow(field, operator, values);
127 127 }
128 128 $('#cb_'+fieldId).prop('checked', true);
129 129 toggleFilter(field);
130 130 $('#add_filter_select').val('').find('option').each(function() {
131 131 if ($(this).attr('value') == field) {
132 132 $(this).attr('disabled', true);
133 133 }
134 134 });
135 135 }
136 136
137 137 function buildFilterRow(field, operator, values) {
138 138 var fieldId = field.replace('.', '_');
139 139 var filterTable = $("#filters-table");
140 140 var filterOptions = availableFilters[field];
141 141 if (!filterOptions) return;
142 142 var operators = operatorByType[filterOptions['type']];
143 143 var filterValues = filterOptions['values'];
144 144 var i, select;
145 145
146 146 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
147 147 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
148 148 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
149 149 '<td class="values"></td>'
150 150 );
151 151 filterTable.append(tr);
152 152
153 153 select = tr.find('td.operator select');
154 154 for (i = 0; i < operators.length; i++) {
155 155 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
156 156 if (operators[i] == operator) { option.attr('selected', true); }
157 157 select.append(option);
158 158 }
159 159 select.change(function(){ toggleOperator(field); });
160 160
161 161 switch (filterOptions['type']) {
162 162 case "list":
163 163 case "list_optional":
164 164 case "list_status":
165 165 case "list_subprojects":
166 166 tr.find('td.values').append(
167 167 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
168 168 ' <span class="toggle-multiselect">&nbsp;</span></span>'
169 169 );
170 170 select = tr.find('td.values select');
171 171 if (values.length > 1) { select.attr('multiple', true); }
172 172 for (i = 0; i < filterValues.length; i++) {
173 173 var filterValue = filterValues[i];
174 174 var option = $('<option>');
175 175 if ($.isArray(filterValue)) {
176 176 option.val(filterValue[1]).text(filterValue[0]);
177 177 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
178 178 } else {
179 179 option.val(filterValue).text(filterValue);
180 180 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
181 181 }
182 182 select.append(option);
183 183 }
184 184 break;
185 185 case "date":
186 186 case "date_past":
187 187 tr.find('td.values').append(
188 188 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
189 189 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
190 190 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
191 191 );
192 192 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
193 193 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
194 194 $('#values_'+fieldId).val(values[0]);
195 195 break;
196 196 case "string":
197 197 case "text":
198 198 tr.find('td.values').append(
199 199 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
200 200 );
201 201 $('#values_'+fieldId).val(values[0]);
202 202 break;
203 203 case "relation":
204 204 tr.find('td.values').append(
205 205 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
206 206 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
207 207 );
208 208 $('#values_'+fieldId).val(values[0]);
209 209 select = tr.find('td.values select');
210 210 for (i = 0; i < allProjects.length; i++) {
211 211 var filterValue = allProjects[i];
212 212 var option = $('<option>');
213 213 option.val(filterValue[1]).text(filterValue[0]);
214 214 if (values[0] == filterValue[1]) { option.attr('selected', true); }
215 215 select.append(option);
216 216 }
217 217 break;
218 218 case "integer":
219 219 case "float":
220 220 tr.find('td.values').append(
221 221 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
222 222 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
223 223 );
224 224 $('#values_'+fieldId+'_1').val(values[0]);
225 225 $('#values_'+fieldId+'_2').val(values[1]);
226 226 break;
227 227 }
228 228 }
229 229
230 230 function toggleFilter(field) {
231 231 var fieldId = field.replace('.', '_');
232 232 if ($('#cb_' + fieldId).is(':checked')) {
233 233 $("#operators_" + fieldId).show().removeAttr('disabled');
234 234 toggleOperator(field);
235 235 } else {
236 236 $("#operators_" + fieldId).hide().attr('disabled', true);
237 237 enableValues(field, []);
238 238 }
239 239 }
240 240
241 241 function enableValues(field, indexes) {
242 242 var fieldId = field.replace('.', '_');
243 243 $('#tr_'+fieldId+' td.values .value').each(function(index) {
244 244 if ($.inArray(index, indexes) >= 0) {
245 245 $(this).removeAttr('disabled');
246 246 $(this).parents('span').first().show();
247 247 } else {
248 248 $(this).val('');
249 249 $(this).attr('disabled', true);
250 250 $(this).parents('span').first().hide();
251 251 }
252 252
253 253 if ($(this).hasClass('group')) {
254 254 $(this).addClass('open');
255 255 } else {
256 256 $(this).show();
257 257 }
258 258 });
259 259 }
260 260
261 261 function toggleOperator(field) {
262 262 var fieldId = field.replace('.', '_');
263 263 var operator = $("#operators_" + fieldId);
264 264 switch (operator.val()) {
265 265 case "!*":
266 266 case "*":
267 267 case "t":
268 268 case "ld":
269 269 case "w":
270 270 case "lw":
271 271 case "l2w":
272 272 case "m":
273 273 case "lm":
274 274 case "y":
275 275 case "o":
276 276 case "c":
277 277 enableValues(field, []);
278 278 break;
279 279 case "><":
280 280 enableValues(field, [0,1]);
281 281 break;
282 282 case "<t+":
283 283 case ">t+":
284 284 case "><t+":
285 285 case "t+":
286 286 case ">t-":
287 287 case "<t-":
288 288 case "><t-":
289 289 case "t-":
290 290 enableValues(field, [2]);
291 291 break;
292 292 case "=p":
293 293 case "=!p":
294 294 case "!p":
295 295 enableValues(field, [1]);
296 296 break;
297 297 default:
298 298 enableValues(field, [0]);
299 299 break;
300 300 }
301 301 }
302 302
303 303 function toggleMultiSelect(el) {
304 304 if (el.attr('multiple')) {
305 305 el.removeAttr('multiple');
306 306 el.attr('size', 1);
307 307 } else {
308 308 el.attr('multiple', true);
309 309 if (el.children().length > 10)
310 310 el.attr('size', 10);
311 311 else
312 312 el.attr('size', 4);
313 313 }
314 314 }
315 315
316 316 function showTab(name, url) {
317 317 $('div#content .tab-content').hide();
318 318 $('div.tabs a').removeClass('selected');
319 319 $('#tab-content-' + name).show();
320 320 $('#tab-' + name).addClass('selected');
321 321 //replaces current URL with the "href" attribute of the current link
322 322 //(only triggered if supported by browser)
323 323 if ("replaceState" in window.history) {
324 324 window.history.replaceState(null, document.title, url);
325 325 }
326 326 return false;
327 327 }
328 328
329 329 function moveTabRight(el) {
330 330 var lis = $(el).parents('div.tabs').first().find('ul').children();
331 331 var tabsWidth = 0;
332 332 var i = 0;
333 333 lis.each(function() {
334 334 if ($(this).is(':visible')) {
335 335 tabsWidth += $(this).width() + 6;
336 336 }
337 337 });
338 338 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
339 339 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
340 340 lis.eq(i).hide();
341 341 }
342 342
343 343 function moveTabLeft(el) {
344 344 var lis = $(el).parents('div.tabs').first().find('ul').children();
345 345 var i = 0;
346 346 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
347 347 if (i > 0) {
348 348 lis.eq(i-1).show();
349 349 }
350 350 }
351 351
352 352 function displayTabsButtons() {
353 353 var lis;
354 354 var tabsWidth = 0;
355 355 var el;
356 356 $('div.tabs').each(function() {
357 357 el = $(this);
358 358 lis = el.find('ul').children();
359 359 lis.each(function(){
360 360 if ($(this).is(':visible')) {
361 361 tabsWidth += $(this).width() + 6;
362 362 }
363 363 });
364 364 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
365 365 el.find('div.tabs-buttons').hide();
366 366 } else {
367 367 el.find('div.tabs-buttons').show();
368 368 }
369 369 });
370 370 }
371 371
372 372 function setPredecessorFieldsVisibility() {
373 373 var relationType = $('#relation_relation_type');
374 374 if (relationType.val() == "precedes" || relationType.val() == "follows") {
375 375 $('#predecessor_fields').show();
376 376 } else {
377 377 $('#predecessor_fields').hide();
378 378 }
379 379 }
380 380
381 function showModal(id, width) {
381 function showModal(id, width, title) {
382 382 var el = $('#'+id).first();
383 383 if (el.length === 0 || el.is(':visible')) {return;}
384 var title = el.find('h3.title').text();
384 if (!title) title = el.find('h3.title').text();
385 385 el.dialog({
386 386 width: width,
387 387 modal: true,
388 388 resizable: false,
389 389 dialogClass: 'modal',
390 390 title: title
391 391 });
392 392 el.find("input[type=text], input[type=submit]").first().focus();
393 393 }
394 394
395 395 function hideModal(el) {
396 396 var modal;
397 397 if (el) {
398 398 modal = $(el).parents('.ui-dialog-content');
399 399 } else {
400 400 modal = $('#ajax-modal');
401 401 }
402 402 modal.dialog("close");
403 403 }
404 404
405 405 function submitPreview(url, form, target) {
406 406 $.ajax({
407 407 url: url,
408 408 type: 'post',
409 409 data: $('#'+form).serialize(),
410 410 success: function(data){
411 411 $('#'+target).html(data);
412 412 }
413 413 });
414 414 }
415 415
416 416 function collapseScmEntry(id) {
417 417 $('.'+id).each(function() {
418 418 if ($(this).hasClass('open')) {
419 419 collapseScmEntry($(this).attr('id'));
420 420 }
421 421 $(this).hide();
422 422 });
423 423 $('#'+id).removeClass('open');
424 424 }
425 425
426 426 function expandScmEntry(id) {
427 427 $('.'+id).each(function() {
428 428 $(this).show();
429 429 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
430 430 expandScmEntry($(this).attr('id'));
431 431 }
432 432 });
433 433 $('#'+id).addClass('open');
434 434 }
435 435
436 436 function scmEntryClick(id, url) {
437 437 var el = $('#'+id);
438 438 if (el.hasClass('open')) {
439 439 collapseScmEntry(id);
440 440 el.addClass('collapsed');
441 441 return false;
442 442 } else if (el.hasClass('loaded')) {
443 443 expandScmEntry(id);
444 444 el.removeClass('collapsed');
445 445 return false;
446 446 }
447 447 if (el.hasClass('loading')) {
448 448 return false;
449 449 }
450 450 el.addClass('loading');
451 451 $.ajax({
452 452 url: url,
453 453 success: function(data) {
454 454 el.after(data);
455 455 el.addClass('open').addClass('loaded').removeClass('loading');
456 456 }
457 457 });
458 458 return true;
459 459 }
460 460
461 461 function randomKey(size) {
462 462 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
463 463 var key = '';
464 464 for (var i = 0; i < size; i++) {
465 465 key += chars.charAt(Math.floor(Math.random() * chars.length));
466 466 }
467 467 return key;
468 468 }
469 469
470 470 function updateIssueFrom(url) {
471 471 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
472 472 $(this).data('valuebeforeupdate', $(this).val());
473 473 });
474 474 $.ajax({
475 475 url: url,
476 476 type: 'post',
477 477 data: $('#issue-form').serialize()
478 478 });
479 479 }
480 480
481 481 function replaceIssueFormWith(html){
482 482 var replacement = $(html);
483 483 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
484 484 var object_id = $(this).attr('id');
485 485 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
486 486 replacement.find('#'+object_id).val($(this).val());
487 487 }
488 488 });
489 489 $('#all_attributes').empty();
490 490 $('#all_attributes').prepend(replacement);
491 491 }
492 492
493 493 function updateBulkEditFrom(url) {
494 494 $.ajax({
495 495 url: url,
496 496 type: 'post',
497 497 data: $('#bulk_edit_form').serialize()
498 498 });
499 499 }
500 500
501 501 function observeAutocompleteField(fieldId, url, options) {
502 502 $(document).ready(function() {
503 503 $('#'+fieldId).autocomplete($.extend({
504 504 source: url,
505 505 minLength: 2,
506 506 search: function(){$('#'+fieldId).addClass('ajax-loading');},
507 507 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
508 508 }, options));
509 509 $('#'+fieldId).addClass('autocomplete');
510 510 });
511 511 }
512 512
513 513 function observeSearchfield(fieldId, targetId, url) {
514 514 $('#'+fieldId).each(function() {
515 515 var $this = $(this);
516 516 $this.addClass('autocomplete');
517 517 $this.attr('data-value-was', $this.val());
518 518 var check = function() {
519 519 var val = $this.val();
520 520 if ($this.attr('data-value-was') != val){
521 521 $this.attr('data-value-was', val);
522 522 $.ajax({
523 523 url: url,
524 524 type: 'get',
525 525 data: {q: $this.val()},
526 526 success: function(data){ if(targetId) $('#'+targetId).html(data); },
527 527 beforeSend: function(){ $this.addClass('ajax-loading'); },
528 528 complete: function(){ $this.removeClass('ajax-loading'); }
529 529 });
530 530 }
531 531 };
532 532 var reset = function() {
533 533 if (timer) {
534 534 clearInterval(timer);
535 535 timer = setInterval(check, 300);
536 536 }
537 537 };
538 538 var timer = setInterval(check, 300);
539 539 $this.bind('keyup click mousemove', reset);
540 540 });
541 541 }
542 542
543 543 function beforeShowDatePicker(input, inst) {
544 544 var default_date = null;
545 545 switch ($(input).attr("id")) {
546 546 case "issue_start_date" :
547 547 if ($("#issue_due_date").size() > 0) {
548 548 default_date = $("#issue_due_date").val();
549 549 }
550 550 break;
551 551 case "issue_due_date" :
552 552 if ($("#issue_start_date").size() > 0) {
553 553 default_date = $("#issue_start_date").val();
554 554 }
555 555 break;
556 556 }
557 557 $(input).datepicker("option", "defaultDate", default_date);
558 558 }
559 559
560 560 function initMyPageSortable(list, url) {
561 561 $('#list-'+list).sortable({
562 562 connectWith: '.block-receiver',
563 563 tolerance: 'pointer',
564 564 update: function(){
565 565 $.ajax({
566 566 url: url,
567 567 type: 'post',
568 568 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
569 569 });
570 570 }
571 571 });
572 572 $("#list-top, #list-left, #list-right").disableSelection();
573 573 }
574 574
575 575 var warnLeavingUnsavedMessage;
576 576 function warnLeavingUnsaved(message) {
577 577 warnLeavingUnsavedMessage = message;
578 578 $(document).on('submit', 'form', function(){
579 579 $('textarea').removeData('changed');
580 580 });
581 581 $(document).on('change', 'textarea', function(){
582 582 $(this).data('changed', 'changed');
583 583 });
584 584 window.onbeforeunload = function(){
585 585 var warn = false;
586 586 $('textarea').blur().each(function(){
587 587 if ($(this).data('changed')) {
588 588 warn = true;
589 589 }
590 590 });
591 591 if (warn) {return warnLeavingUnsavedMessage;}
592 592 };
593 593 }
594 594
595 595 function setupAjaxIndicator() {
596 596 $(document).bind('ajaxSend', function(event, xhr, settings) {
597 597 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
598 598 $('#ajax-indicator').show();
599 599 }
600 600 });
601 601 $(document).bind('ajaxStop', function() {
602 602 $('#ajax-indicator').hide();
603 603 });
604 604 }
605 605
606 606 function hideOnLoad() {
607 607 $('.hol').hide();
608 608 }
609 609
610 610 function addFormObserversForDoubleSubmit() {
611 611 $('form[method=post]').each(function() {
612 612 if (!$(this).hasClass('multiple-submit')) {
613 613 $(this).submit(function(form_submission) {
614 614 if ($(form_submission.target).attr('data-submitted')) {
615 615 form_submission.preventDefault();
616 616 } else {
617 617 $(form_submission.target).attr('data-submitted', true);
618 618 }
619 619 });
620 620 }
621 621 });
622 622 }
623 623
624 624 function defaultFocus(){
625 625 if ($('#content :focus').length == 0) {
626 626 $('#content input[type=text], #content textarea').first().focus();
627 627 }
628 628 }
629 629
630 630 function blockEventPropagation(event) {
631 631 event.stopPropagation();
632 632 event.preventDefault();
633 633 }
634 634
635 635 function toggleDisabledOnChange() {
636 636 var checked = $(this).is(':checked');
637 637 $($(this).data('disables')).attr('disabled', checked);
638 638 $($(this).data('enables')).attr('disabled', !checked);
639 639 }
640 640 function toggleDisabledInit() {
641 641 $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
642 642 }
643 643 $(document).ready(function(){
644 644 $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
645 645 toggleDisabledInit();
646 646 });
647 647
648 648 $(document).ready(setupAjaxIndicator);
649 649 $(document).ready(hideOnLoad);
650 650 $(document).ready(addFormObserversForDoubleSubmit);
651 651 $(document).ready(defaultFocus);
652 652
@@ -1,1220 +1,1222
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 5 #content h1, h2, h3, h4 {color: #555;}
6 6 h2, .wiki h1 {font-size: 20px;}
7 7 h3, .wiki h2 {font-size: 16px;}
8 8 h4, .wiki h3 {font-size: 13px;}
9 9 h4 {border-bottom: 1px dotted #bbb;}
10 10
11 11 /***** Layout *****/
12 12 #wrapper {background: white;}
13 13
14 14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
15 15 #top-menu ul {margin: 0; padding: 0;}
16 16 #top-menu li {
17 17 float:left;
18 18 list-style-type:none;
19 19 margin: 0px 0px 0px 0px;
20 20 padding: 0px 0px 0px 0px;
21 21 white-space:nowrap;
22 22 }
23 23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
24 24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
25 25
26 26 #account {float:right;}
27 27
28 28 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
29 29 #header a {color:#f8f8f8;}
30 30 #header h1 a.ancestor { font-size: 80%; }
31 31 #quick-search {float:right;}
32 32
33 33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
34 34 #main-menu ul {margin: 0; padding: 0;}
35 35 #main-menu li {
36 36 float:left;
37 37 list-style-type:none;
38 38 margin: 0px 2px 0px 0px;
39 39 padding: 0px 0px 0px 0px;
40 40 white-space:nowrap;
41 41 }
42 42 #main-menu li a {
43 43 display: block;
44 44 color: #fff;
45 45 text-decoration: none;
46 46 font-weight: bold;
47 47 margin: 0;
48 48 padding: 4px 10px 4px 10px;
49 49 }
50 50 #main-menu li a:hover {background:#759FCF; color:#fff;}
51 51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
52 52
53 53 #admin-menu ul {margin: 0; padding: 0;}
54 54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
55 55
56 56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
57 57 #admin-menu a.projects { background-image: url(../images/projects.png); }
58 58 #admin-menu a.users { background-image: url(../images/user.png); }
59 59 #admin-menu a.groups { background-image: url(../images/group.png); }
60 60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
61 61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
62 62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
63 63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
64 64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
65 65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
66 66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
67 67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
68 68 #admin-menu a.info { background-image: url(../images/help.png); }
69 69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
70 70
71 71 #main {background-color:#EEEEEE;}
72 72
73 73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
74 74 * html #sidebar{ width: 22%; }
75 75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
76 76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
77 77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
78 78 #sidebar .contextual { margin-right: 1em; }
79 79 #sidebar ul {margin: 0; padding: 0;}
80 80 #sidebar ul li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
81 81
82 82 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
83 83 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
84 84 html>body #content { min-height: 600px; }
85 85 * html body #content { height: 600px; } /* IE */
86 86
87 87 #main.nosidebar #sidebar{ display: none; }
88 88 #main.nosidebar #content{ width: auto; border-right: 0; }
89 89
90 90 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
91 91
92 92 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
93 93 #login-form table td {padding: 6px;}
94 94 #login-form label {font-weight: bold;}
95 95 #login-form input#username, #login-form input#password { width: 300px; }
96 96
97 97 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
98 98 div.modal h3.title {display:none;}
99 99 div.modal p.buttons {text-align:right; margin-bottom:0;}
100 100 div.modal .box p {margin: 0.3em 0;}
101 101
102 102 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
103 103
104 104 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
105 105
106 106 /***** Links *****/
107 107 a, a:link, a:visited{ color: #169; text-decoration: none; }
108 108 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
109 109 a img{ border: 0; }
110 110
111 111 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
112 112 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
113 113 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
114 114
115 115 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
116 116 #sidebar a.selected:hover {text-decoration:none;}
117 117 #admin-menu a {line-height:1.7em;}
118 118 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
119 119
120 120 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
121 121 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
122 122
123 123 a#toggle-completed-versions {color:#999;}
124 124 /***** Tables *****/
125 125 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
126 126 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
127 127 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
128 128 table.list td.id { width: 2%; text-align: center;}
129 129 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
130 130 table.list td.tick {width:15%}
131 131 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
132 132 table.list td.checkbox input {padding:0px;}
133 133 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
134 134 table.list td.buttons a { padding-right: 0.6em; }
135 table.list td.buttons img {vertical-align:middle;}
135 136 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
136 137 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
137 138
138 139 tr.project td.name a { white-space:nowrap; }
139 140 tr.project.closed, tr.project.archived { color: #aaa; }
140 141 tr.project.closed a, tr.project.archived a { color: #aaa; }
141 142
142 143 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
143 144 tr.project.idnt-1 td.name {padding-left: 0.5em;}
144 145 tr.project.idnt-2 td.name {padding-left: 2em;}
145 146 tr.project.idnt-3 td.name {padding-left: 3.5em;}
146 147 tr.project.idnt-4 td.name {padding-left: 5em;}
147 148 tr.project.idnt-5 td.name {padding-left: 6.5em;}
148 149 tr.project.idnt-6 td.name {padding-left: 8em;}
149 150 tr.project.idnt-7 td.name {padding-left: 9.5em;}
150 151 tr.project.idnt-8 td.name {padding-left: 11em;}
151 152 tr.project.idnt-9 td.name {padding-left: 12.5em;}
152 153
153 154 tr.issue { text-align: center; white-space: nowrap; }
154 155 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
155 156 tr.issue td.relations { text-align: left; }
156 157 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
157 158 tr.issue td.relations span {white-space: nowrap;}
158 159 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
159 160 table.issues td.description pre {white-space:normal;}
160 161
161 162 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
162 163 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
163 164 tr.issue.idnt-2 td.subject {padding-left: 2em;}
164 165 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
165 166 tr.issue.idnt-4 td.subject {padding-left: 5em;}
166 167 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
167 168 tr.issue.idnt-6 td.subject {padding-left: 8em;}
168 169 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
169 170 tr.issue.idnt-8 td.subject {padding-left: 11em;}
170 171 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
171 172
172 173 table.issue-report {table-layout:fixed;}
173 174
174 175 tr.entry { border: 1px solid #f8f8f8; }
175 176 tr.entry td { white-space: nowrap; }
176 177 tr.entry td.filename {width:30%; text-align:left;}
177 178 tr.entry td.filename_no_report {width:70%; text-align:left;}
178 179 tr.entry td.size { text-align: right; font-size: 90%; }
179 180 tr.entry td.revision, tr.entry td.author { text-align: center; }
180 181 tr.entry td.age { text-align: right; }
181 182 tr.entry.file td.filename a { margin-left: 16px; }
182 183 tr.entry.file td.filename_no_report a { margin-left: 16px; }
183 184
184 185 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
185 186 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
186 187
187 188 tr.changeset { height: 20px }
188 189 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
189 190 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
190 191 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
191 192 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
192 193
193 194 table.files tbody th {text-align:left;}
194 195 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
195 196 table.files tr.file td.digest { font-size: 80%; }
196 197
197 198 table.members td.roles, table.memberships td.roles { width: 45%; }
198 199
199 200 tr.message { height: 2.6em; }
200 201 tr.message td.subject { padding-left: 20px; }
201 202 tr.message td.created_on { white-space: nowrap; }
202 203 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
203 204 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
204 205 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
205 206
206 207 tr.version.closed, tr.version.closed a { color: #999; }
207 208 tr.version td.name { padding-left: 20px; }
208 209 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
209 210 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
210 211
211 212 tr.user td {width:13%;white-space: nowrap;}
212 tr.user td.username, tr.user td.firstname, tr.user td.lastname, tr.user td.email {text-align:left;}
213 td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
213 214 tr.user td.email { width:18%; }
214 215 tr.user.locked, tr.user.registered { color: #aaa; }
215 216 tr.user.locked a, tr.user.registered a { color: #aaa; }
216 217
217 218 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
218 219
219 220 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
220 221
221 222 tr.time-entry { text-align: center; white-space: nowrap; }
222 223 tr.time-entry td.issue, tr.time-entry td.comments { text-align: left; white-space: normal; }
223 224 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
224 225 td.hours .hours-dec { font-size: 0.9em; }
225 226
226 227 table.plugins td { vertical-align: middle; }
227 228 table.plugins td.configure { text-align: right; padding-right: 1em; }
228 229 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
229 230 table.plugins span.description { display: block; font-size: 0.9em; }
230 231 table.plugins span.url { display: block; font-size: 0.9em; }
231 232
232 233 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; text-align:left; }
233 234 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
234 235 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
235 236 tr.group:hover a.toggle-all { display:inline;}
236 237 a.toggle-all:hover {text-decoration:none;}
237 238
238 239 table.list tbody tr:hover { background-color:#ffffdd; }
239 240 table.list tbody tr.group:hover { background-color:inherit; }
240 241 table td {padding:2px;}
241 242 table p {margin:0;}
242 243 .odd {background-color:#f6f7f8;}
243 244 .even {background-color: #fff;}
244 245
245 246 tr.builtin td.name {font-style:italic;}
246 247
247 248 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
248 249 a.sort.asc { background-image: url(../images/sort_asc.png); }
249 250 a.sort.desc { background-image: url(../images/sort_desc.png); }
250 251
251 252 table.attributes { width: 100% }
252 253 table.attributes th { vertical-align: top; text-align: left; }
253 254 table.attributes td { vertical-align: top; }
254 255
255 256 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
256 257 table.boards td.last-message {text-align:left;font-size:80%;}
257 258
258 259 table.messages td.last_message {text-align:left;}
259 260
260 261 #query_form_content {font-size:90%;}
261 262
262 263 table.query-columns {
263 264 border-collapse: collapse;
264 265 border: 0;
265 266 }
266 267
267 268 table.query-columns td.buttons {
268 269 vertical-align: middle;
269 270 text-align: center;
270 271 }
271 272 table.query-columns td.buttons input[type=button] {width:35px;}
272 273
273 274 td.center {text-align:center;}
274 275
275 276 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
276 277
277 278 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
278 279 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
279 280 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
280 281 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
281 282
282 283 #watchers select {width: 95%; display: block;}
283 284 #watchers a.delete {opacity: 0.4; vertical-align: middle;}
284 285 #watchers a.delete:hover {opacity: 1;}
285 286 #watchers img.gravatar {margin: 0 4px 2px 0;}
286 287
287 288 span#watchers_inputs {overflow:auto; display:block;}
288 289 span.search_for_watchers {display:block;}
289 290 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
290 291 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
291 292
292 293
293 294 .highlight { background-color: #FCFD8D;}
294 295 .highlight.token-1 { background-color: #faa;}
295 296 .highlight.token-2 { background-color: #afa;}
296 297 .highlight.token-3 { background-color: #aaf;}
297 298
298 299 .box{
299 300 padding:6px;
300 301 margin-bottom: 10px;
301 302 background-color:#f6f6f6;
302 303 color:#505050;
303 304 line-height:1.5em;
304 305 border: 1px solid #e4e4e4;
305 306 word-wrap: break-word;
306 307 }
307 308
308 309 div.square {
309 310 border: 1px solid #999;
310 311 float: left;
311 312 margin: .3em .4em 0 .4em;
312 313 overflow: hidden;
313 314 width: .6em; height: .6em;
314 315 }
315 316 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
316 317 .contextual input, .contextual select {font-size:0.9em;}
317 318 .message .contextual { margin-top: 0; }
318 319
319 320 .splitcontent {overflow:auto;}
320 321 .splitcontentleft{float:left; width:49%;}
321 322 .splitcontentright{float:right; width:49%;}
322 323 form {display: inline;}
323 324 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
324 325 fieldset {border: 1px solid #e4e4e4; margin:0;}
325 326 legend {color: #484848;}
326 327 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
327 328 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
328 329 blockquote blockquote { margin-left: 0;}
329 330 abbr { border-bottom: 1px dotted; cursor: help; }
330 331 textarea.wiki-edit {width:99%; resize:vertical;}
331 332 li p {margin-top: 0;}
332 333 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
333 334 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
334 335 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
335 336 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
336 337
337 338 div.issue div.subject div div { padding-left: 16px; }
338 339 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
339 340 div.issue div.subject>div>p { margin-top: 0.5em; }
340 341 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
341 342 div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
342 343 div.issue .next-prev-links {color:#999;}
343 344 div.issue table.attributes th {width:22%;}
344 345 div.issue table.attributes td {width:28%;}
345 346
346 347 #issue_tree table.issues, #relations table.issues { border: 0; }
347 348 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
348 349 #relations td.buttons {padding:0;}
349 350
350 351 fieldset.collapsible {border-width: 1px 0 0 0;}
351 352 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
352 353 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
353 354
354 355 fieldset#date-range p { margin: 2px 0 2px 0; }
355 356 fieldset#filters table { border-collapse: collapse; }
356 357 fieldset#filters table td { padding: 0; vertical-align: middle; }
357 358 fieldset#filters tr.filter { height: 2.1em; }
358 359 fieldset#filters td.field { width:230px; }
359 360 fieldset#filters td.operator { width:180px; }
360 361 fieldset#filters td.operator select {max-width:170px;}
361 362 fieldset#filters td.values { white-space:nowrap; }
362 363 fieldset#filters td.values select {min-width:130px;}
363 364 fieldset#filters td.values input {height:1em;}
364 365 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
365 366
366 367 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
367 368 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
368 369
369 370 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
370 371 div#issue-changesets div.changeset { padding: 4px;}
371 372 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
372 373 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
373 374
374 375 .journal ul.details img {margin:0 0 -3px 4px;}
375 376 div.journal {overflow:auto;}
376 377 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
377 378
378 379 div#activity dl, #search-results { margin-left: 2em; }
379 380 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
380 381 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
381 382 div#activity dt.me .time { border-bottom: 1px solid #999; }
382 383 div#activity dt .time { color: #777; font-size: 80%; }
383 384 div#activity dd .description, #search-results dd .description { font-style: italic; }
384 385 div#activity span.project:after, #search-results span.project:after { content: " -"; }
385 386 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
386 387 div#activity dt.grouped {margin-left:5em;}
387 388 div#activity dd.grouped {margin-left:9em;}
388 389
389 390 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
390 391
391 392 div#search-results-counts {float:right;}
392 393 div#search-results-counts ul { margin-top: 0.5em; }
393 394 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
394 395
395 396 dt.issue { background-image: url(../images/ticket.png); }
396 397 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
397 398 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
398 399 dt.issue-note { background-image: url(../images/ticket_note.png); }
399 400 dt.changeset { background-image: url(../images/changeset.png); }
400 401 dt.news { background-image: url(../images/news.png); }
401 402 dt.message { background-image: url(../images/message.png); }
402 403 dt.reply { background-image: url(../images/comments.png); }
403 404 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
404 405 dt.attachment { background-image: url(../images/attachment.png); }
405 406 dt.document { background-image: url(../images/document.png); }
406 407 dt.project { background-image: url(../images/projects.png); }
407 408 dt.time-entry { background-image: url(../images/time.png); }
408 409
409 410 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
410 411
411 412 div#roadmap .related-issues { margin-bottom: 1em; }
412 413 div#roadmap .related-issues td.checkbox { display: none; }
413 414 div#roadmap .wiki h1:first-child { display: none; }
414 415 div#roadmap .wiki h1 { font-size: 120%; }
415 416 div#roadmap .wiki h2 { font-size: 110%; }
416 417 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
417 418
418 419 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
419 420 div#version-summary fieldset { margin-bottom: 1em; }
420 421 div#version-summary fieldset.time-tracking table { width:100%; }
421 422 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
422 423
423 424 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
424 425 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
425 426 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
426 427 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
427 428 table#time-report .hours-dec { font-size: 0.9em; }
428 429
429 430 div.wiki-page .contextual a {opacity: 0.4}
430 431 div.wiki-page .contextual a:hover {opacity: 1}
431 432
432 433 form .attributes select { width: 60%; }
433 434 input#issue_subject { width: 99%; }
434 435 select#issue_done_ratio { width: 95px; }
435 436
436 437 ul.projects {margin:0; padding-left:1em;}
437 438 ul.projects ul {padding-left:1.6em;}
438 439 ul.projects.root {margin:0; padding:0;}
439 440 ul.projects li {list-style-type:none;}
440 441
441 442 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
442 443 #projects-index ul.projects li.root {margin-bottom: 1em;}
443 444 #projects-index ul.projects li.child {margin-top: 1em;}
444 445 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
445 446 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
446 447
447 448 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
448 449
449 450 #related-issues li img {vertical-align:middle;}
450 451
451 452 ul.properties {padding:0; font-size: 0.9em; color: #777;}
452 453 ul.properties li {list-style-type:none;}
453 454 ul.properties li span {font-style:italic;}
454 455
455 456 .total-hours { font-size: 110%; font-weight: bold; }
456 457 .total-hours span.hours-int { font-size: 120%; }
457 458
458 459 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
459 460 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
460 461
461 462 #workflow_copy_form select { width: 200px; }
462 463 table.transitions td.enabled {background: #bfb;}
463 464 #workflow_form table select {font-size:90%; max-width:100px;}
464 465 table.fields_permissions td.readonly {background:#ddd;}
465 466 table.fields_permissions td.required {background:#d88;}
466 467
467 468 select.expandable {vertical-align:top;}
468 469
469 470 textarea#custom_field_possible_values {width: 95%; resize:vertical}
470 471 textarea#custom_field_default_value {width: 95%; resize:vertical}
471 472
472 473 input#content_comments {width: 99%}
473 474
474 475 p.pagination {margin-top:8px; font-size: 90%}
475 476
476 477 #search-form fieldset p {margin:0.2em 0;}
477 478
478 479 /***** Tabular forms ******/
479 480 .tabular p{
480 481 margin: 0;
481 482 padding: 3px 0 3px 0;
482 483 padding-left: 180px; /* width of left column containing the label elements */
483 484 min-height: 1.8em;
484 485 clear:left;
485 486 }
486 487
487 488 html>body .tabular p {overflow:hidden;}
488 489
489 490 .tabular input, .tabular select {max-width:95%}
490 491 .tabular textarea {width:95%; resize:vertical;}
491 492 .tabular span[title] {border-bottom:1px dotted #aaa;}
492 493
493 494 .tabular label{
494 495 font-weight: bold;
495 496 float: left;
496 497 text-align: right;
497 498 /* width of left column */
498 499 margin-left: -180px;
499 500 /* width of labels. Should be smaller than left column to create some right margin */
500 501 width: 175px;
501 502 }
502 503
503 504 .tabular label.floating{
504 505 font-weight: normal;
505 506 margin-left: 0px;
506 507 text-align: left;
507 508 width: 270px;
508 509 }
509 510
510 511 .tabular label.block{
511 512 font-weight: normal;
512 513 margin-left: 0px !important;
513 514 text-align: left;
514 515 float: none;
515 516 display: block;
516 517 width: auto !important;
517 518 }
518 519
519 520 .tabular label.inline{
520 521 font-weight: normal;
521 522 float:none;
522 523 margin-left: 5px !important;
523 524 width: auto;
524 525 }
525 526
526 527 label.no-css {
527 528 font-weight: inherit;
528 529 float:none;
529 530 text-align:left;
530 531 margin-left:0px;
531 532 width:auto;
532 533 }
533 534 input#time_entry_comments { width: 90%;}
534 535
535 536 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
536 537
537 538 .tabular.settings p{ padding-left: 300px; }
538 539 .tabular.settings label{ margin-left: -300px; width: 295px; }
539 540 .tabular.settings textarea { width: 99%; }
540 541
541 542 .settings.enabled_scm table {width:100%}
542 543 .settings.enabled_scm td.scm_name{ font-weight: bold; }
543 544
544 545 fieldset.settings label { display: block; }
545 546 fieldset#notified_events .parent { padding-left: 20px; }
546 547
547 548 span.required {color: #bb0000;}
548 549 .summary {font-style: italic;}
549 550
550 551 .check_box_group {
551 552 display:block;
552 553 width:95%;
553 554 max-height:300px;
554 555 overflow-y:auto;
555 556 padding:2px 4px 4px 2px;
556 557 background:#fff;
557 558 border:1px solid #9EB1C2;
558 559 border-radius:2px
559 560 }
560 561 .check_box_group label {
561 562 font-weight: normal;
562 563 margin-left: 0px !important;
563 564 text-align: left;
564 565 float: none;
565 566 display: block;
566 567 width: auto;
567 568 }
568 569 .check_box_group.bool_cf {border:0; background:inherit;}
569 570 .check_box_group.bool_cf label {display: inline;}
570 571
571 572 #attachments_fields input.description {margin-left:4px; width:340px;}
572 573 #attachments_fields span {display:block; white-space:nowrap;}
573 574 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
574 575 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
575 576 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
576 577 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
577 578 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
578 579 a.remove-upload:hover {text-decoration:none !important;}
579 580
580 581 div.fileover { background-color: lavender; }
581 582
582 583 div.attachments { margin-top: 12px; }
583 584 div.attachments p { margin:4px 0 2px 0; }
584 585 div.attachments img { vertical-align: middle; }
585 586 div.attachments span.author { font-size: 0.9em; color: #888; }
586 587
587 588 div.thumbnails {margin-top:0.6em;}
588 589 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
589 590 div.thumbnails img {margin: 3px;}
590 591
591 592 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
592 593 .other-formats span + span:before { content: "| "; }
593 594
594 595 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
595 596
596 597 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
597 598 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
598 599
599 600 textarea.text_cf {width:95%; resize:vertical;}
600 601 input.string_cf, input.link_cf {width:95%;}
601 602 select.bool_cf {width:auto !important;}
602 603
603 604 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
604 605
605 606 #tab-content-users .splitcontentleft {width: 64%;}
606 607 #tab-content-users .splitcontentright {width: 34%;}
607 608 #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
608 609 #tab-content-users fieldset legend {font-weight: bold;}
609 610 #tab-content-users fieldset label {display: block;}
610 611 #tab-content-users #principals {max-height: 400px; overflow: auto;}
611 612
612 613 #users_for_watcher {height: 200px; overflow:auto;}
613 614 #users_for_watcher label {display: block;}
614 615
615 616 table.members td.name {padding-left: 20px;}
616 617 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
617 618
618 619 input#principal_search, input#user_search {width:90%}
619 620
620 621 input.autocomplete {
621 622 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
622 623 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
623 624 }
624 625 input.autocomplete.ajax-loading {
625 626 background-image: url(../images/loading.gif);
626 627 }
627 628
628 629 .role-visibility {padding-left:2em;}
629 630
630 631 .objects-selection {
631 632 height: 300px;
632 633 overflow: auto;
633 634 }
634 635
635 636 .objects-selection label {
636 637 display: block;
637 638 }
638 639
639 640 .objects-selection>div {
640 641 column-count: auto;
641 642 column-width: 200px;
642 643 -webkit-column-count: auto;
643 644 -webkit-column-width: 200px;
644 645 -webkit-column-gap : 0.5rem;
645 646 -webkit-column-rule: 1px solid #ccc;
646 647 -moz-column-count: auto;
647 648 -moz-column-width: 200px;
648 649 -moz-column-gap : 0.5rem;
649 650 -moz-column-rule: 1px solid #ccc;
650 651 }
651 652
652 653 /***** Flash & error messages ****/
653 654 #errorExplanation, div.flash, .nodata, .warning, .conflict {
654 655 padding: 4px 4px 4px 30px;
655 656 margin-bottom: 12px;
656 657 font-size: 1.1em;
657 658 border: 2px solid;
658 659 }
659 660
660 661 div.flash {margin-top: 8px;}
661 662
662 663 div.flash.error, #errorExplanation {
663 664 background: url(../images/exclamation.png) 8px 50% no-repeat;
664 665 background-color: #ffe3e3;
665 666 border-color: #dd0000;
666 667 color: #880000;
667 668 }
668 669
669 670 div.flash.notice {
670 671 background: url(../images/true.png) 8px 5px no-repeat;
671 672 background-color: #dfffdf;
672 673 border-color: #9fcf9f;
673 674 color: #005f00;
674 675 }
675 676
676 677 div.flash.warning, .conflict {
677 678 background: url(../images/warning.png) 8px 5px no-repeat;
678 679 background-color: #FFEBC1;
679 680 border-color: #FDBF3B;
680 681 color: #A6750C;
681 682 text-align: left;
682 683 }
683 684
684 685 .nodata, .warning {
685 686 text-align: center;
686 687 background-color: #FFEBC1;
687 688 border-color: #FDBF3B;
688 689 color: #A6750C;
689 690 }
690 691
691 692 #errorExplanation ul { font-size: 0.9em;}
692 693 #errorExplanation h2, #errorExplanation p { display: none; }
693 694
694 695 .conflict-details {font-size:80%;}
695 696
696 697 /***** Ajax indicator ******/
697 698 #ajax-indicator {
698 699 position: absolute; /* fixed not supported by IE */
699 700 background-color:#eee;
700 701 border: 1px solid #bbb;
701 702 top:35%;
702 703 left:40%;
703 704 width:20%;
704 705 font-weight:bold;
705 706 text-align:center;
706 707 padding:0.6em;
707 708 z-index:100;
708 709 opacity: 0.5;
709 710 }
710 711
711 712 html>body #ajax-indicator { position: fixed; }
712 713
713 714 #ajax-indicator span {
714 715 background-position: 0% 40%;
715 716 background-repeat: no-repeat;
716 717 background-image: url(../images/loading.gif);
717 718 padding-left: 26px;
718 719 vertical-align: bottom;
719 720 }
720 721
721 722 /***** Calendar *****/
722 723 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
723 724 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
724 725 table.cal thead th.week-number {width: auto;}
725 726 table.cal tbody tr {height: 100px;}
726 727 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
727 728 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
728 729 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
729 730 table.cal td.odd p.day-num {color: #bbb;}
730 731 table.cal td.today {background:#ffffdd;}
731 732 table.cal td.today p.day-num {font-weight: bold;}
732 733 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
733 734 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
734 735 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
735 736 p.cal.legend span {display:block;}
736 737
737 738 /***** Tooltips ******/
738 739 .tooltip{position:relative;z-index:24;}
739 740 .tooltip:hover{z-index:25;color:#000;}
740 741 .tooltip span.tip{display: none; text-align:left;}
741 742
742 743 div.tooltip:hover span.tip{
743 744 display:block;
744 745 position:absolute;
745 746 top:12px; left:24px; width:270px;
746 747 border:1px solid #555;
747 748 background-color:#fff;
748 749 padding: 4px;
749 750 font-size: 0.8em;
750 751 color:#505050;
751 752 }
752 753
753 754 img.ui-datepicker-trigger {
754 755 cursor: pointer;
755 756 vertical-align: middle;
756 757 margin-left: 4px;
757 758 }
758 759
759 760 /***** Progress bar *****/
760 761 table.progress {
761 762 border-collapse: collapse;
762 763 border-spacing: 0pt;
763 764 empty-cells: show;
764 765 text-align: center;
765 766 float:left;
766 767 margin: 1px 6px 1px 0px;
767 768 }
768 769
769 770 table.progress td { height: 1em; }
770 771 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
771 772 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
772 773 table.progress td.todo { background: #eee none repeat scroll 0%; }
773 774 p.percent {font-size: 80%;}
774 775 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
775 776
776 777 #roadmap table.progress td { height: 1.2em; }
777 778 /***** Tabs *****/
778 779 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
779 780 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
780 781 #content .tabs ul li {
781 782 float:left;
782 783 list-style-type:none;
783 784 white-space:nowrap;
784 785 margin-right:4px;
785 786 background:#fff;
786 787 position:relative;
787 788 margin-bottom:-1px;
788 789 }
789 790 #content .tabs ul li a{
790 791 display:block;
791 792 font-size: 0.9em;
792 793 text-decoration:none;
793 794 line-height:1.3em;
794 795 padding:4px 6px 4px 6px;
795 796 border: 1px solid #ccc;
796 797 border-bottom: 1px solid #bbbbbb;
797 798 background-color: #f6f6f6;
798 799 color:#999;
799 800 font-weight:bold;
800 801 border-top-left-radius:3px;
801 802 border-top-right-radius:3px;
802 803 }
803 804
804 805 #content .tabs ul li a:hover {
805 806 background-color: #ffffdd;
806 807 text-decoration:none;
807 808 }
808 809
809 810 #content .tabs ul li a.selected {
810 811 background-color: #fff;
811 812 border: 1px solid #bbbbbb;
812 813 border-bottom: 1px solid #fff;
813 814 color:#444;
814 815 }
815 816
816 817 #content .tabs ul li a.selected:hover {background-color: #fff;}
817 818
818 819 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
819 820
820 821 button.tab-left, button.tab-right {
821 822 font-size: 0.9em;
822 823 cursor: pointer;
823 824 height:24px;
824 825 border: 1px solid #ccc;
825 826 border-bottom: 1px solid #bbbbbb;
826 827 position:absolute;
827 828 padding:4px;
828 829 width: 20px;
829 830 bottom: -1px;
830 831 }
831 832
832 833 button.tab-left {
833 834 right: 20px;
834 835 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
835 836 border-top-left-radius:3px;
836 837 }
837 838
838 839 button.tab-right {
839 840 right: 0;
840 841 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
841 842 border-top-right-radius:3px;
842 843 }
843 844
844 845 /***** Diff *****/
845 846 .diff_out { background: #fcc; }
846 847 .diff_out span { background: #faa; }
847 848 .diff_in { background: #cfc; }
848 849 .diff_in span { background: #afa; }
849 850
850 851 .text-diff {
851 852 padding: 1em;
852 853 background-color:#f6f6f6;
853 854 color:#505050;
854 855 border: 1px solid #e4e4e4;
855 856 }
856 857
857 858 /***** Wiki *****/
858 859 div.wiki table {
859 860 border-collapse: collapse;
860 861 margin-bottom: 1em;
861 862 }
862 863
863 864 div.wiki table, div.wiki td, div.wiki th {
864 865 border: 1px solid #bbb;
865 866 padding: 4px;
866 867 }
867 868
868 869 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
869 870
870 871 div.wiki .external {
871 872 background-position: 0% 60%;
872 873 background-repeat: no-repeat;
873 874 padding-left: 12px;
874 875 background-image: url(../images/external.png);
875 876 }
876 877
877 878 div.wiki a {word-wrap: break-word;}
878 879 div.wiki a.new {color: #b73535;}
879 880
880 881 div.wiki ul, div.wiki ol {margin-bottom:1em;}
881 882
882 883 div.wiki pre {
883 884 margin: 1em 1em 1em 1.6em;
884 885 padding: 8px;
885 886 background-color: #fafafa;
886 887 border: 1px solid #e2e2e2;
887 888 width:auto;
888 889 overflow-x: auto;
889 890 overflow-y: hidden;
890 891 }
891 892
892 893 div.wiki ul.toc {
893 894 background-color: #ffffdd;
894 895 border: 1px solid #e4e4e4;
895 896 padding: 4px;
896 897 line-height: 1.2em;
897 898 margin-bottom: 12px;
898 899 margin-right: 12px;
899 900 margin-left: 0;
900 901 display: table
901 902 }
902 903 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
903 904
904 905 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
905 906 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
906 907 div.wiki ul.toc ul { margin: 0; padding: 0; }
907 908 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
908 909 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
909 910 div.wiki ul.toc a {
910 911 font-size: 0.9em;
911 912 font-weight: normal;
912 913 text-decoration: none;
913 914 color: #606060;
914 915 }
915 916 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
916 917
917 918 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
918 919 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
919 920 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
920 921
921 922 div.wiki img {vertical-align:middle; max-width:100%;}
922 923
923 924 /***** My page layout *****/
924 925 .block-receiver {
925 926 border:1px dashed #c0c0c0;
926 927 margin-bottom: 20px;
927 928 padding: 15px 0 15px 0;
928 929 }
929 930
930 931 .mypage-box {
931 932 margin:0 0 20px 0;
932 933 color:#505050;
933 934 line-height:1.5em;
934 935 }
935 936
936 937 .handle {cursor: move;}
937 938
938 939 a.close-icon {
939 940 display:block;
940 941 margin-top:3px;
941 942 overflow:hidden;
942 943 width:12px;
943 944 height:12px;
944 945 background-repeat: no-repeat;
945 946 cursor:pointer;
946 947 background-image:url('../images/close.png');
947 948 }
948 949 a.close-icon:hover {background-image:url('../images/close_hl.png');}
949 950
950 951 /***** Gantt chart *****/
951 952 .gantt_hdr {
952 953 position:absolute;
953 954 top:0;
954 955 height:16px;
955 956 border-top: 1px solid #c0c0c0;
956 957 border-bottom: 1px solid #c0c0c0;
957 958 border-right: 1px solid #c0c0c0;
958 959 text-align: center;
959 960 overflow: hidden;
960 961 }
961 962
962 963 .gantt_hdr.nwday {background-color:#f1f1f1;}
963 964
964 965 .gantt_subjects { font-size: 0.8em; }
965 966 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
966 967
967 968 .task {
968 969 position: absolute;
969 970 height:8px;
970 971 font-size:0.8em;
971 972 color:#888;
972 973 padding:0;
973 974 margin:0;
974 975 line-height:16px;
975 976 white-space:nowrap;
976 977 }
977 978
978 979 .task.label {width:100%;}
979 980 .task.label.project, .task.label.version { font-weight: bold; }
980 981
981 982 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
982 983 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
983 984 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
984 985
985 986 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
986 987 .task_late.parent, .task_done.parent { height: 3px;}
987 988 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
988 989 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
989 990
990 991 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
991 992 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
992 993 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
993 994 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
994 995
995 996 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
996 997 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
997 998 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
998 999 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
999 1000
1000 1001 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1001 1002 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
1002 1003
1003 1004 /***** Icons *****/
1004 1005 .icon {
1005 1006 background-position: 0% 50%;
1006 1007 background-repeat: no-repeat;
1007 1008 padding-left: 20px;
1008 1009 padding-top: 2px;
1009 1010 padding-bottom: 3px;
1010 1011 }
1011 1012
1012 1013 .icon-add { background-image: url(../images/add.png); }
1013 1014 .icon-edit { background-image: url(../images/edit.png); }
1014 1015 .icon-copy { background-image: url(../images/copy.png); }
1015 1016 .icon-duplicate { background-image: url(../images/duplicate.png); }
1016 1017 .icon-del { background-image: url(../images/delete.png); }
1017 1018 .icon-move { background-image: url(../images/move.png); }
1018 1019 .icon-save { background-image: url(../images/save.png); }
1019 1020 .icon-cancel { background-image: url(../images/cancel.png); }
1020 1021 .icon-multiple { background-image: url(../images/table_multiple.png); }
1021 1022 .icon-folder { background-image: url(../images/folder.png); }
1022 1023 .open .icon-folder { background-image: url(../images/folder_open.png); }
1023 1024 .icon-package { background-image: url(../images/package.png); }
1024 1025 .icon-user { background-image: url(../images/user.png); }
1025 1026 .icon-projects { background-image: url(../images/projects.png); }
1026 1027 .icon-help { background-image: url(../images/help.png); }
1027 1028 .icon-attachment { background-image: url(../images/attachment.png); }
1028 1029 .icon-history { background-image: url(../images/history.png); }
1029 1030 .icon-time { background-image: url(../images/time.png); }
1030 1031 .icon-time-add { background-image: url(../images/time_add.png); }
1031 1032 .icon-stats { background-image: url(../images/stats.png); }
1032 1033 .icon-warning { background-image: url(../images/warning.png); }
1033 1034 .icon-fav { background-image: url(../images/fav.png); }
1034 1035 .icon-fav-off { background-image: url(../images/fav_off.png); }
1035 1036 .icon-reload { background-image: url(../images/reload.png); }
1036 1037 .icon-lock { background-image: url(../images/locked.png); }
1037 1038 .icon-unlock { background-image: url(../images/unlock.png); }
1038 1039 .icon-checked { background-image: url(../images/true.png); }
1039 1040 .icon-details { background-image: url(../images/zoom_in.png); }
1040 1041 .icon-report { background-image: url(../images/report.png); }
1041 1042 .icon-comment { background-image: url(../images/comment.png); }
1042 1043 .icon-summary { background-image: url(../images/lightning.png); }
1043 1044 .icon-server-authentication { background-image: url(../images/server_key.png); }
1044 1045 .icon-issue { background-image: url(../images/ticket.png); }
1045 1046 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1046 1047 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1047 1048 .icon-passwd { background-image: url(../images/textfield_key.png); }
1048 1049 .icon-test { background-image: url(../images/bullet_go.png); }
1050 .icon-email-add { background-image: url(../images/email_add.png); }
1049 1051
1050 1052 .icon-file { background-image: url(../images/files/default.png); }
1051 1053 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1052 1054 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1053 1055 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1054 1056 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1055 1057 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1056 1058 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1057 1059 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1058 1060 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1059 1061 .icon-file.text-css { background-image: url(../images/files/css.png); }
1060 1062 .icon-file.text-html { background-image: url(../images/files/html.png); }
1061 1063 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1062 1064 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1063 1065 .icon-file.image-png { background-image: url(../images/files/image.png); }
1064 1066 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1065 1067 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1066 1068 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1067 1069 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1068 1070
1069 1071 img.gravatar {
1070 1072 padding: 2px;
1071 1073 border: solid 1px #d5d5d5;
1072 1074 background: #fff;
1073 1075 vertical-align: middle;
1074 1076 }
1075 1077
1076 1078 div.issue img.gravatar {
1077 1079 float: left;
1078 1080 margin: 0 6px 0 0;
1079 1081 padding: 5px;
1080 1082 }
1081 1083
1082 1084 div.issue table img.gravatar {
1083 1085 height: 14px;
1084 1086 width: 14px;
1085 1087 padding: 2px;
1086 1088 float: left;
1087 1089 margin: 0 0.5em 0 0;
1088 1090 }
1089 1091
1090 1092 h2 img.gravatar {margin: -2px 4px -4px 0;}
1091 1093 h3 img.gravatar {margin: -4px 4px -4px 0;}
1092 1094 h4 img.gravatar {margin: -6px 4px -4px 0;}
1093 1095 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1094 1096 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1095 1097 /* Used on 12px Gravatar img tags without the icon background */
1096 1098 .icon-gravatar {float: left; margin-right: 4px;}
1097 1099
1098 1100 #activity dt, .journal {clear: left;}
1099 1101
1100 1102 .journal-link {float: right;}
1101 1103
1102 1104 h2 img { vertical-align:middle; }
1103 1105
1104 1106 .hascontextmenu { cursor: context-menu; }
1105 1107
1106 1108 /* Custom JQuery styles */
1107 1109 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1108 1110
1109 1111
1110 1112 /************* CodeRay styles *************/
1111 1113 .syntaxhl div {display: inline;}
1112 1114 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1113 1115 .syntaxhl .code pre { overflow: auto }
1114 1116 .syntaxhl .debug { color: white !important; background: blue !important; }
1115 1117
1116 1118 .syntaxhl .annotation { color:#007 }
1117 1119 .syntaxhl .attribute-name { color:#b48 }
1118 1120 .syntaxhl .attribute-value { color:#700 }
1119 1121 .syntaxhl .binary { color:#509 }
1120 1122 .syntaxhl .char .content { color:#D20 }
1121 1123 .syntaxhl .char .delimiter { color:#710 }
1122 1124 .syntaxhl .char { color:#D20 }
1123 1125 .syntaxhl .class { color:#258; font-weight:bold }
1124 1126 .syntaxhl .class-variable { color:#369 }
1125 1127 .syntaxhl .color { color:#0A0 }
1126 1128 .syntaxhl .comment { color:#385 }
1127 1129 .syntaxhl .comment .char { color:#385 }
1128 1130 .syntaxhl .comment .delimiter { color:#385 }
1129 1131 .syntaxhl .complex { color:#A08 }
1130 1132 .syntaxhl .constant { color:#258; font-weight:bold }
1131 1133 .syntaxhl .decorator { color:#B0B }
1132 1134 .syntaxhl .definition { color:#099; font-weight:bold }
1133 1135 .syntaxhl .delimiter { color:black }
1134 1136 .syntaxhl .directive { color:#088; font-weight:bold }
1135 1137 .syntaxhl .doc { color:#970 }
1136 1138 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1137 1139 .syntaxhl .doctype { color:#34b }
1138 1140 .syntaxhl .entity { color:#800; font-weight:bold }
1139 1141 .syntaxhl .error { color:#F00; background-color:#FAA }
1140 1142 .syntaxhl .escape { color:#666 }
1141 1143 .syntaxhl .exception { color:#C00; font-weight:bold }
1142 1144 .syntaxhl .float { color:#06D }
1143 1145 .syntaxhl .function { color:#06B; font-weight:bold }
1144 1146 .syntaxhl .global-variable { color:#d70 }
1145 1147 .syntaxhl .hex { color:#02b }
1146 1148 .syntaxhl .imaginary { color:#f00 }
1147 1149 .syntaxhl .include { color:#B44; font-weight:bold }
1148 1150 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1149 1151 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1150 1152 .syntaxhl .instance-variable { color:#33B }
1151 1153 .syntaxhl .integer { color:#06D }
1152 1154 .syntaxhl .key .char { color: #60f }
1153 1155 .syntaxhl .key .delimiter { color: #404 }
1154 1156 .syntaxhl .key { color: #606 }
1155 1157 .syntaxhl .keyword { color:#939; font-weight:bold }
1156 1158 .syntaxhl .label { color:#970; font-weight:bold }
1157 1159 .syntaxhl .local-variable { color:#963 }
1158 1160 .syntaxhl .namespace { color:#707; font-weight:bold }
1159 1161 .syntaxhl .octal { color:#40E }
1160 1162 .syntaxhl .operator { }
1161 1163 .syntaxhl .predefined { color:#369; font-weight:bold }
1162 1164 .syntaxhl .predefined-constant { color:#069 }
1163 1165 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1164 1166 .syntaxhl .preprocessor { color:#579 }
1165 1167 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1166 1168 .syntaxhl .regexp .content { color:#808 }
1167 1169 .syntaxhl .regexp .delimiter { color:#404 }
1168 1170 .syntaxhl .regexp .modifier { color:#C2C }
1169 1171 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1170 1172 .syntaxhl .reserved { color:#080; font-weight:bold }
1171 1173 .syntaxhl .shell .content { color:#2B2 }
1172 1174 .syntaxhl .shell .delimiter { color:#161 }
1173 1175 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1174 1176 .syntaxhl .string .char { color: #46a }
1175 1177 .syntaxhl .string .content { color: #46a }
1176 1178 .syntaxhl .string .delimiter { color: #46a }
1177 1179 .syntaxhl .string .modifier { color: #46a }
1178 1180 .syntaxhl .symbol .content { color:#d33 }
1179 1181 .syntaxhl .symbol .delimiter { color:#d33 }
1180 1182 .syntaxhl .symbol { color:#d33 }
1181 1183 .syntaxhl .tag { color:#070 }
1182 1184 .syntaxhl .type { color:#339; font-weight:bold }
1183 1185 .syntaxhl .value { color: #088; }
1184 1186 .syntaxhl .variable { color:#037 }
1185 1187
1186 1188 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1187 1189 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1188 1190 .syntaxhl .change { color: #bbf; background: #007; }
1189 1191 .syntaxhl .head { color: #f8f; background: #505 }
1190 1192 .syntaxhl .head .filename { color: white; }
1191 1193
1192 1194 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1193 1195 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1194 1196
1195 1197 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1196 1198 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1197 1199 .syntaxhl .change .change { color: #88f }
1198 1200 .syntaxhl .head .head { color: #f4f }
1199 1201
1200 1202 /***** Media print specific styles *****/
1201 1203 @media print {
1202 1204 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1203 1205 #main { background: #fff; }
1204 1206 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1205 1207 #wiki_add_attachment { display:none; }
1206 1208 .hide-when-print { display: none; }
1207 1209 .autoscroll {overflow-x: visible;}
1208 1210 table.list {margin-top:0.5em;}
1209 1211 table.list th, table.list td {border: 1px solid #aaa;}
1210 1212 }
1211 1213
1212 1214 /* Accessibility specific styles */
1213 1215 .hidden-for-sighted {
1214 1216 position:absolute;
1215 1217 left:-10000px;
1216 1218 top:auto;
1217 1219 width:1px;
1218 1220 height:1px;
1219 1221 overflow:hidden;
1220 1222 }
@@ -1,180 +1,171
1 1 ---
2 users_004:
3 created_on: 2006-07-19 19:34:07 +02:00
4 status: 1
5 last_login_on:
6 language: en
7 # password = foo
8 salt: 3126f764c3c5ac61cbfc103f25f934cf
9 hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b
10 updated_on: 2006-07-19 19:34:07 +02:00
11 admin: false
12 mail: rhill@somenet.foo
13 lastname: Hill
14 firstname: Robert
15 id: 4
16 auth_source_id:
17 mail_notification: all
18 login: rhill
19 type: User
20 2 users_001:
21 3 created_on: 2006-07-19 19:12:21 +02:00
22 4 status: 1
23 5 last_login_on: 2006-07-19 22:57:52 +02:00
24 6 language: en
25 7 # password = admin
26 8 salt: 82090c953c4a0000a7db253b0691a6b4
27 9 hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150
28 10 updated_on: 2006-07-19 22:57:52 +02:00
29 11 admin: true
30 mail: admin@somenet.foo
31 12 lastname: Admin
32 13 firstname: Redmine
33 14 id: 1
34 15 auth_source_id:
35 16 mail_notification: all
36 17 login: admin
37 18 type: User
38 19 users_002:
39 20 created_on: 2006-07-19 19:32:09 +02:00
40 21 status: 1
41 22 last_login_on: 2006-07-19 22:42:15 +02:00
42 23 language: en
43 24 # password = jsmith
44 25 salt: 67eb4732624d5a7753dcea7ce0bb7d7d
45 26 hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc
46 27 updated_on: 2006-07-19 22:42:15 +02:00
47 28 admin: false
48 mail: jsmith@somenet.foo
49 29 lastname: Smith
50 30 firstname: John
51 31 id: 2
52 32 auth_source_id:
53 33 mail_notification: all
54 34 login: jsmith
55 35 type: User
56 36 users_003:
57 37 created_on: 2006-07-19 19:33:19 +02:00
58 38 status: 1
59 39 last_login_on:
60 40 language: en
61 41 # password = foo
62 42 salt: 7599f9963ec07b5a3b55b354407120c0
63 43 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
64 44 updated_on: 2006-07-19 19:33:19 +02:00
65 45 admin: false
66 mail: dlopper@somenet.foo
67 46 lastname: Lopper
68 47 firstname: Dave
69 48 id: 3
70 49 auth_source_id:
71 50 mail_notification: all
72 51 login: dlopper
73 52 type: User
53 users_004:
54 created_on: 2006-07-19 19:34:07 +02:00
55 status: 1
56 last_login_on:
57 language: en
58 # password = foo
59 salt: 3126f764c3c5ac61cbfc103f25f934cf
60 hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b
61 updated_on: 2006-07-19 19:34:07 +02:00
62 admin: false
63 lastname: Hill
64 firstname: Robert
65 id: 4
66 auth_source_id:
67 mail_notification: all
68 login: rhill
69 type: User
74 70 users_005:
75 71 id: 5
76 72 created_on: 2006-07-19 19:33:19 +02:00
77 73 # Locked
78 74 status: 3
79 75 last_login_on:
80 76 language: en
81 77 hashed_password: 1
82 78 updated_on: 2006-07-19 19:33:19 +02:00
83 79 admin: false
84 mail: dlopper2@somenet.foo
85 80 lastname: Lopper2
86 81 firstname: Dave2
87 82 auth_source_id:
88 83 mail_notification: all
89 84 login: dlopper2
90 85 type: User
91 86 users_006:
92 87 id: 6
93 88 created_on: 2006-07-19 19:33:19 +02:00
94 89 status: 0
95 90 last_login_on:
96 91 language: ''
97 92 hashed_password: 1
98 93 updated_on: 2006-07-19 19:33:19 +02:00
99 94 admin: false
100 mail: ''
101 95 lastname: Anonymous
102 96 firstname: ''
103 97 auth_source_id:
104 98 mail_notification: only_my_events
105 99 login: ''
106 100 type: AnonymousUser
107 101 users_007:
108 102 # A user who does not belong to any project
109 103 id: 7
110 104 created_on: 2006-07-19 19:33:19 +02:00
111 105 status: 1
112 106 last_login_on:
113 107 language: 'en'
114 108 # password = foo
115 109 salt: 7599f9963ec07b5a3b55b354407120c0
116 110 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
117 111 updated_on: 2006-07-19 19:33:19 +02:00
118 112 admin: false
119 mail: someone@foo.bar
120 113 lastname: One
121 114 firstname: Some
122 115 auth_source_id:
123 116 mail_notification: only_my_events
124 117 login: someone
125 118 type: User
126 119 users_008:
127 120 id: 8
128 121 created_on: 2006-07-19 19:33:19 +02:00
129 122 status: 1
130 123 last_login_on:
131 124 language: 'it'
132 125 # password = foo
133 126 salt: 7599f9963ec07b5a3b55b354407120c0
134 127 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
135 128 updated_on: 2006-07-19 19:33:19 +02:00
136 129 admin: false
137 mail: miscuser8@foo.bar
138 130 lastname: Misc
139 131 firstname: User
140 132 auth_source_id:
141 133 mail_notification: only_my_events
142 134 login: miscuser8
143 135 type: User
144 136 users_009:
145 137 id: 9
146 138 created_on: 2006-07-19 19:33:19 +02:00
147 139 status: 1
148 140 last_login_on:
149 141 language: 'it'
150 142 hashed_password: 1
151 143 updated_on: 2006-07-19 19:33:19 +02:00
152 144 admin: false
153 mail: miscuser9@foo.bar
154 145 lastname: Misc
155 146 firstname: User
156 147 auth_source_id:
157 148 mail_notification: only_my_events
158 149 login: miscuser9
159 150 type: User
160 151 groups_010:
161 152 id: 10
162 153 lastname: A Team
163 154 type: Group
164 155 status: 1
165 156 groups_011:
166 157 id: 11
167 158 lastname: B Team
168 159 type: Group
169 160 status: 1
170 161 groups_non_member:
171 162 id: 12
172 163 lastname: Non member users
173 164 type: GroupNonMember
174 165 status: 1
175 166 groups_anonymous:
176 167 id: 13
177 168 lastname: Anonymous users
178 169 type: GroupAnonymous
179 170 status: 1
180 171
@@ -1,168 +1,168
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 AdminControllerTest < ActionController::TestCase
21 fixtures :projects, :users, :roles
21 fixtures :projects, :users, :email_addresses, :roles
22 22
23 23 def setup
24 24 User.current = nil
25 25 @request.session[:user_id] = 1 # admin
26 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_select 'div.nodata', 0
31 31 end
32 32
33 33 def test_index_with_no_configuration_data
34 34 delete_configuration_data
35 35 get :index
36 36 assert_select 'div.nodata'
37 37 end
38 38
39 39 def test_projects
40 40 get :projects
41 41 assert_response :success
42 42 assert_template 'projects'
43 43 assert_not_nil assigns(:projects)
44 44 # active projects only
45 45 assert_nil assigns(:projects).detect {|u| !u.active?}
46 46 end
47 47
48 48 def test_projects_with_status_filter
49 49 get :projects, :status => 1
50 50 assert_response :success
51 51 assert_template 'projects'
52 52 assert_not_nil assigns(:projects)
53 53 # active projects only
54 54 assert_nil assigns(:projects).detect {|u| !u.active?}
55 55 end
56 56
57 57 def test_projects_with_name_filter
58 58 get :projects, :name => 'store', :status => ''
59 59 assert_response :success
60 60 assert_template 'projects'
61 61 projects = assigns(:projects)
62 62 assert_not_nil projects
63 63 assert_equal 1, projects.size
64 64 assert_equal 'OnlineStore', projects.first.name
65 65 end
66 66
67 67 def test_load_default_configuration_data
68 68 delete_configuration_data
69 69 post :default_configuration, :lang => 'fr'
70 70 assert_response :redirect
71 71 assert_nil flash[:error]
72 72 assert IssueStatus.find_by_name('Nouveau')
73 73 end
74 74
75 75 def test_load_default_configuration_data_should_rescue_error
76 76 delete_configuration_data
77 77 Redmine::DefaultData::Loader.stubs(:load).raises(Exception.new("Something went wrong"))
78 78 post :default_configuration, :lang => 'fr'
79 79 assert_response :redirect
80 80 assert_not_nil flash[:error]
81 81 assert_match /Something went wrong/, flash[:error]
82 82 end
83 83
84 84 def test_test_email
85 85 user = User.find(1)
86 86 user.pref.no_self_notified = '1'
87 87 user.pref.save!
88 88 ActionMailer::Base.deliveries.clear
89 89
90 90 get :test_email
91 91 assert_redirected_to '/settings?tab=notifications'
92 92 mail = ActionMailer::Base.deliveries.last
93 93 assert_not_nil mail
94 94 user = User.find(1)
95 95 assert_equal [user.mail], mail.bcc
96 96 end
97 97
98 98 def test_test_email_failure_should_display_the_error
99 99 Mailer.stubs(:test_email).raises(Exception, 'Some error message')
100 100 get :test_email
101 101 assert_redirected_to '/settings?tab=notifications'
102 102 assert_match /Some error message/, flash[:error]
103 103 end
104 104
105 105 def test_no_plugins
106 106 Redmine::Plugin.stubs(:registered_plugins).returns({})
107 107
108 108 get :plugins
109 109 assert_response :success
110 110 assert_template 'plugins'
111 111 assert_equal [], assigns(:plugins)
112 112 end
113 113
114 114 def test_plugins
115 115 # Register a few plugins
116 116 Redmine::Plugin.register :foo do
117 117 name 'Foo plugin'
118 118 author 'John Smith'
119 119 description 'This is a test plugin'
120 120 version '0.0.1'
121 121 settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'foo/settings'
122 122 end
123 123 Redmine::Plugin.register :bar do
124 124 end
125 125
126 126 get :plugins
127 127 assert_response :success
128 128 assert_template 'plugins'
129 129
130 130 assert_select 'tr#plugin-foo' do
131 131 assert_select 'td span.name', :text => 'Foo plugin'
132 132 assert_select 'td.configure a[href="/settings/plugin/foo"]'
133 133 end
134 134 assert_select 'tr#plugin-bar' do
135 135 assert_select 'td span.name', :text => 'Bar'
136 136 assert_select 'td.configure a', 0
137 137 end
138 138 end
139 139
140 140 def test_info
141 141 get :info
142 142 assert_response :success
143 143 assert_template 'info'
144 144 end
145 145
146 146 def test_admin_menu_plugin_extension
147 147 Redmine::MenuManager.map :admin_menu do |menu|
148 148 menu.push :test_admin_menu_plugin_extension, '/foo/bar', :caption => 'Test'
149 149 end
150 150
151 151 get :index
152 152 assert_response :success
153 153 assert_select 'div#admin-menu a[href="/foo/bar"]', :text => 'Test'
154 154
155 155 Redmine::MenuManager.map :admin_menu do |menu|
156 156 menu.delete :test_admin_menu_plugin_extension
157 157 end
158 158 end
159 159
160 160 private
161 161
162 162 def delete_configuration_data
163 163 Role.delete_all('builtin = 0')
164 164 Tracker.delete_all
165 165 IssueStatus.delete_all
166 166 Enumeration.delete_all
167 167 end
168 168 end
@@ -1,185 +1,185
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 DocumentsControllerTest < ActionController::TestCase
21 fixtures :projects, :users, :roles, :members, :member_roles,
21 fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
22 22 :enabled_modules, :documents, :enumerations,
23 23 :groups_users, :attachments
24 24
25 25 def setup
26 26 User.current = nil
27 27 end
28 28
29 29 def test_index
30 30 # Sets a default category
31 31 e = Enumeration.find_by_name('Technical documentation')
32 32 e.update_attributes(:is_default => true)
33 33
34 34 get :index, :project_id => 'ecookbook'
35 35 assert_response :success
36 36 assert_template 'index'
37 37 assert_not_nil assigns(:grouped)
38 38
39 39 # Default category selected in the new document form
40 40 assert_select 'select[name=?]', 'document[category_id]' do
41 41 assert_select 'option[selected=selected]', :text => 'Technical documentation'
42 42
43 43 assert ! DocumentCategory.find(16).active?
44 44 assert_select 'option[value="16"]', 0
45 45 end
46 46 end
47 47
48 48 def test_index_grouped_by_date
49 49 get :index, :project_id => 'ecookbook', :sort_by => 'date'
50 50 assert_response :success
51 51 assert_select 'h3', :text => '2007-02-12'
52 52 end
53 53
54 54 def test_index_grouped_by_title
55 55 get :index, :project_id => 'ecookbook', :sort_by => 'title'
56 56 assert_response :success
57 57 assert_select 'h3', :text => 'T'
58 58 end
59 59
60 60 def test_index_grouped_by_author
61 61 get :index, :project_id => 'ecookbook', :sort_by => 'author'
62 62 assert_response :success
63 63 assert_select 'h3', :text => 'John Smith'
64 64 end
65 65
66 66 def test_index_with_long_description
67 67 # adds a long description to the first document
68 68 doc = documents(:documents_001)
69 69 doc.update_attributes(:description => <<LOREM)
70 70 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut egestas, mi vehicula varius varius, ipsum massa fermentum orci, eget tristique ante sem vel mi. Nulla facilisi. Donec enim libero, luctus ac sagittis sit amet, vehicula sagittis magna. Duis ultrices molestie ante, eget scelerisque sem iaculis vitae. Etiam fermentum mauris vitae metus pharetra condimentum fermentum est pretium. Proin sollicitudin elementum quam quis pharetra. Aenean facilisis nunc quis elit volutpat mollis. Aenean eleifend varius euismod. Ut dolor est, congue eget dapibus eget, elementum eu odio. Integer et lectus neque, nec scelerisque nisi. EndOfLineHere
71 71
72 72 Vestibulum non velit mi. Aliquam scelerisque libero ut nulla fringilla a sollicitudin magna rhoncus. Praesent a nunc lorem, ac porttitor eros. Sed ac diam nec neque interdum adipiscing quis quis justo. Donec arcu nunc, fringilla eu dictum at, venenatis ac sem. Vestibulum quis elit urna, ac mattis sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
73 73 LOREM
74 74
75 75 get :index, :project_id => 'ecookbook'
76 76 assert_response :success
77 77 assert_template 'index'
78 78
79 79 # should only truncate on new lines to avoid breaking wiki formatting
80 80 assert_select '.wiki p', :text => (doc.description.split("\n").first + '...')
81 81 assert_select '.wiki p', :text => Regexp.new(Regexp.escape("EndOfLineHere..."))
82 82 end
83 83
84 84 def test_show
85 85 get :show, :id => 1
86 86 assert_response :success
87 87 assert_template 'show'
88 88 end
89 89
90 90 def test_new
91 91 @request.session[:user_id] = 2
92 92 get :new, :project_id => 1
93 93 assert_response :success
94 94 assert_template 'new'
95 95 end
96 96
97 97 def test_create_with_one_attachment
98 98 ActionMailer::Base.deliveries.clear
99 99 @request.session[:user_id] = 2
100 100 set_tmp_attachments_directory
101 101
102 102 with_settings :notified_events => %w(document_added) do
103 103 post :create, :project_id => 'ecookbook',
104 104 :document => { :title => 'DocumentsControllerTest#test_post_new',
105 105 :description => 'This is a new document',
106 106 :category_id => 2},
107 107 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
108 108 end
109 109 assert_redirected_to '/projects/ecookbook/documents'
110 110
111 111 document = Document.find_by_title('DocumentsControllerTest#test_post_new')
112 112 assert_not_nil document
113 113 assert_equal Enumeration.find(2), document.category
114 114 assert_equal 1, document.attachments.size
115 115 assert_equal 'testfile.txt', document.attachments.first.filename
116 116 assert_equal 1, ActionMailer::Base.deliveries.size
117 117 end
118 118
119 119 def test_create_with_failure
120 120 @request.session[:user_id] = 2
121 121 assert_no_difference 'Document.count' do
122 122 post :create, :project_id => 'ecookbook', :document => { :title => ''}
123 123 end
124 124 assert_response :success
125 125 assert_template 'new'
126 126 end
127 127
128 128 def test_create_non_default_category
129 129 @request.session[:user_id] = 2
130 130 category2 = Enumeration.find_by_name('User documentation')
131 131 category2.update_attributes(:is_default => true)
132 132 category1 = Enumeration.find_by_name('Uncategorized')
133 133 post :create,
134 134 :project_id => 'ecookbook',
135 135 :document => { :title => 'no default',
136 136 :description => 'This is a new document',
137 137 :category_id => category1.id }
138 138 assert_redirected_to '/projects/ecookbook/documents'
139 139 doc = Document.find_by_title('no default')
140 140 assert_not_nil doc
141 141 assert_equal category1.id, doc.category_id
142 142 assert_equal category1, doc.category
143 143 end
144 144
145 145 def test_edit
146 146 @request.session[:user_id] = 2
147 147 get :edit, :id => 1
148 148 assert_response :success
149 149 assert_template 'edit'
150 150 end
151 151
152 152 def test_update
153 153 @request.session[:user_id] = 2
154 154 put :update, :id => 1, :document => {:title => 'test_update'}
155 155 assert_redirected_to '/documents/1'
156 156 document = Document.find(1)
157 157 assert_equal 'test_update', document.title
158 158 end
159 159
160 160 def test_update_with_failure
161 161 @request.session[:user_id] = 2
162 162 put :update, :id => 1, :document => {:title => ''}
163 163 assert_response :success
164 164 assert_template 'edit'
165 165 end
166 166
167 167 def test_destroy
168 168 @request.session[:user_id] = 2
169 169 assert_difference 'Document.count', -1 do
170 170 delete :destroy, :id => 1
171 171 end
172 172 assert_redirected_to '/projects/ecookbook/documents'
173 173 assert_nil Document.find_by_id(1)
174 174 end
175 175
176 176 def test_add_attachment
177 177 @request.session[:user_id] = 2
178 178 assert_difference 'Attachment.count' do
179 179 post :add_attachment, :id => 1,
180 180 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
181 181 end
182 182 attachment = Attachment.order('id DESC').first
183 183 assert_equal Document.find(1), attachment.container
184 184 end
185 185 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now