##// END OF EJS Templates
Auto-populate fields while creating a new user with LDAP (#10286)....
Jean-Philippe Lang -
r10850:7b8ebb7e3ffc
parent child
Show More
@@ -1,82 +1,96
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AuthSourcesController < ApplicationController
18 class AuthSourcesController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20 menu_item :ldap_authentication
20 menu_item :ldap_authentication
21
21
22 before_filter :require_admin
22 before_filter :require_admin
23 before_filter :find_auth_source, :only => [:edit, :update, :test_connection, :destroy]
23 before_filter :find_auth_source, :only => [:edit, :update, :test_connection, :destroy]
24
24
25 def index
25 def index
26 @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25
26 @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25
27 end
27 end
28
28
29 def new
29 def new
30 klass_name = params[:type] || 'AuthSourceLdap'
30 klass_name = params[:type] || 'AuthSourceLdap'
31 @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source])
31 @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source])
32 render_404 unless @auth_source
32 render_404 unless @auth_source
33 end
33 end
34
34
35 def create
35 def create
36 @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source])
36 @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source])
37 if @auth_source.save
37 if @auth_source.save
38 flash[:notice] = l(:notice_successful_create)
38 flash[:notice] = l(:notice_successful_create)
39 redirect_to auth_sources_path
39 redirect_to auth_sources_path
40 else
40 else
41 render :action => 'new'
41 render :action => 'new'
42 end
42 end
43 end
43 end
44
44
45 def edit
45 def edit
46 end
46 end
47
47
48 def update
48 def update
49 if @auth_source.update_attributes(params[:auth_source])
49 if @auth_source.update_attributes(params[:auth_source])
50 flash[:notice] = l(:notice_successful_update)
50 flash[:notice] = l(:notice_successful_update)
51 redirect_to auth_sources_path
51 redirect_to auth_sources_path
52 else
52 else
53 render :action => 'edit'
53 render :action => 'edit'
54 end
54 end
55 end
55 end
56
56
57 def test_connection
57 def test_connection
58 begin
58 begin
59 @auth_source.test_connection
59 @auth_source.test_connection
60 flash[:notice] = l(:notice_successful_connection)
60 flash[:notice] = l(:notice_successful_connection)
61 rescue Exception => e
61 rescue Exception => e
62 flash[:error] = l(:error_unable_to_connect, e.message)
62 flash[:error] = l(:error_unable_to_connect, e.message)
63 end
63 end
64 redirect_to auth_sources_path
64 redirect_to auth_sources_path
65 end
65 end
66
66
67 def destroy
67 def destroy
68 unless @auth_source.users.exists?
68 unless @auth_source.users.exists?
69 @auth_source.destroy
69 @auth_source.destroy
70 flash[:notice] = l(:notice_successful_delete)
70 flash[:notice] = l(:notice_successful_delete)
71 end
71 end
72 redirect_to auth_sources_path
72 redirect_to auth_sources_path
73 end
73 end
74
74
75 def autocomplete_for_new_user
76 results = AuthSource.search(params[:term])
77
78 render :json => results.map {|result| {
79 'value' => result[:login],
80 'label' => "#{result[:login]} (#{result[:firstname]} #{result[:lastname]})",
81 'login' => result[:login].to_s,
82 'firstname' => result[:firstname].to_s,
83 'lastname' => result[:lastname].to_s,
84 'mail' => result[:mail].to_s,
85 'auth_source_id' => result[:auth_source_id].to_s
86 }}
87 end
88
75 private
89 private
76
90
77 def find_auth_source
91 def find_auth_source
78 @auth_source = AuthSource.find(params[:id])
92 @auth_source = AuthSource.find(params[:id])
79 rescue ActiveRecord::RecordNotFound
93 rescue ActiveRecord::RecordNotFound
80 render_404
94 render_404
81 end
95 end
82 end
96 end
@@ -1,74 +1,92
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 # Generic exception for when the AuthSource can not be reached
18 # Generic exception for when the AuthSource can not be reached
19 # (eg. can not connect to the LDAP)
19 # (eg. can not connect to the LDAP)
20 class AuthSourceException < Exception; end
20 class AuthSourceException < Exception; end
21 class AuthSourceTimeoutException < AuthSourceException; end
21 class AuthSourceTimeoutException < AuthSourceException; end
22
22
23 class AuthSource < ActiveRecord::Base
23 class AuthSource < ActiveRecord::Base
24 include Redmine::SubclassFactory
24 include Redmine::SubclassFactory
25 include Redmine::Ciphering
25 include Redmine::Ciphering
26
26
27 has_many :users
27 has_many :users
28
28
29 validates_presence_of :name
29 validates_presence_of :name
30 validates_uniqueness_of :name
30 validates_uniqueness_of :name
31 validates_length_of :name, :maximum => 60
31 validates_length_of :name, :maximum => 60
32
32
33 def authenticate(login, password)
33 def authenticate(login, password)
34 end
34 end
35
35
36 def test_connection
36 def test_connection
37 end
37 end
38
38
39 def auth_method_name
39 def auth_method_name
40 "Abstract"
40 "Abstract"
41 end
41 end
42
42
43 def account_password
43 def account_password
44 read_ciphered_attribute(:account_password)
44 read_ciphered_attribute(:account_password)
45 end
45 end
46
46
47 def account_password=(arg)
47 def account_password=(arg)
48 write_ciphered_attribute(:account_password, arg)
48 write_ciphered_attribute(:account_password, arg)
49 end
49 end
50
50
51 def searchable?
52 false
53 end
54
55 def self.search(q)
56 results = []
57 AuthSource.all.each do |source|
58 begin
59 if source.searchable?
60 results += source.search(q)
61 end
62 rescue AuthSourceException => e
63 logger.error "Error while searching users in #{source.name}: #{e.message}"
64 end
65 end
66 results
67 end
68
51 def allow_password_changes?
69 def allow_password_changes?
52 self.class.allow_password_changes?
70 self.class.allow_password_changes?
53 end
71 end
54
72
55 # Does this auth source backend allow password changes?
73 # Does this auth source backend allow password changes?
56 def self.allow_password_changes?
74 def self.allow_password_changes?
57 false
75 false
58 end
76 end
59
77
60 # Try to authenticate a user not yet registered against available sources
78 # Try to authenticate a user not yet registered against available sources
61 def self.authenticate(login, password)
79 def self.authenticate(login, password)
62 AuthSource.where(:onthefly_register => true).all.each do |source|
80 AuthSource.where(:onthefly_register => true).all.each do |source|
63 begin
81 begin
64 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
82 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
65 attrs = source.authenticate(login, password)
83 attrs = source.authenticate(login, password)
66 rescue => e
84 rescue => e
67 logger.error "Error during authentication: #{e.message}"
85 logger.error "Error during authentication: #{e.message}"
68 attrs = nil
86 attrs = nil
69 end
87 end
70 return attrs if attrs
88 return attrs if attrs
71 end
89 end
72 return nil
90 return nil
73 end
91 end
74 end
92 end
@@ -1,173 +1,201
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'iconv'
18 require 'iconv'
19 require 'net/ldap'
19 require 'net/ldap'
20 require 'net/ldap/dn'
20 require 'net/ldap/dn'
21 require 'timeout'
21 require 'timeout'
22
22
23 class AuthSourceLdap < AuthSource
23 class AuthSourceLdap < AuthSource
24 validates_presence_of :host, :port, :attr_login
24 validates_presence_of :host, :port, :attr_login
25 validates_length_of :name, :host, :maximum => 60, :allow_nil => true
25 validates_length_of :name, :host, :maximum => 60, :allow_nil => true
26 validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
26 validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
27 validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
27 validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
28 validates_numericality_of :port, :only_integer => true
28 validates_numericality_of :port, :only_integer => true
29 validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
29 validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
30 validate :validate_filter
30 validate :validate_filter
31
31
32 before_validation :strip_ldap_attributes
32 before_validation :strip_ldap_attributes
33
33
34 def initialize(attributes=nil, *args)
34 def initialize(attributes=nil, *args)
35 super
35 super
36 self.port = 389 if self.port == 0
36 self.port = 389 if self.port == 0
37 end
37 end
38
38
39 def authenticate(login, password)
39 def authenticate(login, password)
40 return nil if login.blank? || password.blank?
40 return nil if login.blank? || password.blank?
41
41
42 with_timeout do
42 with_timeout do
43 attrs = get_user_dn(login, password)
43 attrs = get_user_dn(login, password)
44 if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
44 if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
45 logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
45 logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
46 return attrs.except(:dn)
46 return attrs.except(:dn)
47 end
47 end
48 end
48 end
49 rescue Net::LDAP::LdapError => e
49 rescue Net::LDAP::LdapError => e
50 raise AuthSourceException.new(e.message)
50 raise AuthSourceException.new(e.message)
51 end
51 end
52
52
53 # test the connection to the LDAP
53 # test the connection to the LDAP
54 def test_connection
54 def test_connection
55 with_timeout do
55 with_timeout do
56 ldap_con = initialize_ldap_con(self.account, self.account_password)
56 ldap_con = initialize_ldap_con(self.account, self.account_password)
57 ldap_con.open { }
57 ldap_con.open { }
58 end
58 end
59 rescue Net::LDAP::LdapError => e
59 rescue Net::LDAP::LdapError => e
60 raise AuthSourceException.new(e.message)
60 raise AuthSourceException.new(e.message)
61 end
61 end
62
62
63 def auth_method_name
63 def auth_method_name
64 "LDAP"
64 "LDAP"
65 end
65 end
66
66
67 # Returns true if this source can be searched for users
68 def searchable?
69 !account.to_s.include?("$login") && %w(login firstname lastname mail).all? {|a| send("attr_#{a}?")}
70 end
71
72 # Searches the source for users and returns an array of results
73 def search(q)
74 q = q.to_s.strip
75 return [] unless searchable? && q.present?
76
77 results = []
78 search_filter = base_filter & Net::LDAP::Filter.begins(self.attr_login, q)
79 ldap_con = initialize_ldap_con(self.account, self.account_password)
80 ldap_con.search(:base => self.base_dn,
81 :filter => search_filter,
82 :attributes => ['dn', self.attr_login, self.attr_firstname, self.attr_lastname, self.attr_mail],
83 :size => 10) do |entry|
84 attrs = get_user_attributes_from_ldap_entry(entry)
85 attrs[:login] = AuthSourceLdap.get_attr(entry, self.attr_login)
86 results << attrs
87 end
88 results
89 rescue Net::LDAP::LdapError => e
90 raise AuthSourceException.new(e.message)
91 end
92
67 private
93 private
68
94
69 def with_timeout(&block)
95 def with_timeout(&block)
70 timeout = self.timeout
96 timeout = self.timeout
71 timeout = 20 unless timeout && timeout > 0
97 timeout = 20 unless timeout && timeout > 0
72 Timeout.timeout(timeout) do
98 Timeout.timeout(timeout) do
73 return yield
99 return yield
74 end
100 end
75 rescue Timeout::Error => e
101 rescue Timeout::Error => e
76 raise AuthSourceTimeoutException.new(e.message)
102 raise AuthSourceTimeoutException.new(e.message)
77 end
103 end
78
104
79 def ldap_filter
105 def ldap_filter
80 if filter.present?
106 if filter.present?
81 Net::LDAP::Filter.construct(filter)
107 Net::LDAP::Filter.construct(filter)
82 end
108 end
83 rescue Net::LDAP::LdapError
109 rescue Net::LDAP::LdapError
84 nil
110 nil
85 end
111 end
86
112
113 def base_filter
114 filter = Net::LDAP::Filter.eq("objectClass", "*")
115 if f = ldap_filter
116 filter = filter & f
117 end
118 filter
119 end
120
87 def validate_filter
121 def validate_filter
88 if filter.present? && ldap_filter.nil?
122 if filter.present? && ldap_filter.nil?
89 errors.add(:filter, :invalid)
123 errors.add(:filter, :invalid)
90 end
124 end
91 end
125 end
92
126
93 def strip_ldap_attributes
127 def strip_ldap_attributes
94 [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
128 [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
95 write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
129 write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
96 end
130 end
97 end
131 end
98
132
99 def initialize_ldap_con(ldap_user, ldap_password)
133 def initialize_ldap_con(ldap_user, ldap_password)
100 options = { :host => self.host,
134 options = { :host => self.host,
101 :port => self.port,
135 :port => self.port,
102 :encryption => (self.tls ? :simple_tls : nil)
136 :encryption => (self.tls ? :simple_tls : nil)
103 }
137 }
104 options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
138 options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
105 Net::LDAP.new options
139 Net::LDAP.new options
106 end
140 end
107
141
108 def get_user_attributes_from_ldap_entry(entry)
142 def get_user_attributes_from_ldap_entry(entry)
109 {
143 {
110 :dn => entry.dn,
144 :dn => entry.dn,
111 :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
145 :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
112 :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
146 :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
113 :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
147 :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
114 :auth_source_id => self.id
148 :auth_source_id => self.id
115 }
149 }
116 end
150 end
117
151
118 # Return the attributes needed for the LDAP search. It will only
152 # Return the attributes needed for the LDAP search. It will only
119 # include the user attributes if on-the-fly registration is enabled
153 # include the user attributes if on-the-fly registration is enabled
120 def search_attributes
154 def search_attributes
121 if onthefly_register?
155 if onthefly_register?
122 ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
156 ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
123 else
157 else
124 ['dn']
158 ['dn']
125 end
159 end
126 end
160 end
127
161
128 # Check if a DN (user record) authenticates with the password
162 # Check if a DN (user record) authenticates with the password
129 def authenticate_dn(dn, password)
163 def authenticate_dn(dn, password)
130 if dn.present? && password.present?
164 if dn.present? && password.present?
131 initialize_ldap_con(dn, password).bind
165 initialize_ldap_con(dn, password).bind
132 end
166 end
133 end
167 end
134
168
135 # Get the user's dn and any attributes for them, given their login
169 # Get the user's dn and any attributes for them, given their login
136 def get_user_dn(login, password)
170 def get_user_dn(login, password)
137 ldap_con = nil
171 ldap_con = nil
138 if self.account && self.account.include?("$login")
172 if self.account && self.account.include?("$login")
139 ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
173 ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
140 else
174 else
141 ldap_con = initialize_ldap_con(self.account, self.account_password)
175 ldap_con = initialize_ldap_con(self.account, self.account_password)
142 end
176 end
143 login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
144 object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
145 attrs = {}
177 attrs = {}
146
178 search_filter = base_filter & Net::LDAP::Filter.eq(self.attr_login, login)
147 search_filter = object_filter & login_filter
148 if f = ldap_filter
149 search_filter = search_filter & f
150 end
151
179
152 ldap_con.search( :base => self.base_dn,
180 ldap_con.search( :base => self.base_dn,
153 :filter => search_filter,
181 :filter => search_filter,
154 :attributes=> search_attributes) do |entry|
182 :attributes=> search_attributes) do |entry|
155
183
156 if onthefly_register?
184 if onthefly_register?
157 attrs = get_user_attributes_from_ldap_entry(entry)
185 attrs = get_user_attributes_from_ldap_entry(entry)
158 else
186 else
159 attrs = {:dn => entry.dn}
187 attrs = {:dn => entry.dn}
160 end
188 end
161
189
162 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
190 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
163 end
191 end
164
192
165 attrs
193 attrs
166 end
194 end
167
195
168 def self.get_attr(entry, attr_name)
196 def self.get_attr(entry, attr_name)
169 if !attr_name.blank?
197 if !attr_name.blank?
170 entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
198 entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
171 end
199 end
172 end
200 end
173 end
201 end
@@ -1,12 +1,30
1 <h2><%= link_to l(:label_user_plural), users_path %> &#187; <%=l(:label_user_new)%></h2>
1 <h2><%= link_to l(:label_user_plural), users_path %> &#187; <%=l(:label_user_new)%></h2>
2
2
3 <%= labelled_form_for @user do |f| %>
3 <%= labelled_form_for @user do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <% if email_delivery_enabled? %>
5 <% if email_delivery_enabled? %>
6 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
6 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
7 <% end %>
7 <% end %>
8 <p>
8 <p>
9 <%= submit_tag l(:button_create) %>
9 <%= submit_tag l(:button_create) %>
10 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
10 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
11 </p>
11 </p>
12 <% end %>
12 <% end %>
13
14 <% if @auth_sources.present? && @auth_sources.any?(&:searchable?) %>
15 <%= javascript_tag do %>
16 observeAutocompleteField('user_login', '<%= escape_javascript autocomplete_for_new_user_auth_sources_path %>', {
17 select: function(event, ui) {
18 $('input#user_firstname').val(ui.item.firstname);
19 $('input#user_lastname').val(ui.item.lastname);
20 $('input#user_mail').val(ui.item.mail);
21 $('select#user_auth_source_id option').each(function(){
22 if ($(this).attr('value') == ui.item.auth_source_id) {
23 $(this).attr('selected', true);
24 $('select#user_auth_source_id').trigger('change');
25 }
26 });
27 }
28 });
29 <% end %>
30 <% end %>
@@ -1,341 +1,344
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 RedmineApp::Application.routes.draw do
18 RedmineApp::Application.routes.draw do
19 root :to => 'welcome#index', :as => 'home'
19 root :to => 'welcome#index', :as => 'home'
20
20
21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 match 'account/activate', :to => 'account#activate', :via => :get
25 match 'account/activate', :to => 'account#activate', :via => :get
26
26
27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post]
27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post]
28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post]
28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post]
29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post]
29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post]
30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post]
30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post]
31
31
32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34
34
35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39
39
40 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
40 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44
44
45 # Misc issue routes. TODO: move into resources
45 # Misc issue routes. TODO: move into resources
46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50
50
51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
53
53
54 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
54 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
55 get '/issues/gantt', :to => 'gantts#show'
55 get '/issues/gantt', :to => 'gantts#show'
56
56
57 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
57 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
58 get '/issues/calendar', :to => 'calendars#show'
58 get '/issues/calendar', :to => 'calendars#show'
59
59
60 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
60 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
61 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
61 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
62
62
63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
74
74
75 resources :users
75 resources :users
76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
79
79
80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
87
87
88 resources :projects do
88 resources :projects do
89 member do
89 member do
90 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
90 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
91 post 'modules'
91 post 'modules'
92 post 'archive'
92 post 'archive'
93 post 'unarchive'
93 post 'unarchive'
94 post 'close'
94 post 'close'
95 post 'reopen'
95 post 'reopen'
96 match 'copy', :via => [:get, :post]
96 match 'copy', :via => [:get, :post]
97 end
97 end
98
98
99 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
99 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
100 collection do
100 collection do
101 get 'autocomplete'
101 get 'autocomplete'
102 end
102 end
103 end
103 end
104
104
105 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
105 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
106
106
107 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
107 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
108 resources :issues, :only => [:index, :new, :create] do
108 resources :issues, :only => [:index, :new, :create] do
109 resources :time_entries, :controller => 'timelog' do
109 resources :time_entries, :controller => 'timelog' do
110 collection do
110 collection do
111 get 'report'
111 get 'report'
112 end
112 end
113 end
113 end
114 end
114 end
115 # issue form update
115 # issue form update
116 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
116 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
117
117
118 resources :files, :only => [:index, :new, :create]
118 resources :files, :only => [:index, :new, :create]
119
119
120 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
120 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
121 collection do
121 collection do
122 put 'close_completed'
122 put 'close_completed'
123 end
123 end
124 end
124 end
125 get 'versions.:format', :to => 'versions#index'
125 get 'versions.:format', :to => 'versions#index'
126 get 'roadmap', :to => 'versions#index', :format => false
126 get 'roadmap', :to => 'versions#index', :format => false
127 get 'versions', :to => 'versions#index'
127 get 'versions', :to => 'versions#index'
128
128
129 resources :news, :except => [:show, :edit, :update, :destroy]
129 resources :news, :except => [:show, :edit, :update, :destroy]
130 resources :time_entries, :controller => 'timelog' do
130 resources :time_entries, :controller => 'timelog' do
131 get 'report', :on => :collection
131 get 'report', :on => :collection
132 end
132 end
133 resources :queries, :only => [:new, :create]
133 resources :queries, :only => [:new, :create]
134 resources :issue_categories, :shallow => true
134 resources :issue_categories, :shallow => true
135 resources :documents, :except => [:show, :edit, :update, :destroy]
135 resources :documents, :except => [:show, :edit, :update, :destroy]
136 resources :boards
136 resources :boards
137 resources :repositories, :shallow => true, :except => [:index, :show] do
137 resources :repositories, :shallow => true, :except => [:index, :show] do
138 member do
138 member do
139 match 'committers', :via => [:get, :post]
139 match 'committers', :via => [:get, :post]
140 end
140 end
141 end
141 end
142
142
143 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
143 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
144 resources :wiki, :except => [:index, :new, :create], :as => 'wiki_page' do
144 resources :wiki, :except => [:index, :new, :create], :as => 'wiki_page' do
145 member do
145 member do
146 get 'rename'
146 get 'rename'
147 post 'rename'
147 post 'rename'
148 get 'history'
148 get 'history'
149 get 'diff'
149 get 'diff'
150 match 'preview', :via => [:post, :put]
150 match 'preview', :via => [:post, :put]
151 post 'protect'
151 post 'protect'
152 post 'add_attachment'
152 post 'add_attachment'
153 end
153 end
154 collection do
154 collection do
155 get 'export'
155 get 'export'
156 get 'date_index'
156 get 'date_index'
157 end
157 end
158 end
158 end
159 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
159 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
160 get 'wiki/:id/:version', :to => 'wiki#show'
160 get 'wiki/:id/:version', :to => 'wiki#show'
161 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
161 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
162 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
162 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
163 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
163 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
164 end
164 end
165
165
166 resources :issues do
166 resources :issues do
167 collection do
167 collection do
168 match 'bulk_edit', :via => [:get, :post]
168 match 'bulk_edit', :via => [:get, :post]
169 post 'bulk_update'
169 post 'bulk_update'
170 end
170 end
171 resources :time_entries, :controller => 'timelog' do
171 resources :time_entries, :controller => 'timelog' do
172 collection do
172 collection do
173 get 'report'
173 get 'report'
174 end
174 end
175 end
175 end
176 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
176 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
177 end
177 end
178 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
178 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
179
179
180 resources :queries, :except => [:show]
180 resources :queries, :except => [:show]
181
181
182 resources :news, :only => [:index, :show, :edit, :update, :destroy]
182 resources :news, :only => [:index, :show, :edit, :update, :destroy]
183 match '/news/:id/comments', :to => 'comments#create', :via => :post
183 match '/news/:id/comments', :to => 'comments#create', :via => :post
184 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
184 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
185
185
186 resources :versions, :only => [:show, :edit, :update, :destroy] do
186 resources :versions, :only => [:show, :edit, :update, :destroy] do
187 post 'status_by', :on => :member
187 post 'status_by', :on => :member
188 end
188 end
189
189
190 resources :documents, :only => [:show, :edit, :update, :destroy] do
190 resources :documents, :only => [:show, :edit, :update, :destroy] do
191 post 'add_attachment', :on => :member
191 post 'add_attachment', :on => :member
192 end
192 end
193
193
194 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
194 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
195
195
196 resources :time_entries, :controller => 'timelog', :except => :destroy do
196 resources :time_entries, :controller => 'timelog', :except => :destroy do
197 collection do
197 collection do
198 get 'report'
198 get 'report'
199 get 'bulk_edit'
199 get 'bulk_edit'
200 post 'bulk_update'
200 post 'bulk_update'
201 end
201 end
202 end
202 end
203 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
203 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
204 # TODO: delete /time_entries for bulk deletion
204 # TODO: delete /time_entries for bulk deletion
205 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
205 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
206
206
207 get 'projects/:id/activity', :to => 'activities#index'
207 get 'projects/:id/activity', :to => 'activities#index'
208 get 'projects/:id/activity.:format', :to => 'activities#index'
208 get 'projects/:id/activity.:format', :to => 'activities#index'
209 get 'activity', :to => 'activities#index'
209 get 'activity', :to => 'activities#index'
210
210
211 # repositories routes
211 # repositories routes
212 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
212 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
213 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
213 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
214
214
215 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
215 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
216 :to => 'repositories#changes'
216 :to => 'repositories#changes'
217
217
218 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
218 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
219 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
219 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
220 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
220 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
221 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
221 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
222 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
222 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
223 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
223 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
224 :controller => 'repositories',
224 :controller => 'repositories',
225 :format => false,
225 :format => false,
226 :constraints => {
226 :constraints => {
227 :action => /(browse|show|entry|raw|annotate|diff)/,
227 :action => /(browse|show|entry|raw|annotate|diff)/,
228 :rev => /[a-z0-9\.\-_]+/
228 :rev => /[a-z0-9\.\-_]+/
229 }
229 }
230
230
231 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
231 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
232 get 'projects/:id/repository/graph', :to => 'repositories#graph'
232 get 'projects/:id/repository/graph', :to => 'repositories#graph'
233
233
234 get 'projects/:id/repository/changes(/*path(.:ext))',
234 get 'projects/:id/repository/changes(/*path(.:ext))',
235 :to => 'repositories#changes'
235 :to => 'repositories#changes'
236
236
237 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
237 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
238 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
238 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
239 get 'projects/:id/repository/revision', :to => 'repositories#revision'
239 get 'projects/:id/repository/revision', :to => 'repositories#revision'
240 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
240 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
241 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
241 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
242 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
242 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
243 :controller => 'repositories',
243 :controller => 'repositories',
244 :format => false,
244 :format => false,
245 :constraints => {
245 :constraints => {
246 :action => /(browse|show|entry|raw|annotate|diff)/,
246 :action => /(browse|show|entry|raw|annotate|diff)/,
247 :rev => /[a-z0-9\.\-_]+/
247 :rev => /[a-z0-9\.\-_]+/
248 }
248 }
249 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
249 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
250 :controller => 'repositories',
250 :controller => 'repositories',
251 :action => /(browse|show|entry|raw|changes|annotate|diff)/
251 :action => /(browse|show|entry|raw|changes|annotate|diff)/
252 get 'projects/:id/repository/:action(/*path(.:ext))',
252 get 'projects/:id/repository/:action(/*path(.:ext))',
253 :controller => 'repositories',
253 :controller => 'repositories',
254 :action => /(browse|show|entry|raw|changes|annotate|diff)/
254 :action => /(browse|show|entry|raw|changes|annotate|diff)/
255
255
256 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
256 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
257 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
257 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
258
258
259 # additional routes for having the file name at the end of url
259 # additional routes for having the file name at the end of url
260 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
260 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
261 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
261 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
262 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
262 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
263 match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/
263 match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/
264 resources :attachments, :only => [:show, :destroy]
264 resources :attachments, :only => [:show, :destroy]
265
265
266 resources :groups do
266 resources :groups do
267 member do
267 member do
268 get 'autocomplete_for_user'
268 get 'autocomplete_for_user'
269 end
269 end
270 end
270 end
271
271
272 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
272 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
273 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
273 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
274 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
274 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
275 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
275 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
276
276
277 resources :trackers, :except => :show do
277 resources :trackers, :except => :show do
278 collection do
278 collection do
279 match 'fields', :via => [:get, :post]
279 match 'fields', :via => [:get, :post]
280 end
280 end
281 end
281 end
282 resources :issue_statuses, :except => :show do
282 resources :issue_statuses, :except => :show do
283 collection do
283 collection do
284 post 'update_issue_done_ratio'
284 post 'update_issue_done_ratio'
285 end
285 end
286 end
286 end
287 resources :custom_fields, :except => :show
287 resources :custom_fields, :except => :show
288 resources :roles do
288 resources :roles do
289 collection do
289 collection do
290 match 'permissions', :via => [:get, :post]
290 match 'permissions', :via => [:get, :post]
291 end
291 end
292 end
292 end
293 resources :enumerations, :except => :show
293 resources :enumerations, :except => :show
294 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
294 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
295
295
296 get 'projects/:id/search', :controller => 'search', :action => 'index'
296 get 'projects/:id/search', :controller => 'search', :action => 'index'
297 get 'search', :controller => 'search', :action => 'index'
297 get 'search', :controller => 'search', :action => 'index'
298
298
299 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
299 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
300
300
301 match 'admin', :controller => 'admin', :action => 'index', :via => :get
301 match 'admin', :controller => 'admin', :action => 'index', :via => :get
302 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
302 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
303 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
303 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
304 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
304 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
305 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
305 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
306 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
306 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
307
307
308 resources :auth_sources do
308 resources :auth_sources do
309 member do
309 member do
310 get 'test_connection', :as => 'try_connection'
310 get 'test_connection', :as => 'try_connection'
311 end
311 end
312 collection do
313 get 'autocomplete_for_new_user'
314 end
312 end
315 end
313
316
314 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
317 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
315 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
318 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
316 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
319 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
317 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
320 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
318 match 'settings', :controller => 'settings', :action => 'index', :via => :get
321 match 'settings', :controller => 'settings', :action => 'index', :via => :get
319 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
322 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
320 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
323 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
321
324
322 match 'sys/projects', :to => 'sys#projects', :via => :get
325 match 'sys/projects', :to => 'sys#projects', :via => :get
323 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
326 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
324 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
327 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
325
328
326 match 'uploads', :to => 'attachments#upload', :via => :post
329 match 'uploads', :to => 'attachments#upload', :via => :post
327
330
328 get 'robots.txt', :to => 'welcome#robots'
331 get 'robots.txt', :to => 'welcome#robots'
329
332
330 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
333 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
331 file = File.join(plugin_dir, "config/routes.rb")
334 file = File.join(plugin_dir, "config/routes.rb")
332 if File.exists?(file)
335 if File.exists?(file)
333 begin
336 begin
334 instance_eval File.read(file)
337 instance_eval File.read(file)
335 rescue Exception => e
338 rescue Exception => e
336 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
339 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
337 exit 1
340 exit 1
338 end
341 end
339 end
342 end
340 end
343 end
341 end
344 end
@@ -1,586 +1,586
1 /* Redmine - project management software
1 /* Redmine - project management software
2 Copyright (C) 2006-2012 Jean-Philippe Lang */
2 Copyright (C) 2006-2012 Jean-Philippe Lang */
3
3
4 function checkAll(id, checked) {
4 function checkAll(id, checked) {
5 if (checked) {
5 if (checked) {
6 $('#'+id).find('input[type=checkbox]').attr('checked', true);
6 $('#'+id).find('input[type=checkbox]').attr('checked', true);
7 } else {
7 } else {
8 $('#'+id).find('input[type=checkbox]').removeAttr('checked');
8 $('#'+id).find('input[type=checkbox]').removeAttr('checked');
9 }
9 }
10 }
10 }
11
11
12 function toggleCheckboxesBySelector(selector) {
12 function toggleCheckboxesBySelector(selector) {
13 var all_checked = true;
13 var all_checked = true;
14 $(selector).each(function(index) {
14 $(selector).each(function(index) {
15 if (!$(this).is(':checked')) { all_checked = false; }
15 if (!$(this).is(':checked')) { all_checked = false; }
16 });
16 });
17 $(selector).attr('checked', !all_checked)
17 $(selector).attr('checked', !all_checked)
18 }
18 }
19
19
20 function showAndScrollTo(id, focus) {
20 function showAndScrollTo(id, focus) {
21 $('#'+id).show();
21 $('#'+id).show();
22 if (focus!=null) {
22 if (focus!=null) {
23 $('#'+focus).focus();
23 $('#'+focus).focus();
24 }
24 }
25 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
25 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
26 }
26 }
27
27
28 function toggleRowGroup(el) {
28 function toggleRowGroup(el) {
29 var tr = $(el).parents('tr').first();
29 var tr = $(el).parents('tr').first();
30 var n = tr.next();
30 var n = tr.next();
31 tr.toggleClass('open');
31 tr.toggleClass('open');
32 while (n.length && !n.hasClass('group')) {
32 while (n.length && !n.hasClass('group')) {
33 n.toggle();
33 n.toggle();
34 n = n.next('tr');
34 n = n.next('tr');
35 }
35 }
36 }
36 }
37
37
38 function collapseAllRowGroups(el) {
38 function collapseAllRowGroups(el) {
39 var tbody = $(el).parents('tbody').first();
39 var tbody = $(el).parents('tbody').first();
40 tbody.children('tr').each(function(index) {
40 tbody.children('tr').each(function(index) {
41 if ($(this).hasClass('group')) {
41 if ($(this).hasClass('group')) {
42 $(this).removeClass('open');
42 $(this).removeClass('open');
43 } else {
43 } else {
44 $(this).hide();
44 $(this).hide();
45 }
45 }
46 });
46 });
47 }
47 }
48
48
49 function expandAllRowGroups(el) {
49 function expandAllRowGroups(el) {
50 var tbody = $(el).parents('tbody').first();
50 var tbody = $(el).parents('tbody').first();
51 tbody.children('tr').each(function(index) {
51 tbody.children('tr').each(function(index) {
52 if ($(this).hasClass('group')) {
52 if ($(this).hasClass('group')) {
53 $(this).addClass('open');
53 $(this).addClass('open');
54 } else {
54 } else {
55 $(this).show();
55 $(this).show();
56 }
56 }
57 });
57 });
58 }
58 }
59
59
60 function toggleAllRowGroups(el) {
60 function toggleAllRowGroups(el) {
61 var tr = $(el).parents('tr').first();
61 var tr = $(el).parents('tr').first();
62 if (tr.hasClass('open')) {
62 if (tr.hasClass('open')) {
63 collapseAllRowGroups(el);
63 collapseAllRowGroups(el);
64 } else {
64 } else {
65 expandAllRowGroups(el);
65 expandAllRowGroups(el);
66 }
66 }
67 }
67 }
68
68
69 function toggleFieldset(el) {
69 function toggleFieldset(el) {
70 var fieldset = $(el).parents('fieldset').first();
70 var fieldset = $(el).parents('fieldset').first();
71 fieldset.toggleClass('collapsed');
71 fieldset.toggleClass('collapsed');
72 fieldset.children('div').toggle();
72 fieldset.children('div').toggle();
73 }
73 }
74
74
75 function hideFieldset(el) {
75 function hideFieldset(el) {
76 var fieldset = $(el).parents('fieldset').first();
76 var fieldset = $(el).parents('fieldset').first();
77 fieldset.toggleClass('collapsed');
77 fieldset.toggleClass('collapsed');
78 fieldset.children('div').hide();
78 fieldset.children('div').hide();
79 }
79 }
80
80
81 function initFilters(){
81 function initFilters(){
82 $('#add_filter_select').change(function(){
82 $('#add_filter_select').change(function(){
83 addFilter($(this).val(), '', []);
83 addFilter($(this).val(), '', []);
84 });
84 });
85 $('#filters-table td.field input[type=checkbox]').each(function(){
85 $('#filters-table td.field input[type=checkbox]').each(function(){
86 toggleFilter($(this).val());
86 toggleFilter($(this).val());
87 });
87 });
88 $('#filters-table td.field input[type=checkbox]').live('click',function(){
88 $('#filters-table td.field input[type=checkbox]').live('click',function(){
89 toggleFilter($(this).val());
89 toggleFilter($(this).val());
90 });
90 });
91 $('#filters-table .toggle-multiselect').live('click',function(){
91 $('#filters-table .toggle-multiselect').live('click',function(){
92 toggleMultiSelect($(this).siblings('select'));
92 toggleMultiSelect($(this).siblings('select'));
93 });
93 });
94 $('#filters-table input[type=text]').live('keypress', function(e){
94 $('#filters-table input[type=text]').live('keypress', function(e){
95 if (e.keyCode == 13) submit_query_form("query_form");
95 if (e.keyCode == 13) submit_query_form("query_form");
96 });
96 });
97 }
97 }
98
98
99 function addFilter(field, operator, values) {
99 function addFilter(field, operator, values) {
100 var fieldId = field.replace('.', '_');
100 var fieldId = field.replace('.', '_');
101 var tr = $('#tr_'+fieldId);
101 var tr = $('#tr_'+fieldId);
102 if (tr.length > 0) {
102 if (tr.length > 0) {
103 tr.show();
103 tr.show();
104 } else {
104 } else {
105 buildFilterRow(field, operator, values);
105 buildFilterRow(field, operator, values);
106 }
106 }
107 $('#cb_'+fieldId).attr('checked', true);
107 $('#cb_'+fieldId).attr('checked', true);
108 toggleFilter(field);
108 toggleFilter(field);
109 $('#add_filter_select').val('').children('option').each(function(){
109 $('#add_filter_select').val('').children('option').each(function(){
110 if ($(this).attr('value') == field) {
110 if ($(this).attr('value') == field) {
111 $(this).attr('disabled', true);
111 $(this).attr('disabled', true);
112 }
112 }
113 });
113 });
114 }
114 }
115
115
116 function buildFilterRow(field, operator, values) {
116 function buildFilterRow(field, operator, values) {
117 var fieldId = field.replace('.', '_');
117 var fieldId = field.replace('.', '_');
118 var filterTable = $("#filters-table");
118 var filterTable = $("#filters-table");
119 var filterOptions = availableFilters[field];
119 var filterOptions = availableFilters[field];
120 var operators = operatorByType[filterOptions['type']];
120 var operators = operatorByType[filterOptions['type']];
121 var filterValues = filterOptions['values'];
121 var filterValues = filterOptions['values'];
122 var i, select;
122 var i, select;
123
123
124 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
124 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
125 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
125 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
126 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
126 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
127 '<td class="values"></td>'
127 '<td class="values"></td>'
128 );
128 );
129 filterTable.append(tr);
129 filterTable.append(tr);
130
130
131 select = tr.find('td.operator select');
131 select = tr.find('td.operator select');
132 for (i=0;i<operators.length;i++){
132 for (i=0;i<operators.length;i++){
133 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
133 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
134 if (operators[i] == operator) {option.attr('selected', true)};
134 if (operators[i] == operator) {option.attr('selected', true)};
135 select.append(option);
135 select.append(option);
136 }
136 }
137 select.change(function(){toggleOperator(field)});
137 select.change(function(){toggleOperator(field)});
138
138
139 switch (filterOptions['type']){
139 switch (filterOptions['type']){
140 case "list":
140 case "list":
141 case "list_optional":
141 case "list_optional":
142 case "list_status":
142 case "list_status":
143 case "list_subprojects":
143 case "list_subprojects":
144 tr.find('td.values').append(
144 tr.find('td.values').append(
145 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
145 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
146 ' <span class="toggle-multiselect">&nbsp;</span></span>'
146 ' <span class="toggle-multiselect">&nbsp;</span></span>'
147 );
147 );
148 select = tr.find('td.values select');
148 select = tr.find('td.values select');
149 if (values.length > 1) {select.attr('multiple', true)};
149 if (values.length > 1) {select.attr('multiple', true)};
150 for (i=0;i<filterValues.length;i++){
150 for (i=0;i<filterValues.length;i++){
151 var filterValue = filterValues[i];
151 var filterValue = filterValues[i];
152 var option = $('<option>');
152 var option = $('<option>');
153 if ($.isArray(filterValue)) {
153 if ($.isArray(filterValue)) {
154 option.val(filterValue[1]).text(filterValue[0]);
154 option.val(filterValue[1]).text(filterValue[0]);
155 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
155 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
156 } else {
156 } else {
157 option.val(filterValue).text(filterValue);
157 option.val(filterValue).text(filterValue);
158 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
158 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
159 }
159 }
160 select.append(option);
160 select.append(option);
161 }
161 }
162 break;
162 break;
163 case "date":
163 case "date":
164 case "date_past":
164 case "date_past":
165 tr.find('td.values').append(
165 tr.find('td.values').append(
166 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
166 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
167 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
167 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
168 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
168 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
169 );
169 );
170 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
170 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
171 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
171 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
172 $('#values_'+fieldId).val(values[0]);
172 $('#values_'+fieldId).val(values[0]);
173 break;
173 break;
174 case "string":
174 case "string":
175 case "text":
175 case "text":
176 tr.find('td.values').append(
176 tr.find('td.values').append(
177 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
177 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
178 );
178 );
179 $('#values_'+fieldId).val(values[0]);
179 $('#values_'+fieldId).val(values[0]);
180 break;
180 break;
181 case "relation":
181 case "relation":
182 tr.find('td.values').append(
182 tr.find('td.values').append(
183 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
183 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
184 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
184 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
185 );
185 );
186 $('#values_'+fieldId).val(values[0]);
186 $('#values_'+fieldId).val(values[0]);
187 select = tr.find('td.values select');
187 select = tr.find('td.values select');
188 for (i=0;i<allProjects.length;i++){
188 for (i=0;i<allProjects.length;i++){
189 var filterValue = allProjects[i];
189 var filterValue = allProjects[i];
190 var option = $('<option>');
190 var option = $('<option>');
191 option.val(filterValue[1]).text(filterValue[0]);
191 option.val(filterValue[1]).text(filterValue[0]);
192 if (values[0] == filterValue[1]) {option.attr('selected', true)};
192 if (values[0] == filterValue[1]) {option.attr('selected', true)};
193 select.append(option);
193 select.append(option);
194 }
194 }
195 case "integer":
195 case "integer":
196 case "float":
196 case "float":
197 tr.find('td.values').append(
197 tr.find('td.values').append(
198 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
198 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
199 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
199 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
200 );
200 );
201 $('#values_'+fieldId+'_1').val(values[0]);
201 $('#values_'+fieldId+'_1').val(values[0]);
202 $('#values_'+fieldId+'_2').val(values[1]);
202 $('#values_'+fieldId+'_2').val(values[1]);
203 break;
203 break;
204 }
204 }
205 }
205 }
206
206
207 function toggleFilter(field) {
207 function toggleFilter(field) {
208 var fieldId = field.replace('.', '_');
208 var fieldId = field.replace('.', '_');
209 if ($('#cb_' + fieldId).is(':checked')) {
209 if ($('#cb_' + fieldId).is(':checked')) {
210 $("#operators_" + fieldId).show().removeAttr('disabled');
210 $("#operators_" + fieldId).show().removeAttr('disabled');
211 toggleOperator(field);
211 toggleOperator(field);
212 } else {
212 } else {
213 $("#operators_" + fieldId).hide().attr('disabled', true);
213 $("#operators_" + fieldId).hide().attr('disabled', true);
214 enableValues(field, []);
214 enableValues(field, []);
215 }
215 }
216 }
216 }
217
217
218 function enableValues(field, indexes) {
218 function enableValues(field, indexes) {
219 var fieldId = field.replace('.', '_');
219 var fieldId = field.replace('.', '_');
220 $('#tr_'+fieldId+' td.values .value').each(function(index) {
220 $('#tr_'+fieldId+' td.values .value').each(function(index) {
221 if ($.inArray(index, indexes) >= 0) {
221 if ($.inArray(index, indexes) >= 0) {
222 $(this).removeAttr('disabled');
222 $(this).removeAttr('disabled');
223 $(this).parents('span').first().show();
223 $(this).parents('span').first().show();
224 } else {
224 } else {
225 $(this).val('');
225 $(this).val('');
226 $(this).attr('disabled', true);
226 $(this).attr('disabled', true);
227 $(this).parents('span').first().hide();
227 $(this).parents('span').first().hide();
228 }
228 }
229
229
230 if ($(this).hasClass('group')) {
230 if ($(this).hasClass('group')) {
231 $(this).addClass('open');
231 $(this).addClass('open');
232 } else {
232 } else {
233 $(this).show();
233 $(this).show();
234 }
234 }
235 });
235 });
236 }
236 }
237
237
238 function toggleOperator(field) {
238 function toggleOperator(field) {
239 var fieldId = field.replace('.', '_');
239 var fieldId = field.replace('.', '_');
240 var operator = $("#operators_" + fieldId);
240 var operator = $("#operators_" + fieldId);
241 switch (operator.val()) {
241 switch (operator.val()) {
242 case "!*":
242 case "!*":
243 case "*":
243 case "*":
244 case "t":
244 case "t":
245 case "ld":
245 case "ld":
246 case "w":
246 case "w":
247 case "lw":
247 case "lw":
248 case "l2w":
248 case "l2w":
249 case "m":
249 case "m":
250 case "lm":
250 case "lm":
251 case "y":
251 case "y":
252 case "o":
252 case "o":
253 case "c":
253 case "c":
254 enableValues(field, []);
254 enableValues(field, []);
255 break;
255 break;
256 case "><":
256 case "><":
257 enableValues(field, [0,1]);
257 enableValues(field, [0,1]);
258 break;
258 break;
259 case "<t+":
259 case "<t+":
260 case ">t+":
260 case ">t+":
261 case "><t+":
261 case "><t+":
262 case "t+":
262 case "t+":
263 case ">t-":
263 case ">t-":
264 case "<t-":
264 case "<t-":
265 case "><t-":
265 case "><t-":
266 case "t-":
266 case "t-":
267 enableValues(field, [2]);
267 enableValues(field, [2]);
268 break;
268 break;
269 case "=p":
269 case "=p":
270 case "=!p":
270 case "=!p":
271 case "!p":
271 case "!p":
272 enableValues(field, [1]);
272 enableValues(field, [1]);
273 break;
273 break;
274 default:
274 default:
275 enableValues(field, [0]);
275 enableValues(field, [0]);
276 break;
276 break;
277 }
277 }
278 }
278 }
279
279
280 function toggleMultiSelect(el) {
280 function toggleMultiSelect(el) {
281 if (el.attr('multiple')) {
281 if (el.attr('multiple')) {
282 el.removeAttr('multiple');
282 el.removeAttr('multiple');
283 } else {
283 } else {
284 el.attr('multiple', true);
284 el.attr('multiple', true);
285 }
285 }
286 }
286 }
287
287
288 function submit_query_form(id) {
288 function submit_query_form(id) {
289 selectAllOptions("selected_columns");
289 selectAllOptions("selected_columns");
290 $('#'+id).submit();
290 $('#'+id).submit();
291 }
291 }
292
292
293 function showTab(name) {
293 function showTab(name) {
294 $('div#content .tab-content').hide();
294 $('div#content .tab-content').hide();
295 $('div.tabs a').removeClass('selected');
295 $('div.tabs a').removeClass('selected');
296 $('#tab-content-' + name).show();
296 $('#tab-content-' + name).show();
297 $('#tab-' + name).addClass('selected');
297 $('#tab-' + name).addClass('selected');
298 return false;
298 return false;
299 }
299 }
300
300
301 function moveTabRight(el) {
301 function moveTabRight(el) {
302 var lis = $(el).parents('div.tabs').first().find('ul').children();
302 var lis = $(el).parents('div.tabs').first().find('ul').children();
303 var tabsWidth = 0;
303 var tabsWidth = 0;
304 var i = 0;
304 var i = 0;
305 lis.each(function(){
305 lis.each(function(){
306 if ($(this).is(':visible')) {
306 if ($(this).is(':visible')) {
307 tabsWidth += $(this).width() + 6;
307 tabsWidth += $(this).width() + 6;
308 }
308 }
309 });
309 });
310 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
310 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
311 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
311 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
312 lis.eq(i).hide();
312 lis.eq(i).hide();
313 }
313 }
314
314
315 function moveTabLeft(el) {
315 function moveTabLeft(el) {
316 var lis = $(el).parents('div.tabs').first().find('ul').children();
316 var lis = $(el).parents('div.tabs').first().find('ul').children();
317 var i = 0;
317 var i = 0;
318 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
318 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
319 if (i>0) {
319 if (i>0) {
320 lis.eq(i-1).show();
320 lis.eq(i-1).show();
321 }
321 }
322 }
322 }
323
323
324 function displayTabsButtons() {
324 function displayTabsButtons() {
325 var lis;
325 var lis;
326 var tabsWidth = 0;
326 var tabsWidth = 0;
327 var el;
327 var el;
328 $('div.tabs').each(function() {
328 $('div.tabs').each(function() {
329 el = $(this);
329 el = $(this);
330 lis = el.find('ul').children();
330 lis = el.find('ul').children();
331 lis.each(function(){
331 lis.each(function(){
332 if ($(this).is(':visible')) {
332 if ($(this).is(':visible')) {
333 tabsWidth += $(this).width() + 6;
333 tabsWidth += $(this).width() + 6;
334 }
334 }
335 });
335 });
336 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
336 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
337 el.find('div.tabs-buttons').hide();
337 el.find('div.tabs-buttons').hide();
338 } else {
338 } else {
339 el.find('div.tabs-buttons').show();
339 el.find('div.tabs-buttons').show();
340 }
340 }
341 });
341 });
342 }
342 }
343
343
344 function setPredecessorFieldsVisibility() {
344 function setPredecessorFieldsVisibility() {
345 var relationType = $('#relation_relation_type');
345 var relationType = $('#relation_relation_type');
346 if (relationType.val() == "precedes" || relationType.val() == "follows") {
346 if (relationType.val() == "precedes" || relationType.val() == "follows") {
347 $('#predecessor_fields').show();
347 $('#predecessor_fields').show();
348 } else {
348 } else {
349 $('#predecessor_fields').hide();
349 $('#predecessor_fields').hide();
350 }
350 }
351 }
351 }
352
352
353 function showModal(id, width) {
353 function showModal(id, width) {
354 var el = $('#'+id).first();
354 var el = $('#'+id).first();
355 if (el.length == 0 || el.is(':visible')) {return;}
355 if (el.length == 0 || el.is(':visible')) {return;}
356 var title = el.find('h3.title').text();
356 var title = el.find('h3.title').text();
357 el.dialog({
357 el.dialog({
358 width: width,
358 width: width,
359 modal: true,
359 modal: true,
360 resizable: false,
360 resizable: false,
361 dialogClass: 'modal',
361 dialogClass: 'modal',
362 title: title
362 title: title
363 });
363 });
364 el.find("input[type=text], input[type=submit]").first().focus();
364 el.find("input[type=text], input[type=submit]").first().focus();
365 }
365 }
366
366
367 function hideModal(el) {
367 function hideModal(el) {
368 var modal;
368 var modal;
369 if (el) {
369 if (el) {
370 modal = $(el).parents('.ui-dialog-content');
370 modal = $(el).parents('.ui-dialog-content');
371 } else {
371 } else {
372 modal = $('#ajax-modal');
372 modal = $('#ajax-modal');
373 }
373 }
374 modal.dialog("close");
374 modal.dialog("close");
375 }
375 }
376
376
377 function submitPreview(url, form, target) {
377 function submitPreview(url, form, target) {
378 $.ajax({
378 $.ajax({
379 url: url,
379 url: url,
380 type: 'post',
380 type: 'post',
381 data: $('#'+form).serialize(),
381 data: $('#'+form).serialize(),
382 success: function(data){
382 success: function(data){
383 $('#'+target).html(data);
383 $('#'+target).html(data);
384 }
384 }
385 });
385 });
386 }
386 }
387
387
388 function collapseScmEntry(id) {
388 function collapseScmEntry(id) {
389 $('.'+id).each(function() {
389 $('.'+id).each(function() {
390 if ($(this).hasClass('open')) {
390 if ($(this).hasClass('open')) {
391 collapseScmEntry($(this).attr('id'));
391 collapseScmEntry($(this).attr('id'));
392 }
392 }
393 $(this).hide();
393 $(this).hide();
394 });
394 });
395 $('#'+id).removeClass('open');
395 $('#'+id).removeClass('open');
396 }
396 }
397
397
398 function expandScmEntry(id) {
398 function expandScmEntry(id) {
399 $('.'+id).each(function() {
399 $('.'+id).each(function() {
400 $(this).show();
400 $(this).show();
401 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
401 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
402 expandScmEntry($(this).attr('id'));
402 expandScmEntry($(this).attr('id'));
403 }
403 }
404 });
404 });
405 $('#'+id).addClass('open');
405 $('#'+id).addClass('open');
406 }
406 }
407
407
408 function scmEntryClick(id, url) {
408 function scmEntryClick(id, url) {
409 el = $('#'+id);
409 el = $('#'+id);
410 if (el.hasClass('open')) {
410 if (el.hasClass('open')) {
411 collapseScmEntry(id);
411 collapseScmEntry(id);
412 el.addClass('collapsed');
412 el.addClass('collapsed');
413 return false;
413 return false;
414 } else if (el.hasClass('loaded')) {
414 } else if (el.hasClass('loaded')) {
415 expandScmEntry(id);
415 expandScmEntry(id);
416 el.removeClass('collapsed');
416 el.removeClass('collapsed');
417 return false;
417 return false;
418 }
418 }
419 if (el.hasClass('loading')) {
419 if (el.hasClass('loading')) {
420 return false;
420 return false;
421 }
421 }
422 el.addClass('loading');
422 el.addClass('loading');
423 $.ajax({
423 $.ajax({
424 url: url,
424 url: url,
425 success: function(data){
425 success: function(data){
426 el.after(data);
426 el.after(data);
427 el.addClass('open').addClass('loaded').removeClass('loading');
427 el.addClass('open').addClass('loaded').removeClass('loading');
428 }
428 }
429 });
429 });
430 return true;
430 return true;
431 }
431 }
432
432
433 function randomKey(size) {
433 function randomKey(size) {
434 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
434 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
435 var key = '';
435 var key = '';
436 for (i = 0; i < size; i++) {
436 for (i = 0; i < size; i++) {
437 key += chars[Math.floor(Math.random() * chars.length)];
437 key += chars[Math.floor(Math.random() * chars.length)];
438 }
438 }
439 return key;
439 return key;
440 }
440 }
441
441
442 // Can't use Rails' remote select because we need the form data
442 // Can't use Rails' remote select because we need the form data
443 function updateIssueFrom(url) {
443 function updateIssueFrom(url) {
444 $.ajax({
444 $.ajax({
445 url: url,
445 url: url,
446 type: 'post',
446 type: 'post',
447 data: $('#issue-form').serialize()
447 data: $('#issue-form').serialize()
448 });
448 });
449 }
449 }
450
450
451 function updateBulkEditFrom(url) {
451 function updateBulkEditFrom(url) {
452 $.ajax({
452 $.ajax({
453 url: url,
453 url: url,
454 type: 'post',
454 type: 'post',
455 data: $('#bulk_edit_form').serialize()
455 data: $('#bulk_edit_form').serialize()
456 });
456 });
457 }
457 }
458
458
459 function observeAutocompleteField(fieldId, url) {
459 function observeAutocompleteField(fieldId, url, options) {
460 $(document).ready(function() {
460 $(document).ready(function() {
461 $('#'+fieldId).autocomplete({
461 $('#'+fieldId).autocomplete($.extend({
462 source: url,
462 source: url,
463 minLength: 2
463 minLength: 2
464 });
464 }, options));
465 });
465 });
466 }
466 }
467
467
468 function observeSearchfield(fieldId, targetId, url) {
468 function observeSearchfield(fieldId, targetId, url) {
469 $('#'+fieldId).each(function() {
469 $('#'+fieldId).each(function() {
470 var $this = $(this);
470 var $this = $(this);
471 $this.attr('data-value-was', $this.val());
471 $this.attr('data-value-was', $this.val());
472 var check = function() {
472 var check = function() {
473 var val = $this.val();
473 var val = $this.val();
474 if ($this.attr('data-value-was') != val){
474 if ($this.attr('data-value-was') != val){
475 $this.attr('data-value-was', val);
475 $this.attr('data-value-was', val);
476 $.ajax({
476 $.ajax({
477 url: url,
477 url: url,
478 type: 'get',
478 type: 'get',
479 data: {q: $this.val()},
479 data: {q: $this.val()},
480 success: function(data){ $('#'+targetId).html(data); },
480 success: function(data){ $('#'+targetId).html(data); },
481 beforeSend: function(){ $this.addClass('ajax-loading'); },
481 beforeSend: function(){ $this.addClass('ajax-loading'); },
482 complete: function(){ $this.removeClass('ajax-loading'); }
482 complete: function(){ $this.removeClass('ajax-loading'); }
483 });
483 });
484 }
484 }
485 };
485 };
486 var reset = function() {
486 var reset = function() {
487 if (timer) {
487 if (timer) {
488 clearInterval(timer);
488 clearInterval(timer);
489 timer = setInterval(check, 300);
489 timer = setInterval(check, 300);
490 }
490 }
491 };
491 };
492 var timer = setInterval(check, 300);
492 var timer = setInterval(check, 300);
493 $this.bind('keyup click mousemove', reset);
493 $this.bind('keyup click mousemove', reset);
494 });
494 });
495 }
495 }
496
496
497 function observeProjectModules() {
497 function observeProjectModules() {
498 var f = function() {
498 var f = function() {
499 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
499 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
500 if ($('#project_enabled_module_names_issue_tracking').attr('checked')) {
500 if ($('#project_enabled_module_names_issue_tracking').attr('checked')) {
501 $('#project_trackers').show();
501 $('#project_trackers').show();
502 }else{
502 }else{
503 $('#project_trackers').hide();
503 $('#project_trackers').hide();
504 }
504 }
505 };
505 };
506
506
507 $(window).load(f);
507 $(window).load(f);
508 $('#project_enabled_module_names_issue_tracking').change(f);
508 $('#project_enabled_module_names_issue_tracking').change(f);
509 }
509 }
510
510
511 function initMyPageSortable(list, url) {
511 function initMyPageSortable(list, url) {
512 $('#list-'+list).sortable({
512 $('#list-'+list).sortable({
513 connectWith: '.block-receiver',
513 connectWith: '.block-receiver',
514 tolerance: 'pointer',
514 tolerance: 'pointer',
515 update: function(){
515 update: function(){
516 $.ajax({
516 $.ajax({
517 url: url,
517 url: url,
518 type: 'post',
518 type: 'post',
519 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
519 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
520 });
520 });
521 }
521 }
522 });
522 });
523 $("#list-top, #list-left, #list-right").disableSelection();
523 $("#list-top, #list-left, #list-right").disableSelection();
524 }
524 }
525
525
526 var warnLeavingUnsavedMessage;
526 var warnLeavingUnsavedMessage;
527 function warnLeavingUnsaved(message) {
527 function warnLeavingUnsaved(message) {
528 warnLeavingUnsavedMessage = message;
528 warnLeavingUnsavedMessage = message;
529
529
530 $('form').submit(function(){
530 $('form').submit(function(){
531 $('textarea').removeData('changed');
531 $('textarea').removeData('changed');
532 });
532 });
533 $('textarea').change(function(){
533 $('textarea').change(function(){
534 $(this).data('changed', 'changed');
534 $(this).data('changed', 'changed');
535 });
535 });
536 window.onbeforeunload = function(){
536 window.onbeforeunload = function(){
537 var warn = false;
537 var warn = false;
538 $('textarea').blur().each(function(){
538 $('textarea').blur().each(function(){
539 if ($(this).data('changed')) {
539 if ($(this).data('changed')) {
540 warn = true;
540 warn = true;
541 }
541 }
542 });
542 });
543 if (warn) {return warnLeavingUnsavedMessage;}
543 if (warn) {return warnLeavingUnsavedMessage;}
544 };
544 };
545 };
545 };
546
546
547 function setupAjaxIndicator() {
547 function setupAjaxIndicator() {
548
548
549 $('#ajax-indicator').bind('ajaxSend', function(event, xhr, settings) {
549 $('#ajax-indicator').bind('ajaxSend', function(event, xhr, settings) {
550
550
551 if ($('.ajax-loading').length == 0 && settings.contentType != 'application/octet-stream') {
551 if ($('.ajax-loading').length == 0 && settings.contentType != 'application/octet-stream') {
552 $('#ajax-indicator').show();
552 $('#ajax-indicator').show();
553 }
553 }
554 });
554 });
555
555
556 $('#ajax-indicator').bind('ajaxStop', function() {
556 $('#ajax-indicator').bind('ajaxStop', function() {
557 $('#ajax-indicator').hide();
557 $('#ajax-indicator').hide();
558 });
558 });
559 }
559 }
560
560
561 function hideOnLoad() {
561 function hideOnLoad() {
562 $('.hol').hide();
562 $('.hol').hide();
563 }
563 }
564
564
565 function addFormObserversForDoubleSubmit() {
565 function addFormObserversForDoubleSubmit() {
566 $('form[method=post]').each(function() {
566 $('form[method=post]').each(function() {
567 if (!$(this).hasClass('multiple-submit')) {
567 if (!$(this).hasClass('multiple-submit')) {
568 $(this).submit(function(form_submission) {
568 $(this).submit(function(form_submission) {
569 if ($(form_submission.target).attr('data-submitted')) {
569 if ($(form_submission.target).attr('data-submitted')) {
570 form_submission.preventDefault();
570 form_submission.preventDefault();
571 } else {
571 } else {
572 $(form_submission.target).attr('data-submitted', true);
572 $(form_submission.target).attr('data-submitted', true);
573 }
573 }
574 });
574 });
575 }
575 }
576 });
576 });
577 }
577 }
578
578
579 function blockEventPropagation(event) {
579 function blockEventPropagation(event) {
580 event.stopPropagation();
580 event.stopPropagation();
581 event.preventDefault();
581 event.preventDefault();
582 }
582 }
583
583
584 $(document).ready(setupAjaxIndicator);
584 $(document).ready(setupAjaxIndicator);
585 $(document).ready(hideOnLoad);
585 $(document).ready(hideOnLoad);
586 $(document).ready(addFormObserversForDoubleSubmit);
586 $(document).ready(addFormObserversForDoubleSubmit);
@@ -1,152 +1,168
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class AuthSourcesControllerTest < ActionController::TestCase
20 class AuthSourcesControllerTest < ActionController::TestCase
21 fixtures :users, :auth_sources
21 fixtures :users, :auth_sources
22
22
23 def setup
23 def setup
24 @request.session[:user_id] = 1
24 @request.session[:user_id] = 1
25 end
25 end
26
26
27 def test_index
27 def test_index
28 get :index
28 get :index
29
29
30 assert_response :success
30 assert_response :success
31 assert_template 'index'
31 assert_template 'index'
32 assert_not_nil assigns(:auth_sources)
32 assert_not_nil assigns(:auth_sources)
33 end
33 end
34
34
35 def test_new
35 def test_new
36 get :new
36 get :new
37
37
38 assert_response :success
38 assert_response :success
39 assert_template 'new'
39 assert_template 'new'
40
40
41 source = assigns(:auth_source)
41 source = assigns(:auth_source)
42 assert_equal AuthSourceLdap, source.class
42 assert_equal AuthSourceLdap, source.class
43 assert source.new_record?
43 assert source.new_record?
44
44
45 assert_select 'form#auth_source_form' do
45 assert_select 'form#auth_source_form' do
46 assert_select 'input[name=type][value=AuthSourceLdap]'
46 assert_select 'input[name=type][value=AuthSourceLdap]'
47 assert_select 'input[name=?]', 'auth_source[host]'
47 assert_select 'input[name=?]', 'auth_source[host]'
48 end
48 end
49 end
49 end
50
50
51 def test_new_with_invalid_type_should_respond_with_404
51 def test_new_with_invalid_type_should_respond_with_404
52 get :new, :type => 'foo'
52 get :new, :type => 'foo'
53 assert_response 404
53 assert_response 404
54 end
54 end
55
55
56 def test_create
56 def test_create
57 assert_difference 'AuthSourceLdap.count' do
57 assert_difference 'AuthSourceLdap.count' do
58 post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '127.0.0.1', :port => '389', :attr_login => 'cn'}
58 post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '127.0.0.1', :port => '389', :attr_login => 'cn'}
59 assert_redirected_to '/auth_sources'
59 assert_redirected_to '/auth_sources'
60 end
60 end
61
61
62 source = AuthSourceLdap.order('id DESC').first
62 source = AuthSourceLdap.order('id DESC').first
63 assert_equal 'Test', source.name
63 assert_equal 'Test', source.name
64 assert_equal '127.0.0.1', source.host
64 assert_equal '127.0.0.1', source.host
65 assert_equal 389, source.port
65 assert_equal 389, source.port
66 assert_equal 'cn', source.attr_login
66 assert_equal 'cn', source.attr_login
67 end
67 end
68
68
69 def test_create_with_failure
69 def test_create_with_failure
70 assert_no_difference 'AuthSourceLdap.count' do
70 assert_no_difference 'AuthSourceLdap.count' do
71 post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '', :port => '389', :attr_login => 'cn'}
71 post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '', :port => '389', :attr_login => 'cn'}
72 assert_response :success
72 assert_response :success
73 assert_template 'new'
73 assert_template 'new'
74 end
74 end
75 assert_error_tag :content => /host can&#x27;t be blank/i
75 assert_error_tag :content => /host can&#x27;t be blank/i
76 end
76 end
77
77
78 def test_edit
78 def test_edit
79 get :edit, :id => 1
79 get :edit, :id => 1
80
80
81 assert_response :success
81 assert_response :success
82 assert_template 'edit'
82 assert_template 'edit'
83
83
84 assert_select 'form#auth_source_form' do
84 assert_select 'form#auth_source_form' do
85 assert_select 'input[name=?]', 'auth_source[host]'
85 assert_select 'input[name=?]', 'auth_source[host]'
86 end
86 end
87 end
87 end
88
88
89 def test_edit_should_not_contain_password
89 def test_edit_should_not_contain_password
90 AuthSource.find(1).update_column :account_password, 'secret'
90 AuthSource.find(1).update_column :account_password, 'secret'
91
91
92 get :edit, :id => 1
92 get :edit, :id => 1
93 assert_response :success
93 assert_response :success
94 assert_select 'input[value=secret]', 0
94 assert_select 'input[value=secret]', 0
95 assert_select 'input[name=dummy_password][value=?]', /x+/
95 assert_select 'input[name=dummy_password][value=?]', /x+/
96 end
96 end
97
97
98 def test_edit_invalid_should_respond_with_404
98 def test_edit_invalid_should_respond_with_404
99 get :edit, :id => 99
99 get :edit, :id => 99
100 assert_response 404
100 assert_response 404
101 end
101 end
102
102
103 def test_update
103 def test_update
104 put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '192.168.0.10', :port => '389', :attr_login => 'uid'}
104 put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '192.168.0.10', :port => '389', :attr_login => 'uid'}
105 assert_redirected_to '/auth_sources'
105 assert_redirected_to '/auth_sources'
106
106
107 source = AuthSourceLdap.find(1)
107 source = AuthSourceLdap.find(1)
108 assert_equal 'Renamed', source.name
108 assert_equal 'Renamed', source.name
109 assert_equal '192.168.0.10', source.host
109 assert_equal '192.168.0.10', source.host
110 end
110 end
111
111
112 def test_update_with_failure
112 def test_update_with_failure
113 put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '', :port => '389', :attr_login => 'uid'}
113 put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '', :port => '389', :attr_login => 'uid'}
114 assert_response :success
114 assert_response :success
115 assert_template 'edit'
115 assert_template 'edit'
116 assert_error_tag :content => /host can&#x27;t be blank/i
116 assert_error_tag :content => /host can&#x27;t be blank/i
117 end
117 end
118
118
119 def test_destroy
119 def test_destroy
120 assert_difference 'AuthSourceLdap.count', -1 do
120 assert_difference 'AuthSourceLdap.count', -1 do
121 delete :destroy, :id => 1
121 delete :destroy, :id => 1
122 assert_redirected_to '/auth_sources'
122 assert_redirected_to '/auth_sources'
123 end
123 end
124 end
124 end
125
125
126 def test_destroy_auth_source_in_use
126 def test_destroy_auth_source_in_use
127 User.find(2).update_attribute :auth_source_id, 1
127 User.find(2).update_attribute :auth_source_id, 1
128
128
129 assert_no_difference 'AuthSourceLdap.count' do
129 assert_no_difference 'AuthSourceLdap.count' do
130 delete :destroy, :id => 1
130 delete :destroy, :id => 1
131 assert_redirected_to '/auth_sources'
131 assert_redirected_to '/auth_sources'
132 end
132 end
133 end
133 end
134
134
135 def test_test_connection
135 def test_test_connection
136 AuthSourceLdap.any_instance.stubs(:test_connection).returns(true)
136 AuthSourceLdap.any_instance.stubs(:test_connection).returns(true)
137
137
138 get :test_connection, :id => 1
138 get :test_connection, :id => 1
139 assert_redirected_to '/auth_sources'
139 assert_redirected_to '/auth_sources'
140 assert_not_nil flash[:notice]
140 assert_not_nil flash[:notice]
141 assert_match /successful/i, flash[:notice]
141 assert_match /successful/i, flash[:notice]
142 end
142 end
143
143
144 def test_test_connection_with_failure
144 def test_test_connection_with_failure
145 AuthSourceLdap.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError.new("Something went wrong"))
145 AuthSourceLdap.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError.new("Something went wrong"))
146
146
147 get :test_connection, :id => 1
147 get :test_connection, :id => 1
148 assert_redirected_to '/auth_sources'
148 assert_redirected_to '/auth_sources'
149 assert_not_nil flash[:error]
149 assert_not_nil flash[:error]
150 assert_include 'Something went wrong', flash[:error]
150 assert_include 'Something went wrong', flash[:error]
151 end
151 end
152
153 def test_autocomplete_for_new_user
154 AuthSource.expects(:search).with('foo').returns([
155 {:login => 'foo1', :firstname => 'John', :lastname => 'Smith', :mail => 'foo1@example.net', :auth_source_id => 1},
156 {:login => 'Smith', :firstname => 'John', :lastname => 'Doe', :mail => 'foo2@example.net', :auth_source_id => 1}
157 ])
158
159 get :autocomplete_for_new_user, :term => 'foo'
160 assert_response :success
161 assert_equal 'application/json', response.content_type
162 json = ActiveSupport::JSON.decode(response.body)
163 assert_kind_of Array, json
164 assert_equal 2, json.size
165 assert_equal 'foo1', json.first['value']
166 assert_equal 'foo1 (John Smith)', json.first['label']
167 end
152 end
168 end
@@ -1,55 +1,59
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class RoutingAuthSourcesTest < ActionController::IntegrationTest
20 class RoutingAuthSourcesTest < ActionController::IntegrationTest
21 def test_auth_sources
21 def test_auth_sources
22 assert_routing(
22 assert_routing(
23 { :method => 'get', :path => "/auth_sources" },
23 { :method => 'get', :path => "/auth_sources" },
24 { :controller => 'auth_sources', :action => 'index' }
24 { :controller => 'auth_sources', :action => 'index' }
25 )
25 )
26 assert_routing(
26 assert_routing(
27 { :method => 'get', :path => "/auth_sources/new" },
27 { :method => 'get', :path => "/auth_sources/new" },
28 { :controller => 'auth_sources', :action => 'new' }
28 { :controller => 'auth_sources', :action => 'new' }
29 )
29 )
30 assert_routing(
30 assert_routing(
31 { :method => 'post', :path => "/auth_sources" },
31 { :method => 'post', :path => "/auth_sources" },
32 { :controller => 'auth_sources', :action => 'create' }
32 { :controller => 'auth_sources', :action => 'create' }
33 )
33 )
34 assert_routing(
34 assert_routing(
35 { :method => 'get', :path => "/auth_sources/1234/edit" },
35 { :method => 'get', :path => "/auth_sources/1234/edit" },
36 { :controller => 'auth_sources', :action => 'edit',
36 { :controller => 'auth_sources', :action => 'edit',
37 :id => '1234' }
37 :id => '1234' }
38 )
38 )
39 assert_routing(
39 assert_routing(
40 { :method => 'put', :path => "/auth_sources/1234" },
40 { :method => 'put', :path => "/auth_sources/1234" },
41 { :controller => 'auth_sources', :action => 'update',
41 { :controller => 'auth_sources', :action => 'update',
42 :id => '1234' }
42 :id => '1234' }
43 )
43 )
44 assert_routing(
44 assert_routing(
45 { :method => 'delete', :path => "/auth_sources/1234" },
45 { :method => 'delete', :path => "/auth_sources/1234" },
46 { :controller => 'auth_sources', :action => 'destroy',
46 { :controller => 'auth_sources', :action => 'destroy',
47 :id => '1234' }
47 :id => '1234' }
48 )
48 )
49 assert_routing(
49 assert_routing(
50 { :method => 'get', :path => "/auth_sources/1234/test_connection" },
50 { :method => 'get', :path => "/auth_sources/1234/test_connection" },
51 { :controller => 'auth_sources', :action => 'test_connection',
51 { :controller => 'auth_sources', :action => 'test_connection',
52 :id => '1234' }
52 :id => '1234' }
53 )
53 )
54 assert_routing(
55 { :method => 'get', :path => "/auth_sources/autocomplete_for_new_user" },
56 { :controller => 'auth_sources', :action => 'autocomplete_for_new_user' }
57 )
54 end
58 end
55 end
59 end
@@ -1,130 +1,154
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class AuthSourceLdapTest < ActiveSupport::TestCase
20 class AuthSourceLdapTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22 fixtures :auth_sources
22 fixtures :auth_sources
23
23
24 def setup
24 def setup
25 end
25 end
26
26
27 def test_create
27 def test_create
28 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName')
28 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName')
29 assert a.save
29 assert a.save
30 end
30 end
31
31
32 def test_should_strip_ldap_attributes
32 def test_should_strip_ldap_attributes
33 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
33 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
34 :attr_firstname => 'givenName ')
34 :attr_firstname => 'givenName ')
35 assert a.save
35 assert a.save
36 assert_equal 'givenName', a.reload.attr_firstname
36 assert_equal 'givenName', a.reload.attr_firstname
37 end
37 end
38
38
39 def test_replace_port_zero_to_389
39 def test_replace_port_zero_to_389
40 a = AuthSourceLdap.new(
40 a = AuthSourceLdap.new(
41 :name => 'My LDAP', :host => 'ldap.example.net', :port => 0,
41 :name => 'My LDAP', :host => 'ldap.example.net', :port => 0,
42 :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
42 :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
43 :attr_firstname => 'givenName ')
43 :attr_firstname => 'givenName ')
44 assert a.save
44 assert a.save
45 assert_equal 389, a.port
45 assert_equal 389, a.port
46 end
46 end
47
47
48 def test_filter_should_be_validated
48 def test_filter_should_be_validated
49 set_language_if_valid 'en'
49 set_language_if_valid 'en'
50
50
51 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :attr_login => 'sn')
51 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :attr_login => 'sn')
52 a.filter = "(mail=*@redmine.org"
52 a.filter = "(mail=*@redmine.org"
53 assert !a.valid?
53 assert !a.valid?
54 assert_include "LDAP filter is invalid", a.errors.full_messages
54 assert_include "LDAP filter is invalid", a.errors.full_messages
55
55
56 a.filter = "(mail=*@redmine.org)"
56 a.filter = "(mail=*@redmine.org)"
57 assert a.valid?
57 assert a.valid?
58 end
58 end
59
59
60 if ldap_configured?
60 if ldap_configured?
61 context '#authenticate' do
61 context '#authenticate' do
62 setup do
62 setup do
63 @auth = AuthSourceLdap.find(1)
63 @auth = AuthSourceLdap.find(1)
64 @auth.update_attribute :onthefly_register, true
64 @auth.update_attribute :onthefly_register, true
65 end
65 end
66
66
67 context 'with a valid LDAP user' do
67 context 'with a valid LDAP user' do
68 should 'return the user attributes' do
68 should 'return the user attributes' do
69 attributes = @auth.authenticate('example1','123456')
69 attributes = @auth.authenticate('example1','123456')
70 assert attributes.is_a?(Hash), "An hash was not returned"
70 assert attributes.is_a?(Hash), "An hash was not returned"
71 assert_equal 'Example', attributes[:firstname]
71 assert_equal 'Example', attributes[:firstname]
72 assert_equal 'One', attributes[:lastname]
72 assert_equal 'One', attributes[:lastname]
73 assert_equal 'example1@redmine.org', attributes[:mail]
73 assert_equal 'example1@redmine.org', attributes[:mail]
74 assert_equal @auth.id, attributes[:auth_source_id]
74 assert_equal @auth.id, attributes[:auth_source_id]
75 attributes.keys.each do |attribute|
75 attributes.keys.each do |attribute|
76 assert User.new.respond_to?("#{attribute}="), "Unexpected :#{attribute} attribute returned"
76 assert User.new.respond_to?("#{attribute}="), "Unexpected :#{attribute} attribute returned"
77 end
77 end
78 end
78 end
79 end
79 end
80
80
81 context 'with an invalid LDAP user' do
81 context 'with an invalid LDAP user' do
82 should 'return nil' do
82 should 'return nil' do
83 assert_equal nil, @auth.authenticate('nouser','123456')
83 assert_equal nil, @auth.authenticate('nouser','123456')
84 end
84 end
85 end
85 end
86
86
87 context 'without a login' do
87 context 'without a login' do
88 should 'return nil' do
88 should 'return nil' do
89 assert_equal nil, @auth.authenticate('','123456')
89 assert_equal nil, @auth.authenticate('','123456')
90 end
90 end
91 end
91 end
92
92
93 context 'without a password' do
93 context 'without a password' do
94 should 'return nil' do
94 should 'return nil' do
95 assert_equal nil, @auth.authenticate('edavis','')
95 assert_equal nil, @auth.authenticate('edavis','')
96 end
96 end
97 end
97 end
98
98
99 context 'without filter' do
99 context 'without filter' do
100 should 'return any user' do
100 should 'return any user' do
101 assert @auth.authenticate('example1','123456')
101 assert @auth.authenticate('example1','123456')
102 assert @auth.authenticate('edavis', '123456')
102 assert @auth.authenticate('edavis', '123456')
103 end
103 end
104 end
104 end
105
105
106 context 'with filter' do
106 context 'with filter' do
107 setup do
107 setup do
108 @auth.filter = "(mail=*@redmine.org)"
108 @auth.filter = "(mail=*@redmine.org)"
109 end
109 end
110
110
111 should 'return user who matches the filter only' do
111 should 'return user who matches the filter only' do
112 assert @auth.authenticate('example1','123456')
112 assert @auth.authenticate('example1','123456')
113 assert_nil @auth.authenticate('edavis', '123456')
113 assert_nil @auth.authenticate('edavis', '123456')
114 end
114 end
115 end
115 end
116 end
116 end
117
117
118 def test_authenticate_should_timeout
118 def test_authenticate_should_timeout
119 auth_source = AuthSourceLdap.find(1)
119 auth_source = AuthSourceLdap.find(1)
120 auth_source.timeout = 1
120 auth_source.timeout = 1
121 def auth_source.initialize_ldap_con(*args); sleep(5); end
121 def auth_source.initialize_ldap_con(*args); sleep(5); end
122
122
123 assert_raise AuthSourceTimeoutException do
123 assert_raise AuthSourceTimeoutException do
124 auth_source.authenticate 'example1', '123456'
124 auth_source.authenticate 'example1', '123456'
125 end
125 end
126 end
126 end
127
128 def test_search_should_return_matching_entries
129 results = AuthSource.search("exa")
130 assert_equal 1, results.size
131 result = results.first
132 assert_kind_of Hash, result
133 assert_equal "example1", result[:login]
134 assert_equal "Example", result[:firstname]
135 assert_equal "One", result[:lastname]
136 assert_equal "example1@redmine.org", result[:mail]
137 assert_equal 1, result[:auth_source_id]
138 end
139
140 def test_search_with_no_match_should_return_an_empty_array
141 results = AuthSource.search("wro")
142 assert_equal [], results
143 end
144
145 def test_search_with_exception_should_return_an_empty_array
146 Net::LDAP.stubs(:new).raises(Net::LDAP::LdapError, 'Cannot connect')
147
148 results = AuthSource.search("exa")
149 assert_equal [], results
150 end
127 else
151 else
128 puts '(Test LDAP server not configured)'
152 puts '(Test LDAP server not configured)'
129 end
153 end
130 end
154 end
General Comments 0
You need to be logged in to leave comments. Login now