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