##// END OF EJS Templates
Adds "sorted" scope to Principal and User and sort users/groups properly....
Jean-Philippe Lang -
r11029:e53a5de918c1
parent child
Show More
@@ -1,95 +1,95
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 WatchersController < ApplicationController
18 class WatchersController < ApplicationController
19 before_filter :find_project
19 before_filter :find_project
20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
21 before_filter :authorize, :only => [:new, :destroy]
21 before_filter :authorize, :only => [:new, :destroy]
22
22
23 def watch
23 def watch
24 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
24 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
25 render_403
25 render_403
26 else
26 else
27 set_watcher(User.current, true)
27 set_watcher(User.current, true)
28 end
28 end
29 end
29 end
30
30
31 def unwatch
31 def unwatch
32 set_watcher(User.current, false)
32 set_watcher(User.current, false)
33 end
33 end
34
34
35 def new
35 def new
36 end
36 end
37
37
38 def create
38 def create
39 if params[:watcher].is_a?(Hash) && request.post?
39 if params[:watcher].is_a?(Hash) && request.post?
40 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
40 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
41 user_ids.each do |user_id|
41 user_ids.each do |user_id|
42 Watcher.create(:watchable => @watched, :user_id => user_id)
42 Watcher.create(:watchable => @watched, :user_id => user_id)
43 end
43 end
44 end
44 end
45 respond_to do |format|
45 respond_to do |format|
46 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
46 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
47 format.js
47 format.js
48 end
48 end
49 end
49 end
50
50
51 def append
51 def append
52 if params[:watcher].is_a?(Hash)
52 if params[:watcher].is_a?(Hash)
53 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
53 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
54 @users = User.active.find_all_by_id(user_ids)
54 @users = User.active.find_all_by_id(user_ids)
55 end
55 end
56 end
56 end
57
57
58 def destroy
58 def destroy
59 @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
59 @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
60 respond_to do |format|
60 respond_to do |format|
61 format.html { redirect_to :back }
61 format.html { redirect_to :back }
62 format.js
62 format.js
63 end
63 end
64 end
64 end
65
65
66 def autocomplete_for_user
66 def autocomplete_for_user
67 @users = User.active.like(params[:q]).limit(100).all
67 @users = User.active.sorted.like(params[:q]).limit(100).all
68 if @watched
68 if @watched
69 @users -= @watched.watcher_users
69 @users -= @watched.watcher_users
70 end
70 end
71 render :layout => false
71 render :layout => false
72 end
72 end
73
73
74 private
74 private
75 def find_project
75 def find_project
76 if params[:object_type] && params[:object_id]
76 if params[:object_type] && params[:object_id]
77 klass = Object.const_get(params[:object_type].camelcase)
77 klass = Object.const_get(params[:object_type].camelcase)
78 return false unless klass.respond_to?('watched_by')
78 return false unless klass.respond_to?('watched_by')
79 @watched = klass.find(params[:object_id])
79 @watched = klass.find(params[:object_id])
80 @project = @watched.project
80 @project = @watched.project
81 elsif params[:project_id]
81 elsif params[:project_id]
82 @project = Project.visible.find_by_param(params[:project_id])
82 @project = Project.visible.find_by_param(params[:project_id])
83 end
83 end
84 rescue
84 rescue
85 render_404
85 render_404
86 end
86 end
87
87
88 def set_watcher(user, watching)
88 def set_watcher(user, watching)
89 @watched.set_watcher(user, watching)
89 @watched.set_watcher(user, watching)
90 respond_to do |format|
90 respond_to do |format|
91 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
91 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
92 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => @watched} }
92 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => @watched} }
93 end
93 end
94 end
94 end
95 end
95 end
@@ -1,1240 +1,1240
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28
28
29 extend Forwardable
29 extend Forwardable
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31
31
32 # Return true if user is authorized for controller/action, otherwise false
32 # Return true if user is authorized for controller/action, otherwise false
33 def authorize_for(controller, action)
33 def authorize_for(controller, action)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 end
35 end
36
36
37 # Display a link if user is authorized
37 # Display a link if user is authorized
38 #
38 #
39 # @param [String] name Anchor text (passed to link_to)
39 # @param [String] name Anchor text (passed to link_to)
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active? || (User.current.admin? && user.logged?)
51 if user.active? || (User.current.admin? && user.logged?)
52 link_to name, user_path(user), :class => user.css_classes
52 link_to name, user_path(user), :class => user.css_classes
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 #
69 #
70 def link_to_issue(issue, options={})
70 def link_to_issue(issue, options={})
71 title = nil
71 title = nil
72 subject = nil
72 subject = nil
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 if options[:subject] == false
74 if options[:subject] == false
75 title = truncate(issue.subject, :length => 60)
75 title = truncate(issue.subject, :length => 60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if options[:truncate]
78 if options[:truncate]
79 subject = truncate(subject, :length => options[:truncate])
79 subject = truncate(subject, :length => options[:truncate])
80 end
80 end
81 end
81 end
82 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
82 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
83 s << h(": #{subject}") if subject
83 s << h(": #{subject}") if subject
84 s = h("#{issue.project} - ") + s if options[:project]
84 s = h("#{issue.project} - ") + s if options[:project]
85 s
85 s
86 end
86 end
87
87
88 # Generates a link to an attachment.
88 # Generates a link to an attachment.
89 # Options:
89 # Options:
90 # * :text - Link text (default to attachment filename)
90 # * :text - Link text (default to attachment filename)
91 # * :download - Force download (default: false)
91 # * :download - Force download (default: false)
92 def link_to_attachment(attachment, options={})
92 def link_to_attachment(attachment, options={})
93 text = options.delete(:text) || attachment.filename
93 text = options.delete(:text) || attachment.filename
94 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
94 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
95 html_options = options.slice!(:only_path)
95 html_options = options.slice!(:only_path)
96 url = send(route_method, attachment, attachment.filename, options)
96 url = send(route_method, attachment, attachment.filename, options)
97 link_to text, url, html_options
97 link_to text, url, html_options
98 end
98 end
99
99
100 # Generates a link to a SCM revision
100 # Generates a link to a SCM revision
101 # Options:
101 # Options:
102 # * :text - Link text (default to the formatted revision)
102 # * :text - Link text (default to the formatted revision)
103 def link_to_revision(revision, repository, options={})
103 def link_to_revision(revision, repository, options={})
104 if repository.is_a?(Project)
104 if repository.is_a?(Project)
105 repository = repository.repository
105 repository = repository.repository
106 end
106 end
107 text = options.delete(:text) || format_revision(revision)
107 text = options.delete(:text) || format_revision(revision)
108 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
109 link_to(
109 link_to(
110 h(text),
110 h(text),
111 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
111 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
112 :title => l(:label_revision_id, format_revision(revision))
112 :title => l(:label_revision_id, format_revision(revision))
113 )
113 )
114 end
114 end
115
115
116 # Generates a link to a message
116 # Generates a link to a message
117 def link_to_message(message, options={}, html_options = nil)
117 def link_to_message(message, options={}, html_options = nil)
118 link_to(
118 link_to(
119 truncate(message.subject, :length => 60),
119 truncate(message.subject, :length => 60),
120 board_message_path(message.board_id, message.parent_id || message.id, {
120 board_message_path(message.board_id, message.parent_id || message.id, {
121 :r => (message.parent_id && message.id),
121 :r => (message.parent_id && message.id),
122 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
123 }.merge(options)),
123 }.merge(options)),
124 html_options
124 html_options
125 )
125 )
126 end
126 end
127
127
128 # Generates a link to a project if active
128 # Generates a link to a project if active
129 # Examples:
129 # Examples:
130 #
130 #
131 # link_to_project(project) # => link to the specified project overview
131 # link_to_project(project) # => link to the specified project overview
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 #
134 #
135 def link_to_project(project, options={}, html_options = nil)
135 def link_to_project(project, options={}, html_options = nil)
136 if project.archived?
136 if project.archived?
137 h(project.name)
137 h(project.name)
138 elsif options.key?(:action)
138 elsif options.key?(:action)
139 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
139 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
140 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
140 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 link_to project.name, url, html_options
141 link_to project.name, url, html_options
142 else
142 else
143 link_to project.name, project_path(project, options), html_options
143 link_to project.name, project_path(project, options), html_options
144 end
144 end
145 end
145 end
146
146
147 # Generates a link to a project settings if active
147 # Generates a link to a project settings if active
148 def link_to_project_settings(project, options={}, html_options=nil)
148 def link_to_project_settings(project, options={}, html_options=nil)
149 if project.active?
149 if project.active?
150 link_to project.name, settings_project_path(project, options), html_options
150 link_to project.name, settings_project_path(project, options), html_options
151 elsif project.archived?
151 elsif project.archived?
152 h(project.name)
152 h(project.name)
153 else
153 else
154 link_to project.name, project_path(project, options), html_options
154 link_to project.name, project_path(project, options), html_options
155 end
155 end
156 end
156 end
157
157
158 def wiki_page_path(page, options={})
158 def wiki_page_path(page, options={})
159 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
159 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
160 end
160 end
161
161
162 def thumbnail_tag(attachment)
162 def thumbnail_tag(attachment)
163 link_to image_tag(thumbnail_path(attachment)),
163 link_to image_tag(thumbnail_path(attachment)),
164 named_attachment_path(attachment, attachment.filename),
164 named_attachment_path(attachment, attachment.filename),
165 :title => attachment.filename
165 :title => attachment.filename
166 end
166 end
167
167
168 def toggle_link(name, id, options={})
168 def toggle_link(name, id, options={})
169 onclick = "$('##{id}').toggle(); "
169 onclick = "$('##{id}').toggle(); "
170 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
170 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
171 onclick << "return false;"
171 onclick << "return false;"
172 link_to(name, "#", :onclick => onclick)
172 link_to(name, "#", :onclick => onclick)
173 end
173 end
174
174
175 def image_to_function(name, function, html_options = {})
175 def image_to_function(name, function, html_options = {})
176 html_options.symbolize_keys!
176 html_options.symbolize_keys!
177 tag(:input, html_options.merge({
177 tag(:input, html_options.merge({
178 :type => "image", :src => image_path(name),
178 :type => "image", :src => image_path(name),
179 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
179 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
180 }))
180 }))
181 end
181 end
182
182
183 def format_activity_title(text)
183 def format_activity_title(text)
184 h(truncate_single_line(text, :length => 100))
184 h(truncate_single_line(text, :length => 100))
185 end
185 end
186
186
187 def format_activity_day(date)
187 def format_activity_day(date)
188 date == User.current.today ? l(:label_today).titleize : format_date(date)
188 date == User.current.today ? l(:label_today).titleize : format_date(date)
189 end
189 end
190
190
191 def format_activity_description(text)
191 def format_activity_description(text)
192 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
192 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
193 ).gsub(/[\r\n]+/, "<br />").html_safe
193 ).gsub(/[\r\n]+/, "<br />").html_safe
194 end
194 end
195
195
196 def format_version_name(version)
196 def format_version_name(version)
197 if version.project == @project
197 if version.project == @project
198 h(version)
198 h(version)
199 else
199 else
200 h("#{version.project} - #{version}")
200 h("#{version.project} - #{version}")
201 end
201 end
202 end
202 end
203
203
204 def due_date_distance_in_words(date)
204 def due_date_distance_in_words(date)
205 if date
205 if date
206 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
206 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
207 end
207 end
208 end
208 end
209
209
210 # Renders a tree of projects as a nested set of unordered lists
210 # Renders a tree of projects as a nested set of unordered lists
211 # The given collection may be a subset of the whole project tree
211 # The given collection may be a subset of the whole project tree
212 # (eg. some intermediate nodes are private and can not be seen)
212 # (eg. some intermediate nodes are private and can not be seen)
213 def render_project_nested_lists(projects)
213 def render_project_nested_lists(projects)
214 s = ''
214 s = ''
215 if projects.any?
215 if projects.any?
216 ancestors = []
216 ancestors = []
217 original_project = @project
217 original_project = @project
218 projects.sort_by(&:lft).each do |project|
218 projects.sort_by(&:lft).each do |project|
219 # set the project environment to please macros.
219 # set the project environment to please macros.
220 @project = project
220 @project = project
221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
222 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
222 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
223 else
223 else
224 ancestors.pop
224 ancestors.pop
225 s << "</li>"
225 s << "</li>"
226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
227 ancestors.pop
227 ancestors.pop
228 s << "</ul></li>\n"
228 s << "</ul></li>\n"
229 end
229 end
230 end
230 end
231 classes = (ancestors.empty? ? 'root' : 'child')
231 classes = (ancestors.empty? ? 'root' : 'child')
232 s << "<li class='#{classes}'><div class='#{classes}'>"
232 s << "<li class='#{classes}'><div class='#{classes}'>"
233 s << h(block_given? ? yield(project) : project.name)
233 s << h(block_given? ? yield(project) : project.name)
234 s << "</div>\n"
234 s << "</div>\n"
235 ancestors << project
235 ancestors << project
236 end
236 end
237 s << ("</li></ul>\n" * ancestors.size)
237 s << ("</li></ul>\n" * ancestors.size)
238 @project = original_project
238 @project = original_project
239 end
239 end
240 s.html_safe
240 s.html_safe
241 end
241 end
242
242
243 def render_page_hierarchy(pages, node=nil, options={})
243 def render_page_hierarchy(pages, node=nil, options={})
244 content = ''
244 content = ''
245 if pages[node]
245 if pages[node]
246 content << "<ul class=\"pages-hierarchy\">\n"
246 content << "<ul class=\"pages-hierarchy\">\n"
247 pages[node].each do |page|
247 pages[node].each do |page|
248 content << "<li>"
248 content << "<li>"
249 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
249 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
250 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
250 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
251 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
251 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
252 content << "</li>\n"
252 content << "</li>\n"
253 end
253 end
254 content << "</ul>\n"
254 content << "</ul>\n"
255 end
255 end
256 content.html_safe
256 content.html_safe
257 end
257 end
258
258
259 # Renders flash messages
259 # Renders flash messages
260 def render_flash_messages
260 def render_flash_messages
261 s = ''
261 s = ''
262 flash.each do |k,v|
262 flash.each do |k,v|
263 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
263 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
264 end
264 end
265 s.html_safe
265 s.html_safe
266 end
266 end
267
267
268 # Renders tabs and their content
268 # Renders tabs and their content
269 def render_tabs(tabs)
269 def render_tabs(tabs)
270 if tabs.any?
270 if tabs.any?
271 render :partial => 'common/tabs', :locals => {:tabs => tabs}
271 render :partial => 'common/tabs', :locals => {:tabs => tabs}
272 else
272 else
273 content_tag 'p', l(:label_no_data), :class => "nodata"
273 content_tag 'p', l(:label_no_data), :class => "nodata"
274 end
274 end
275 end
275 end
276
276
277 # Renders the project quick-jump box
277 # Renders the project quick-jump box
278 def render_project_jump_box
278 def render_project_jump_box
279 return unless User.current.logged?
279 return unless User.current.logged?
280 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
280 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
281 if projects.any?
281 if projects.any?
282 options =
282 options =
283 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
283 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
284 '<option value="" disabled="disabled">---</option>').html_safe
284 '<option value="" disabled="disabled">---</option>').html_safe
285
285
286 options << project_tree_options_for_select(projects, :selected => @project) do |p|
286 options << project_tree_options_for_select(projects, :selected => @project) do |p|
287 { :value => project_path(:id => p, :jump => current_menu_item) }
287 { :value => project_path(:id => p, :jump => current_menu_item) }
288 end
288 end
289
289
290 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
290 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
291 end
291 end
292 end
292 end
293
293
294 def project_tree_options_for_select(projects, options = {})
294 def project_tree_options_for_select(projects, options = {})
295 s = ''
295 s = ''
296 project_tree(projects) do |project, level|
296 project_tree(projects) do |project, level|
297 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
297 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
298 tag_options = {:value => project.id}
298 tag_options = {:value => project.id}
299 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
299 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
300 tag_options[:selected] = 'selected'
300 tag_options[:selected] = 'selected'
301 else
301 else
302 tag_options[:selected] = nil
302 tag_options[:selected] = nil
303 end
303 end
304 tag_options.merge!(yield(project)) if block_given?
304 tag_options.merge!(yield(project)) if block_given?
305 s << content_tag('option', name_prefix + h(project), tag_options)
305 s << content_tag('option', name_prefix + h(project), tag_options)
306 end
306 end
307 s.html_safe
307 s.html_safe
308 end
308 end
309
309
310 # Yields the given block for each project with its level in the tree
310 # Yields the given block for each project with its level in the tree
311 #
311 #
312 # Wrapper for Project#project_tree
312 # Wrapper for Project#project_tree
313 def project_tree(projects, &block)
313 def project_tree(projects, &block)
314 Project.project_tree(projects, &block)
314 Project.project_tree(projects, &block)
315 end
315 end
316
316
317 def principals_check_box_tags(name, principals)
317 def principals_check_box_tags(name, principals)
318 s = ''
318 s = ''
319 principals.sort.each do |principal|
319 principals.each do |principal|
320 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
320 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
321 end
321 end
322 s.html_safe
322 s.html_safe
323 end
323 end
324
324
325 # Returns a string for users/groups option tags
325 # Returns a string for users/groups option tags
326 def principals_options_for_select(collection, selected=nil)
326 def principals_options_for_select(collection, selected=nil)
327 s = ''
327 s = ''
328 if collection.include?(User.current)
328 if collection.include?(User.current)
329 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
329 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
330 end
330 end
331 groups = ''
331 groups = ''
332 collection.sort.each do |element|
332 collection.sort.each do |element|
333 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
333 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
334 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
334 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
335 end
335 end
336 unless groups.empty?
336 unless groups.empty?
337 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
337 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
338 end
338 end
339 s.html_safe
339 s.html_safe
340 end
340 end
341
341
342 # Options for the new membership projects combo-box
342 # Options for the new membership projects combo-box
343 def options_for_membership_project_select(principal, projects)
343 def options_for_membership_project_select(principal, projects)
344 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
344 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
345 options << project_tree_options_for_select(projects) do |p|
345 options << project_tree_options_for_select(projects) do |p|
346 {:disabled => principal.projects.include?(p)}
346 {:disabled => principal.projects.include?(p)}
347 end
347 end
348 options
348 options
349 end
349 end
350
350
351 # Truncates and returns the string as a single line
351 # Truncates and returns the string as a single line
352 def truncate_single_line(string, *args)
352 def truncate_single_line(string, *args)
353 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
353 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
354 end
354 end
355
355
356 # Truncates at line break after 250 characters or options[:length]
356 # Truncates at line break after 250 characters or options[:length]
357 def truncate_lines(string, options={})
357 def truncate_lines(string, options={})
358 length = options[:length] || 250
358 length = options[:length] || 250
359 if string.to_s =~ /\A(.{#{length}}.*?)$/m
359 if string.to_s =~ /\A(.{#{length}}.*?)$/m
360 "#{$1}..."
360 "#{$1}..."
361 else
361 else
362 string
362 string
363 end
363 end
364 end
364 end
365
365
366 def anchor(text)
366 def anchor(text)
367 text.to_s.gsub(' ', '_')
367 text.to_s.gsub(' ', '_')
368 end
368 end
369
369
370 def html_hours(text)
370 def html_hours(text)
371 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
371 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
372 end
372 end
373
373
374 def authoring(created, author, options={})
374 def authoring(created, author, options={})
375 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
375 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
376 end
376 end
377
377
378 def time_tag(time)
378 def time_tag(time)
379 text = distance_of_time_in_words(Time.now, time)
379 text = distance_of_time_in_words(Time.now, time)
380 if @project
380 if @project
381 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
381 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
382 else
382 else
383 content_tag('acronym', text, :title => format_time(time))
383 content_tag('acronym', text, :title => format_time(time))
384 end
384 end
385 end
385 end
386
386
387 def syntax_highlight_lines(name, content)
387 def syntax_highlight_lines(name, content)
388 lines = []
388 lines = []
389 syntax_highlight(name, content).each_line { |line| lines << line }
389 syntax_highlight(name, content).each_line { |line| lines << line }
390 lines
390 lines
391 end
391 end
392
392
393 def syntax_highlight(name, content)
393 def syntax_highlight(name, content)
394 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
394 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
395 end
395 end
396
396
397 def to_path_param(path)
397 def to_path_param(path)
398 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
398 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
399 str.blank? ? nil : str
399 str.blank? ? nil : str
400 end
400 end
401
401
402 def reorder_links(name, url, method = :post)
402 def reorder_links(name, url, method = :post)
403 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
403 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
404 url.merge({"#{name}[move_to]" => 'highest'}),
404 url.merge({"#{name}[move_to]" => 'highest'}),
405 :method => method, :title => l(:label_sort_highest)) +
405 :method => method, :title => l(:label_sort_highest)) +
406 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
406 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
407 url.merge({"#{name}[move_to]" => 'higher'}),
407 url.merge({"#{name}[move_to]" => 'higher'}),
408 :method => method, :title => l(:label_sort_higher)) +
408 :method => method, :title => l(:label_sort_higher)) +
409 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
409 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
410 url.merge({"#{name}[move_to]" => 'lower'}),
410 url.merge({"#{name}[move_to]" => 'lower'}),
411 :method => method, :title => l(:label_sort_lower)) +
411 :method => method, :title => l(:label_sort_lower)) +
412 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
412 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
413 url.merge({"#{name}[move_to]" => 'lowest'}),
413 url.merge({"#{name}[move_to]" => 'lowest'}),
414 :method => method, :title => l(:label_sort_lowest))
414 :method => method, :title => l(:label_sort_lowest))
415 end
415 end
416
416
417 def breadcrumb(*args)
417 def breadcrumb(*args)
418 elements = args.flatten
418 elements = args.flatten
419 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
419 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
420 end
420 end
421
421
422 def other_formats_links(&block)
422 def other_formats_links(&block)
423 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
423 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
424 yield Redmine::Views::OtherFormatsBuilder.new(self)
424 yield Redmine::Views::OtherFormatsBuilder.new(self)
425 concat('</p>'.html_safe)
425 concat('</p>'.html_safe)
426 end
426 end
427
427
428 def page_header_title
428 def page_header_title
429 if @project.nil? || @project.new_record?
429 if @project.nil? || @project.new_record?
430 h(Setting.app_title)
430 h(Setting.app_title)
431 else
431 else
432 b = []
432 b = []
433 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
433 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
434 if ancestors.any?
434 if ancestors.any?
435 root = ancestors.shift
435 root = ancestors.shift
436 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
436 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
437 if ancestors.size > 2
437 if ancestors.size > 2
438 b << "\xe2\x80\xa6"
438 b << "\xe2\x80\xa6"
439 ancestors = ancestors[-2, 2]
439 ancestors = ancestors[-2, 2]
440 end
440 end
441 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
441 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
442 end
442 end
443 b << h(@project)
443 b << h(@project)
444 b.join(" \xc2\xbb ").html_safe
444 b.join(" \xc2\xbb ").html_safe
445 end
445 end
446 end
446 end
447
447
448 def html_title(*args)
448 def html_title(*args)
449 if args.empty?
449 if args.empty?
450 title = @html_title || []
450 title = @html_title || []
451 title << @project.name if @project
451 title << @project.name if @project
452 title << Setting.app_title unless Setting.app_title == title.last
452 title << Setting.app_title unless Setting.app_title == title.last
453 title.select {|t| !t.blank? }.join(' - ')
453 title.select {|t| !t.blank? }.join(' - ')
454 else
454 else
455 @html_title ||= []
455 @html_title ||= []
456 @html_title += args
456 @html_title += args
457 end
457 end
458 end
458 end
459
459
460 # Returns the theme, controller name, and action as css classes for the
460 # Returns the theme, controller name, and action as css classes for the
461 # HTML body.
461 # HTML body.
462 def body_css_classes
462 def body_css_classes
463 css = []
463 css = []
464 if theme = Redmine::Themes.theme(Setting.ui_theme)
464 if theme = Redmine::Themes.theme(Setting.ui_theme)
465 css << 'theme-' + theme.name
465 css << 'theme-' + theme.name
466 end
466 end
467
467
468 css << 'controller-' + controller_name
468 css << 'controller-' + controller_name
469 css << 'action-' + action_name
469 css << 'action-' + action_name
470 css.join(' ')
470 css.join(' ')
471 end
471 end
472
472
473 def accesskey(s)
473 def accesskey(s)
474 Redmine::AccessKeys.key_for s
474 Redmine::AccessKeys.key_for s
475 end
475 end
476
476
477 # Formats text according to system settings.
477 # Formats text according to system settings.
478 # 2 ways to call this method:
478 # 2 ways to call this method:
479 # * with a String: textilizable(text, options)
479 # * with a String: textilizable(text, options)
480 # * with an object and one of its attribute: textilizable(issue, :description, options)
480 # * with an object and one of its attribute: textilizable(issue, :description, options)
481 def textilizable(*args)
481 def textilizable(*args)
482 options = args.last.is_a?(Hash) ? args.pop : {}
482 options = args.last.is_a?(Hash) ? args.pop : {}
483 case args.size
483 case args.size
484 when 1
484 when 1
485 obj = options[:object]
485 obj = options[:object]
486 text = args.shift
486 text = args.shift
487 when 2
487 when 2
488 obj = args.shift
488 obj = args.shift
489 attr = args.shift
489 attr = args.shift
490 text = obj.send(attr).to_s
490 text = obj.send(attr).to_s
491 else
491 else
492 raise ArgumentError, 'invalid arguments to textilizable'
492 raise ArgumentError, 'invalid arguments to textilizable'
493 end
493 end
494 return '' if text.blank?
494 return '' if text.blank?
495 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
495 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
496 only_path = options.delete(:only_path) == false ? false : true
496 only_path = options.delete(:only_path) == false ? false : true
497
497
498 text = text.dup
498 text = text.dup
499 macros = catch_macros(text)
499 macros = catch_macros(text)
500 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
500 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
501
501
502 @parsed_headings = []
502 @parsed_headings = []
503 @heading_anchors = {}
503 @heading_anchors = {}
504 @current_section = 0 if options[:edit_section_links]
504 @current_section = 0 if options[:edit_section_links]
505
505
506 parse_sections(text, project, obj, attr, only_path, options)
506 parse_sections(text, project, obj, attr, only_path, options)
507 text = parse_non_pre_blocks(text, obj, macros) do |text|
507 text = parse_non_pre_blocks(text, obj, macros) do |text|
508 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
508 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
509 send method_name, text, project, obj, attr, only_path, options
509 send method_name, text, project, obj, attr, only_path, options
510 end
510 end
511 end
511 end
512 parse_headings(text, project, obj, attr, only_path, options)
512 parse_headings(text, project, obj, attr, only_path, options)
513
513
514 if @parsed_headings.any?
514 if @parsed_headings.any?
515 replace_toc(text, @parsed_headings)
515 replace_toc(text, @parsed_headings)
516 end
516 end
517
517
518 text.html_safe
518 text.html_safe
519 end
519 end
520
520
521 def parse_non_pre_blocks(text, obj, macros)
521 def parse_non_pre_blocks(text, obj, macros)
522 s = StringScanner.new(text)
522 s = StringScanner.new(text)
523 tags = []
523 tags = []
524 parsed = ''
524 parsed = ''
525 while !s.eos?
525 while !s.eos?
526 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
526 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
527 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
527 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
528 if tags.empty?
528 if tags.empty?
529 yield text
529 yield text
530 inject_macros(text, obj, macros) if macros.any?
530 inject_macros(text, obj, macros) if macros.any?
531 else
531 else
532 inject_macros(text, obj, macros, false) if macros.any?
532 inject_macros(text, obj, macros, false) if macros.any?
533 end
533 end
534 parsed << text
534 parsed << text
535 if tag
535 if tag
536 if closing
536 if closing
537 if tags.last == tag.downcase
537 if tags.last == tag.downcase
538 tags.pop
538 tags.pop
539 end
539 end
540 else
540 else
541 tags << tag.downcase
541 tags << tag.downcase
542 end
542 end
543 parsed << full_tag
543 parsed << full_tag
544 end
544 end
545 end
545 end
546 # Close any non closing tags
546 # Close any non closing tags
547 while tag = tags.pop
547 while tag = tags.pop
548 parsed << "</#{tag}>"
548 parsed << "</#{tag}>"
549 end
549 end
550 parsed
550 parsed
551 end
551 end
552
552
553 def parse_inline_attachments(text, project, obj, attr, only_path, options)
553 def parse_inline_attachments(text, project, obj, attr, only_path, options)
554 # when using an image link, try to use an attachment, if possible
554 # when using an image link, try to use an attachment, if possible
555 attachments = options[:attachments] || []
555 attachments = options[:attachments] || []
556 attachments += obj.attachments if obj.respond_to?(:attachments)
556 attachments += obj.attachments if obj.respond_to?(:attachments)
557 if attachments.present?
557 if attachments.present?
558 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
558 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
559 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
559 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
560 # search for the picture in attachments
560 # search for the picture in attachments
561 if found = Attachment.latest_attach(attachments, filename)
561 if found = Attachment.latest_attach(attachments, filename)
562 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
562 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
563 desc = found.description.to_s.gsub('"', '')
563 desc = found.description.to_s.gsub('"', '')
564 if !desc.blank? && alttext.blank?
564 if !desc.blank? && alttext.blank?
565 alt = " title=\"#{desc}\" alt=\"#{desc}\""
565 alt = " title=\"#{desc}\" alt=\"#{desc}\""
566 end
566 end
567 "src=\"#{image_url}\"#{alt}"
567 "src=\"#{image_url}\"#{alt}"
568 else
568 else
569 m
569 m
570 end
570 end
571 end
571 end
572 end
572 end
573 end
573 end
574
574
575 # Wiki links
575 # Wiki links
576 #
576 #
577 # Examples:
577 # Examples:
578 # [[mypage]]
578 # [[mypage]]
579 # [[mypage|mytext]]
579 # [[mypage|mytext]]
580 # wiki links can refer other project wikis, using project name or identifier:
580 # wiki links can refer other project wikis, using project name or identifier:
581 # [[project:]] -> wiki starting page
581 # [[project:]] -> wiki starting page
582 # [[project:|mytext]]
582 # [[project:|mytext]]
583 # [[project:mypage]]
583 # [[project:mypage]]
584 # [[project:mypage|mytext]]
584 # [[project:mypage|mytext]]
585 def parse_wiki_links(text, project, obj, attr, only_path, options)
585 def parse_wiki_links(text, project, obj, attr, only_path, options)
586 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
586 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
587 link_project = project
587 link_project = project
588 esc, all, page, title = $1, $2, $3, $5
588 esc, all, page, title = $1, $2, $3, $5
589 if esc.nil?
589 if esc.nil?
590 if page =~ /^([^\:]+)\:(.*)$/
590 if page =~ /^([^\:]+)\:(.*)$/
591 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
591 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
592 page = $2
592 page = $2
593 title ||= $1 if page.blank?
593 title ||= $1 if page.blank?
594 end
594 end
595
595
596 if link_project && link_project.wiki
596 if link_project && link_project.wiki
597 # extract anchor
597 # extract anchor
598 anchor = nil
598 anchor = nil
599 if page =~ /^(.+?)\#(.+)$/
599 if page =~ /^(.+?)\#(.+)$/
600 page, anchor = $1, $2
600 page, anchor = $1, $2
601 end
601 end
602 anchor = sanitize_anchor_name(anchor) if anchor.present?
602 anchor = sanitize_anchor_name(anchor) if anchor.present?
603 # check if page exists
603 # check if page exists
604 wiki_page = link_project.wiki.find_page(page)
604 wiki_page = link_project.wiki.find_page(page)
605 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
605 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
606 "##{anchor}"
606 "##{anchor}"
607 else
607 else
608 case options[:wiki_links]
608 case options[:wiki_links]
609 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
609 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
610 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
610 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
611 else
611 else
612 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
612 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
613 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
613 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
614 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
614 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
615 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
615 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
616 end
616 end
617 end
617 end
618 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
618 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
619 else
619 else
620 # project or wiki doesn't exist
620 # project or wiki doesn't exist
621 all
621 all
622 end
622 end
623 else
623 else
624 all
624 all
625 end
625 end
626 end
626 end
627 end
627 end
628
628
629 # Redmine links
629 # Redmine links
630 #
630 #
631 # Examples:
631 # Examples:
632 # Issues:
632 # Issues:
633 # #52 -> Link to issue #52
633 # #52 -> Link to issue #52
634 # Changesets:
634 # Changesets:
635 # r52 -> Link to revision 52
635 # r52 -> Link to revision 52
636 # commit:a85130f -> Link to scmid starting with a85130f
636 # commit:a85130f -> Link to scmid starting with a85130f
637 # Documents:
637 # Documents:
638 # document#17 -> Link to document with id 17
638 # document#17 -> Link to document with id 17
639 # document:Greetings -> Link to the document with title "Greetings"
639 # document:Greetings -> Link to the document with title "Greetings"
640 # document:"Some document" -> Link to the document with title "Some document"
640 # document:"Some document" -> Link to the document with title "Some document"
641 # Versions:
641 # Versions:
642 # version#3 -> Link to version with id 3
642 # version#3 -> Link to version with id 3
643 # version:1.0.0 -> Link to version named "1.0.0"
643 # version:1.0.0 -> Link to version named "1.0.0"
644 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
644 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
645 # Attachments:
645 # Attachments:
646 # attachment:file.zip -> Link to the attachment of the current object named file.zip
646 # attachment:file.zip -> Link to the attachment of the current object named file.zip
647 # Source files:
647 # Source files:
648 # source:some/file -> Link to the file located at /some/file in the project's repository
648 # source:some/file -> Link to the file located at /some/file in the project's repository
649 # source:some/file@52 -> Link to the file's revision 52
649 # source:some/file@52 -> Link to the file's revision 52
650 # source:some/file#L120 -> Link to line 120 of the file
650 # source:some/file#L120 -> Link to line 120 of the file
651 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
651 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
652 # export:some/file -> Force the download of the file
652 # export:some/file -> Force the download of the file
653 # Forum messages:
653 # Forum messages:
654 # message#1218 -> Link to message with id 1218
654 # message#1218 -> Link to message with id 1218
655 #
655 #
656 # Links can refer other objects from other projects, using project identifier:
656 # Links can refer other objects from other projects, using project identifier:
657 # identifier:r52
657 # identifier:r52
658 # identifier:document:"Some document"
658 # identifier:document:"Some document"
659 # identifier:version:1.0.0
659 # identifier:version:1.0.0
660 # identifier:source:some/file
660 # identifier:source:some/file
661 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
661 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
662 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
662 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
663 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
663 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
664 link = nil
664 link = nil
665 project = default_project
665 project = default_project
666 if project_identifier
666 if project_identifier
667 project = Project.visible.find_by_identifier(project_identifier)
667 project = Project.visible.find_by_identifier(project_identifier)
668 end
668 end
669 if esc.nil?
669 if esc.nil?
670 if prefix.nil? && sep == 'r'
670 if prefix.nil? && sep == 'r'
671 if project
671 if project
672 repository = nil
672 repository = nil
673 if repo_identifier
673 if repo_identifier
674 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
674 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
675 else
675 else
676 repository = project.repository
676 repository = project.repository
677 end
677 end
678 # project.changesets.visible raises an SQL error because of a double join on repositories
678 # project.changesets.visible raises an SQL error because of a double join on repositories
679 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
679 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
680 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
680 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
681 :class => 'changeset',
681 :class => 'changeset',
682 :title => truncate_single_line(changeset.comments, :length => 100))
682 :title => truncate_single_line(changeset.comments, :length => 100))
683 end
683 end
684 end
684 end
685 elsif sep == '#'
685 elsif sep == '#'
686 oid = identifier.to_i
686 oid = identifier.to_i
687 case prefix
687 case prefix
688 when nil
688 when nil
689 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
689 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
690 anchor = comment_id ? "note-#{comment_id}" : nil
690 anchor = comment_id ? "note-#{comment_id}" : nil
691 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
691 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
692 :class => issue.css_classes,
692 :class => issue.css_classes,
693 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
693 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
694 end
694 end
695 when 'document'
695 when 'document'
696 if document = Document.visible.find_by_id(oid)
696 if document = Document.visible.find_by_id(oid)
697 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
697 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
698 :class => 'document'
698 :class => 'document'
699 end
699 end
700 when 'version'
700 when 'version'
701 if version = Version.visible.find_by_id(oid)
701 if version = Version.visible.find_by_id(oid)
702 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
702 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
703 :class => 'version'
703 :class => 'version'
704 end
704 end
705 when 'message'
705 when 'message'
706 if message = Message.visible.find_by_id(oid, :include => :parent)
706 if message = Message.visible.find_by_id(oid, :include => :parent)
707 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
707 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
708 end
708 end
709 when 'forum'
709 when 'forum'
710 if board = Board.visible.find_by_id(oid)
710 if board = Board.visible.find_by_id(oid)
711 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
711 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
712 :class => 'board'
712 :class => 'board'
713 end
713 end
714 when 'news'
714 when 'news'
715 if news = News.visible.find_by_id(oid)
715 if news = News.visible.find_by_id(oid)
716 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
716 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
717 :class => 'news'
717 :class => 'news'
718 end
718 end
719 when 'project'
719 when 'project'
720 if p = Project.visible.find_by_id(oid)
720 if p = Project.visible.find_by_id(oid)
721 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
721 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
722 end
722 end
723 end
723 end
724 elsif sep == ':'
724 elsif sep == ':'
725 # removes the double quotes if any
725 # removes the double quotes if any
726 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
726 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
727 case prefix
727 case prefix
728 when 'document'
728 when 'document'
729 if project && document = project.documents.visible.find_by_title(name)
729 if project && document = project.documents.visible.find_by_title(name)
730 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
730 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
731 :class => 'document'
731 :class => 'document'
732 end
732 end
733 when 'version'
733 when 'version'
734 if project && version = project.versions.visible.find_by_name(name)
734 if project && version = project.versions.visible.find_by_name(name)
735 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
735 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
736 :class => 'version'
736 :class => 'version'
737 end
737 end
738 when 'forum'
738 when 'forum'
739 if project && board = project.boards.visible.find_by_name(name)
739 if project && board = project.boards.visible.find_by_name(name)
740 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
740 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
741 :class => 'board'
741 :class => 'board'
742 end
742 end
743 when 'news'
743 when 'news'
744 if project && news = project.news.visible.find_by_title(name)
744 if project && news = project.news.visible.find_by_title(name)
745 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
745 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
746 :class => 'news'
746 :class => 'news'
747 end
747 end
748 when 'commit', 'source', 'export'
748 when 'commit', 'source', 'export'
749 if project
749 if project
750 repository = nil
750 repository = nil
751 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
751 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
752 repo_prefix, repo_identifier, name = $1, $2, $3
752 repo_prefix, repo_identifier, name = $1, $2, $3
753 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
753 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
754 else
754 else
755 repository = project.repository
755 repository = project.repository
756 end
756 end
757 if prefix == 'commit'
757 if prefix == 'commit'
758 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
758 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
759 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
759 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
760 :class => 'changeset',
760 :class => 'changeset',
761 :title => truncate_single_line(h(changeset.comments), :length => 100)
761 :title => truncate_single_line(h(changeset.comments), :length => 100)
762 end
762 end
763 else
763 else
764 if repository && User.current.allowed_to?(:browse_repository, project)
764 if repository && User.current.allowed_to?(:browse_repository, project)
765 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
765 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
766 path, rev, anchor = $1, $3, $5
766 path, rev, anchor = $1, $3, $5
767 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
767 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
768 :path => to_path_param(path),
768 :path => to_path_param(path),
769 :rev => rev,
769 :rev => rev,
770 :anchor => anchor},
770 :anchor => anchor},
771 :class => (prefix == 'export' ? 'source download' : 'source')
771 :class => (prefix == 'export' ? 'source download' : 'source')
772 end
772 end
773 end
773 end
774 repo_prefix = nil
774 repo_prefix = nil
775 end
775 end
776 when 'attachment'
776 when 'attachment'
777 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
777 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
778 if attachments && attachment = Attachment.latest_attach(attachments, name)
778 if attachments && attachment = Attachment.latest_attach(attachments, name)
779 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
779 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
780 end
780 end
781 when 'project'
781 when 'project'
782 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
782 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
783 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
783 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
784 end
784 end
785 end
785 end
786 end
786 end
787 end
787 end
788 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
788 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
789 end
789 end
790 end
790 end
791
791
792 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
792 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
793
793
794 def parse_sections(text, project, obj, attr, only_path, options)
794 def parse_sections(text, project, obj, attr, only_path, options)
795 return unless options[:edit_section_links]
795 return unless options[:edit_section_links]
796 text.gsub!(HEADING_RE) do
796 text.gsub!(HEADING_RE) do
797 heading = $1
797 heading = $1
798 @current_section += 1
798 @current_section += 1
799 if @current_section > 1
799 if @current_section > 1
800 content_tag('div',
800 content_tag('div',
801 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
801 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
802 :class => 'contextual',
802 :class => 'contextual',
803 :title => l(:button_edit_section)) + heading.html_safe
803 :title => l(:button_edit_section)) + heading.html_safe
804 else
804 else
805 heading
805 heading
806 end
806 end
807 end
807 end
808 end
808 end
809
809
810 # Headings and TOC
810 # Headings and TOC
811 # Adds ids and links to headings unless options[:headings] is set to false
811 # Adds ids and links to headings unless options[:headings] is set to false
812 def parse_headings(text, project, obj, attr, only_path, options)
812 def parse_headings(text, project, obj, attr, only_path, options)
813 return if options[:headings] == false
813 return if options[:headings] == false
814
814
815 text.gsub!(HEADING_RE) do
815 text.gsub!(HEADING_RE) do
816 level, attrs, content = $2.to_i, $3, $4
816 level, attrs, content = $2.to_i, $3, $4
817 item = strip_tags(content).strip
817 item = strip_tags(content).strip
818 anchor = sanitize_anchor_name(item)
818 anchor = sanitize_anchor_name(item)
819 # used for single-file wiki export
819 # used for single-file wiki export
820 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
820 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
821 @heading_anchors[anchor] ||= 0
821 @heading_anchors[anchor] ||= 0
822 idx = (@heading_anchors[anchor] += 1)
822 idx = (@heading_anchors[anchor] += 1)
823 if idx > 1
823 if idx > 1
824 anchor = "#{anchor}-#{idx}"
824 anchor = "#{anchor}-#{idx}"
825 end
825 end
826 @parsed_headings << [level, anchor, item]
826 @parsed_headings << [level, anchor, item]
827 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
827 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
828 end
828 end
829 end
829 end
830
830
831 MACROS_RE = /(
831 MACROS_RE = /(
832 (!)? # escaping
832 (!)? # escaping
833 (
833 (
834 \{\{ # opening tag
834 \{\{ # opening tag
835 ([\w]+) # macro name
835 ([\w]+) # macro name
836 (\(([^\n\r]*?)\))? # optional arguments
836 (\(([^\n\r]*?)\))? # optional arguments
837 ([\n\r].*?[\n\r])? # optional block of text
837 ([\n\r].*?[\n\r])? # optional block of text
838 \}\} # closing tag
838 \}\} # closing tag
839 )
839 )
840 )/mx unless const_defined?(:MACROS_RE)
840 )/mx unless const_defined?(:MACROS_RE)
841
841
842 MACRO_SUB_RE = /(
842 MACRO_SUB_RE = /(
843 \{\{
843 \{\{
844 macro\((\d+)\)
844 macro\((\d+)\)
845 \}\}
845 \}\}
846 )/x unless const_defined?(:MACRO_SUB_RE)
846 )/x unless const_defined?(:MACRO_SUB_RE)
847
847
848 # Extracts macros from text
848 # Extracts macros from text
849 def catch_macros(text)
849 def catch_macros(text)
850 macros = {}
850 macros = {}
851 text.gsub!(MACROS_RE) do
851 text.gsub!(MACROS_RE) do
852 all, macro = $1, $4.downcase
852 all, macro = $1, $4.downcase
853 if macro_exists?(macro) || all =~ MACRO_SUB_RE
853 if macro_exists?(macro) || all =~ MACRO_SUB_RE
854 index = macros.size
854 index = macros.size
855 macros[index] = all
855 macros[index] = all
856 "{{macro(#{index})}}"
856 "{{macro(#{index})}}"
857 else
857 else
858 all
858 all
859 end
859 end
860 end
860 end
861 macros
861 macros
862 end
862 end
863
863
864 # Executes and replaces macros in text
864 # Executes and replaces macros in text
865 def inject_macros(text, obj, macros, execute=true)
865 def inject_macros(text, obj, macros, execute=true)
866 text.gsub!(MACRO_SUB_RE) do
866 text.gsub!(MACRO_SUB_RE) do
867 all, index = $1, $2.to_i
867 all, index = $1, $2.to_i
868 orig = macros.delete(index)
868 orig = macros.delete(index)
869 if execute && orig && orig =~ MACROS_RE
869 if execute && orig && orig =~ MACROS_RE
870 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
870 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
871 if esc.nil?
871 if esc.nil?
872 h(exec_macro(macro, obj, args, block) || all)
872 h(exec_macro(macro, obj, args, block) || all)
873 else
873 else
874 h(all)
874 h(all)
875 end
875 end
876 elsif orig
876 elsif orig
877 h(orig)
877 h(orig)
878 else
878 else
879 h(all)
879 h(all)
880 end
880 end
881 end
881 end
882 end
882 end
883
883
884 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
884 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
885
885
886 # Renders the TOC with given headings
886 # Renders the TOC with given headings
887 def replace_toc(text, headings)
887 def replace_toc(text, headings)
888 text.gsub!(TOC_RE) do
888 text.gsub!(TOC_RE) do
889 # Keep only the 4 first levels
889 # Keep only the 4 first levels
890 headings = headings.select{|level, anchor, item| level <= 4}
890 headings = headings.select{|level, anchor, item| level <= 4}
891 if headings.empty?
891 if headings.empty?
892 ''
892 ''
893 else
893 else
894 div_class = 'toc'
894 div_class = 'toc'
895 div_class << ' right' if $1 == '>'
895 div_class << ' right' if $1 == '>'
896 div_class << ' left' if $1 == '<'
896 div_class << ' left' if $1 == '<'
897 out = "<ul class=\"#{div_class}\"><li>"
897 out = "<ul class=\"#{div_class}\"><li>"
898 root = headings.map(&:first).min
898 root = headings.map(&:first).min
899 current = root
899 current = root
900 started = false
900 started = false
901 headings.each do |level, anchor, item|
901 headings.each do |level, anchor, item|
902 if level > current
902 if level > current
903 out << '<ul><li>' * (level - current)
903 out << '<ul><li>' * (level - current)
904 elsif level < current
904 elsif level < current
905 out << "</li></ul>\n" * (current - level) + "</li><li>"
905 out << "</li></ul>\n" * (current - level) + "</li><li>"
906 elsif started
906 elsif started
907 out << '</li><li>'
907 out << '</li><li>'
908 end
908 end
909 out << "<a href=\"##{anchor}\">#{item}</a>"
909 out << "<a href=\"##{anchor}\">#{item}</a>"
910 current = level
910 current = level
911 started = true
911 started = true
912 end
912 end
913 out << '</li></ul>' * (current - root)
913 out << '</li></ul>' * (current - root)
914 out << '</li></ul>'
914 out << '</li></ul>'
915 end
915 end
916 end
916 end
917 end
917 end
918
918
919 # Same as Rails' simple_format helper without using paragraphs
919 # Same as Rails' simple_format helper without using paragraphs
920 def simple_format_without_paragraph(text)
920 def simple_format_without_paragraph(text)
921 text.to_s.
921 text.to_s.
922 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
922 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
923 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
923 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
924 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
924 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
925 html_safe
925 html_safe
926 end
926 end
927
927
928 def lang_options_for_select(blank=true)
928 def lang_options_for_select(blank=true)
929 (blank ? [["(auto)", ""]] : []) + languages_options
929 (blank ? [["(auto)", ""]] : []) + languages_options
930 end
930 end
931
931
932 def label_tag_for(name, option_tags = nil, options = {})
932 def label_tag_for(name, option_tags = nil, options = {})
933 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
933 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
934 content_tag("label", label_text)
934 content_tag("label", label_text)
935 end
935 end
936
936
937 def labelled_form_for(*args, &proc)
937 def labelled_form_for(*args, &proc)
938 args << {} unless args.last.is_a?(Hash)
938 args << {} unless args.last.is_a?(Hash)
939 options = args.last
939 options = args.last
940 if args.first.is_a?(Symbol)
940 if args.first.is_a?(Symbol)
941 options.merge!(:as => args.shift)
941 options.merge!(:as => args.shift)
942 end
942 end
943 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
943 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
944 form_for(*args, &proc)
944 form_for(*args, &proc)
945 end
945 end
946
946
947 def labelled_fields_for(*args, &proc)
947 def labelled_fields_for(*args, &proc)
948 args << {} unless args.last.is_a?(Hash)
948 args << {} unless args.last.is_a?(Hash)
949 options = args.last
949 options = args.last
950 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
950 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
951 fields_for(*args, &proc)
951 fields_for(*args, &proc)
952 end
952 end
953
953
954 def labelled_remote_form_for(*args, &proc)
954 def labelled_remote_form_for(*args, &proc)
955 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
955 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
956 args << {} unless args.last.is_a?(Hash)
956 args << {} unless args.last.is_a?(Hash)
957 options = args.last
957 options = args.last
958 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
958 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
959 form_for(*args, &proc)
959 form_for(*args, &proc)
960 end
960 end
961
961
962 def error_messages_for(*objects)
962 def error_messages_for(*objects)
963 html = ""
963 html = ""
964 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
964 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
965 errors = objects.map {|o| o.errors.full_messages}.flatten
965 errors = objects.map {|o| o.errors.full_messages}.flatten
966 if errors.any?
966 if errors.any?
967 html << "<div id='errorExplanation'><ul>\n"
967 html << "<div id='errorExplanation'><ul>\n"
968 errors.each do |error|
968 errors.each do |error|
969 html << "<li>#{h error}</li>\n"
969 html << "<li>#{h error}</li>\n"
970 end
970 end
971 html << "</ul></div>\n"
971 html << "</ul></div>\n"
972 end
972 end
973 html.html_safe
973 html.html_safe
974 end
974 end
975
975
976 def delete_link(url, options={})
976 def delete_link(url, options={})
977 options = {
977 options = {
978 :method => :delete,
978 :method => :delete,
979 :data => {:confirm => l(:text_are_you_sure)},
979 :data => {:confirm => l(:text_are_you_sure)},
980 :class => 'icon icon-del'
980 :class => 'icon icon-del'
981 }.merge(options)
981 }.merge(options)
982
982
983 link_to l(:button_delete), url, options
983 link_to l(:button_delete), url, options
984 end
984 end
985
985
986 def preview_link(url, form, target='preview', options={})
986 def preview_link(url, form, target='preview', options={})
987 content_tag 'a', l(:label_preview), {
987 content_tag 'a', l(:label_preview), {
988 :href => "#",
988 :href => "#",
989 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
989 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
990 :accesskey => accesskey(:preview)
990 :accesskey => accesskey(:preview)
991 }.merge(options)
991 }.merge(options)
992 end
992 end
993
993
994 def link_to_function(name, function, html_options={})
994 def link_to_function(name, function, html_options={})
995 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
995 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
996 end
996 end
997
997
998 # Helper to render JSON in views
998 # Helper to render JSON in views
999 def raw_json(arg)
999 def raw_json(arg)
1000 arg.to_json.to_s.gsub('/', '\/').html_safe
1000 arg.to_json.to_s.gsub('/', '\/').html_safe
1001 end
1001 end
1002
1002
1003 def back_url
1003 def back_url
1004 url = params[:back_url]
1004 url = params[:back_url]
1005 if url.nil? && referer = request.env['HTTP_REFERER']
1005 if url.nil? && referer = request.env['HTTP_REFERER']
1006 url = CGI.unescape(referer.to_s)
1006 url = CGI.unescape(referer.to_s)
1007 end
1007 end
1008 url
1008 url
1009 end
1009 end
1010
1010
1011 def back_url_hidden_field_tag
1011 def back_url_hidden_field_tag
1012 url = back_url
1012 url = back_url
1013 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1013 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1014 end
1014 end
1015
1015
1016 def check_all_links(form_name)
1016 def check_all_links(form_name)
1017 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1017 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1018 " | ".html_safe +
1018 " | ".html_safe +
1019 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1019 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1020 end
1020 end
1021
1021
1022 def progress_bar(pcts, options={})
1022 def progress_bar(pcts, options={})
1023 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1023 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1024 pcts = pcts.collect(&:round)
1024 pcts = pcts.collect(&:round)
1025 pcts[1] = pcts[1] - pcts[0]
1025 pcts[1] = pcts[1] - pcts[0]
1026 pcts << (100 - pcts[1] - pcts[0])
1026 pcts << (100 - pcts[1] - pcts[0])
1027 width = options[:width] || '100px;'
1027 width = options[:width] || '100px;'
1028 legend = options[:legend] || ''
1028 legend = options[:legend] || ''
1029 content_tag('table',
1029 content_tag('table',
1030 content_tag('tr',
1030 content_tag('tr',
1031 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1031 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1032 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1032 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1033 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1033 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1034 ), :class => 'progress', :style => "width: #{width};").html_safe +
1034 ), :class => 'progress', :style => "width: #{width};").html_safe +
1035 content_tag('p', legend, :class => 'percent').html_safe
1035 content_tag('p', legend, :class => 'percent').html_safe
1036 end
1036 end
1037
1037
1038 def checked_image(checked=true)
1038 def checked_image(checked=true)
1039 if checked
1039 if checked
1040 image_tag 'toggle_check.png'
1040 image_tag 'toggle_check.png'
1041 end
1041 end
1042 end
1042 end
1043
1043
1044 def context_menu(url)
1044 def context_menu(url)
1045 unless @context_menu_included
1045 unless @context_menu_included
1046 content_for :header_tags do
1046 content_for :header_tags do
1047 javascript_include_tag('context_menu') +
1047 javascript_include_tag('context_menu') +
1048 stylesheet_link_tag('context_menu')
1048 stylesheet_link_tag('context_menu')
1049 end
1049 end
1050 if l(:direction) == 'rtl'
1050 if l(:direction) == 'rtl'
1051 content_for :header_tags do
1051 content_for :header_tags do
1052 stylesheet_link_tag('context_menu_rtl')
1052 stylesheet_link_tag('context_menu_rtl')
1053 end
1053 end
1054 end
1054 end
1055 @context_menu_included = true
1055 @context_menu_included = true
1056 end
1056 end
1057 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1057 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1058 end
1058 end
1059
1059
1060 def calendar_for(field_id)
1060 def calendar_for(field_id)
1061 include_calendar_headers_tags
1061 include_calendar_headers_tags
1062 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1062 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1063 end
1063 end
1064
1064
1065 def include_calendar_headers_tags
1065 def include_calendar_headers_tags
1066 unless @calendar_headers_tags_included
1066 unless @calendar_headers_tags_included
1067 @calendar_headers_tags_included = true
1067 @calendar_headers_tags_included = true
1068 content_for :header_tags do
1068 content_for :header_tags do
1069 start_of_week = Setting.start_of_week
1069 start_of_week = Setting.start_of_week
1070 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1070 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1071 # Redmine uses 1..7 (monday..sunday) in settings and locales
1071 # Redmine uses 1..7 (monday..sunday) in settings and locales
1072 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1072 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1073 start_of_week = start_of_week.to_i % 7
1073 start_of_week = start_of_week.to_i % 7
1074
1074
1075 tags = javascript_tag(
1075 tags = javascript_tag(
1076 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1076 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1077 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1077 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1078 path_to_image('/images/calendar.png') +
1078 path_to_image('/images/calendar.png') +
1079 "', showButtonPanel: true};")
1079 "', showButtonPanel: true};")
1080 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1080 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1081 unless jquery_locale == 'en'
1081 unless jquery_locale == 'en'
1082 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1082 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1083 end
1083 end
1084 tags
1084 tags
1085 end
1085 end
1086 end
1086 end
1087 end
1087 end
1088
1088
1089 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1089 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1090 # Examples:
1090 # Examples:
1091 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1091 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1092 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1092 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1093 #
1093 #
1094 def stylesheet_link_tag(*sources)
1094 def stylesheet_link_tag(*sources)
1095 options = sources.last.is_a?(Hash) ? sources.pop : {}
1095 options = sources.last.is_a?(Hash) ? sources.pop : {}
1096 plugin = options.delete(:plugin)
1096 plugin = options.delete(:plugin)
1097 sources = sources.map do |source|
1097 sources = sources.map do |source|
1098 if plugin
1098 if plugin
1099 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1099 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1100 elsif current_theme && current_theme.stylesheets.include?(source)
1100 elsif current_theme && current_theme.stylesheets.include?(source)
1101 current_theme.stylesheet_path(source)
1101 current_theme.stylesheet_path(source)
1102 else
1102 else
1103 source
1103 source
1104 end
1104 end
1105 end
1105 end
1106 super sources, options
1106 super sources, options
1107 end
1107 end
1108
1108
1109 # Overrides Rails' image_tag with themes and plugins support.
1109 # Overrides Rails' image_tag with themes and plugins support.
1110 # Examples:
1110 # Examples:
1111 # image_tag('image.png') # => picks image.png from the current theme or defaults
1111 # image_tag('image.png') # => picks image.png from the current theme or defaults
1112 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1112 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1113 #
1113 #
1114 def image_tag(source, options={})
1114 def image_tag(source, options={})
1115 if plugin = options.delete(:plugin)
1115 if plugin = options.delete(:plugin)
1116 source = "/plugin_assets/#{plugin}/images/#{source}"
1116 source = "/plugin_assets/#{plugin}/images/#{source}"
1117 elsif current_theme && current_theme.images.include?(source)
1117 elsif current_theme && current_theme.images.include?(source)
1118 source = current_theme.image_path(source)
1118 source = current_theme.image_path(source)
1119 end
1119 end
1120 super source, options
1120 super source, options
1121 end
1121 end
1122
1122
1123 # Overrides Rails' javascript_include_tag with plugins support
1123 # Overrides Rails' javascript_include_tag with plugins support
1124 # Examples:
1124 # Examples:
1125 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1125 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1126 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1126 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1127 #
1127 #
1128 def javascript_include_tag(*sources)
1128 def javascript_include_tag(*sources)
1129 options = sources.last.is_a?(Hash) ? sources.pop : {}
1129 options = sources.last.is_a?(Hash) ? sources.pop : {}
1130 if plugin = options.delete(:plugin)
1130 if plugin = options.delete(:plugin)
1131 sources = sources.map do |source|
1131 sources = sources.map do |source|
1132 if plugin
1132 if plugin
1133 "/plugin_assets/#{plugin}/javascripts/#{source}"
1133 "/plugin_assets/#{plugin}/javascripts/#{source}"
1134 else
1134 else
1135 source
1135 source
1136 end
1136 end
1137 end
1137 end
1138 end
1138 end
1139 super sources, options
1139 super sources, options
1140 end
1140 end
1141
1141
1142 def content_for(name, content = nil, &block)
1142 def content_for(name, content = nil, &block)
1143 @has_content ||= {}
1143 @has_content ||= {}
1144 @has_content[name] = true
1144 @has_content[name] = true
1145 super(name, content, &block)
1145 super(name, content, &block)
1146 end
1146 end
1147
1147
1148 def has_content?(name)
1148 def has_content?(name)
1149 (@has_content && @has_content[name]) || false
1149 (@has_content && @has_content[name]) || false
1150 end
1150 end
1151
1151
1152 def sidebar_content?
1152 def sidebar_content?
1153 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1153 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1154 end
1154 end
1155
1155
1156 def view_layouts_base_sidebar_hook_response
1156 def view_layouts_base_sidebar_hook_response
1157 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1157 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1158 end
1158 end
1159
1159
1160 def email_delivery_enabled?
1160 def email_delivery_enabled?
1161 !!ActionMailer::Base.perform_deliveries
1161 !!ActionMailer::Base.perform_deliveries
1162 end
1162 end
1163
1163
1164 # Returns the avatar image tag for the given +user+ if avatars are enabled
1164 # Returns the avatar image tag for the given +user+ if avatars are enabled
1165 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1165 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1166 def avatar(user, options = { })
1166 def avatar(user, options = { })
1167 if Setting.gravatar_enabled?
1167 if Setting.gravatar_enabled?
1168 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1168 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1169 email = nil
1169 email = nil
1170 if user.respond_to?(:mail)
1170 if user.respond_to?(:mail)
1171 email = user.mail
1171 email = user.mail
1172 elsif user.to_s =~ %r{<(.+?)>}
1172 elsif user.to_s =~ %r{<(.+?)>}
1173 email = $1
1173 email = $1
1174 end
1174 end
1175 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1175 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1176 else
1176 else
1177 ''
1177 ''
1178 end
1178 end
1179 end
1179 end
1180
1180
1181 def sanitize_anchor_name(anchor)
1181 def sanitize_anchor_name(anchor)
1182 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1182 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1183 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1183 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1184 else
1184 else
1185 # TODO: remove when ruby1.8 is no longer supported
1185 # TODO: remove when ruby1.8 is no longer supported
1186 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1186 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1187 end
1187 end
1188 end
1188 end
1189
1189
1190 # Returns the javascript tags that are included in the html layout head
1190 # Returns the javascript tags that are included in the html layout head
1191 def javascript_heads
1191 def javascript_heads
1192 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1192 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1193 unless User.current.pref.warn_on_leaving_unsaved == '0'
1193 unless User.current.pref.warn_on_leaving_unsaved == '0'
1194 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1194 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1195 end
1195 end
1196 tags
1196 tags
1197 end
1197 end
1198
1198
1199 def favicon
1199 def favicon
1200 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1200 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1201 end
1201 end
1202
1202
1203 def robot_exclusion_tag
1203 def robot_exclusion_tag
1204 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1204 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1205 end
1205 end
1206
1206
1207 # Returns true if arg is expected in the API response
1207 # Returns true if arg is expected in the API response
1208 def include_in_api_response?(arg)
1208 def include_in_api_response?(arg)
1209 unless @included_in_api_response
1209 unless @included_in_api_response
1210 param = params[:include]
1210 param = params[:include]
1211 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1211 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1212 @included_in_api_response.collect!(&:strip)
1212 @included_in_api_response.collect!(&:strip)
1213 end
1213 end
1214 @included_in_api_response.include?(arg.to_s)
1214 @included_in_api_response.include?(arg.to_s)
1215 end
1215 end
1216
1216
1217 # Returns options or nil if nometa param or X-Redmine-Nometa header
1217 # Returns options or nil if nometa param or X-Redmine-Nometa header
1218 # was set in the request
1218 # was set in the request
1219 def api_meta(options)
1219 def api_meta(options)
1220 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1220 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1221 # compatibility mode for activeresource clients that raise
1221 # compatibility mode for activeresource clients that raise
1222 # an error when unserializing an array with attributes
1222 # an error when unserializing an array with attributes
1223 nil
1223 nil
1224 else
1224 else
1225 options
1225 options
1226 end
1226 end
1227 end
1227 end
1228
1228
1229 private
1229 private
1230
1230
1231 def wiki_helper
1231 def wiki_helper
1232 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1232 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1233 extend helper
1233 extend helper
1234 return self
1234 return self
1235 end
1235 end
1236
1236
1237 def link_to_content_update(text, url_params = {}, html_options = {})
1237 def link_to_content_update(text, url_params = {}, html_options = {})
1238 link_to(text, url_params, html_options)
1238 link_to(text, url_params, html_options)
1239 end
1239 end
1240 end
1240 end
@@ -1,42 +1,42
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module GroupsHelper
20 module GroupsHelper
21 def group_settings_tabs
21 def group_settings_tabs
22 tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
22 tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
23 {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
23 {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
24 {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
24 {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
25 ]
25 ]
26 end
26 end
27
27
28 def render_principals_for_new_group_users(group)
28 def render_principals_for_new_group_users(group)
29 scope = User.active.not_in_group(group).like(params[:q])
29 scope = User.active.sorted.not_in_group(group).like(params[:q])
30 principal_count = scope.count
30 principal_count = scope.count
31 principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
31 principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
32 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all
32 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all
33
33
34 s = content_tag('div', principals_check_box_tags('user_ids[]', principals), :id => 'principals')
34 s = content_tag('div', principals_check_box_tags('user_ids[]', principals), :id => 'principals')
35
35
36 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
36 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
37 link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
37 link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
38 }
38 }
39
39
40 s + content_tag('p', links, :class => 'pagination')
40 s + content_tag('p', links, :class => 'pagination')
41 end
41 end
42 end
42 end
@@ -1,35 +1,35
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module MembersHelper
20 module MembersHelper
21 def render_principals_for_new_members(project)
21 def render_principals_for_new_members(project)
22 scope = Principal.active.not_member_of(project).like(params[:q]).order('type, login, lastname ASC')
22 scope = Principal.active.sorted.not_member_of(project).like(params[:q])
23 principal_count = scope.count
23 principal_count = scope.count
24 principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
24 principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
25 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all
25 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all
26
26
27 s = content_tag('div', principals_check_box_tags('membership[user_ids][]', principals), :id => 'principals')
27 s = content_tag('div', principals_check_box_tags('membership[user_ids][]', principals), :id => 'principals')
28
28
29 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
29 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
30 link_to text, autocomplete_project_memberships_path(project, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
30 link_to text, autocomplete_project_memberships_path(project, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
31 }
31 }
32
32
33 s + content_tag('p', links, :class => 'pagination')
33 s + content_tag('p', links, :class => 'pagination')
34 end
34 end
35 end
35 end
@@ -1,102 +1,112
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 Principal < ActiveRecord::Base
18 class Principal < ActiveRecord::Base
19 self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
19 self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
20
20
21 # Account statuses
21 # Account statuses
22 STATUS_ANONYMOUS = 0
22 STATUS_ANONYMOUS = 0
23 STATUS_ACTIVE = 1
23 STATUS_ACTIVE = 1
24 STATUS_REGISTERED = 2
24 STATUS_REGISTERED = 2
25 STATUS_LOCKED = 3
25 STATUS_LOCKED = 3
26
26
27 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
27 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
28 has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}", :order => "#{Project.table_name}.name"
28 has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}", :order => "#{Project.table_name}.name"
29 has_many :projects, :through => :memberships
29 has_many :projects, :through => :memberships
30 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
30 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
31
31
32 # Groups and active users
32 # Groups and active users
33 scope :active, lambda { where(:status => STATUS_ACTIVE) }
33 scope :active, lambda { where(:status => STATUS_ACTIVE) }
34
34
35 scope :like, lambda {|q|
35 scope :like, lambda {|q|
36 q = q.to_s
36 q = q.to_s
37 if q.blank?
37 if q.blank?
38 where({})
38 where({})
39 else
39 else
40 pattern = "%#{q}%"
40 pattern = "%#{q}%"
41 sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
41 sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
42 params = {:p => pattern}
42 params = {:p => pattern}
43 if q =~ /^(.+)\s+(.+)$/
43 if q =~ /^(.+)\s+(.+)$/
44 a, b = "#{$1}%", "#{$2}%"
44 a, b = "#{$1}%", "#{$2}%"
45 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))"
45 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))"
46 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))"
46 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))"
47 params.merge!(:a => a, :b => b)
47 params.merge!(:a => a, :b => b)
48 end
48 end
49 where(sql, params)
49 where(sql, params)
50 end
50 end
51 }
51 }
52
52
53 # Principals that are members of a collection of projects
53 # Principals that are members of a collection of projects
54 scope :member_of, lambda {|projects|
54 scope :member_of, lambda {|projects|
55 projects = [projects] unless projects.is_a?(Array)
55 projects = [projects] unless projects.is_a?(Array)
56 if projects.empty?
56 if projects.empty?
57 where("1=0")
57 where("1=0")
58 else
58 else
59 ids = projects.map(&:id)
59 ids = projects.map(&:id)
60 active.where("#{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
60 active.where("#{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
61 end
61 end
62 }
62 }
63 # Principals that are not members of projects
63 # Principals that are not members of projects
64 scope :not_member_of, lambda {|projects|
64 scope :not_member_of, lambda {|projects|
65 projects = [projects] unless projects.is_a?(Array)
65 projects = [projects] unless projects.is_a?(Array)
66 if projects.empty?
66 if projects.empty?
67 where("1=0")
67 where("1=0")
68 else
68 else
69 ids = projects.map(&:id)
69 ids = projects.map(&:id)
70 where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
70 where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
71 end
71 end
72 }
72 }
73 scope :sorted, lambda { order(*Principal.fields_for_order_statement)}
73
74
74 before_create :set_default_empty_values
75 before_create :set_default_empty_values
75
76
76 def name(formatter = nil)
77 def name(formatter = nil)
77 to_s
78 to_s
78 end
79 end
79
80
80 def <=>(principal)
81 def <=>(principal)
81 if principal.nil?
82 if principal.nil?
82 -1
83 -1
83 elsif self.class.name == principal.class.name
84 elsif self.class.name == principal.class.name
84 self.to_s.downcase <=> principal.to_s.downcase
85 self.to_s.downcase <=> principal.to_s.downcase
85 else
86 else
86 # groups after users
87 # groups after users
87 principal.class.name <=> self.class.name
88 principal.class.name <=> self.class.name
88 end
89 end
89 end
90 end
90
91
92 # Returns an array of fields names than can be used to make an order statement for principals.
93 # Users are sorted before Groups.
94 # Examples:
95 def self.fields_for_order_statement(table=nil)
96 table ||= table_name
97 columns = ['type DESC'] + (User.name_formatter[:order] - ['id']) + ['lastname', 'id']
98 columns.uniq.map {|field| "#{table}.#{field}"}
99 end
100
91 protected
101 protected
92
102
93 # Make sure we don't try to insert NULL values (see #4632)
103 # Make sure we don't try to insert NULL values (see #4632)
94 def set_default_empty_values
104 def set_default_empty_values
95 self.login ||= ''
105 self.login ||= ''
96 self.hashed_password ||= ''
106 self.hashed_password ||= ''
97 self.firstname ||= ''
107 self.firstname ||= ''
98 self.lastname ||= ''
108 self.lastname ||= ''
99 self.mail ||= ''
109 self.mail ||= ''
100 true
110 true
101 end
111 end
102 end
112 end
@@ -1,709 +1,710
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstname => {
35 :firstname => {
36 :string => '#{firstname}',
36 :string => '#{firstname}',
37 :order => %w(firstname id),
37 :order => %w(firstname id),
38 :setting_order => 3
38 :setting_order => 3
39 },
39 },
40 :lastname_firstname => {
40 :lastname_firstname => {
41 :string => '#{lastname} #{firstname}',
41 :string => '#{lastname} #{firstname}',
42 :order => %w(lastname firstname id),
42 :order => %w(lastname firstname id),
43 :setting_order => 4
43 :setting_order => 4
44 },
44 },
45 :lastname_coma_firstname => {
45 :lastname_coma_firstname => {
46 :string => '#{lastname}, #{firstname}',
46 :string => '#{lastname}, #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 5
48 :setting_order => 5
49 },
49 },
50 :lastname => {
50 :lastname => {
51 :string => '#{lastname}',
51 :string => '#{lastname}',
52 :order => %w(lastname id),
52 :order => %w(lastname id),
53 :setting_order => 6
53 :setting_order => 6
54 },
54 },
55 :username => {
55 :username => {
56 :string => '#{login}',
56 :string => '#{login}',
57 :order => %w(login id),
57 :order => %w(login id),
58 :setting_order => 7
58 :setting_order => 7
59 },
59 },
60 }
60 }
61
61
62 MAIL_NOTIFICATION_OPTIONS = [
62 MAIL_NOTIFICATION_OPTIONS = [
63 ['all', :label_user_mail_option_all],
63 ['all', :label_user_mail_option_all],
64 ['selected', :label_user_mail_option_selected],
64 ['selected', :label_user_mail_option_selected],
65 ['only_my_events', :label_user_mail_option_only_my_events],
65 ['only_my_events', :label_user_mail_option_only_my_events],
66 ['only_assigned', :label_user_mail_option_only_assigned],
66 ['only_assigned', :label_user_mail_option_only_assigned],
67 ['only_owner', :label_user_mail_option_only_owner],
67 ['only_owner', :label_user_mail_option_only_owner],
68 ['none', :label_user_mail_option_none]
68 ['none', :label_user_mail_option_none]
69 ]
69 ]
70
70
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 has_many :changesets, :dependent => :nullify
73 has_many :changesets, :dependent => :nullify
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 belongs_to :auth_source
77 belongs_to :auth_source
78
78
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
81
81
82 acts_as_customizable
82 acts_as_customizable
83
83
84 attr_accessor :password, :password_confirmation
84 attr_accessor :password, :password_confirmation
85 attr_accessor :last_before_login_on
85 attr_accessor :last_before_login_on
86 # Prevents unauthorized assignments
86 # Prevents unauthorized assignments
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88
88
89 LOGIN_LENGTH_LIMIT = 60
89 LOGIN_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
91
91
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
95 # Login must contain lettres, numbers, underscores only
95 # Login must contain lettres, numbers, underscores only
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 validates_length_of :firstname, :lastname, :maximum => 30
98 validates_length_of :firstname, :lastname, :maximum => 30
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 validate :validate_password_length
103 validate :validate_password_length
104
104
105 before_create :set_mail_notification
105 before_create :set_mail_notification
106 before_save :update_hashed_password
106 before_save :update_hashed_password
107 before_destroy :remove_references_before_destroy
107 before_destroy :remove_references_before_destroy
108
108
109 scope :in_group, lambda {|group|
109 scope :in_group, lambda {|group|
110 group_id = group.is_a?(Group) ? group.id : group.to_i
110 group_id = group.is_a?(Group) ? group.id : group.to_i
111 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
111 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
112 }
112 }
113 scope :not_in_group, lambda {|group|
113 scope :not_in_group, lambda {|group|
114 group_id = group.is_a?(Group) ? group.id : group.to_i
114 group_id = group.is_a?(Group) ? group.id : group.to_i
115 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
115 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
116 }
116 }
117 scope :sorted, lambda { order(*User.fields_for_order_statement)}
117
118
118 def set_mail_notification
119 def set_mail_notification
119 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
120 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
120 true
121 true
121 end
122 end
122
123
123 def update_hashed_password
124 def update_hashed_password
124 # update hashed_password if password was set
125 # update hashed_password if password was set
125 if self.password && self.auth_source_id.blank?
126 if self.password && self.auth_source_id.blank?
126 salt_password(password)
127 salt_password(password)
127 end
128 end
128 end
129 end
129
130
130 def reload(*args)
131 def reload(*args)
131 @name = nil
132 @name = nil
132 @projects_by_role = nil
133 @projects_by_role = nil
133 super
134 super
134 end
135 end
135
136
136 def mail=(arg)
137 def mail=(arg)
137 write_attribute(:mail, arg.to_s.strip)
138 write_attribute(:mail, arg.to_s.strip)
138 end
139 end
139
140
140 def identity_url=(url)
141 def identity_url=(url)
141 if url.blank?
142 if url.blank?
142 write_attribute(:identity_url, '')
143 write_attribute(:identity_url, '')
143 else
144 else
144 begin
145 begin
145 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
146 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
146 rescue OpenIdAuthentication::InvalidOpenId
147 rescue OpenIdAuthentication::InvalidOpenId
147 # Invlaid url, don't save
148 # Invlaid url, don't save
148 end
149 end
149 end
150 end
150 self.read_attribute(:identity_url)
151 self.read_attribute(:identity_url)
151 end
152 end
152
153
153 # Returns the user that matches provided login and password, or nil
154 # Returns the user that matches provided login and password, or nil
154 def self.try_to_login(login, password)
155 def self.try_to_login(login, password)
155 login = login.to_s
156 login = login.to_s
156 password = password.to_s
157 password = password.to_s
157
158
158 # Make sure no one can sign in with an empty password
159 # Make sure no one can sign in with an empty password
159 return nil if password.empty?
160 return nil if password.empty?
160 user = find_by_login(login)
161 user = find_by_login(login)
161 if user
162 if user
162 # user is already in local database
163 # user is already in local database
163 return nil if !user.active?
164 return nil if !user.active?
164 if user.auth_source
165 if user.auth_source
165 # user has an external authentication method
166 # user has an external authentication method
166 return nil unless user.auth_source.authenticate(login, password)
167 return nil unless user.auth_source.authenticate(login, password)
167 else
168 else
168 # authentication with local password
169 # authentication with local password
169 return nil unless user.check_password?(password)
170 return nil unless user.check_password?(password)
170 end
171 end
171 else
172 else
172 # user is not yet registered, try to authenticate with available sources
173 # user is not yet registered, try to authenticate with available sources
173 attrs = AuthSource.authenticate(login, password)
174 attrs = AuthSource.authenticate(login, password)
174 if attrs
175 if attrs
175 user = new(attrs)
176 user = new(attrs)
176 user.login = login
177 user.login = login
177 user.language = Setting.default_language
178 user.language = Setting.default_language
178 if user.save
179 if user.save
179 user.reload
180 user.reload
180 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
181 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
181 end
182 end
182 end
183 end
183 end
184 end
184 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
185 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
185 user
186 user
186 rescue => text
187 rescue => text
187 raise text
188 raise text
188 end
189 end
189
190
190 # Returns the user who matches the given autologin +key+ or nil
191 # Returns the user who matches the given autologin +key+ or nil
191 def self.try_to_autologin(key)
192 def self.try_to_autologin(key)
192 tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
193 tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
193 # Make sure there's only 1 token that matches the key
194 # Make sure there's only 1 token that matches the key
194 if tokens.size == 1
195 if tokens.size == 1
195 token = tokens.first
196 token = tokens.first
196 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
197 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
197 token.user.update_attribute(:last_login_on, Time.now)
198 token.user.update_attribute(:last_login_on, Time.now)
198 token.user
199 token.user
199 end
200 end
200 end
201 end
201 end
202 end
202
203
203 def self.name_formatter(formatter = nil)
204 def self.name_formatter(formatter = nil)
204 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
205 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
205 end
206 end
206
207
207 # Returns an array of fields names than can be used to make an order statement for users
208 # Returns an array of fields names than can be used to make an order statement for users
208 # according to how user names are displayed
209 # according to how user names are displayed
209 # Examples:
210 # Examples:
210 #
211 #
211 # User.fields_for_order_statement => ['users.login', 'users.id']
212 # User.fields_for_order_statement => ['users.login', 'users.id']
212 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
213 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
213 def self.fields_for_order_statement(table=nil)
214 def self.fields_for_order_statement(table=nil)
214 table ||= table_name
215 table ||= table_name
215 name_formatter[:order].map {|field| "#{table}.#{field}"}
216 name_formatter[:order].map {|field| "#{table}.#{field}"}
216 end
217 end
217
218
218 # Return user's full name for display
219 # Return user's full name for display
219 def name(formatter = nil)
220 def name(formatter = nil)
220 f = self.class.name_formatter(formatter)
221 f = self.class.name_formatter(formatter)
221 if formatter
222 if formatter
222 eval('"' + f[:string] + '"')
223 eval('"' + f[:string] + '"')
223 else
224 else
224 @name ||= eval('"' + f[:string] + '"')
225 @name ||= eval('"' + f[:string] + '"')
225 end
226 end
226 end
227 end
227
228
228 def active?
229 def active?
229 self.status == STATUS_ACTIVE
230 self.status == STATUS_ACTIVE
230 end
231 end
231
232
232 def registered?
233 def registered?
233 self.status == STATUS_REGISTERED
234 self.status == STATUS_REGISTERED
234 end
235 end
235
236
236 def locked?
237 def locked?
237 self.status == STATUS_LOCKED
238 self.status == STATUS_LOCKED
238 end
239 end
239
240
240 def activate
241 def activate
241 self.status = STATUS_ACTIVE
242 self.status = STATUS_ACTIVE
242 end
243 end
243
244
244 def register
245 def register
245 self.status = STATUS_REGISTERED
246 self.status = STATUS_REGISTERED
246 end
247 end
247
248
248 def lock
249 def lock
249 self.status = STATUS_LOCKED
250 self.status = STATUS_LOCKED
250 end
251 end
251
252
252 def activate!
253 def activate!
253 update_attribute(:status, STATUS_ACTIVE)
254 update_attribute(:status, STATUS_ACTIVE)
254 end
255 end
255
256
256 def register!
257 def register!
257 update_attribute(:status, STATUS_REGISTERED)
258 update_attribute(:status, STATUS_REGISTERED)
258 end
259 end
259
260
260 def lock!
261 def lock!
261 update_attribute(:status, STATUS_LOCKED)
262 update_attribute(:status, STATUS_LOCKED)
262 end
263 end
263
264
264 # Returns true if +clear_password+ is the correct user's password, otherwise false
265 # Returns true if +clear_password+ is the correct user's password, otherwise false
265 def check_password?(clear_password)
266 def check_password?(clear_password)
266 if auth_source_id.present?
267 if auth_source_id.present?
267 auth_source.authenticate(self.login, clear_password)
268 auth_source.authenticate(self.login, clear_password)
268 else
269 else
269 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
270 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
270 end
271 end
271 end
272 end
272
273
273 # Generates a random salt and computes hashed_password for +clear_password+
274 # Generates a random salt and computes hashed_password for +clear_password+
274 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
275 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
275 def salt_password(clear_password)
276 def salt_password(clear_password)
276 self.salt = User.generate_salt
277 self.salt = User.generate_salt
277 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
278 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
278 end
279 end
279
280
280 # Does the backend storage allow this user to change their password?
281 # Does the backend storage allow this user to change their password?
281 def change_password_allowed?
282 def change_password_allowed?
282 return true if auth_source.nil?
283 return true if auth_source.nil?
283 return auth_source.allow_password_changes?
284 return auth_source.allow_password_changes?
284 end
285 end
285
286
286 # Generate and set a random password. Useful for automated user creation
287 # Generate and set a random password. Useful for automated user creation
287 # Based on Token#generate_token_value
288 # Based on Token#generate_token_value
288 #
289 #
289 def random_password
290 def random_password
290 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
291 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
291 password = ''
292 password = ''
292 40.times { |i| password << chars[rand(chars.size-1)] }
293 40.times { |i| password << chars[rand(chars.size-1)] }
293 self.password = password
294 self.password = password
294 self.password_confirmation = password
295 self.password_confirmation = password
295 self
296 self
296 end
297 end
297
298
298 def pref
299 def pref
299 self.preference ||= UserPreference.new(:user => self)
300 self.preference ||= UserPreference.new(:user => self)
300 end
301 end
301
302
302 def time_zone
303 def time_zone
303 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
304 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
304 end
305 end
305
306
306 def wants_comments_in_reverse_order?
307 def wants_comments_in_reverse_order?
307 self.pref[:comments_sorting] == 'desc'
308 self.pref[:comments_sorting] == 'desc'
308 end
309 end
309
310
310 # Return user's RSS key (a 40 chars long string), used to access feeds
311 # Return user's RSS key (a 40 chars long string), used to access feeds
311 def rss_key
312 def rss_key
312 if rss_token.nil?
313 if rss_token.nil?
313 create_rss_token(:action => 'feeds')
314 create_rss_token(:action => 'feeds')
314 end
315 end
315 rss_token.value
316 rss_token.value
316 end
317 end
317
318
318 # Return user's API key (a 40 chars long string), used to access the API
319 # Return user's API key (a 40 chars long string), used to access the API
319 def api_key
320 def api_key
320 if api_token.nil?
321 if api_token.nil?
321 create_api_token(:action => 'api')
322 create_api_token(:action => 'api')
322 end
323 end
323 api_token.value
324 api_token.value
324 end
325 end
325
326
326 # Return an array of project ids for which the user has explicitly turned mail notifications on
327 # Return an array of project ids for which the user has explicitly turned mail notifications on
327 def notified_projects_ids
328 def notified_projects_ids
328 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
329 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
329 end
330 end
330
331
331 def notified_project_ids=(ids)
332 def notified_project_ids=(ids)
332 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
333 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
333 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
334 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
334 @notified_projects_ids = nil
335 @notified_projects_ids = nil
335 notified_projects_ids
336 notified_projects_ids
336 end
337 end
337
338
338 def valid_notification_options
339 def valid_notification_options
339 self.class.valid_notification_options(self)
340 self.class.valid_notification_options(self)
340 end
341 end
341
342
342 # Only users that belong to more than 1 project can select projects for which they are notified
343 # Only users that belong to more than 1 project can select projects for which they are notified
343 def self.valid_notification_options(user=nil)
344 def self.valid_notification_options(user=nil)
344 # Note that @user.membership.size would fail since AR ignores
345 # Note that @user.membership.size would fail since AR ignores
345 # :include association option when doing a count
346 # :include association option when doing a count
346 if user.nil? || user.memberships.length < 1
347 if user.nil? || user.memberships.length < 1
347 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
348 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
348 else
349 else
349 MAIL_NOTIFICATION_OPTIONS
350 MAIL_NOTIFICATION_OPTIONS
350 end
351 end
351 end
352 end
352
353
353 # Find a user account by matching the exact login and then a case-insensitive
354 # Find a user account by matching the exact login and then a case-insensitive
354 # version. Exact matches will be given priority.
355 # version. Exact matches will be given priority.
355 def self.find_by_login(login)
356 def self.find_by_login(login)
356 if login.present?
357 if login.present?
357 login = login.to_s
358 login = login.to_s
358 # First look for an exact match
359 # First look for an exact match
359 user = where(:login => login).all.detect {|u| u.login == login}
360 user = where(:login => login).all.detect {|u| u.login == login}
360 unless user
361 unless user
361 # Fail over to case-insensitive if none was found
362 # Fail over to case-insensitive if none was found
362 user = where("LOWER(login) = ?", login.downcase).first
363 user = where("LOWER(login) = ?", login.downcase).first
363 end
364 end
364 user
365 user
365 end
366 end
366 end
367 end
367
368
368 def self.find_by_rss_key(key)
369 def self.find_by_rss_key(key)
369 token = Token.find_by_action_and_value('feeds', key.to_s)
370 token = Token.find_by_action_and_value('feeds', key.to_s)
370 token && token.user.active? ? token.user : nil
371 token && token.user.active? ? token.user : nil
371 end
372 end
372
373
373 def self.find_by_api_key(key)
374 def self.find_by_api_key(key)
374 token = Token.find_by_action_and_value('api', key.to_s)
375 token = Token.find_by_action_and_value('api', key.to_s)
375 token && token.user.active? ? token.user : nil
376 token && token.user.active? ? token.user : nil
376 end
377 end
377
378
378 # Makes find_by_mail case-insensitive
379 # Makes find_by_mail case-insensitive
379 def self.find_by_mail(mail)
380 def self.find_by_mail(mail)
380 where("LOWER(mail) = ?", mail.to_s.downcase).first
381 where("LOWER(mail) = ?", mail.to_s.downcase).first
381 end
382 end
382
383
383 # Returns true if the default admin account can no longer be used
384 # Returns true if the default admin account can no longer be used
384 def self.default_admin_account_changed?
385 def self.default_admin_account_changed?
385 !User.active.find_by_login("admin").try(:check_password?, "admin")
386 !User.active.find_by_login("admin").try(:check_password?, "admin")
386 end
387 end
387
388
388 def to_s
389 def to_s
389 name
390 name
390 end
391 end
391
392
392 CSS_CLASS_BY_STATUS = {
393 CSS_CLASS_BY_STATUS = {
393 STATUS_ANONYMOUS => 'anon',
394 STATUS_ANONYMOUS => 'anon',
394 STATUS_ACTIVE => 'active',
395 STATUS_ACTIVE => 'active',
395 STATUS_REGISTERED => 'registered',
396 STATUS_REGISTERED => 'registered',
396 STATUS_LOCKED => 'locked'
397 STATUS_LOCKED => 'locked'
397 }
398 }
398
399
399 def css_classes
400 def css_classes
400 "user #{CSS_CLASS_BY_STATUS[status]}"
401 "user #{CSS_CLASS_BY_STATUS[status]}"
401 end
402 end
402
403
403 # Returns the current day according to user's time zone
404 # Returns the current day according to user's time zone
404 def today
405 def today
405 if time_zone.nil?
406 if time_zone.nil?
406 Date.today
407 Date.today
407 else
408 else
408 Time.now.in_time_zone(time_zone).to_date
409 Time.now.in_time_zone(time_zone).to_date
409 end
410 end
410 end
411 end
411
412
412 # Returns the day of +time+ according to user's time zone
413 # Returns the day of +time+ according to user's time zone
413 def time_to_date(time)
414 def time_to_date(time)
414 if time_zone.nil?
415 if time_zone.nil?
415 time.to_date
416 time.to_date
416 else
417 else
417 time.in_time_zone(time_zone).to_date
418 time.in_time_zone(time_zone).to_date
418 end
419 end
419 end
420 end
420
421
421 def logged?
422 def logged?
422 true
423 true
423 end
424 end
424
425
425 def anonymous?
426 def anonymous?
426 !logged?
427 !logged?
427 end
428 end
428
429
429 # Return user's roles for project
430 # Return user's roles for project
430 def roles_for_project(project)
431 def roles_for_project(project)
431 roles = []
432 roles = []
432 # No role on archived projects
433 # No role on archived projects
433 return roles if project.nil? || project.archived?
434 return roles if project.nil? || project.archived?
434 if logged?
435 if logged?
435 # Find project membership
436 # Find project membership
436 membership = memberships.detect {|m| m.project_id == project.id}
437 membership = memberships.detect {|m| m.project_id == project.id}
437 if membership
438 if membership
438 roles = membership.roles
439 roles = membership.roles
439 else
440 else
440 @role_non_member ||= Role.non_member
441 @role_non_member ||= Role.non_member
441 roles << @role_non_member
442 roles << @role_non_member
442 end
443 end
443 else
444 else
444 @role_anonymous ||= Role.anonymous
445 @role_anonymous ||= Role.anonymous
445 roles << @role_anonymous
446 roles << @role_anonymous
446 end
447 end
447 roles
448 roles
448 end
449 end
449
450
450 # Return true if the user is a member of project
451 # Return true if the user is a member of project
451 def member_of?(project)
452 def member_of?(project)
452 !roles_for_project(project).detect {|role| role.member?}.nil?
453 !roles_for_project(project).detect {|role| role.member?}.nil?
453 end
454 end
454
455
455 # Returns a hash of user's projects grouped by roles
456 # Returns a hash of user's projects grouped by roles
456 def projects_by_role
457 def projects_by_role
457 return @projects_by_role if @projects_by_role
458 return @projects_by_role if @projects_by_role
458
459
459 @projects_by_role = Hash.new([])
460 @projects_by_role = Hash.new([])
460 memberships.each do |membership|
461 memberships.each do |membership|
461 if membership.project
462 if membership.project
462 membership.roles.each do |role|
463 membership.roles.each do |role|
463 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
464 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
464 @projects_by_role[role] << membership.project
465 @projects_by_role[role] << membership.project
465 end
466 end
466 end
467 end
467 end
468 end
468 @projects_by_role.each do |role, projects|
469 @projects_by_role.each do |role, projects|
469 projects.uniq!
470 projects.uniq!
470 end
471 end
471
472
472 @projects_by_role
473 @projects_by_role
473 end
474 end
474
475
475 # Returns true if user is arg or belongs to arg
476 # Returns true if user is arg or belongs to arg
476 def is_or_belongs_to?(arg)
477 def is_or_belongs_to?(arg)
477 if arg.is_a?(User)
478 if arg.is_a?(User)
478 self == arg
479 self == arg
479 elsif arg.is_a?(Group)
480 elsif arg.is_a?(Group)
480 arg.users.include?(self)
481 arg.users.include?(self)
481 else
482 else
482 false
483 false
483 end
484 end
484 end
485 end
485
486
486 # Return true if the user is allowed to do the specified action on a specific context
487 # Return true if the user is allowed to do the specified action on a specific context
487 # Action can be:
488 # Action can be:
488 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
489 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
489 # * a permission Symbol (eg. :edit_project)
490 # * a permission Symbol (eg. :edit_project)
490 # Context can be:
491 # Context can be:
491 # * a project : returns true if user is allowed to do the specified action on this project
492 # * a project : returns true if user is allowed to do the specified action on this project
492 # * an array of projects : returns true if user is allowed on every project
493 # * an array of projects : returns true if user is allowed on every project
493 # * nil with options[:global] set : check if user has at least one role allowed for this action,
494 # * nil with options[:global] set : check if user has at least one role allowed for this action,
494 # or falls back to Non Member / Anonymous permissions depending if the user is logged
495 # or falls back to Non Member / Anonymous permissions depending if the user is logged
495 def allowed_to?(action, context, options={}, &block)
496 def allowed_to?(action, context, options={}, &block)
496 if context && context.is_a?(Project)
497 if context && context.is_a?(Project)
497 return false unless context.allows_to?(action)
498 return false unless context.allows_to?(action)
498 # Admin users are authorized for anything else
499 # Admin users are authorized for anything else
499 return true if admin?
500 return true if admin?
500
501
501 roles = roles_for_project(context)
502 roles = roles_for_project(context)
502 return false unless roles
503 return false unless roles
503 roles.any? {|role|
504 roles.any? {|role|
504 (context.is_public? || role.member?) &&
505 (context.is_public? || role.member?) &&
505 role.allowed_to?(action) &&
506 role.allowed_to?(action) &&
506 (block_given? ? yield(role, self) : true)
507 (block_given? ? yield(role, self) : true)
507 }
508 }
508 elsif context && context.is_a?(Array)
509 elsif context && context.is_a?(Array)
509 if context.empty?
510 if context.empty?
510 false
511 false
511 else
512 else
512 # Authorize if user is authorized on every element of the array
513 # Authorize if user is authorized on every element of the array
513 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
514 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
514 end
515 end
515 elsif options[:global]
516 elsif options[:global]
516 # Admin users are always authorized
517 # Admin users are always authorized
517 return true if admin?
518 return true if admin?
518
519
519 # authorize if user has at least one role that has this permission
520 # authorize if user has at least one role that has this permission
520 roles = memberships.collect {|m| m.roles}.flatten.uniq
521 roles = memberships.collect {|m| m.roles}.flatten.uniq
521 roles << (self.logged? ? Role.non_member : Role.anonymous)
522 roles << (self.logged? ? Role.non_member : Role.anonymous)
522 roles.any? {|role|
523 roles.any? {|role|
523 role.allowed_to?(action) &&
524 role.allowed_to?(action) &&
524 (block_given? ? yield(role, self) : true)
525 (block_given? ? yield(role, self) : true)
525 }
526 }
526 else
527 else
527 false
528 false
528 end
529 end
529 end
530 end
530
531
531 # Is the user allowed to do the specified action on any project?
532 # Is the user allowed to do the specified action on any project?
532 # See allowed_to? for the actions and valid options.
533 # See allowed_to? for the actions and valid options.
533 def allowed_to_globally?(action, options, &block)
534 def allowed_to_globally?(action, options, &block)
534 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
535 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
535 end
536 end
536
537
537 # Returns true if the user is allowed to delete his own account
538 # Returns true if the user is allowed to delete his own account
538 def own_account_deletable?
539 def own_account_deletable?
539 Setting.unsubscribe? &&
540 Setting.unsubscribe? &&
540 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
541 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
541 end
542 end
542
543
543 safe_attributes 'login',
544 safe_attributes 'login',
544 'firstname',
545 'firstname',
545 'lastname',
546 'lastname',
546 'mail',
547 'mail',
547 'mail_notification',
548 'mail_notification',
548 'language',
549 'language',
549 'custom_field_values',
550 'custom_field_values',
550 'custom_fields',
551 'custom_fields',
551 'identity_url'
552 'identity_url'
552
553
553 safe_attributes 'status',
554 safe_attributes 'status',
554 'auth_source_id',
555 'auth_source_id',
555 :if => lambda {|user, current_user| current_user.admin?}
556 :if => lambda {|user, current_user| current_user.admin?}
556
557
557 safe_attributes 'group_ids',
558 safe_attributes 'group_ids',
558 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
559 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
559
560
560 # Utility method to help check if a user should be notified about an
561 # Utility method to help check if a user should be notified about an
561 # event.
562 # event.
562 #
563 #
563 # TODO: only supports Issue events currently
564 # TODO: only supports Issue events currently
564 def notify_about?(object)
565 def notify_about?(object)
565 case mail_notification
566 case mail_notification
566 when 'all'
567 when 'all'
567 true
568 true
568 when 'selected'
569 when 'selected'
569 # user receives notifications for created/assigned issues on unselected projects
570 # user receives notifications for created/assigned issues on unselected projects
570 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
571 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
571 true
572 true
572 else
573 else
573 false
574 false
574 end
575 end
575 when 'none'
576 when 'none'
576 false
577 false
577 when 'only_my_events'
578 when 'only_my_events'
578 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
579 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
579 true
580 true
580 else
581 else
581 false
582 false
582 end
583 end
583 when 'only_assigned'
584 when 'only_assigned'
584 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
585 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
585 true
586 true
586 else
587 else
587 false
588 false
588 end
589 end
589 when 'only_owner'
590 when 'only_owner'
590 if object.is_a?(Issue) && object.author == self
591 if object.is_a?(Issue) && object.author == self
591 true
592 true
592 else
593 else
593 false
594 false
594 end
595 end
595 else
596 else
596 false
597 false
597 end
598 end
598 end
599 end
599
600
600 def self.current=(user)
601 def self.current=(user)
601 Thread.current[:current_user] = user
602 Thread.current[:current_user] = user
602 end
603 end
603
604
604 def self.current
605 def self.current
605 Thread.current[:current_user] ||= User.anonymous
606 Thread.current[:current_user] ||= User.anonymous
606 end
607 end
607
608
608 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
609 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
609 # one anonymous user per database.
610 # one anonymous user per database.
610 def self.anonymous
611 def self.anonymous
611 anonymous_user = AnonymousUser.first
612 anonymous_user = AnonymousUser.first
612 if anonymous_user.nil?
613 if anonymous_user.nil?
613 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
614 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
614 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
615 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
615 end
616 end
616 anonymous_user
617 anonymous_user
617 end
618 end
618
619
619 # Salts all existing unsalted passwords
620 # Salts all existing unsalted passwords
620 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
621 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
621 # This method is used in the SaltPasswords migration and is to be kept as is
622 # This method is used in the SaltPasswords migration and is to be kept as is
622 def self.salt_unsalted_passwords!
623 def self.salt_unsalted_passwords!
623 transaction do
624 transaction do
624 User.where("salt IS NULL OR salt = ''").find_each do |user|
625 User.where("salt IS NULL OR salt = ''").find_each do |user|
625 next if user.hashed_password.blank?
626 next if user.hashed_password.blank?
626 salt = User.generate_salt
627 salt = User.generate_salt
627 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
628 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
628 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
629 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
629 end
630 end
630 end
631 end
631 end
632 end
632
633
633 protected
634 protected
634
635
635 def validate_password_length
636 def validate_password_length
636 # Password length validation based on setting
637 # Password length validation based on setting
637 if !password.nil? && password.size < Setting.password_min_length.to_i
638 if !password.nil? && password.size < Setting.password_min_length.to_i
638 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
639 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
639 end
640 end
640 end
641 end
641
642
642 private
643 private
643
644
644 # Removes references that are not handled by associations
645 # Removes references that are not handled by associations
645 # Things that are not deleted are reassociated with the anonymous user
646 # Things that are not deleted are reassociated with the anonymous user
646 def remove_references_before_destroy
647 def remove_references_before_destroy
647 return if self.id.nil?
648 return if self.id.nil?
648
649
649 substitute = User.anonymous
650 substitute = User.anonymous
650 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
651 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
651 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
652 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
652 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
653 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
653 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
654 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
654 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
655 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
655 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
656 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
656 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
657 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
657 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
658 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
658 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
659 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
659 # Remove private queries and keep public ones
660 # Remove private queries and keep public ones
660 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
661 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
661 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
662 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
662 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
663 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
663 Token.delete_all ['user_id = ?', id]
664 Token.delete_all ['user_id = ?', id]
664 Watcher.delete_all ['user_id = ?', id]
665 Watcher.delete_all ['user_id = ?', id]
665 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
666 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
666 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
667 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
667 end
668 end
668
669
669 # Return password digest
670 # Return password digest
670 def self.hash_password(clear_password)
671 def self.hash_password(clear_password)
671 Digest::SHA1.hexdigest(clear_password || "")
672 Digest::SHA1.hexdigest(clear_password || "")
672 end
673 end
673
674
674 # Returns a 128bits random salt as a hex string (32 chars long)
675 # Returns a 128bits random salt as a hex string (32 chars long)
675 def self.generate_salt
676 def self.generate_salt
676 Redmine::Utils.random_hex(16)
677 Redmine::Utils.random_hex(16)
677 end
678 end
678
679
679 end
680 end
680
681
681 class AnonymousUser < User
682 class AnonymousUser < User
682 validate :validate_anonymous_uniqueness, :on => :create
683 validate :validate_anonymous_uniqueness, :on => :create
683
684
684 def validate_anonymous_uniqueness
685 def validate_anonymous_uniqueness
685 # There should be only one AnonymousUser in the database
686 # There should be only one AnonymousUser in the database
686 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
687 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
687 end
688 end
688
689
689 def available_custom_fields
690 def available_custom_fields
690 []
691 []
691 end
692 end
692
693
693 # Overrides a few properties
694 # Overrides a few properties
694 def logged?; false end
695 def logged?; false end
695 def admin; false end
696 def admin; false end
696 def name(*args); I18n.t(:label_user_anonymous) end
697 def name(*args); I18n.t(:label_user_anonymous) end
697 def mail; nil end
698 def mail; nil end
698 def time_zone; nil end
699 def time_zone; nil end
699 def rss_key; nil end
700 def rss_key; nil end
700
701
701 def pref
702 def pref
702 UserPreference.new(:user => self)
703 UserPreference.new(:user => self)
703 end
704 end
704
705
705 # Anonymous user can not be destroyed
706 # Anonymous user can not be destroyed
706 def destroy
707 def destroy
707 false
708 false
708 end
709 end
709 end
710 end
@@ -1,118 +1,132
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class PrincipalTest < ActiveSupport::TestCase
22 class PrincipalTest < ActiveSupport::TestCase
23 fixtures :users, :projects, :members, :member_roles
23 fixtures :users, :projects, :members, :member_roles
24
24
25 def test_active_scope_should_return_groups_and_active_users
25 def test_active_scope_should_return_groups_and_active_users
26 result = Principal.active.all
26 result = Principal.active.all
27 assert_include Group.first, result
27 assert_include Group.first, result
28 assert_not_nil result.detect {|p| p.is_a?(User)}
28 assert_not_nil result.detect {|p| p.is_a?(User)}
29 assert_nil result.detect {|p| p.is_a?(User) && !p.active?}
29 assert_nil result.detect {|p| p.is_a?(User) && !p.active?}
30 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
30 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
31 end
31 end
32
32
33 def test_member_of_scope_should_return_the_union_of_all_members
33 def test_member_of_scope_should_return_the_union_of_all_members
34 projects = Project.find_all_by_id(1, 2)
34 projects = Project.find_all_by_id(1, 2)
35 assert_equal projects.map(&:principals).flatten.sort, Principal.member_of(projects).sort
35 assert_equal projects.map(&:principals).flatten.sort, Principal.member_of(projects).sort
36 end
36 end
37
37
38 def test_member_of_scope_should_be_empty_for_no_projects
38 def test_member_of_scope_should_be_empty_for_no_projects
39 assert_equal [], Principal.member_of([]).sort
39 assert_equal [], Principal.member_of([]).sort
40 end
40 end
41
41
42 def test_not_member_of_scope_should_return_users_that_have_no_memberships
42 def test_not_member_of_scope_should_return_users_that_have_no_memberships
43 projects = Project.find_all_by_id(1, 2)
43 projects = Project.find_all_by_id(1, 2)
44 expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort
44 expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort
45 assert_equal expected, Principal.not_member_of(projects).sort
45 assert_equal expected, Principal.not_member_of(projects).sort
46 end
46 end
47
47
48 def test_not_member_of_scope_should_be_empty_for_no_projects
48 def test_not_member_of_scope_should_be_empty_for_no_projects
49 assert_equal [], Principal.not_member_of([]).sort
49 assert_equal [], Principal.not_member_of([]).sort
50 end
50 end
51
51
52 def test_sorted_scope_should_sort_users_before_groups
53 scope = Principal.where("type <> ?", 'AnonymousUser')
54 expected_order = scope.all.sort do |a, b|
55 if a.is_a?(User) && b.is_a?(Group)
56 -1
57 elsif a.is_a?(Group) && b.is_a?(User)
58 1
59 else
60 a.name.downcase <=> b.name.downcase
61 end
62 end
63 assert_equal expected_order.map(&:name).map(&:downcase), scope.sorted.all.map(&:name).map(&:downcase)
64 end
65
52 context "#like" do
66 context "#like" do
53 setup do
67 setup do
54 Principal.create!(:login => 'login')
68 Principal.create!(:login => 'login')
55 Principal.create!(:login => 'login2')
69 Principal.create!(:login => 'login2')
56
70
57 Principal.create!(:firstname => 'firstname')
71 Principal.create!(:firstname => 'firstname')
58 Principal.create!(:firstname => 'firstname2')
72 Principal.create!(:firstname => 'firstname2')
59
73
60 Principal.create!(:lastname => 'lastname')
74 Principal.create!(:lastname => 'lastname')
61 Principal.create!(:lastname => 'lastname2')
75 Principal.create!(:lastname => 'lastname2')
62
76
63 Principal.create!(:mail => 'mail@example.com')
77 Principal.create!(:mail => 'mail@example.com')
64 Principal.create!(:mail => 'mail2@example.com')
78 Principal.create!(:mail => 'mail2@example.com')
65
79
66 @palmer = Principal.create!(:firstname => 'David', :lastname => 'Palmer')
80 @palmer = Principal.create!(:firstname => 'David', :lastname => 'Palmer')
67 end
81 end
68
82
69 should "search login" do
83 should "search login" do
70 results = Principal.like('login')
84 results = Principal.like('login')
71
85
72 assert_equal 2, results.count
86 assert_equal 2, results.count
73 assert results.all? {|u| u.login.match(/login/) }
87 assert results.all? {|u| u.login.match(/login/) }
74 end
88 end
75
89
76 should "search firstname" do
90 should "search firstname" do
77 results = Principal.like('firstname')
91 results = Principal.like('firstname')
78
92
79 assert_equal 2, results.count
93 assert_equal 2, results.count
80 assert results.all? {|u| u.firstname.match(/firstname/) }
94 assert results.all? {|u| u.firstname.match(/firstname/) }
81 end
95 end
82
96
83 should "search lastname" do
97 should "search lastname" do
84 results = Principal.like('lastname')
98 results = Principal.like('lastname')
85
99
86 assert_equal 2, results.count
100 assert_equal 2, results.count
87 assert results.all? {|u| u.lastname.match(/lastname/) }
101 assert results.all? {|u| u.lastname.match(/lastname/) }
88 end
102 end
89
103
90 should "search mail" do
104 should "search mail" do
91 results = Principal.like('mail')
105 results = Principal.like('mail')
92
106
93 assert_equal 2, results.count
107 assert_equal 2, results.count
94 assert results.all? {|u| u.mail.match(/mail/) }
108 assert results.all? {|u| u.mail.match(/mail/) }
95 end
109 end
96
110
97 should "search firstname and lastname" do
111 should "search firstname and lastname" do
98 results = Principal.like('david palm')
112 results = Principal.like('david palm')
99
113
100 assert_equal 1, results.count
114 assert_equal 1, results.count
101 assert_equal @palmer, results.first
115 assert_equal @palmer, results.first
102 end
116 end
103
117
104 should "search lastname and firstname" do
118 should "search lastname and firstname" do
105 results = Principal.like('palmer davi')
119 results = Principal.like('palmer davi')
106
120
107 assert_equal 1, results.count
121 assert_equal 1, results.count
108 assert_equal @palmer, results.first
122 assert_equal @palmer, results.first
109 end
123 end
110 end
124 end
111
125
112 def test_like_scope_with_cyrillic_name
126 def test_like_scope_with_cyrillic_name
113 user = User.generate!(:firstname => 'Π‘ΠΎΠ±ΠΎΠ»Π΅Π²', :lastname => 'ДСнис')
127 user = User.generate!(:firstname => 'Π‘ΠΎΠ±ΠΎΠ»Π΅Π²', :lastname => 'ДСнис')
114 results = Principal.like('Π‘ΠΎΠ±ΠΎ')
128 results = Principal.like('Π‘ΠΎΠ±ΠΎ')
115 assert_equal 1, results.count
129 assert_equal 1, results.count
116 assert_equal user, results.first
130 assert_equal user, results.first
117 end
131 end
118 end
132 end
@@ -1,1075 +1,1079
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 UserTest < ActiveSupport::TestCase
20 class UserTest < ActiveSupport::TestCase
21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources,
21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources,
22 :trackers, :issue_statuses,
22 :trackers, :issue_statuses,
23 :projects_trackers,
23 :projects_trackers,
24 :watchers,
24 :watchers,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :journals, :journal_details,
26 :journals, :journal_details,
27 :groups_users,
27 :groups_users,
28 :enabled_modules,
28 :enabled_modules,
29 :workflows
29 :workflows
30
30
31 def setup
31 def setup
32 @admin = User.find(1)
32 @admin = User.find(1)
33 @jsmith = User.find(2)
33 @jsmith = User.find(2)
34 @dlopper = User.find(3)
34 @dlopper = User.find(3)
35 end
35 end
36
36
37 def test_sorted_scope_should_sort_user_by_display_name
38 assert_equal User.all.map(&:name).map(&:downcase).sort, User.sorted.all.map(&:name).map(&:downcase)
39 end
40
37 def test_generate
41 def test_generate
38 User.generate!(:firstname => 'Testing connection')
42 User.generate!(:firstname => 'Testing connection')
39 User.generate!(:firstname => 'Testing connection')
43 User.generate!(:firstname => 'Testing connection')
40 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
44 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
41 end
45 end
42
46
43 def test_truth
47 def test_truth
44 assert_kind_of User, @jsmith
48 assert_kind_of User, @jsmith
45 end
49 end
46
50
47 def test_mail_should_be_stripped
51 def test_mail_should_be_stripped
48 u = User.new
52 u = User.new
49 u.mail = " foo@bar.com "
53 u.mail = " foo@bar.com "
50 assert_equal "foo@bar.com", u.mail
54 assert_equal "foo@bar.com", u.mail
51 end
55 end
52
56
53 def test_mail_validation
57 def test_mail_validation
54 u = User.new
58 u = User.new
55 u.mail = ''
59 u.mail = ''
56 assert !u.valid?
60 assert !u.valid?
57 assert_include I18n.translate('activerecord.errors.messages.blank'), u.errors[:mail]
61 assert_include I18n.translate('activerecord.errors.messages.blank'), u.errors[:mail]
58 end
62 end
59
63
60 def test_login_length_validation
64 def test_login_length_validation
61 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
65 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
62 user.login = "x" * (User::LOGIN_LENGTH_LIMIT+1)
66 user.login = "x" * (User::LOGIN_LENGTH_LIMIT+1)
63 assert !user.valid?
67 assert !user.valid?
64
68
65 user.login = "x" * (User::LOGIN_LENGTH_LIMIT)
69 user.login = "x" * (User::LOGIN_LENGTH_LIMIT)
66 assert user.valid?
70 assert user.valid?
67 assert user.save
71 assert user.save
68 end
72 end
69
73
70 def test_create
74 def test_create
71 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
75 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
72
76
73 user.login = "jsmith"
77 user.login = "jsmith"
74 user.password, user.password_confirmation = "password", "password"
78 user.password, user.password_confirmation = "password", "password"
75 # login uniqueness
79 # login uniqueness
76 assert !user.save
80 assert !user.save
77 assert_equal 1, user.errors.count
81 assert_equal 1, user.errors.count
78
82
79 user.login = "newuser"
83 user.login = "newuser"
80 user.password, user.password_confirmation = "password", "pass"
84 user.password, user.password_confirmation = "password", "pass"
81 # password confirmation
85 # password confirmation
82 assert !user.save
86 assert !user.save
83 assert_equal 1, user.errors.count
87 assert_equal 1, user.errors.count
84
88
85 user.password, user.password_confirmation = "password", "password"
89 user.password, user.password_confirmation = "password", "password"
86 assert user.save
90 assert user.save
87 end
91 end
88
92
89 def test_user_before_create_should_set_the_mail_notification_to_the_default_setting
93 def test_user_before_create_should_set_the_mail_notification_to_the_default_setting
90 @user1 = User.generate!
94 @user1 = User.generate!
91 assert_equal 'only_my_events', @user1.mail_notification
95 assert_equal 'only_my_events', @user1.mail_notification
92 with_settings :default_notification_option => 'all' do
96 with_settings :default_notification_option => 'all' do
93 @user2 = User.generate!
97 @user2 = User.generate!
94 assert_equal 'all', @user2.mail_notification
98 assert_equal 'all', @user2.mail_notification
95 end
99 end
96 end
100 end
97
101
98 def test_user_login_should_be_case_insensitive
102 def test_user_login_should_be_case_insensitive
99 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
103 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
100 u.login = 'newuser'
104 u.login = 'newuser'
101 u.password, u.password_confirmation = "password", "password"
105 u.password, u.password_confirmation = "password", "password"
102 assert u.save
106 assert u.save
103 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
107 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
104 u.login = 'NewUser'
108 u.login = 'NewUser'
105 u.password, u.password_confirmation = "password", "password"
109 u.password, u.password_confirmation = "password", "password"
106 assert !u.save
110 assert !u.save
107 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:login]
111 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:login]
108 end
112 end
109
113
110 def test_mail_uniqueness_should_not_be_case_sensitive
114 def test_mail_uniqueness_should_not_be_case_sensitive
111 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
115 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
112 u.login = 'newuser1'
116 u.login = 'newuser1'
113 u.password, u.password_confirmation = "password", "password"
117 u.password, u.password_confirmation = "password", "password"
114 assert u.save
118 assert u.save
115
119
116 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
120 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
117 u.login = 'newuser2'
121 u.login = 'newuser2'
118 u.password, u.password_confirmation = "password", "password"
122 u.password, u.password_confirmation = "password", "password"
119 assert !u.save
123 assert !u.save
120 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:mail]
124 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:mail]
121 end
125 end
122
126
123 def test_update
127 def test_update
124 assert_equal "admin", @admin.login
128 assert_equal "admin", @admin.login
125 @admin.login = "john"
129 @admin.login = "john"
126 assert @admin.save, @admin.errors.full_messages.join("; ")
130 assert @admin.save, @admin.errors.full_messages.join("; ")
127 @admin.reload
131 @admin.reload
128 assert_equal "john", @admin.login
132 assert_equal "john", @admin.login
129 end
133 end
130
134
131 def test_update_should_not_fail_for_legacy_user_with_different_case_logins
135 def test_update_should_not_fail_for_legacy_user_with_different_case_logins
132 u1 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser1@somenet.foo")
136 u1 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser1@somenet.foo")
133 u1.login = 'newuser1'
137 u1.login = 'newuser1'
134 assert u1.save
138 assert u1.save
135
139
136 u2 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser2@somenet.foo")
140 u2 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser2@somenet.foo")
137 u2.login = 'newuser1'
141 u2.login = 'newuser1'
138 assert u2.save(:validate => false)
142 assert u2.save(:validate => false)
139
143
140 user = User.find(u2.id)
144 user = User.find(u2.id)
141 user.firstname = "firstname"
145 user.firstname = "firstname"
142 assert user.save, "Save failed"
146 assert user.save, "Save failed"
143 end
147 end
144
148
145 def test_destroy_should_delete_members_and_roles
149 def test_destroy_should_delete_members_and_roles
146 members = Member.find_all_by_user_id(2)
150 members = Member.find_all_by_user_id(2)
147 ms = members.size
151 ms = members.size
148 rs = members.collect(&:roles).flatten.size
152 rs = members.collect(&:roles).flatten.size
149
153
150 assert_difference 'Member.count', - ms do
154 assert_difference 'Member.count', - ms do
151 assert_difference 'MemberRole.count', - rs do
155 assert_difference 'MemberRole.count', - rs do
152 User.find(2).destroy
156 User.find(2).destroy
153 end
157 end
154 end
158 end
155
159
156 assert_nil User.find_by_id(2)
160 assert_nil User.find_by_id(2)
157 assert Member.find_all_by_user_id(2).empty?
161 assert Member.find_all_by_user_id(2).empty?
158 end
162 end
159
163
160 def test_destroy_should_update_attachments
164 def test_destroy_should_update_attachments
161 attachment = Attachment.create!(:container => Project.find(1),
165 attachment = Attachment.create!(:container => Project.find(1),
162 :file => uploaded_test_file("testfile.txt", "text/plain"),
166 :file => uploaded_test_file("testfile.txt", "text/plain"),
163 :author_id => 2)
167 :author_id => 2)
164
168
165 User.find(2).destroy
169 User.find(2).destroy
166 assert_nil User.find_by_id(2)
170 assert_nil User.find_by_id(2)
167 assert_equal User.anonymous, attachment.reload.author
171 assert_equal User.anonymous, attachment.reload.author
168 end
172 end
169
173
170 def test_destroy_should_update_comments
174 def test_destroy_should_update_comments
171 comment = Comment.create!(
175 comment = Comment.create!(
172 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
176 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
173 :author => User.find(2),
177 :author => User.find(2),
174 :comments => 'foo'
178 :comments => 'foo'
175 )
179 )
176
180
177 User.find(2).destroy
181 User.find(2).destroy
178 assert_nil User.find_by_id(2)
182 assert_nil User.find_by_id(2)
179 assert_equal User.anonymous, comment.reload.author
183 assert_equal User.anonymous, comment.reload.author
180 end
184 end
181
185
182 def test_destroy_should_update_issues
186 def test_destroy_should_update_issues
183 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
187 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
184
188
185 User.find(2).destroy
189 User.find(2).destroy
186 assert_nil User.find_by_id(2)
190 assert_nil User.find_by_id(2)
187 assert_equal User.anonymous, issue.reload.author
191 assert_equal User.anonymous, issue.reload.author
188 end
192 end
189
193
190 def test_destroy_should_unassign_issues
194 def test_destroy_should_unassign_issues
191 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
195 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
192
196
193 User.find(2).destroy
197 User.find(2).destroy
194 assert_nil User.find_by_id(2)
198 assert_nil User.find_by_id(2)
195 assert_nil issue.reload.assigned_to
199 assert_nil issue.reload.assigned_to
196 end
200 end
197
201
198 def test_destroy_should_update_journals
202 def test_destroy_should_update_journals
199 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
203 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
200 issue.init_journal(User.find(2), "update")
204 issue.init_journal(User.find(2), "update")
201 issue.save!
205 issue.save!
202
206
203 User.find(2).destroy
207 User.find(2).destroy
204 assert_nil User.find_by_id(2)
208 assert_nil User.find_by_id(2)
205 assert_equal User.anonymous, issue.journals.first.reload.user
209 assert_equal User.anonymous, issue.journals.first.reload.user
206 end
210 end
207
211
208 def test_destroy_should_update_journal_details_old_value
212 def test_destroy_should_update_journal_details_old_value
209 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
213 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
210 issue.init_journal(User.find(1), "update")
214 issue.init_journal(User.find(1), "update")
211 issue.assigned_to_id = nil
215 issue.assigned_to_id = nil
212 assert_difference 'JournalDetail.count' do
216 assert_difference 'JournalDetail.count' do
213 issue.save!
217 issue.save!
214 end
218 end
215 journal_detail = JournalDetail.first(:order => 'id DESC')
219 journal_detail = JournalDetail.first(:order => 'id DESC')
216 assert_equal '2', journal_detail.old_value
220 assert_equal '2', journal_detail.old_value
217
221
218 User.find(2).destroy
222 User.find(2).destroy
219 assert_nil User.find_by_id(2)
223 assert_nil User.find_by_id(2)
220 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
224 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
221 end
225 end
222
226
223 def test_destroy_should_update_journal_details_value
227 def test_destroy_should_update_journal_details_value
224 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
228 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
225 issue.init_journal(User.find(1), "update")
229 issue.init_journal(User.find(1), "update")
226 issue.assigned_to_id = 2
230 issue.assigned_to_id = 2
227 assert_difference 'JournalDetail.count' do
231 assert_difference 'JournalDetail.count' do
228 issue.save!
232 issue.save!
229 end
233 end
230 journal_detail = JournalDetail.first(:order => 'id DESC')
234 journal_detail = JournalDetail.first(:order => 'id DESC')
231 assert_equal '2', journal_detail.value
235 assert_equal '2', journal_detail.value
232
236
233 User.find(2).destroy
237 User.find(2).destroy
234 assert_nil User.find_by_id(2)
238 assert_nil User.find_by_id(2)
235 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
239 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
236 end
240 end
237
241
238 def test_destroy_should_update_messages
242 def test_destroy_should_update_messages
239 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
243 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
240 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
244 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
241
245
242 User.find(2).destroy
246 User.find(2).destroy
243 assert_nil User.find_by_id(2)
247 assert_nil User.find_by_id(2)
244 assert_equal User.anonymous, message.reload.author
248 assert_equal User.anonymous, message.reload.author
245 end
249 end
246
250
247 def test_destroy_should_update_news
251 def test_destroy_should_update_news
248 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
252 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
249
253
250 User.find(2).destroy
254 User.find(2).destroy
251 assert_nil User.find_by_id(2)
255 assert_nil User.find_by_id(2)
252 assert_equal User.anonymous, news.reload.author
256 assert_equal User.anonymous, news.reload.author
253 end
257 end
254
258
255 def test_destroy_should_delete_private_queries
259 def test_destroy_should_delete_private_queries
256 query = Query.new(:name => 'foo', :is_public => false)
260 query = Query.new(:name => 'foo', :is_public => false)
257 query.project_id = 1
261 query.project_id = 1
258 query.user_id = 2
262 query.user_id = 2
259 query.save!
263 query.save!
260
264
261 User.find(2).destroy
265 User.find(2).destroy
262 assert_nil User.find_by_id(2)
266 assert_nil User.find_by_id(2)
263 assert_nil Query.find_by_id(query.id)
267 assert_nil Query.find_by_id(query.id)
264 end
268 end
265
269
266 def test_destroy_should_update_public_queries
270 def test_destroy_should_update_public_queries
267 query = Query.new(:name => 'foo', :is_public => true)
271 query = Query.new(:name => 'foo', :is_public => true)
268 query.project_id = 1
272 query.project_id = 1
269 query.user_id = 2
273 query.user_id = 2
270 query.save!
274 query.save!
271
275
272 User.find(2).destroy
276 User.find(2).destroy
273 assert_nil User.find_by_id(2)
277 assert_nil User.find_by_id(2)
274 assert_equal User.anonymous, query.reload.user
278 assert_equal User.anonymous, query.reload.user
275 end
279 end
276
280
277 def test_destroy_should_update_time_entries
281 def test_destroy_should_update_time_entries
278 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
282 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
279 entry.project_id = 1
283 entry.project_id = 1
280 entry.user_id = 2
284 entry.user_id = 2
281 entry.save!
285 entry.save!
282
286
283 User.find(2).destroy
287 User.find(2).destroy
284 assert_nil User.find_by_id(2)
288 assert_nil User.find_by_id(2)
285 assert_equal User.anonymous, entry.reload.user
289 assert_equal User.anonymous, entry.reload.user
286 end
290 end
287
291
288 def test_destroy_should_delete_tokens
292 def test_destroy_should_delete_tokens
289 token = Token.create!(:user_id => 2, :value => 'foo')
293 token = Token.create!(:user_id => 2, :value => 'foo')
290
294
291 User.find(2).destroy
295 User.find(2).destroy
292 assert_nil User.find_by_id(2)
296 assert_nil User.find_by_id(2)
293 assert_nil Token.find_by_id(token.id)
297 assert_nil Token.find_by_id(token.id)
294 end
298 end
295
299
296 def test_destroy_should_delete_watchers
300 def test_destroy_should_delete_watchers
297 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
301 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
298 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
302 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
299
303
300 User.find(2).destroy
304 User.find(2).destroy
301 assert_nil User.find_by_id(2)
305 assert_nil User.find_by_id(2)
302 assert_nil Watcher.find_by_id(watcher.id)
306 assert_nil Watcher.find_by_id(watcher.id)
303 end
307 end
304
308
305 def test_destroy_should_update_wiki_contents
309 def test_destroy_should_update_wiki_contents
306 wiki_content = WikiContent.create!(
310 wiki_content = WikiContent.create!(
307 :text => 'foo',
311 :text => 'foo',
308 :author_id => 2,
312 :author_id => 2,
309 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
313 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
310 )
314 )
311 wiki_content.text = 'bar'
315 wiki_content.text = 'bar'
312 assert_difference 'WikiContent::Version.count' do
316 assert_difference 'WikiContent::Version.count' do
313 wiki_content.save!
317 wiki_content.save!
314 end
318 end
315
319
316 User.find(2).destroy
320 User.find(2).destroy
317 assert_nil User.find_by_id(2)
321 assert_nil User.find_by_id(2)
318 assert_equal User.anonymous, wiki_content.reload.author
322 assert_equal User.anonymous, wiki_content.reload.author
319 wiki_content.versions.each do |version|
323 wiki_content.versions.each do |version|
320 assert_equal User.anonymous, version.reload.author
324 assert_equal User.anonymous, version.reload.author
321 end
325 end
322 end
326 end
323
327
324 def test_destroy_should_nullify_issue_categories
328 def test_destroy_should_nullify_issue_categories
325 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
329 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
326
330
327 User.find(2).destroy
331 User.find(2).destroy
328 assert_nil User.find_by_id(2)
332 assert_nil User.find_by_id(2)
329 assert_nil category.reload.assigned_to_id
333 assert_nil category.reload.assigned_to_id
330 end
334 end
331
335
332 def test_destroy_should_nullify_changesets
336 def test_destroy_should_nullify_changesets
333 changeset = Changeset.create!(
337 changeset = Changeset.create!(
334 :repository => Repository::Subversion.create!(
338 :repository => Repository::Subversion.create!(
335 :project_id => 1,
339 :project_id => 1,
336 :url => 'file:///tmp',
340 :url => 'file:///tmp',
337 :identifier => 'tmp'
341 :identifier => 'tmp'
338 ),
342 ),
339 :revision => '12',
343 :revision => '12',
340 :committed_on => Time.now,
344 :committed_on => Time.now,
341 :committer => 'jsmith'
345 :committer => 'jsmith'
342 )
346 )
343 assert_equal 2, changeset.user_id
347 assert_equal 2, changeset.user_id
344
348
345 User.find(2).destroy
349 User.find(2).destroy
346 assert_nil User.find_by_id(2)
350 assert_nil User.find_by_id(2)
347 assert_nil changeset.reload.user_id
351 assert_nil changeset.reload.user_id
348 end
352 end
349
353
350 def test_anonymous_user_should_not_be_destroyable
354 def test_anonymous_user_should_not_be_destroyable
351 assert_no_difference 'User.count' do
355 assert_no_difference 'User.count' do
352 assert_equal false, User.anonymous.destroy
356 assert_equal false, User.anonymous.destroy
353 end
357 end
354 end
358 end
355
359
356 def test_validate_login_presence
360 def test_validate_login_presence
357 @admin.login = ""
361 @admin.login = ""
358 assert !@admin.save
362 assert !@admin.save
359 assert_equal 1, @admin.errors.count
363 assert_equal 1, @admin.errors.count
360 end
364 end
361
365
362 def test_validate_mail_notification_inclusion
366 def test_validate_mail_notification_inclusion
363 u = User.new
367 u = User.new
364 u.mail_notification = 'foo'
368 u.mail_notification = 'foo'
365 u.save
369 u.save
366 assert_not_nil u.errors[:mail_notification]
370 assert_not_nil u.errors[:mail_notification]
367 end
371 end
368
372
369 context "User#try_to_login" do
373 context "User#try_to_login" do
370 should "fall-back to case-insensitive if user login is not found as-typed." do
374 should "fall-back to case-insensitive if user login is not found as-typed." do
371 user = User.try_to_login("AdMin", "admin")
375 user = User.try_to_login("AdMin", "admin")
372 assert_kind_of User, user
376 assert_kind_of User, user
373 assert_equal "admin", user.login
377 assert_equal "admin", user.login
374 end
378 end
375
379
376 should "select the exact matching user first" do
380 should "select the exact matching user first" do
377 case_sensitive_user = User.generate! do |user|
381 case_sensitive_user = User.generate! do |user|
378 user.password = "admin123"
382 user.password = "admin123"
379 end
383 end
380 # bypass validations to make it appear like existing data
384 # bypass validations to make it appear like existing data
381 case_sensitive_user.update_attribute(:login, 'ADMIN')
385 case_sensitive_user.update_attribute(:login, 'ADMIN')
382
386
383 user = User.try_to_login("ADMIN", "admin123")
387 user = User.try_to_login("ADMIN", "admin123")
384 assert_kind_of User, user
388 assert_kind_of User, user
385 assert_equal "ADMIN", user.login
389 assert_equal "ADMIN", user.login
386
390
387 end
391 end
388 end
392 end
389
393
390 def test_password
394 def test_password
391 user = User.try_to_login("admin", "admin")
395 user = User.try_to_login("admin", "admin")
392 assert_kind_of User, user
396 assert_kind_of User, user
393 assert_equal "admin", user.login
397 assert_equal "admin", user.login
394 user.password = "hello123"
398 user.password = "hello123"
395 assert user.save
399 assert user.save
396
400
397 user = User.try_to_login("admin", "hello123")
401 user = User.try_to_login("admin", "hello123")
398 assert_kind_of User, user
402 assert_kind_of User, user
399 assert_equal "admin", user.login
403 assert_equal "admin", user.login
400 end
404 end
401
405
402 def test_validate_password_length
406 def test_validate_password_length
403 with_settings :password_min_length => '100' do
407 with_settings :password_min_length => '100' do
404 user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo")
408 user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo")
405 user.login = "newuser100"
409 user.login = "newuser100"
406 user.password, user.password_confirmation = "password100", "password100"
410 user.password, user.password_confirmation = "password100", "password100"
407 assert !user.save
411 assert !user.save
408 assert_equal 1, user.errors.count
412 assert_equal 1, user.errors.count
409 end
413 end
410 end
414 end
411
415
412 def test_name_format
416 def test_name_format
413 assert_equal 'John S.', @jsmith.name(:firstname_lastinitial)
417 assert_equal 'John S.', @jsmith.name(:firstname_lastinitial)
414 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
418 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
415 with_settings :user_format => :firstname_lastname do
419 with_settings :user_format => :firstname_lastname do
416 assert_equal 'John Smith', @jsmith.reload.name
420 assert_equal 'John Smith', @jsmith.reload.name
417 end
421 end
418 with_settings :user_format => :username do
422 with_settings :user_format => :username do
419 assert_equal 'jsmith', @jsmith.reload.name
423 assert_equal 'jsmith', @jsmith.reload.name
420 end
424 end
421 with_settings :user_format => :lastname do
425 with_settings :user_format => :lastname do
422 assert_equal 'Smith', @jsmith.reload.name
426 assert_equal 'Smith', @jsmith.reload.name
423 end
427 end
424 end
428 end
425
429
426 def test_today_should_return_the_day_according_to_user_time_zone
430 def test_today_should_return_the_day_according_to_user_time_zone
427 preference = User.find(1).pref
431 preference = User.find(1).pref
428 date = Date.new(2012, 05, 15)
432 date = Date.new(2012, 05, 15)
429 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
433 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
430 Date.stubs(:today).returns(date)
434 Date.stubs(:today).returns(date)
431 Time.stubs(:now).returns(time)
435 Time.stubs(:now).returns(time)
432
436
433 preference.update_attribute :time_zone, 'Baku' # UTC+4
437 preference.update_attribute :time_zone, 'Baku' # UTC+4
434 assert_equal '2012-05-16', User.find(1).today.to_s
438 assert_equal '2012-05-16', User.find(1).today.to_s
435
439
436 preference.update_attribute :time_zone, 'La Paz' # UTC-4
440 preference.update_attribute :time_zone, 'La Paz' # UTC-4
437 assert_equal '2012-05-15', User.find(1).today.to_s
441 assert_equal '2012-05-15', User.find(1).today.to_s
438
442
439 preference.update_attribute :time_zone, ''
443 preference.update_attribute :time_zone, ''
440 assert_equal '2012-05-15', User.find(1).today.to_s
444 assert_equal '2012-05-15', User.find(1).today.to_s
441 end
445 end
442
446
443 def test_time_to_date_should_return_the_date_according_to_user_time_zone
447 def test_time_to_date_should_return_the_date_according_to_user_time_zone
444 preference = User.find(1).pref
448 preference = User.find(1).pref
445 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
449 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
446
450
447 preference.update_attribute :time_zone, 'Baku' # UTC+4
451 preference.update_attribute :time_zone, 'Baku' # UTC+4
448 assert_equal '2012-05-16', User.find(1).time_to_date(time).to_s
452 assert_equal '2012-05-16', User.find(1).time_to_date(time).to_s
449
453
450 preference.update_attribute :time_zone, 'La Paz' # UTC-4
454 preference.update_attribute :time_zone, 'La Paz' # UTC-4
451 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
455 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
452
456
453 preference.update_attribute :time_zone, ''
457 preference.update_attribute :time_zone, ''
454 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
458 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
455 end
459 end
456
460
457 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
461 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
458 with_settings :user_format => 'lastname_coma_firstname' do
462 with_settings :user_format => 'lastname_coma_firstname' do
459 assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement
463 assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement
460 end
464 end
461 end
465 end
462
466
463 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
467 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
464 with_settings :user_format => 'lastname_firstname' do
468 with_settings :user_format => 'lastname_firstname' do
465 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors')
469 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors')
466 end
470 end
467 end
471 end
468
472
469 def test_fields_for_order_statement_with_blank_format_should_return_default
473 def test_fields_for_order_statement_with_blank_format_should_return_default
470 with_settings :user_format => '' do
474 with_settings :user_format => '' do
471 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
475 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
472 end
476 end
473 end
477 end
474
478
475 def test_fields_for_order_statement_with_invalid_format_should_return_default
479 def test_fields_for_order_statement_with_invalid_format_should_return_default
476 with_settings :user_format => 'foo' do
480 with_settings :user_format => 'foo' do
477 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
481 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
478 end
482 end
479 end
483 end
480
484
481 def test_lock
485 def test_lock
482 user = User.try_to_login("jsmith", "jsmith")
486 user = User.try_to_login("jsmith", "jsmith")
483 assert_equal @jsmith, user
487 assert_equal @jsmith, user
484
488
485 @jsmith.status = User::STATUS_LOCKED
489 @jsmith.status = User::STATUS_LOCKED
486 assert @jsmith.save
490 assert @jsmith.save
487
491
488 user = User.try_to_login("jsmith", "jsmith")
492 user = User.try_to_login("jsmith", "jsmith")
489 assert_equal nil, user
493 assert_equal nil, user
490 end
494 end
491
495
492 context ".try_to_login" do
496 context ".try_to_login" do
493 context "with good credentials" do
497 context "with good credentials" do
494 should "return the user" do
498 should "return the user" do
495 user = User.try_to_login("admin", "admin")
499 user = User.try_to_login("admin", "admin")
496 assert_kind_of User, user
500 assert_kind_of User, user
497 assert_equal "admin", user.login
501 assert_equal "admin", user.login
498 end
502 end
499 end
503 end
500
504
501 context "with wrong credentials" do
505 context "with wrong credentials" do
502 should "return nil" do
506 should "return nil" do
503 assert_nil User.try_to_login("admin", "foo")
507 assert_nil User.try_to_login("admin", "foo")
504 end
508 end
505 end
509 end
506 end
510 end
507
511
508 if ldap_configured?
512 if ldap_configured?
509 context "#try_to_login using LDAP" do
513 context "#try_to_login using LDAP" do
510 context "with failed connection to the LDAP server" do
514 context "with failed connection to the LDAP server" do
511 should "return nil" do
515 should "return nil" do
512 @auth_source = AuthSourceLdap.find(1)
516 @auth_source = AuthSourceLdap.find(1)
513 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
517 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
514
518
515 assert_equal nil, User.try_to_login('edavis', 'wrong')
519 assert_equal nil, User.try_to_login('edavis', 'wrong')
516 end
520 end
517 end
521 end
518
522
519 context "with an unsuccessful authentication" do
523 context "with an unsuccessful authentication" do
520 should "return nil" do
524 should "return nil" do
521 assert_equal nil, User.try_to_login('edavis', 'wrong')
525 assert_equal nil, User.try_to_login('edavis', 'wrong')
522 end
526 end
523 end
527 end
524
528
525 context "binding with user's account" do
529 context "binding with user's account" do
526 setup do
530 setup do
527 @auth_source = AuthSourceLdap.find(1)
531 @auth_source = AuthSourceLdap.find(1)
528 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
532 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
529 @auth_source.account_password = ''
533 @auth_source.account_password = ''
530 @auth_source.save!
534 @auth_source.save!
531
535
532 @ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1)
536 @ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1)
533 @ldap_user.login = 'example1'
537 @ldap_user.login = 'example1'
534 @ldap_user.save!
538 @ldap_user.save!
535 end
539 end
536
540
537 context "with a successful authentication" do
541 context "with a successful authentication" do
538 should "return the user" do
542 should "return the user" do
539 assert_equal @ldap_user, User.try_to_login('example1', '123456')
543 assert_equal @ldap_user, User.try_to_login('example1', '123456')
540 end
544 end
541 end
545 end
542
546
543 context "with an unsuccessful authentication" do
547 context "with an unsuccessful authentication" do
544 should "return nil" do
548 should "return nil" do
545 assert_nil User.try_to_login('example1', '11111')
549 assert_nil User.try_to_login('example1', '11111')
546 end
550 end
547 end
551 end
548 end
552 end
549
553
550 context "on the fly registration" do
554 context "on the fly registration" do
551 setup do
555 setup do
552 @auth_source = AuthSourceLdap.find(1)
556 @auth_source = AuthSourceLdap.find(1)
553 @auth_source.update_attribute :onthefly_register, true
557 @auth_source.update_attribute :onthefly_register, true
554 end
558 end
555
559
556 context "with a successful authentication" do
560 context "with a successful authentication" do
557 should "create a new user account if it doesn't exist" do
561 should "create a new user account if it doesn't exist" do
558 assert_difference('User.count') do
562 assert_difference('User.count') do
559 user = User.try_to_login('edavis', '123456')
563 user = User.try_to_login('edavis', '123456')
560 assert !user.admin?
564 assert !user.admin?
561 end
565 end
562 end
566 end
563
567
564 should "retrieve existing user" do
568 should "retrieve existing user" do
565 user = User.try_to_login('edavis', '123456')
569 user = User.try_to_login('edavis', '123456')
566 user.admin = true
570 user.admin = true
567 user.save!
571 user.save!
568
572
569 assert_no_difference('User.count') do
573 assert_no_difference('User.count') do
570 user = User.try_to_login('edavis', '123456')
574 user = User.try_to_login('edavis', '123456')
571 assert user.admin?
575 assert user.admin?
572 end
576 end
573 end
577 end
574 end
578 end
575
579
576 context "binding with user's account" do
580 context "binding with user's account" do
577 setup do
581 setup do
578 @auth_source = AuthSourceLdap.find(1)
582 @auth_source = AuthSourceLdap.find(1)
579 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
583 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
580 @auth_source.account_password = ''
584 @auth_source.account_password = ''
581 @auth_source.save!
585 @auth_source.save!
582 end
586 end
583
587
584 context "with a successful authentication" do
588 context "with a successful authentication" do
585 should "create a new user account if it doesn't exist" do
589 should "create a new user account if it doesn't exist" do
586 assert_difference('User.count') do
590 assert_difference('User.count') do
587 user = User.try_to_login('example1', '123456')
591 user = User.try_to_login('example1', '123456')
588 assert_kind_of User, user
592 assert_kind_of User, user
589 end
593 end
590 end
594 end
591 end
595 end
592
596
593 context "with an unsuccessful authentication" do
597 context "with an unsuccessful authentication" do
594 should "return nil" do
598 should "return nil" do
595 assert_nil User.try_to_login('example1', '11111')
599 assert_nil User.try_to_login('example1', '11111')
596 end
600 end
597 end
601 end
598 end
602 end
599 end
603 end
600 end
604 end
601
605
602 else
606 else
603 puts "Skipping LDAP tests."
607 puts "Skipping LDAP tests."
604 end
608 end
605
609
606 def test_create_anonymous
610 def test_create_anonymous
607 AnonymousUser.delete_all
611 AnonymousUser.delete_all
608 anon = User.anonymous
612 anon = User.anonymous
609 assert !anon.new_record?
613 assert !anon.new_record?
610 assert_kind_of AnonymousUser, anon
614 assert_kind_of AnonymousUser, anon
611 end
615 end
612
616
613 def test_ensure_single_anonymous_user
617 def test_ensure_single_anonymous_user
614 AnonymousUser.delete_all
618 AnonymousUser.delete_all
615 anon1 = User.anonymous
619 anon1 = User.anonymous
616 assert !anon1.new_record?
620 assert !anon1.new_record?
617 assert_kind_of AnonymousUser, anon1
621 assert_kind_of AnonymousUser, anon1
618 anon2 = AnonymousUser.create(
622 anon2 = AnonymousUser.create(
619 :lastname => 'Anonymous', :firstname => '',
623 :lastname => 'Anonymous', :firstname => '',
620 :mail => '', :login => '', :status => 0)
624 :mail => '', :login => '', :status => 0)
621 assert_equal 1, anon2.errors.count
625 assert_equal 1, anon2.errors.count
622 end
626 end
623
627
624 def test_rss_key
628 def test_rss_key
625 assert_nil @jsmith.rss_token
629 assert_nil @jsmith.rss_token
626 key = @jsmith.rss_key
630 key = @jsmith.rss_key
627 assert_equal 40, key.length
631 assert_equal 40, key.length
628
632
629 @jsmith.reload
633 @jsmith.reload
630 assert_equal key, @jsmith.rss_key
634 assert_equal key, @jsmith.rss_key
631 end
635 end
632
636
633 def test_rss_key_should_not_be_generated_twice
637 def test_rss_key_should_not_be_generated_twice
634 assert_difference 'Token.count', 1 do
638 assert_difference 'Token.count', 1 do
635 key1 = @jsmith.rss_key
639 key1 = @jsmith.rss_key
636 key2 = @jsmith.rss_key
640 key2 = @jsmith.rss_key
637 assert_equal key1, key2
641 assert_equal key1, key2
638 end
642 end
639 end
643 end
640
644
641 def test_api_key_should_not_be_generated_twice
645 def test_api_key_should_not_be_generated_twice
642 assert_difference 'Token.count', 1 do
646 assert_difference 'Token.count', 1 do
643 key1 = @jsmith.api_key
647 key1 = @jsmith.api_key
644 key2 = @jsmith.api_key
648 key2 = @jsmith.api_key
645 assert_equal key1, key2
649 assert_equal key1, key2
646 end
650 end
647 end
651 end
648
652
649 context "User#api_key" do
653 context "User#api_key" do
650 should "generate a new one if the user doesn't have one" do
654 should "generate a new one if the user doesn't have one" do
651 user = User.generate!(:api_token => nil)
655 user = User.generate!(:api_token => nil)
652 assert_nil user.api_token
656 assert_nil user.api_token
653
657
654 key = user.api_key
658 key = user.api_key
655 assert_equal 40, key.length
659 assert_equal 40, key.length
656 user.reload
660 user.reload
657 assert_equal key, user.api_key
661 assert_equal key, user.api_key
658 end
662 end
659
663
660 should "return the existing api token value" do
664 should "return the existing api token value" do
661 user = User.generate!
665 user = User.generate!
662 token = Token.create!(:action => 'api')
666 token = Token.create!(:action => 'api')
663 user.api_token = token
667 user.api_token = token
664 assert user.save
668 assert user.save
665
669
666 assert_equal token.value, user.api_key
670 assert_equal token.value, user.api_key
667 end
671 end
668 end
672 end
669
673
670 context "User#find_by_api_key" do
674 context "User#find_by_api_key" do
671 should "return nil if no matching key is found" do
675 should "return nil if no matching key is found" do
672 assert_nil User.find_by_api_key('zzzzzzzzz')
676 assert_nil User.find_by_api_key('zzzzzzzzz')
673 end
677 end
674
678
675 should "return nil if the key is found for an inactive user" do
679 should "return nil if the key is found for an inactive user" do
676 user = User.generate!
680 user = User.generate!
677 user.status = User::STATUS_LOCKED
681 user.status = User::STATUS_LOCKED
678 token = Token.create!(:action => 'api')
682 token = Token.create!(:action => 'api')
679 user.api_token = token
683 user.api_token = token
680 user.save
684 user.save
681
685
682 assert_nil User.find_by_api_key(token.value)
686 assert_nil User.find_by_api_key(token.value)
683 end
687 end
684
688
685 should "return the user if the key is found for an active user" do
689 should "return the user if the key is found for an active user" do
686 user = User.generate!
690 user = User.generate!
687 token = Token.create!(:action => 'api')
691 token = Token.create!(:action => 'api')
688 user.api_token = token
692 user.api_token = token
689 user.save
693 user.save
690
694
691 assert_equal user, User.find_by_api_key(token.value)
695 assert_equal user, User.find_by_api_key(token.value)
692 end
696 end
693 end
697 end
694
698
695 def test_default_admin_account_changed_should_return_false_if_account_was_not_changed
699 def test_default_admin_account_changed_should_return_false_if_account_was_not_changed
696 user = User.find_by_login("admin")
700 user = User.find_by_login("admin")
697 user.password = "admin"
701 user.password = "admin"
698 assert user.save(:validate => false)
702 assert user.save(:validate => false)
699
703
700 assert_equal false, User.default_admin_account_changed?
704 assert_equal false, User.default_admin_account_changed?
701 end
705 end
702
706
703 def test_default_admin_account_changed_should_return_true_if_password_was_changed
707 def test_default_admin_account_changed_should_return_true_if_password_was_changed
704 user = User.find_by_login("admin")
708 user = User.find_by_login("admin")
705 user.password = "newpassword"
709 user.password = "newpassword"
706 user.save!
710 user.save!
707
711
708 assert_equal true, User.default_admin_account_changed?
712 assert_equal true, User.default_admin_account_changed?
709 end
713 end
710
714
711 def test_default_admin_account_changed_should_return_true_if_account_is_disabled
715 def test_default_admin_account_changed_should_return_true_if_account_is_disabled
712 user = User.find_by_login("admin")
716 user = User.find_by_login("admin")
713 user.password = "admin"
717 user.password = "admin"
714 user.status = User::STATUS_LOCKED
718 user.status = User::STATUS_LOCKED
715 assert user.save(:validate => false)
719 assert user.save(:validate => false)
716
720
717 assert_equal true, User.default_admin_account_changed?
721 assert_equal true, User.default_admin_account_changed?
718 end
722 end
719
723
720 def test_default_admin_account_changed_should_return_true_if_account_does_not_exist
724 def test_default_admin_account_changed_should_return_true_if_account_does_not_exist
721 user = User.find_by_login("admin")
725 user = User.find_by_login("admin")
722 user.destroy
726 user.destroy
723
727
724 assert_equal true, User.default_admin_account_changed?
728 assert_equal true, User.default_admin_account_changed?
725 end
729 end
726
730
727 def test_roles_for_project
731 def test_roles_for_project
728 # user with a role
732 # user with a role
729 roles = @jsmith.roles_for_project(Project.find(1))
733 roles = @jsmith.roles_for_project(Project.find(1))
730 assert_kind_of Role, roles.first
734 assert_kind_of Role, roles.first
731 assert_equal "Manager", roles.first.name
735 assert_equal "Manager", roles.first.name
732
736
733 # user with no role
737 # user with no role
734 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
738 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
735 end
739 end
736
740
737 def test_projects_by_role_for_user_with_role
741 def test_projects_by_role_for_user_with_role
738 user = User.find(2)
742 user = User.find(2)
739 assert_kind_of Hash, user.projects_by_role
743 assert_kind_of Hash, user.projects_by_role
740 assert_equal 2, user.projects_by_role.size
744 assert_equal 2, user.projects_by_role.size
741 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
745 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
742 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
746 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
743 end
747 end
744
748
745 def test_accessing_projects_by_role_with_no_projects_should_return_an_empty_array
749 def test_accessing_projects_by_role_with_no_projects_should_return_an_empty_array
746 user = User.find(2)
750 user = User.find(2)
747 assert_equal [], user.projects_by_role[Role.find(3)]
751 assert_equal [], user.projects_by_role[Role.find(3)]
748 # should not update the hash
752 # should not update the hash
749 assert_nil user.projects_by_role.values.detect(&:blank?)
753 assert_nil user.projects_by_role.values.detect(&:blank?)
750 end
754 end
751
755
752 def test_projects_by_role_for_user_with_no_role
756 def test_projects_by_role_for_user_with_no_role
753 user = User.generate!
757 user = User.generate!
754 assert_equal({}, user.projects_by_role)
758 assert_equal({}, user.projects_by_role)
755 end
759 end
756
760
757 def test_projects_by_role_for_anonymous
761 def test_projects_by_role_for_anonymous
758 assert_equal({}, User.anonymous.projects_by_role)
762 assert_equal({}, User.anonymous.projects_by_role)
759 end
763 end
760
764
761 def test_valid_notification_options
765 def test_valid_notification_options
762 # without memberships
766 # without memberships
763 assert_equal 5, User.find(7).valid_notification_options.size
767 assert_equal 5, User.find(7).valid_notification_options.size
764 # with memberships
768 # with memberships
765 assert_equal 6, User.find(2).valid_notification_options.size
769 assert_equal 6, User.find(2).valid_notification_options.size
766 end
770 end
767
771
768 def test_valid_notification_options_class_method
772 def test_valid_notification_options_class_method
769 assert_equal 5, User.valid_notification_options.size
773 assert_equal 5, User.valid_notification_options.size
770 assert_equal 5, User.valid_notification_options(User.find(7)).size
774 assert_equal 5, User.valid_notification_options(User.find(7)).size
771 assert_equal 6, User.valid_notification_options(User.find(2)).size
775 assert_equal 6, User.valid_notification_options(User.find(2)).size
772 end
776 end
773
777
774 def test_mail_notification_all
778 def test_mail_notification_all
775 @jsmith.mail_notification = 'all'
779 @jsmith.mail_notification = 'all'
776 @jsmith.notified_project_ids = []
780 @jsmith.notified_project_ids = []
777 @jsmith.save
781 @jsmith.save
778 @jsmith.reload
782 @jsmith.reload
779 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
783 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
780 end
784 end
781
785
782 def test_mail_notification_selected
786 def test_mail_notification_selected
783 @jsmith.mail_notification = 'selected'
787 @jsmith.mail_notification = 'selected'
784 @jsmith.notified_project_ids = [1]
788 @jsmith.notified_project_ids = [1]
785 @jsmith.save
789 @jsmith.save
786 @jsmith.reload
790 @jsmith.reload
787 assert Project.find(1).recipients.include?(@jsmith.mail)
791 assert Project.find(1).recipients.include?(@jsmith.mail)
788 end
792 end
789
793
790 def test_mail_notification_only_my_events
794 def test_mail_notification_only_my_events
791 @jsmith.mail_notification = 'only_my_events'
795 @jsmith.mail_notification = 'only_my_events'
792 @jsmith.notified_project_ids = []
796 @jsmith.notified_project_ids = []
793 @jsmith.save
797 @jsmith.save
794 @jsmith.reload
798 @jsmith.reload
795 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
799 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
796 end
800 end
797
801
798 def test_comments_sorting_preference
802 def test_comments_sorting_preference
799 assert !@jsmith.wants_comments_in_reverse_order?
803 assert !@jsmith.wants_comments_in_reverse_order?
800 @jsmith.pref.comments_sorting = 'asc'
804 @jsmith.pref.comments_sorting = 'asc'
801 assert !@jsmith.wants_comments_in_reverse_order?
805 assert !@jsmith.wants_comments_in_reverse_order?
802 @jsmith.pref.comments_sorting = 'desc'
806 @jsmith.pref.comments_sorting = 'desc'
803 assert @jsmith.wants_comments_in_reverse_order?
807 assert @jsmith.wants_comments_in_reverse_order?
804 end
808 end
805
809
806 def test_find_by_mail_should_be_case_insensitive
810 def test_find_by_mail_should_be_case_insensitive
807 u = User.find_by_mail('JSmith@somenet.foo')
811 u = User.find_by_mail('JSmith@somenet.foo')
808 assert_not_nil u
812 assert_not_nil u
809 assert_equal 'jsmith@somenet.foo', u.mail
813 assert_equal 'jsmith@somenet.foo', u.mail
810 end
814 end
811
815
812 def test_random_password
816 def test_random_password
813 u = User.new
817 u = User.new
814 u.random_password
818 u.random_password
815 assert !u.password.blank?
819 assert !u.password.blank?
816 assert !u.password_confirmation.blank?
820 assert !u.password_confirmation.blank?
817 end
821 end
818
822
819 context "#change_password_allowed?" do
823 context "#change_password_allowed?" do
820 should "be allowed if no auth source is set" do
824 should "be allowed if no auth source is set" do
821 user = User.generate!
825 user = User.generate!
822 assert user.change_password_allowed?
826 assert user.change_password_allowed?
823 end
827 end
824
828
825 should "delegate to the auth source" do
829 should "delegate to the auth source" do
826 user = User.generate!
830 user = User.generate!
827
831
828 allowed_auth_source = AuthSource.generate!
832 allowed_auth_source = AuthSource.generate!
829 def allowed_auth_source.allow_password_changes?; true; end
833 def allowed_auth_source.allow_password_changes?; true; end
830
834
831 denied_auth_source = AuthSource.generate!
835 denied_auth_source = AuthSource.generate!
832 def denied_auth_source.allow_password_changes?; false; end
836 def denied_auth_source.allow_password_changes?; false; end
833
837
834 assert user.change_password_allowed?
838 assert user.change_password_allowed?
835
839
836 user.auth_source = allowed_auth_source
840 user.auth_source = allowed_auth_source
837 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
841 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
838
842
839 user.auth_source = denied_auth_source
843 user.auth_source = denied_auth_source
840 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
844 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
841 end
845 end
842 end
846 end
843
847
844 def test_own_account_deletable_should_be_true_with_unsubscrive_enabled
848 def test_own_account_deletable_should_be_true_with_unsubscrive_enabled
845 with_settings :unsubscribe => '1' do
849 with_settings :unsubscribe => '1' do
846 assert_equal true, User.find(2).own_account_deletable?
850 assert_equal true, User.find(2).own_account_deletable?
847 end
851 end
848 end
852 end
849
853
850 def test_own_account_deletable_should_be_false_with_unsubscrive_disabled
854 def test_own_account_deletable_should_be_false_with_unsubscrive_disabled
851 with_settings :unsubscribe => '0' do
855 with_settings :unsubscribe => '0' do
852 assert_equal false, User.find(2).own_account_deletable?
856 assert_equal false, User.find(2).own_account_deletable?
853 end
857 end
854 end
858 end
855
859
856 def test_own_account_deletable_should_be_false_for_a_single_admin
860 def test_own_account_deletable_should_be_false_for_a_single_admin
857 User.delete_all(["admin = ? AND id <> ?", true, 1])
861 User.delete_all(["admin = ? AND id <> ?", true, 1])
858
862
859 with_settings :unsubscribe => '1' do
863 with_settings :unsubscribe => '1' do
860 assert_equal false, User.find(1).own_account_deletable?
864 assert_equal false, User.find(1).own_account_deletable?
861 end
865 end
862 end
866 end
863
867
864 def test_own_account_deletable_should_be_true_for_an_admin_if_other_admin_exists
868 def test_own_account_deletable_should_be_true_for_an_admin_if_other_admin_exists
865 User.generate! do |user|
869 User.generate! do |user|
866 user.admin = true
870 user.admin = true
867 end
871 end
868
872
869 with_settings :unsubscribe => '1' do
873 with_settings :unsubscribe => '1' do
870 assert_equal true, User.find(1).own_account_deletable?
874 assert_equal true, User.find(1).own_account_deletable?
871 end
875 end
872 end
876 end
873
877
874 context "#allowed_to?" do
878 context "#allowed_to?" do
875 context "with a unique project" do
879 context "with a unique project" do
876 should "return false if project is archived" do
880 should "return false if project is archived" do
877 project = Project.find(1)
881 project = Project.find(1)
878 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
882 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
879 assert_equal false, @admin.allowed_to?(:view_issues, Project.find(1))
883 assert_equal false, @admin.allowed_to?(:view_issues, Project.find(1))
880 end
884 end
881
885
882 should "return false for write action if project is closed" do
886 should "return false for write action if project is closed" do
883 project = Project.find(1)
887 project = Project.find(1)
884 Project.any_instance.stubs(:status).returns(Project::STATUS_CLOSED)
888 Project.any_instance.stubs(:status).returns(Project::STATUS_CLOSED)
885 assert_equal false, @admin.allowed_to?(:edit_project, Project.find(1))
889 assert_equal false, @admin.allowed_to?(:edit_project, Project.find(1))
886 end
890 end
887
891
888 should "return true for read action if project is closed" do
892 should "return true for read action if project is closed" do
889 project = Project.find(1)
893 project = Project.find(1)
890 Project.any_instance.stubs(:status).returns(Project::STATUS_CLOSED)
894 Project.any_instance.stubs(:status).returns(Project::STATUS_CLOSED)
891 assert_equal true, @admin.allowed_to?(:view_project, Project.find(1))
895 assert_equal true, @admin.allowed_to?(:view_project, Project.find(1))
892 end
896 end
893
897
894 should "return false if related module is disabled" do
898 should "return false if related module is disabled" do
895 project = Project.find(1)
899 project = Project.find(1)
896 project.enabled_module_names = ["issue_tracking"]
900 project.enabled_module_names = ["issue_tracking"]
897 assert_equal true, @admin.allowed_to?(:add_issues, project)
901 assert_equal true, @admin.allowed_to?(:add_issues, project)
898 assert_equal false, @admin.allowed_to?(:view_wiki_pages, project)
902 assert_equal false, @admin.allowed_to?(:view_wiki_pages, project)
899 end
903 end
900
904
901 should "authorize nearly everything for admin users" do
905 should "authorize nearly everything for admin users" do
902 project = Project.find(1)
906 project = Project.find(1)
903 assert ! @admin.member_of?(project)
907 assert ! @admin.member_of?(project)
904 %w(edit_issues delete_issues manage_news add_documents manage_wiki).each do |p|
908 %w(edit_issues delete_issues manage_news add_documents manage_wiki).each do |p|
905 assert_equal true, @admin.allowed_to?(p.to_sym, project)
909 assert_equal true, @admin.allowed_to?(p.to_sym, project)
906 end
910 end
907 end
911 end
908
912
909 should "authorize normal users depending on their roles" do
913 should "authorize normal users depending on their roles" do
910 project = Project.find(1)
914 project = Project.find(1)
911 assert_equal true, @jsmith.allowed_to?(:delete_messages, project) #Manager
915 assert_equal true, @jsmith.allowed_to?(:delete_messages, project) #Manager
912 assert_equal false, @dlopper.allowed_to?(:delete_messages, project) #Developper
916 assert_equal false, @dlopper.allowed_to?(:delete_messages, project) #Developper
913 end
917 end
914 end
918 end
915
919
916 context "with multiple projects" do
920 context "with multiple projects" do
917 should "return false if array is empty" do
921 should "return false if array is empty" do
918 assert_equal false, @admin.allowed_to?(:view_project, [])
922 assert_equal false, @admin.allowed_to?(:view_project, [])
919 end
923 end
920
924
921 should "return true only if user has permission on all these projects" do
925 should "return true only if user has permission on all these projects" do
922 assert_equal true, @admin.allowed_to?(:view_project, Project.all)
926 assert_equal true, @admin.allowed_to?(:view_project, Project.all)
923 assert_equal false, @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
927 assert_equal false, @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
924 assert_equal true, @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
928 assert_equal true, @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
925 assert_equal false, @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
929 assert_equal false, @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
926 end
930 end
927
931
928 should "behave correctly with arrays of 1 project" do
932 should "behave correctly with arrays of 1 project" do
929 assert_equal false, User.anonymous.allowed_to?(:delete_issues, [Project.first])
933 assert_equal false, User.anonymous.allowed_to?(:delete_issues, [Project.first])
930 end
934 end
931 end
935 end
932
936
933 context "with options[:global]" do
937 context "with options[:global]" do
934 should "authorize if user has at least one role that has this permission" do
938 should "authorize if user has at least one role that has this permission" do
935 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
939 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
936 @anonymous = User.find(6)
940 @anonymous = User.find(6)
937 assert_equal true, @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
941 assert_equal true, @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
938 assert_equal false, @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
942 assert_equal false, @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
939 assert_equal true, @dlopper2.allowed_to?(:add_issues, nil, :global => true)
943 assert_equal true, @dlopper2.allowed_to?(:add_issues, nil, :global => true)
940 assert_equal false, @anonymous.allowed_to?(:add_issues, nil, :global => true)
944 assert_equal false, @anonymous.allowed_to?(:add_issues, nil, :global => true)
941 assert_equal true, @anonymous.allowed_to?(:view_issues, nil, :global => true)
945 assert_equal true, @anonymous.allowed_to?(:view_issues, nil, :global => true)
942 end
946 end
943 end
947 end
944 end
948 end
945
949
946 context "User#notify_about?" do
950 context "User#notify_about?" do
947 context "Issues" do
951 context "Issues" do
948 setup do
952 setup do
949 @project = Project.find(1)
953 @project = Project.find(1)
950 @author = User.generate!
954 @author = User.generate!
951 @assignee = User.generate!
955 @assignee = User.generate!
952 @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author)
956 @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author)
953 end
957 end
954
958
955 should "be true for a user with :all" do
959 should "be true for a user with :all" do
956 @author.update_attribute(:mail_notification, 'all')
960 @author.update_attribute(:mail_notification, 'all')
957 assert @author.notify_about?(@issue)
961 assert @author.notify_about?(@issue)
958 end
962 end
959
963
960 should "be false for a user with :none" do
964 should "be false for a user with :none" do
961 @author.update_attribute(:mail_notification, 'none')
965 @author.update_attribute(:mail_notification, 'none')
962 assert ! @author.notify_about?(@issue)
966 assert ! @author.notify_about?(@issue)
963 end
967 end
964
968
965 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
969 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
966 @user = User.generate!(:mail_notification => 'only_my_events')
970 @user = User.generate!(:mail_notification => 'only_my_events')
967 Member.create!(:user => @user, :project => @project, :role_ids => [1])
971 Member.create!(:user => @user, :project => @project, :role_ids => [1])
968 assert ! @user.notify_about?(@issue)
972 assert ! @user.notify_about?(@issue)
969 end
973 end
970
974
971 should "be true for a user with :only_my_events and is the author" do
975 should "be true for a user with :only_my_events and is the author" do
972 @author.update_attribute(:mail_notification, 'only_my_events')
976 @author.update_attribute(:mail_notification, 'only_my_events')
973 assert @author.notify_about?(@issue)
977 assert @author.notify_about?(@issue)
974 end
978 end
975
979
976 should "be true for a user with :only_my_events and is the assignee" do
980 should "be true for a user with :only_my_events and is the assignee" do
977 @assignee.update_attribute(:mail_notification, 'only_my_events')
981 @assignee.update_attribute(:mail_notification, 'only_my_events')
978 assert @assignee.notify_about?(@issue)
982 assert @assignee.notify_about?(@issue)
979 end
983 end
980
984
981 should "be true for a user with :only_assigned and is the assignee" do
985 should "be true for a user with :only_assigned and is the assignee" do
982 @assignee.update_attribute(:mail_notification, 'only_assigned')
986 @assignee.update_attribute(:mail_notification, 'only_assigned')
983 assert @assignee.notify_about?(@issue)
987 assert @assignee.notify_about?(@issue)
984 end
988 end
985
989
986 should "be false for a user with :only_assigned and is not the assignee" do
990 should "be false for a user with :only_assigned and is not the assignee" do
987 @author.update_attribute(:mail_notification, 'only_assigned')
991 @author.update_attribute(:mail_notification, 'only_assigned')
988 assert ! @author.notify_about?(@issue)
992 assert ! @author.notify_about?(@issue)
989 end
993 end
990
994
991 should "be true for a user with :only_owner and is the author" do
995 should "be true for a user with :only_owner and is the author" do
992 @author.update_attribute(:mail_notification, 'only_owner')
996 @author.update_attribute(:mail_notification, 'only_owner')
993 assert @author.notify_about?(@issue)
997 assert @author.notify_about?(@issue)
994 end
998 end
995
999
996 should "be false for a user with :only_owner and is not the author" do
1000 should "be false for a user with :only_owner and is not the author" do
997 @assignee.update_attribute(:mail_notification, 'only_owner')
1001 @assignee.update_attribute(:mail_notification, 'only_owner')
998 assert ! @assignee.notify_about?(@issue)
1002 assert ! @assignee.notify_about?(@issue)
999 end
1003 end
1000
1004
1001 should "be true for a user with :selected and is the author" do
1005 should "be true for a user with :selected and is the author" do
1002 @author.update_attribute(:mail_notification, 'selected')
1006 @author.update_attribute(:mail_notification, 'selected')
1003 assert @author.notify_about?(@issue)
1007 assert @author.notify_about?(@issue)
1004 end
1008 end
1005
1009
1006 should "be true for a user with :selected and is the assignee" do
1010 should "be true for a user with :selected and is the assignee" do
1007 @assignee.update_attribute(:mail_notification, 'selected')
1011 @assignee.update_attribute(:mail_notification, 'selected')
1008 assert @assignee.notify_about?(@issue)
1012 assert @assignee.notify_about?(@issue)
1009 end
1013 end
1010
1014
1011 should "be false for a user with :selected and is not the author or assignee" do
1015 should "be false for a user with :selected and is not the author or assignee" do
1012 @user = User.generate!(:mail_notification => 'selected')
1016 @user = User.generate!(:mail_notification => 'selected')
1013 Member.create!(:user => @user, :project => @project, :role_ids => [1])
1017 Member.create!(:user => @user, :project => @project, :role_ids => [1])
1014 assert ! @user.notify_about?(@issue)
1018 assert ! @user.notify_about?(@issue)
1015 end
1019 end
1016 end
1020 end
1017
1021
1018 context "other events" do
1022 context "other events" do
1019 should 'be added and tested'
1023 should 'be added and tested'
1020 end
1024 end
1021 end
1025 end
1022
1026
1023 def test_salt_unsalted_passwords
1027 def test_salt_unsalted_passwords
1024 # Restore a user with an unsalted password
1028 # Restore a user with an unsalted password
1025 user = User.find(1)
1029 user = User.find(1)
1026 user.salt = nil
1030 user.salt = nil
1027 user.hashed_password = User.hash_password("unsalted")
1031 user.hashed_password = User.hash_password("unsalted")
1028 user.save!
1032 user.save!
1029
1033
1030 User.salt_unsalted_passwords!
1034 User.salt_unsalted_passwords!
1031
1035
1032 user.reload
1036 user.reload
1033 # Salt added
1037 # Salt added
1034 assert !user.salt.blank?
1038 assert !user.salt.blank?
1035 # Password still valid
1039 # Password still valid
1036 assert user.check_password?("unsalted")
1040 assert user.check_password?("unsalted")
1037 assert_equal user, User.try_to_login(user.login, "unsalted")
1041 assert_equal user, User.try_to_login(user.login, "unsalted")
1038 end
1042 end
1039
1043
1040 if Object.const_defined?(:OpenID)
1044 if Object.const_defined?(:OpenID)
1041
1045
1042 def test_setting_identity_url
1046 def test_setting_identity_url
1043 normalized_open_id_url = 'http://example.com/'
1047 normalized_open_id_url = 'http://example.com/'
1044 u = User.new( :identity_url => 'http://example.com/' )
1048 u = User.new( :identity_url => 'http://example.com/' )
1045 assert_equal normalized_open_id_url, u.identity_url
1049 assert_equal normalized_open_id_url, u.identity_url
1046 end
1050 end
1047
1051
1048 def test_setting_identity_url_without_trailing_slash
1052 def test_setting_identity_url_without_trailing_slash
1049 normalized_open_id_url = 'http://example.com/'
1053 normalized_open_id_url = 'http://example.com/'
1050 u = User.new( :identity_url => 'http://example.com' )
1054 u = User.new( :identity_url => 'http://example.com' )
1051 assert_equal normalized_open_id_url, u.identity_url
1055 assert_equal normalized_open_id_url, u.identity_url
1052 end
1056 end
1053
1057
1054 def test_setting_identity_url_without_protocol
1058 def test_setting_identity_url_without_protocol
1055 normalized_open_id_url = 'http://example.com/'
1059 normalized_open_id_url = 'http://example.com/'
1056 u = User.new( :identity_url => 'example.com' )
1060 u = User.new( :identity_url => 'example.com' )
1057 assert_equal normalized_open_id_url, u.identity_url
1061 assert_equal normalized_open_id_url, u.identity_url
1058 end
1062 end
1059
1063
1060 def test_setting_blank_identity_url
1064 def test_setting_blank_identity_url
1061 u = User.new( :identity_url => 'example.com' )
1065 u = User.new( :identity_url => 'example.com' )
1062 u.identity_url = ''
1066 u.identity_url = ''
1063 assert u.identity_url.blank?
1067 assert u.identity_url.blank?
1064 end
1068 end
1065
1069
1066 def test_setting_invalid_identity_url
1070 def test_setting_invalid_identity_url
1067 u = User.new( :identity_url => 'this is not an openid url' )
1071 u = User.new( :identity_url => 'this is not an openid url' )
1068 assert u.identity_url.blank?
1072 assert u.identity_url.blank?
1069 end
1073 end
1070
1074
1071 else
1075 else
1072 puts "Skipping openid tests."
1076 puts "Skipping openid tests."
1073 end
1077 end
1074
1078
1075 end
1079 end
General Comments 0
You need to be logged in to leave comments. Login now