##// END OF EJS Templates
Merged r9719, r9726, r9727 from trunk....
Jean-Philippe Lang -
r9546:bc945d78a625
parent child
Show More
@@ -1,75 +1,75
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ActivitiesController < ApplicationController
18 class ActivitiesController < ApplicationController
19 menu_item :activity
19 menu_item :activity
20 before_filter :find_optional_project
20 before_filter :find_optional_project
21 accept_rss_auth :index
21 accept_rss_auth :index
22
22
23 def index
23 def index
24 @days = Setting.activity_days_default.to_i
24 @days = Setting.activity_days_default.to_i
25
25
26 if params[:from]
26 if params[:from]
27 begin; @date_to = params[:from].to_date + 1; rescue; end
27 begin; @date_to = params[:from].to_date + 1; rescue; end
28 end
28 end
29
29
30 @date_to ||= Date.today + 1
30 @date_to ||= Date.today + 1
31 @date_from = @date_to - @days
31 @date_from = @date_to - @days
32 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
32 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
33 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
33 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
34
34
35 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
35 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
36 :with_subprojects => @with_subprojects,
36 :with_subprojects => @with_subprojects,
37 :author => @author)
37 :author => @author)
38 @activity.scope_select {|t| !params["show_#{t}"].nil?}
38 @activity.scope_select {|t| !params["show_#{t}"].nil?}
39 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
39 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
40
40
41 events = @activity.events(@date_from, @date_to)
41 events = @activity.events(@date_from, @date_to)
42
42
43 if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, User.current, current_language])
43 if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, User.current, current_language])
44 respond_to do |format|
44 respond_to do |format|
45 format.html {
45 format.html {
46 @events_by_day = events.group_by(&:event_date)
46 @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
47 render :layout => false if request.xhr?
47 render :layout => false if request.xhr?
48 }
48 }
49 format.atom {
49 format.atom {
50 title = l(:label_activity)
50 title = l(:label_activity)
51 if @author
51 if @author
52 title = @author.name
52 title = @author.name
53 elsif @activity.scope.size == 1
53 elsif @activity.scope.size == 1
54 title = l("label_#{@activity.scope.first.singularize}_plural")
54 title = l("label_#{@activity.scope.first.singularize}_plural")
55 end
55 end
56 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
56 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
57 }
57 }
58 end
58 end
59 end
59 end
60
60
61 rescue ActiveRecord::RecordNotFound
61 rescue ActiveRecord::RecordNotFound
62 render_404
62 render_404
63 end
63 end
64
64
65 private
65 private
66
66
67 # TODO: refactor, duplicated in projects_controller
67 # TODO: refactor, duplicated in projects_controller
68 def find_optional_project
68 def find_optional_project
69 return true unless params[:id]
69 return true unless params[:id]
70 @project = Project.find(params[:id])
70 @project = Project.find(params[:id])
71 authorize
71 authorize
72 rescue ActiveRecord::RecordNotFound
72 rescue ActiveRecord::RecordNotFound
73 render_404
73 render_404
74 end
74 end
75 end
75 end
@@ -1,1207 +1,1207
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 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
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Display a link to remote if user is authorized
46 # Display a link to remote if user is authorized
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 url = options[:url] || {}
48 url = options[:url] || {}
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 end
50 end
51
51
52 # Displays a link to user's account page if active
52 # Displays a link to user's account page if active
53 def link_to_user(user, options={})
53 def link_to_user(user, options={})
54 if user.is_a?(User)
54 if user.is_a?(User)
55 name = h(user.name(options[:format]))
55 name = h(user.name(options[:format]))
56 if user.active?
56 if user.active?
57 link_to name, :controller => 'users', :action => 'show', :id => user
57 link_to name, :controller => 'users', :action => 'show', :id => user
58 else
58 else
59 name
59 name
60 end
60 end
61 else
61 else
62 h(user.to_s)
62 h(user.to_s)
63 end
63 end
64 end
64 end
65
65
66 # Displays a link to +issue+ with its subject.
66 # Displays a link to +issue+ with its subject.
67 # Examples:
67 # Examples:
68 #
68 #
69 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue) # => Defect #6: This is the subject
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :subject => false) # => Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 if options[:subject] == false
77 if options[:subject] == false
78 title = truncate(issue.subject, :length => 60)
78 title = truncate(issue.subject, :length => 60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if options[:truncate]
81 if options[:truncate]
82 subject = truncate(subject, :length => options[:truncate])
82 subject = truncate(subject, :length => options[:truncate])
83 end
83 end
84 end
84 end
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 :class => issue.css_classes,
86 :class => issue.css_classes,
87 :title => title
87 :title => title
88 s << h(": #{subject}") if subject
88 s << h(": #{subject}") if subject
89 s = h("#{issue.project} - ") + s if options[:project]
89 s = h("#{issue.project} - ") + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 action = options.delete(:download) ? 'download' : 'show'
99 action = options.delete(:download) ? 'download' : 'show'
100 opt_only_path = {}
100 opt_only_path = {}
101 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
101 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
102 options.delete(:only_path)
102 options.delete(:only_path)
103 link_to(h(text),
103 link_to(h(text),
104 {:controller => 'attachments', :action => action,
104 {:controller => 'attachments', :action => action,
105 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
105 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
106 options)
106 options)
107 end
107 end
108
108
109 # Generates a link to a SCM revision
109 # Generates a link to a SCM revision
110 # Options:
110 # Options:
111 # * :text - Link text (default to the formatted revision)
111 # * :text - Link text (default to the formatted revision)
112 def link_to_revision(revision, repository, options={})
112 def link_to_revision(revision, repository, options={})
113 if repository.is_a?(Project)
113 if repository.is_a?(Project)
114 repository = repository.repository
114 repository = repository.repository
115 end
115 end
116 text = options.delete(:text) || format_revision(revision)
116 text = options.delete(:text) || format_revision(revision)
117 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
117 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
118 link_to(
118 link_to(
119 h(text),
119 h(text),
120 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
120 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
121 :title => l(:label_revision_id, format_revision(revision))
121 :title => l(:label_revision_id, format_revision(revision))
122 )
122 )
123 end
123 end
124
124
125 # Generates a link to a message
125 # Generates a link to a message
126 def link_to_message(message, options={}, html_options = nil)
126 def link_to_message(message, options={}, html_options = nil)
127 link_to(
127 link_to(
128 h(truncate(message.subject, :length => 60)),
128 h(truncate(message.subject, :length => 60)),
129 { :controller => 'messages', :action => 'show',
129 { :controller => 'messages', :action => 'show',
130 :board_id => message.board_id,
130 :board_id => message.board_id,
131 :id => message.root,
131 :id => message.root,
132 :r => (message.parent_id && message.id),
132 :r => (message.parent_id && message.id),
133 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
133 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
134 }.merge(options),
134 }.merge(options),
135 html_options
135 html_options
136 )
136 )
137 end
137 end
138
138
139 # Generates a link to a project if active
139 # Generates a link to a project if active
140 # Examples:
140 # Examples:
141 #
141 #
142 # link_to_project(project) # => link to the specified project overview
142 # link_to_project(project) # => link to the specified project overview
143 # link_to_project(project, :action=>'settings') # => link to project settings
143 # link_to_project(project, :action=>'settings') # => link to project settings
144 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
144 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
145 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
145 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
146 #
146 #
147 def link_to_project(project, options={}, html_options = nil)
147 def link_to_project(project, options={}, html_options = nil)
148 if project.active?
148 if project.active?
149 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
149 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
150 link_to(h(project), url, html_options)
150 link_to(h(project), url, html_options)
151 else
151 else
152 h(project)
152 h(project)
153 end
153 end
154 end
154 end
155
155
156 def toggle_link(name, id, options={})
156 def toggle_link(name, id, options={})
157 onclick = "Element.toggle('#{id}'); "
157 onclick = "Element.toggle('#{id}'); "
158 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
158 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
159 onclick << "return false;"
159 onclick << "return false;"
160 link_to(name, "#", :onclick => onclick)
160 link_to(name, "#", :onclick => onclick)
161 end
161 end
162
162
163 def image_to_function(name, function, html_options = {})
163 def image_to_function(name, function, html_options = {})
164 html_options.symbolize_keys!
164 html_options.symbolize_keys!
165 tag(:input, html_options.merge({
165 tag(:input, html_options.merge({
166 :type => "image", :src => image_path(name),
166 :type => "image", :src => image_path(name),
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 }))
168 }))
169 end
169 end
170
170
171 def prompt_to_remote(name, text, param, url, html_options = {})
171 def prompt_to_remote(name, text, param, url, html_options = {})
172 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
172 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
173 link_to name, {}, html_options
173 link_to name, {}, html_options
174 end
174 end
175
175
176 def format_activity_title(text)
176 def format_activity_title(text)
177 h(truncate_single_line(text, :length => 100))
177 h(truncate_single_line(text, :length => 100))
178 end
178 end
179
179
180 def format_activity_day(date)
180 def format_activity_day(date)
181 date == Date.today ? l(:label_today).titleize : format_date(date)
181 date == User.current.today ? l(:label_today).titleize : format_date(date)
182 end
182 end
183
183
184 def format_activity_description(text)
184 def format_activity_description(text)
185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
186 ).gsub(/[\r\n]+/, "<br />").html_safe
186 ).gsub(/[\r\n]+/, "<br />").html_safe
187 end
187 end
188
188
189 def format_version_name(version)
189 def format_version_name(version)
190 if version.project == @project
190 if version.project == @project
191 h(version)
191 h(version)
192 else
192 else
193 h("#{version.project} - #{version}")
193 h("#{version.project} - #{version}")
194 end
194 end
195 end
195 end
196
196
197 def due_date_distance_in_words(date)
197 def due_date_distance_in_words(date)
198 if date
198 if date
199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
200 end
200 end
201 end
201 end
202
202
203 def render_page_hierarchy(pages, node=nil, options={})
203 def render_page_hierarchy(pages, node=nil, options={})
204 content = ''
204 content = ''
205 if pages[node]
205 if pages[node]
206 content << "<ul class=\"pages-hierarchy\">\n"
206 content << "<ul class=\"pages-hierarchy\">\n"
207 pages[node].each do |page|
207 pages[node].each do |page|
208 content << "<li>"
208 content << "<li>"
209 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
209 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
210 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
210 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
211 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
211 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
212 content << "</li>\n"
212 content << "</li>\n"
213 end
213 end
214 content << "</ul>\n"
214 content << "</ul>\n"
215 end
215 end
216 content.html_safe
216 content.html_safe
217 end
217 end
218
218
219 # Renders flash messages
219 # Renders flash messages
220 def render_flash_messages
220 def render_flash_messages
221 s = ''
221 s = ''
222 flash.each do |k,v|
222 flash.each do |k,v|
223 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
223 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
224 end
224 end
225 s.html_safe
225 s.html_safe
226 end
226 end
227
227
228 # Renders tabs and their content
228 # Renders tabs and their content
229 def render_tabs(tabs)
229 def render_tabs(tabs)
230 if tabs.any?
230 if tabs.any?
231 render :partial => 'common/tabs', :locals => {:tabs => tabs}
231 render :partial => 'common/tabs', :locals => {:tabs => tabs}
232 else
232 else
233 content_tag 'p', l(:label_no_data), :class => "nodata"
233 content_tag 'p', l(:label_no_data), :class => "nodata"
234 end
234 end
235 end
235 end
236
236
237 # Renders the project quick-jump box
237 # Renders the project quick-jump box
238 def render_project_jump_box
238 def render_project_jump_box
239 return unless User.current.logged?
239 return unless User.current.logged?
240 projects = User.current.memberships.collect(&:project).compact.uniq
240 projects = User.current.memberships.collect(&:project).compact.uniq
241 if projects.any?
241 if projects.any?
242 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
242 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
243 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
243 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
244 '<option value="" disabled="disabled">---</option>'
244 '<option value="" disabled="disabled">---</option>'
245 s << project_tree_options_for_select(projects, :selected => @project) do |p|
245 s << project_tree_options_for_select(projects, :selected => @project) do |p|
246 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
246 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
247 end
247 end
248 s << '</select>'
248 s << '</select>'
249 s.html_safe
249 s.html_safe
250 end
250 end
251 end
251 end
252
252
253 def project_tree_options_for_select(projects, options = {})
253 def project_tree_options_for_select(projects, options = {})
254 s = ''
254 s = ''
255 project_tree(projects) do |project, level|
255 project_tree(projects) do |project, level|
256 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
256 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
257 tag_options = {:value => project.id}
257 tag_options = {:value => project.id}
258 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
258 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
259 tag_options[:selected] = 'selected'
259 tag_options[:selected] = 'selected'
260 else
260 else
261 tag_options[:selected] = nil
261 tag_options[:selected] = nil
262 end
262 end
263 tag_options.merge!(yield(project)) if block_given?
263 tag_options.merge!(yield(project)) if block_given?
264 s << content_tag('option', name_prefix + h(project), tag_options)
264 s << content_tag('option', name_prefix + h(project), tag_options)
265 end
265 end
266 s.html_safe
266 s.html_safe
267 end
267 end
268
268
269 # Yields the given block for each project with its level in the tree
269 # Yields the given block for each project with its level in the tree
270 #
270 #
271 # Wrapper for Project#project_tree
271 # Wrapper for Project#project_tree
272 def project_tree(projects, &block)
272 def project_tree(projects, &block)
273 Project.project_tree(projects, &block)
273 Project.project_tree(projects, &block)
274 end
274 end
275
275
276 def project_nested_ul(projects, &block)
276 def project_nested_ul(projects, &block)
277 s = ''
277 s = ''
278 if projects.any?
278 if projects.any?
279 ancestors = []
279 ancestors = []
280 projects.sort_by(&:lft).each do |project|
280 projects.sort_by(&:lft).each do |project|
281 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
281 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
282 s << "<ul>\n"
282 s << "<ul>\n"
283 else
283 else
284 ancestors.pop
284 ancestors.pop
285 s << "</li>"
285 s << "</li>"
286 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
286 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
287 ancestors.pop
287 ancestors.pop
288 s << "</ul></li>\n"
288 s << "</ul></li>\n"
289 end
289 end
290 end
290 end
291 s << "<li>"
291 s << "<li>"
292 s << yield(project).to_s
292 s << yield(project).to_s
293 ancestors << project
293 ancestors << project
294 end
294 end
295 s << ("</li></ul>\n" * ancestors.size)
295 s << ("</li></ul>\n" * ancestors.size)
296 end
296 end
297 s.html_safe
297 s.html_safe
298 end
298 end
299
299
300 def principals_check_box_tags(name, principals)
300 def principals_check_box_tags(name, principals)
301 s = ''
301 s = ''
302 principals.sort.each do |principal|
302 principals.sort.each do |principal|
303 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
303 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
304 end
304 end
305 s.html_safe
305 s.html_safe
306 end
306 end
307
307
308 # Returns a string for users/groups option tags
308 # Returns a string for users/groups option tags
309 def principals_options_for_select(collection, selected=nil)
309 def principals_options_for_select(collection, selected=nil)
310 s = ''
310 s = ''
311 if collection.include?(User.current)
311 if collection.include?(User.current)
312 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
312 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
313 end
313 end
314 groups = ''
314 groups = ''
315 collection.sort.each do |element|
315 collection.sort.each do |element|
316 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
316 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
317 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
317 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
318 end
318 end
319 unless groups.empty?
319 unless groups.empty?
320 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
320 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
321 end
321 end
322 s.html_safe
322 s.html_safe
323 end
323 end
324
324
325 # Truncates and returns the string as a single line
325 # Truncates and returns the string as a single line
326 def truncate_single_line(string, *args)
326 def truncate_single_line(string, *args)
327 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
327 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
328 end
328 end
329
329
330 # Truncates at line break after 250 characters or options[:length]
330 # Truncates at line break after 250 characters or options[:length]
331 def truncate_lines(string, options={})
331 def truncate_lines(string, options={})
332 length = options[:length] || 250
332 length = options[:length] || 250
333 if string.to_s =~ /\A(.{#{length}}.*?)$/m
333 if string.to_s =~ /\A(.{#{length}}.*?)$/m
334 "#{$1}..."
334 "#{$1}..."
335 else
335 else
336 string
336 string
337 end
337 end
338 end
338 end
339
339
340 def anchor(text)
340 def anchor(text)
341 text.to_s.gsub(' ', '_')
341 text.to_s.gsub(' ', '_')
342 end
342 end
343
343
344 def html_hours(text)
344 def html_hours(text)
345 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
345 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
346 end
346 end
347
347
348 def authoring(created, author, options={})
348 def authoring(created, author, options={})
349 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
349 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
350 end
350 end
351
351
352 def time_tag(time)
352 def time_tag(time)
353 text = distance_of_time_in_words(Time.now, time)
353 text = distance_of_time_in_words(Time.now, time)
354 if @project
354 if @project
355 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
355 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
356 else
356 else
357 content_tag('acronym', text, :title => format_time(time))
357 content_tag('acronym', text, :title => format_time(time))
358 end
358 end
359 end
359 end
360
360
361 def syntax_highlight_lines(name, content)
361 def syntax_highlight_lines(name, content)
362 lines = []
362 lines = []
363 syntax_highlight(name, content).each_line { |line| lines << line }
363 syntax_highlight(name, content).each_line { |line| lines << line }
364 lines
364 lines
365 end
365 end
366
366
367 def syntax_highlight(name, content)
367 def syntax_highlight(name, content)
368 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
368 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
369 end
369 end
370
370
371 def to_path_param(path)
371 def to_path_param(path)
372 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
372 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
373 str.blank? ? nil : str
373 str.blank? ? nil : str
374 end
374 end
375
375
376 def pagination_links_full(paginator, count=nil, options={})
376 def pagination_links_full(paginator, count=nil, options={})
377 page_param = options.delete(:page_param) || :page
377 page_param = options.delete(:page_param) || :page
378 per_page_links = options.delete(:per_page_links)
378 per_page_links = options.delete(:per_page_links)
379 url_param = params.dup
379 url_param = params.dup
380
380
381 html = ''
381 html = ''
382 if paginator.current.previous
382 if paginator.current.previous
383 # \xc2\xab(utf-8) = &#171;
383 # \xc2\xab(utf-8) = &#171;
384 html << link_to_content_update(
384 html << link_to_content_update(
385 "\xc2\xab " + l(:label_previous),
385 "\xc2\xab " + l(:label_previous),
386 url_param.merge(page_param => paginator.current.previous)) + ' '
386 url_param.merge(page_param => paginator.current.previous)) + ' '
387 end
387 end
388
388
389 html << (pagination_links_each(paginator, options) do |n|
389 html << (pagination_links_each(paginator, options) do |n|
390 link_to_content_update(n.to_s, url_param.merge(page_param => n))
390 link_to_content_update(n.to_s, url_param.merge(page_param => n))
391 end || '')
391 end || '')
392
392
393 if paginator.current.next
393 if paginator.current.next
394 # \xc2\xbb(utf-8) = &#187;
394 # \xc2\xbb(utf-8) = &#187;
395 html << ' ' + link_to_content_update(
395 html << ' ' + link_to_content_update(
396 (l(:label_next) + " \xc2\xbb"),
396 (l(:label_next) + " \xc2\xbb"),
397 url_param.merge(page_param => paginator.current.next))
397 url_param.merge(page_param => paginator.current.next))
398 end
398 end
399
399
400 unless count.nil?
400 unless count.nil?
401 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
401 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
402 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
402 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
403 html << " | #{links}"
403 html << " | #{links}"
404 end
404 end
405 end
405 end
406
406
407 html.html_safe
407 html.html_safe
408 end
408 end
409
409
410 def per_page_links(selected=nil, item_count=nil)
410 def per_page_links(selected=nil, item_count=nil)
411 values = Setting.per_page_options_array
411 values = Setting.per_page_options_array
412 if item_count && values.any?
412 if item_count && values.any?
413 if item_count > values.first
413 if item_count > values.first
414 max = values.detect {|value| value >= item_count} || item_count
414 max = values.detect {|value| value >= item_count} || item_count
415 else
415 else
416 max = item_count
416 max = item_count
417 end
417 end
418 values = values.select {|value| value <= max || value == selected}
418 values = values.select {|value| value <= max || value == selected}
419 end
419 end
420 if values.empty? || (values.size == 1 && values.first == selected)
420 if values.empty? || (values.size == 1 && values.first == selected)
421 return nil
421 return nil
422 end
422 end
423 links = values.collect do |n|
423 links = values.collect do |n|
424 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
424 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
425 end
425 end
426 l(:label_display_per_page, links.join(', '))
426 l(:label_display_per_page, links.join(', '))
427 end
427 end
428
428
429 def reorder_links(name, url, method = :post)
429 def reorder_links(name, url, method = :post)
430 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
430 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
431 url.merge({"#{name}[move_to]" => 'highest'}),
431 url.merge({"#{name}[move_to]" => 'highest'}),
432 :method => method, :title => l(:label_sort_highest)) +
432 :method => method, :title => l(:label_sort_highest)) +
433 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
433 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
434 url.merge({"#{name}[move_to]" => 'higher'}),
434 url.merge({"#{name}[move_to]" => 'higher'}),
435 :method => method, :title => l(:label_sort_higher)) +
435 :method => method, :title => l(:label_sort_higher)) +
436 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
436 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
437 url.merge({"#{name}[move_to]" => 'lower'}),
437 url.merge({"#{name}[move_to]" => 'lower'}),
438 :method => method, :title => l(:label_sort_lower)) +
438 :method => method, :title => l(:label_sort_lower)) +
439 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
439 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
440 url.merge({"#{name}[move_to]" => 'lowest'}),
440 url.merge({"#{name}[move_to]" => 'lowest'}),
441 :method => method, :title => l(:label_sort_lowest))
441 :method => method, :title => l(:label_sort_lowest))
442 end
442 end
443
443
444 def breadcrumb(*args)
444 def breadcrumb(*args)
445 elements = args.flatten
445 elements = args.flatten
446 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
446 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
447 end
447 end
448
448
449 def other_formats_links(&block)
449 def other_formats_links(&block)
450 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
450 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
451 yield Redmine::Views::OtherFormatsBuilder.new(self)
451 yield Redmine::Views::OtherFormatsBuilder.new(self)
452 concat('</p>'.html_safe)
452 concat('</p>'.html_safe)
453 end
453 end
454
454
455 def page_header_title
455 def page_header_title
456 if @project.nil? || @project.new_record?
456 if @project.nil? || @project.new_record?
457 h(Setting.app_title)
457 h(Setting.app_title)
458 else
458 else
459 b = []
459 b = []
460 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
460 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
461 if ancestors.any?
461 if ancestors.any?
462 root = ancestors.shift
462 root = ancestors.shift
463 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
463 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
464 if ancestors.size > 2
464 if ancestors.size > 2
465 b << "\xe2\x80\xa6"
465 b << "\xe2\x80\xa6"
466 ancestors = ancestors[-2, 2]
466 ancestors = ancestors[-2, 2]
467 end
467 end
468 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
468 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
469 end
469 end
470 b << h(@project)
470 b << h(@project)
471 b.join(" \xc2\xbb ").html_safe
471 b.join(" \xc2\xbb ").html_safe
472 end
472 end
473 end
473 end
474
474
475 def html_title(*args)
475 def html_title(*args)
476 if args.empty?
476 if args.empty?
477 title = @html_title || []
477 title = @html_title || []
478 title << @project.name if @project
478 title << @project.name if @project
479 title << Setting.app_title unless Setting.app_title == title.last
479 title << Setting.app_title unless Setting.app_title == title.last
480 title.select {|t| !t.blank? }.join(' - ')
480 title.select {|t| !t.blank? }.join(' - ')
481 else
481 else
482 @html_title ||= []
482 @html_title ||= []
483 @html_title += args
483 @html_title += args
484 end
484 end
485 end
485 end
486
486
487 # Returns the theme, controller name, and action as css classes for the
487 # Returns the theme, controller name, and action as css classes for the
488 # HTML body.
488 # HTML body.
489 def body_css_classes
489 def body_css_classes
490 css = []
490 css = []
491 if theme = Redmine::Themes.theme(Setting.ui_theme)
491 if theme = Redmine::Themes.theme(Setting.ui_theme)
492 css << 'theme-' + theme.name
492 css << 'theme-' + theme.name
493 end
493 end
494
494
495 css << 'controller-' + controller_name
495 css << 'controller-' + controller_name
496 css << 'action-' + action_name
496 css << 'action-' + action_name
497 css.join(' ')
497 css.join(' ')
498 end
498 end
499
499
500 def accesskey(s)
500 def accesskey(s)
501 Redmine::AccessKeys.key_for s
501 Redmine::AccessKeys.key_for s
502 end
502 end
503
503
504 # Formats text according to system settings.
504 # Formats text according to system settings.
505 # 2 ways to call this method:
505 # 2 ways to call this method:
506 # * with a String: textilizable(text, options)
506 # * with a String: textilizable(text, options)
507 # * with an object and one of its attribute: textilizable(issue, :description, options)
507 # * with an object and one of its attribute: textilizable(issue, :description, options)
508 def textilizable(*args)
508 def textilizable(*args)
509 options = args.last.is_a?(Hash) ? args.pop : {}
509 options = args.last.is_a?(Hash) ? args.pop : {}
510 case args.size
510 case args.size
511 when 1
511 when 1
512 obj = options[:object]
512 obj = options[:object]
513 text = args.shift
513 text = args.shift
514 when 2
514 when 2
515 obj = args.shift
515 obj = args.shift
516 attr = args.shift
516 attr = args.shift
517 text = obj.send(attr).to_s
517 text = obj.send(attr).to_s
518 else
518 else
519 raise ArgumentError, 'invalid arguments to textilizable'
519 raise ArgumentError, 'invalid arguments to textilizable'
520 end
520 end
521 return '' if text.blank?
521 return '' if text.blank?
522 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
522 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
523 only_path = options.delete(:only_path) == false ? false : true
523 only_path = options.delete(:only_path) == false ? false : true
524
524
525 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
525 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
526
526
527 @parsed_headings = []
527 @parsed_headings = []
528 @heading_anchors = {}
528 @heading_anchors = {}
529 @current_section = 0 if options[:edit_section_links]
529 @current_section = 0 if options[:edit_section_links]
530
530
531 parse_sections(text, project, obj, attr, only_path, options)
531 parse_sections(text, project, obj, attr, only_path, options)
532 text = parse_non_pre_blocks(text) do |text|
532 text = parse_non_pre_blocks(text) do |text|
533 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
533 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
534 send method_name, text, project, obj, attr, only_path, options
534 send method_name, text, project, obj, attr, only_path, options
535 end
535 end
536 end
536 end
537 parse_headings(text, project, obj, attr, only_path, options)
537 parse_headings(text, project, obj, attr, only_path, options)
538
538
539 if @parsed_headings.any?
539 if @parsed_headings.any?
540 replace_toc(text, @parsed_headings)
540 replace_toc(text, @parsed_headings)
541 end
541 end
542
542
543 text.html_safe
543 text.html_safe
544 end
544 end
545
545
546 def parse_non_pre_blocks(text)
546 def parse_non_pre_blocks(text)
547 s = StringScanner.new(text)
547 s = StringScanner.new(text)
548 tags = []
548 tags = []
549 parsed = ''
549 parsed = ''
550 while !s.eos?
550 while !s.eos?
551 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
551 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
552 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
552 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
553 if tags.empty?
553 if tags.empty?
554 yield text
554 yield text
555 end
555 end
556 parsed << text
556 parsed << text
557 if tag
557 if tag
558 if closing
558 if closing
559 if tags.last == tag.downcase
559 if tags.last == tag.downcase
560 tags.pop
560 tags.pop
561 end
561 end
562 else
562 else
563 tags << tag.downcase
563 tags << tag.downcase
564 end
564 end
565 parsed << full_tag
565 parsed << full_tag
566 end
566 end
567 end
567 end
568 # Close any non closing tags
568 # Close any non closing tags
569 while tag = tags.pop
569 while tag = tags.pop
570 parsed << "</#{tag}>"
570 parsed << "</#{tag}>"
571 end
571 end
572 parsed
572 parsed
573 end
573 end
574
574
575 def parse_inline_attachments(text, project, obj, attr, only_path, options)
575 def parse_inline_attachments(text, project, obj, attr, only_path, options)
576 # when using an image link, try to use an attachment, if possible
576 # when using an image link, try to use an attachment, if possible
577 if options[:attachments] || (obj && obj.respond_to?(:attachments))
577 if options[:attachments] || (obj && obj.respond_to?(:attachments))
578 attachments = options[:attachments] || obj.attachments
578 attachments = options[:attachments] || obj.attachments
579 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
579 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
580 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
580 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
581 # search for the picture in attachments
581 # search for the picture in attachments
582 if found = Attachment.latest_attach(attachments, filename)
582 if found = Attachment.latest_attach(attachments, filename)
583 image_url = url_for :only_path => only_path, :controller => 'attachments',
583 image_url = url_for :only_path => only_path, :controller => 'attachments',
584 :action => 'download', :id => found
584 :action => 'download', :id => found
585 desc = found.description.to_s.gsub('"', '')
585 desc = found.description.to_s.gsub('"', '')
586 if !desc.blank? && alttext.blank?
586 if !desc.blank? && alttext.blank?
587 alt = " title=\"#{desc}\" alt=\"#{desc}\""
587 alt = " title=\"#{desc}\" alt=\"#{desc}\""
588 end
588 end
589 "src=\"#{image_url}\"#{alt}"
589 "src=\"#{image_url}\"#{alt}"
590 else
590 else
591 m
591 m
592 end
592 end
593 end
593 end
594 end
594 end
595 end
595 end
596
596
597 # Wiki links
597 # Wiki links
598 #
598 #
599 # Examples:
599 # Examples:
600 # [[mypage]]
600 # [[mypage]]
601 # [[mypage|mytext]]
601 # [[mypage|mytext]]
602 # wiki links can refer other project wikis, using project name or identifier:
602 # wiki links can refer other project wikis, using project name or identifier:
603 # [[project:]] -> wiki starting page
603 # [[project:]] -> wiki starting page
604 # [[project:|mytext]]
604 # [[project:|mytext]]
605 # [[project:mypage]]
605 # [[project:mypage]]
606 # [[project:mypage|mytext]]
606 # [[project:mypage|mytext]]
607 def parse_wiki_links(text, project, obj, attr, only_path, options)
607 def parse_wiki_links(text, project, obj, attr, only_path, options)
608 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
608 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
609 link_project = project
609 link_project = project
610 esc, all, page, title = $1, $2, $3, $5
610 esc, all, page, title = $1, $2, $3, $5
611 if esc.nil?
611 if esc.nil?
612 if page =~ /^([^\:]+)\:(.*)$/
612 if page =~ /^([^\:]+)\:(.*)$/
613 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
613 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
614 page = $2
614 page = $2
615 title ||= $1 if page.blank?
615 title ||= $1 if page.blank?
616 end
616 end
617
617
618 if link_project && link_project.wiki
618 if link_project && link_project.wiki
619 # extract anchor
619 # extract anchor
620 anchor = nil
620 anchor = nil
621 if page =~ /^(.+?)\#(.+)$/
621 if page =~ /^(.+?)\#(.+)$/
622 page, anchor = $1, $2
622 page, anchor = $1, $2
623 end
623 end
624 anchor = sanitize_anchor_name(anchor) if anchor.present?
624 anchor = sanitize_anchor_name(anchor) if anchor.present?
625 # check if page exists
625 # check if page exists
626 wiki_page = link_project.wiki.find_page(page)
626 wiki_page = link_project.wiki.find_page(page)
627 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
627 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
628 "##{anchor}"
628 "##{anchor}"
629 else
629 else
630 case options[:wiki_links]
630 case options[:wiki_links]
631 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
631 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
632 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
632 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
633 else
633 else
634 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
634 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
635 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
635 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
636 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
636 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
637 :id => wiki_page_id, :anchor => anchor, :parent => parent)
637 :id => wiki_page_id, :anchor => anchor, :parent => parent)
638 end
638 end
639 end
639 end
640 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
640 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
641 else
641 else
642 # project or wiki doesn't exist
642 # project or wiki doesn't exist
643 all
643 all
644 end
644 end
645 else
645 else
646 all
646 all
647 end
647 end
648 end
648 end
649 end
649 end
650
650
651 # Redmine links
651 # Redmine links
652 #
652 #
653 # Examples:
653 # Examples:
654 # Issues:
654 # Issues:
655 # #52 -> Link to issue #52
655 # #52 -> Link to issue #52
656 # Changesets:
656 # Changesets:
657 # r52 -> Link to revision 52
657 # r52 -> Link to revision 52
658 # commit:a85130f -> Link to scmid starting with a85130f
658 # commit:a85130f -> Link to scmid starting with a85130f
659 # Documents:
659 # Documents:
660 # document#17 -> Link to document with id 17
660 # document#17 -> Link to document with id 17
661 # document:Greetings -> Link to the document with title "Greetings"
661 # document:Greetings -> Link to the document with title "Greetings"
662 # document:"Some document" -> Link to the document with title "Some document"
662 # document:"Some document" -> Link to the document with title "Some document"
663 # Versions:
663 # Versions:
664 # version#3 -> Link to version with id 3
664 # version#3 -> Link to version with id 3
665 # version:1.0.0 -> Link to version named "1.0.0"
665 # version:1.0.0 -> Link to version named "1.0.0"
666 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
666 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
667 # Attachments:
667 # Attachments:
668 # attachment:file.zip -> Link to the attachment of the current object named file.zip
668 # attachment:file.zip -> Link to the attachment of the current object named file.zip
669 # Source files:
669 # Source files:
670 # source:some/file -> Link to the file located at /some/file in the project's repository
670 # source:some/file -> Link to the file located at /some/file in the project's repository
671 # source:some/file@52 -> Link to the file's revision 52
671 # source:some/file@52 -> Link to the file's revision 52
672 # source:some/file#L120 -> Link to line 120 of the file
672 # source:some/file#L120 -> Link to line 120 of the file
673 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
673 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
674 # export:some/file -> Force the download of the file
674 # export:some/file -> Force the download of the file
675 # Forum messages:
675 # Forum messages:
676 # message#1218 -> Link to message with id 1218
676 # message#1218 -> Link to message with id 1218
677 #
677 #
678 # Links can refer other objects from other projects, using project identifier:
678 # Links can refer other objects from other projects, using project identifier:
679 # identifier:r52
679 # identifier:r52
680 # identifier:document:"Some document"
680 # identifier:document:"Some document"
681 # identifier:version:1.0.0
681 # identifier:version:1.0.0
682 # identifier:source:some/file
682 # identifier:source:some/file
683 def parse_redmine_links(text, project, obj, attr, only_path, options)
683 def parse_redmine_links(text, project, obj, attr, only_path, options)
684 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|
684 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|
685 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
685 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
686 link = nil
686 link = nil
687 if project_identifier
687 if project_identifier
688 project = Project.visible.find_by_identifier(project_identifier)
688 project = Project.visible.find_by_identifier(project_identifier)
689 end
689 end
690 if esc.nil?
690 if esc.nil?
691 if prefix.nil? && sep == 'r'
691 if prefix.nil? && sep == 'r'
692 if project
692 if project
693 repository = nil
693 repository = nil
694 if repo_identifier
694 if repo_identifier
695 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
695 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
696 else
696 else
697 repository = project.repository
697 repository = project.repository
698 end
698 end
699 # project.changesets.visible raises an SQL error because of a double join on repositories
699 # project.changesets.visible raises an SQL error because of a double join on repositories
700 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
700 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
701 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},
701 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},
702 :class => 'changeset',
702 :class => 'changeset',
703 :title => truncate_single_line(changeset.comments, :length => 100))
703 :title => truncate_single_line(changeset.comments, :length => 100))
704 end
704 end
705 end
705 end
706 elsif sep == '#'
706 elsif sep == '#'
707 oid = identifier.to_i
707 oid = identifier.to_i
708 case prefix
708 case prefix
709 when nil
709 when nil
710 if issue = Issue.visible.find_by_id(oid, :include => :status)
710 if issue = Issue.visible.find_by_id(oid, :include => :status)
711 anchor = comment_id ? "note-#{comment_id}" : nil
711 anchor = comment_id ? "note-#{comment_id}" : nil
712 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
712 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
713 :class => issue.css_classes,
713 :class => issue.css_classes,
714 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
714 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
715 end
715 end
716 when 'document'
716 when 'document'
717 if document = Document.visible.find_by_id(oid)
717 if document = Document.visible.find_by_id(oid)
718 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
718 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
719 :class => 'document'
719 :class => 'document'
720 end
720 end
721 when 'version'
721 when 'version'
722 if version = Version.visible.find_by_id(oid)
722 if version = Version.visible.find_by_id(oid)
723 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
723 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
724 :class => 'version'
724 :class => 'version'
725 end
725 end
726 when 'message'
726 when 'message'
727 if message = Message.visible.find_by_id(oid, :include => :parent)
727 if message = Message.visible.find_by_id(oid, :include => :parent)
728 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
728 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
729 end
729 end
730 when 'forum'
730 when 'forum'
731 if board = Board.visible.find_by_id(oid)
731 if board = Board.visible.find_by_id(oid)
732 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
732 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
733 :class => 'board'
733 :class => 'board'
734 end
734 end
735 when 'news'
735 when 'news'
736 if news = News.visible.find_by_id(oid)
736 if news = News.visible.find_by_id(oid)
737 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
737 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
738 :class => 'news'
738 :class => 'news'
739 end
739 end
740 when 'project'
740 when 'project'
741 if p = Project.visible.find_by_id(oid)
741 if p = Project.visible.find_by_id(oid)
742 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
742 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
743 end
743 end
744 end
744 end
745 elsif sep == ':'
745 elsif sep == ':'
746 # removes the double quotes if any
746 # removes the double quotes if any
747 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
747 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
748 case prefix
748 case prefix
749 when 'document'
749 when 'document'
750 if project && document = project.documents.visible.find_by_title(name)
750 if project && document = project.documents.visible.find_by_title(name)
751 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
751 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
752 :class => 'document'
752 :class => 'document'
753 end
753 end
754 when 'version'
754 when 'version'
755 if project && version = project.versions.visible.find_by_name(name)
755 if project && version = project.versions.visible.find_by_name(name)
756 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
756 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
757 :class => 'version'
757 :class => 'version'
758 end
758 end
759 when 'forum'
759 when 'forum'
760 if project && board = project.boards.visible.find_by_name(name)
760 if project && board = project.boards.visible.find_by_name(name)
761 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
761 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
762 :class => 'board'
762 :class => 'board'
763 end
763 end
764 when 'news'
764 when 'news'
765 if project && news = project.news.visible.find_by_title(name)
765 if project && news = project.news.visible.find_by_title(name)
766 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
766 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
767 :class => 'news'
767 :class => 'news'
768 end
768 end
769 when 'commit', 'source', 'export'
769 when 'commit', 'source', 'export'
770 if project
770 if project
771 repository = nil
771 repository = nil
772 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
772 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
773 repo_prefix, repo_identifier, name = $1, $2, $3
773 repo_prefix, repo_identifier, name = $1, $2, $3
774 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
774 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
775 else
775 else
776 repository = project.repository
776 repository = project.repository
777 end
777 end
778 if prefix == 'commit'
778 if prefix == 'commit'
779 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
779 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
780 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},
780 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},
781 :class => 'changeset',
781 :class => 'changeset',
782 :title => truncate_single_line(h(changeset.comments), :length => 100)
782 :title => truncate_single_line(h(changeset.comments), :length => 100)
783 end
783 end
784 else
784 else
785 if repository && User.current.allowed_to?(:browse_repository, project)
785 if repository && User.current.allowed_to?(:browse_repository, project)
786 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
786 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
787 path, rev, anchor = $1, $3, $5
787 path, rev, anchor = $1, $3, $5
788 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
788 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
789 :path => to_path_param(path),
789 :path => to_path_param(path),
790 :rev => rev,
790 :rev => rev,
791 :anchor => anchor,
791 :anchor => anchor,
792 :format => (prefix == 'export' ? 'raw' : nil)},
792 :format => (prefix == 'export' ? 'raw' : nil)},
793 :class => (prefix == 'export' ? 'source download' : 'source')
793 :class => (prefix == 'export' ? 'source download' : 'source')
794 end
794 end
795 end
795 end
796 repo_prefix = nil
796 repo_prefix = nil
797 end
797 end
798 when 'attachment'
798 when 'attachment'
799 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
799 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
800 if attachments && attachment = attachments.detect {|a| a.filename == name }
800 if attachments && attachment = attachments.detect {|a| a.filename == name }
801 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
801 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
802 :class => 'attachment'
802 :class => 'attachment'
803 end
803 end
804 when 'project'
804 when 'project'
805 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
805 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
806 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
806 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
807 end
807 end
808 end
808 end
809 end
809 end
810 end
810 end
811 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
811 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
812 end
812 end
813 end
813 end
814
814
815 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
815 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
816
816
817 def parse_sections(text, project, obj, attr, only_path, options)
817 def parse_sections(text, project, obj, attr, only_path, options)
818 return unless options[:edit_section_links]
818 return unless options[:edit_section_links]
819 text.gsub!(HEADING_RE) do
819 text.gsub!(HEADING_RE) do
820 heading = $1
820 heading = $1
821 @current_section += 1
821 @current_section += 1
822 if @current_section > 1
822 if @current_section > 1
823 content_tag('div',
823 content_tag('div',
824 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
824 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
825 :class => 'contextual',
825 :class => 'contextual',
826 :title => l(:button_edit_section)) + heading.html_safe
826 :title => l(:button_edit_section)) + heading.html_safe
827 else
827 else
828 heading
828 heading
829 end
829 end
830 end
830 end
831 end
831 end
832
832
833 # Headings and TOC
833 # Headings and TOC
834 # Adds ids and links to headings unless options[:headings] is set to false
834 # Adds ids and links to headings unless options[:headings] is set to false
835 def parse_headings(text, project, obj, attr, only_path, options)
835 def parse_headings(text, project, obj, attr, only_path, options)
836 return if options[:headings] == false
836 return if options[:headings] == false
837
837
838 text.gsub!(HEADING_RE) do
838 text.gsub!(HEADING_RE) do
839 level, attrs, content = $2.to_i, $3, $4
839 level, attrs, content = $2.to_i, $3, $4
840 item = strip_tags(content).strip
840 item = strip_tags(content).strip
841 anchor = sanitize_anchor_name(item)
841 anchor = sanitize_anchor_name(item)
842 # used for single-file wiki export
842 # used for single-file wiki export
843 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
843 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
844 @heading_anchors[anchor] ||= 0
844 @heading_anchors[anchor] ||= 0
845 idx = (@heading_anchors[anchor] += 1)
845 idx = (@heading_anchors[anchor] += 1)
846 if idx > 1
846 if idx > 1
847 anchor = "#{anchor}-#{idx}"
847 anchor = "#{anchor}-#{idx}"
848 end
848 end
849 @parsed_headings << [level, anchor, item]
849 @parsed_headings << [level, anchor, item]
850 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
850 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
851 end
851 end
852 end
852 end
853
853
854 MACROS_RE = /
854 MACROS_RE = /
855 (!)? # escaping
855 (!)? # escaping
856 (
856 (
857 \{\{ # opening tag
857 \{\{ # opening tag
858 ([\w]+) # macro name
858 ([\w]+) # macro name
859 (\(([^\}]*)\))? # optional arguments
859 (\(([^\}]*)\))? # optional arguments
860 \}\} # closing tag
860 \}\} # closing tag
861 )
861 )
862 /x unless const_defined?(:MACROS_RE)
862 /x unless const_defined?(:MACROS_RE)
863
863
864 # Macros substitution
864 # Macros substitution
865 def parse_macros(text, project, obj, attr, only_path, options)
865 def parse_macros(text, project, obj, attr, only_path, options)
866 text.gsub!(MACROS_RE) do
866 text.gsub!(MACROS_RE) do
867 esc, all, macro = $1, $2, $3.downcase
867 esc, all, macro = $1, $2, $3.downcase
868 args = ($5 || '').split(',').each(&:strip)
868 args = ($5 || '').split(',').each(&:strip)
869 if esc.nil?
869 if esc.nil?
870 begin
870 begin
871 exec_macro(macro, obj, args)
871 exec_macro(macro, obj, args)
872 rescue => e
872 rescue => e
873 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
873 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
874 end || all
874 end || all
875 else
875 else
876 all
876 all
877 end
877 end
878 end
878 end
879 end
879 end
880
880
881 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
881 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
882
882
883 # Renders the TOC with given headings
883 # Renders the TOC with given headings
884 def replace_toc(text, headings)
884 def replace_toc(text, headings)
885 text.gsub!(TOC_RE) do
885 text.gsub!(TOC_RE) do
886 if headings.empty?
886 if headings.empty?
887 ''
887 ''
888 else
888 else
889 div_class = 'toc'
889 div_class = 'toc'
890 div_class << ' right' if $1 == '>'
890 div_class << ' right' if $1 == '>'
891 div_class << ' left' if $1 == '<'
891 div_class << ' left' if $1 == '<'
892 out = "<ul class=\"#{div_class}\"><li>"
892 out = "<ul class=\"#{div_class}\"><li>"
893 root = headings.map(&:first).min
893 root = headings.map(&:first).min
894 current = root
894 current = root
895 started = false
895 started = false
896 headings.each do |level, anchor, item|
896 headings.each do |level, anchor, item|
897 if level > current
897 if level > current
898 out << '<ul><li>' * (level - current)
898 out << '<ul><li>' * (level - current)
899 elsif level < current
899 elsif level < current
900 out << "</li></ul>\n" * (current - level) + "</li><li>"
900 out << "</li></ul>\n" * (current - level) + "</li><li>"
901 elsif started
901 elsif started
902 out << '</li><li>'
902 out << '</li><li>'
903 end
903 end
904 out << "<a href=\"##{anchor}\">#{item}</a>"
904 out << "<a href=\"##{anchor}\">#{item}</a>"
905 current = level
905 current = level
906 started = true
906 started = true
907 end
907 end
908 out << '</li></ul>' * (current - root)
908 out << '</li></ul>' * (current - root)
909 out << '</li></ul>'
909 out << '</li></ul>'
910 end
910 end
911 end
911 end
912 end
912 end
913
913
914 # Same as Rails' simple_format helper without using paragraphs
914 # Same as Rails' simple_format helper without using paragraphs
915 def simple_format_without_paragraph(text)
915 def simple_format_without_paragraph(text)
916 text.to_s.
916 text.to_s.
917 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
917 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
918 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
918 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
919 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
919 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
920 html_safe
920 html_safe
921 end
921 end
922
922
923 def lang_options_for_select(blank=true)
923 def lang_options_for_select(blank=true)
924 (blank ? [["(auto)", ""]] : []) +
924 (blank ? [["(auto)", ""]] : []) +
925 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
925 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
926 end
926 end
927
927
928 def label_tag_for(name, option_tags = nil, options = {})
928 def label_tag_for(name, option_tags = nil, options = {})
929 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
929 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
930 content_tag("label", label_text)
930 content_tag("label", label_text)
931 end
931 end
932
932
933 def labelled_tabular_form_for(*args, &proc)
933 def labelled_tabular_form_for(*args, &proc)
934 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
934 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
935 args << {} unless args.last.is_a?(Hash)
935 args << {} unless args.last.is_a?(Hash)
936 options = args.last
936 options = args.last
937 options[:html] ||= {}
937 options[:html] ||= {}
938 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
938 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
939 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
939 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
940 form_for(*args, &proc)
940 form_for(*args, &proc)
941 end
941 end
942
942
943 def labelled_form_for(*args, &proc)
943 def labelled_form_for(*args, &proc)
944 args << {} unless args.last.is_a?(Hash)
944 args << {} unless args.last.is_a?(Hash)
945 options = args.last
945 options = args.last
946 if args.first.is_a?(Symbol)
946 if args.first.is_a?(Symbol)
947 options.merge!(:as => args.shift)
947 options.merge!(:as => args.shift)
948 end
948 end
949 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
949 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
950 form_for(*args, &proc)
950 form_for(*args, &proc)
951 end
951 end
952
952
953 def labelled_fields_for(*args, &proc)
953 def labelled_fields_for(*args, &proc)
954 args << {} unless args.last.is_a?(Hash)
954 args << {} unless args.last.is_a?(Hash)
955 options = args.last
955 options = args.last
956 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
956 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
957 fields_for(*args, &proc)
957 fields_for(*args, &proc)
958 end
958 end
959
959
960 def labelled_remote_form_for(*args, &proc)
960 def labelled_remote_form_for(*args, &proc)
961 args << {} unless args.last.is_a?(Hash)
961 args << {} unless args.last.is_a?(Hash)
962 options = args.last
962 options = args.last
963 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
963 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
964 form_for(*args, &proc)
964 form_for(*args, &proc)
965 end
965 end
966
966
967 def error_messages_for(*objects)
967 def error_messages_for(*objects)
968 html = ""
968 html = ""
969 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
969 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
970 errors = objects.map {|o| o.errors.full_messages}.flatten
970 errors = objects.map {|o| o.errors.full_messages}.flatten
971 if errors.any?
971 if errors.any?
972 html << "<div id='errorExplanation'><ul>\n"
972 html << "<div id='errorExplanation'><ul>\n"
973 errors.each do |error|
973 errors.each do |error|
974 html << "<li>#{h error}</li>\n"
974 html << "<li>#{h error}</li>\n"
975 end
975 end
976 html << "</ul></div>\n"
976 html << "</ul></div>\n"
977 end
977 end
978 html.html_safe
978 html.html_safe
979 end
979 end
980
980
981 def back_url_hidden_field_tag
981 def back_url_hidden_field_tag
982 back_url = params[:back_url] || request.env['HTTP_REFERER']
982 back_url = params[:back_url] || request.env['HTTP_REFERER']
983 back_url = CGI.unescape(back_url.to_s)
983 back_url = CGI.unescape(back_url.to_s)
984 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
984 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
985 end
985 end
986
986
987 def check_all_links(form_name)
987 def check_all_links(form_name)
988 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
988 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
989 " | ".html_safe +
989 " | ".html_safe +
990 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
990 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
991 end
991 end
992
992
993 def progress_bar(pcts, options={})
993 def progress_bar(pcts, options={})
994 pcts = [pcts, pcts] unless pcts.is_a?(Array)
994 pcts = [pcts, pcts] unless pcts.is_a?(Array)
995 pcts = pcts.collect(&:round)
995 pcts = pcts.collect(&:round)
996 pcts[1] = pcts[1] - pcts[0]
996 pcts[1] = pcts[1] - pcts[0]
997 pcts << (100 - pcts[1] - pcts[0])
997 pcts << (100 - pcts[1] - pcts[0])
998 width = options[:width] || '100px;'
998 width = options[:width] || '100px;'
999 legend = options[:legend] || ''
999 legend = options[:legend] || ''
1000 content_tag('table',
1000 content_tag('table',
1001 content_tag('tr',
1001 content_tag('tr',
1002 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1002 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1003 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1003 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1004 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1004 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1005 ), :class => 'progress', :style => "width: #{width};").html_safe +
1005 ), :class => 'progress', :style => "width: #{width};").html_safe +
1006 content_tag('p', legend, :class => 'pourcent').html_safe
1006 content_tag('p', legend, :class => 'pourcent').html_safe
1007 end
1007 end
1008
1008
1009 def checked_image(checked=true)
1009 def checked_image(checked=true)
1010 if checked
1010 if checked
1011 image_tag 'toggle_check.png'
1011 image_tag 'toggle_check.png'
1012 end
1012 end
1013 end
1013 end
1014
1014
1015 def context_menu(url)
1015 def context_menu(url)
1016 unless @context_menu_included
1016 unless @context_menu_included
1017 content_for :header_tags do
1017 content_for :header_tags do
1018 javascript_include_tag('context_menu') +
1018 javascript_include_tag('context_menu') +
1019 stylesheet_link_tag('context_menu')
1019 stylesheet_link_tag('context_menu')
1020 end
1020 end
1021 if l(:direction) == 'rtl'
1021 if l(:direction) == 'rtl'
1022 content_for :header_tags do
1022 content_for :header_tags do
1023 stylesheet_link_tag('context_menu_rtl')
1023 stylesheet_link_tag('context_menu_rtl')
1024 end
1024 end
1025 end
1025 end
1026 @context_menu_included = true
1026 @context_menu_included = true
1027 end
1027 end
1028 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1028 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1029 end
1029 end
1030
1030
1031 def calendar_for(field_id)
1031 def calendar_for(field_id)
1032 include_calendar_headers_tags
1032 include_calendar_headers_tags
1033 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1033 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1034 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1034 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1035 end
1035 end
1036
1036
1037 def include_calendar_headers_tags
1037 def include_calendar_headers_tags
1038 unless @calendar_headers_tags_included
1038 unless @calendar_headers_tags_included
1039 @calendar_headers_tags_included = true
1039 @calendar_headers_tags_included = true
1040 content_for :header_tags do
1040 content_for :header_tags do
1041 start_of_week = case Setting.start_of_week.to_i
1041 start_of_week = case Setting.start_of_week.to_i
1042 when 1
1042 when 1
1043 'Calendar._FD = 1;' # Monday
1043 'Calendar._FD = 1;' # Monday
1044 when 7
1044 when 7
1045 'Calendar._FD = 0;' # Sunday
1045 'Calendar._FD = 0;' # Sunday
1046 when 6
1046 when 6
1047 'Calendar._FD = 6;' # Saturday
1047 'Calendar._FD = 6;' # Saturday
1048 else
1048 else
1049 '' # use language
1049 '' # use language
1050 end
1050 end
1051
1051
1052 javascript_include_tag('calendar/calendar') +
1052 javascript_include_tag('calendar/calendar') +
1053 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1053 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1054 javascript_tag(start_of_week) +
1054 javascript_tag(start_of_week) +
1055 javascript_include_tag('calendar/calendar-setup') +
1055 javascript_include_tag('calendar/calendar-setup') +
1056 stylesheet_link_tag('calendar')
1056 stylesheet_link_tag('calendar')
1057 end
1057 end
1058 end
1058 end
1059 end
1059 end
1060
1060
1061 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1061 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1062 # Examples:
1062 # Examples:
1063 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1063 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1064 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1064 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1065 #
1065 #
1066 def stylesheet_link_tag(*sources)
1066 def stylesheet_link_tag(*sources)
1067 options = sources.last.is_a?(Hash) ? sources.pop : {}
1067 options = sources.last.is_a?(Hash) ? sources.pop : {}
1068 plugin = options.delete(:plugin)
1068 plugin = options.delete(:plugin)
1069 sources = sources.map do |source|
1069 sources = sources.map do |source|
1070 if plugin
1070 if plugin
1071 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1071 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1072 elsif current_theme && current_theme.stylesheets.include?(source)
1072 elsif current_theme && current_theme.stylesheets.include?(source)
1073 current_theme.stylesheet_path(source)
1073 current_theme.stylesheet_path(source)
1074 else
1074 else
1075 source
1075 source
1076 end
1076 end
1077 end
1077 end
1078 super sources, options
1078 super sources, options
1079 end
1079 end
1080
1080
1081 # Overrides Rails' image_tag with themes and plugins support.
1081 # Overrides Rails' image_tag with themes and plugins support.
1082 # Examples:
1082 # Examples:
1083 # image_tag('image.png') # => picks image.png from the current theme or defaults
1083 # image_tag('image.png') # => picks image.png from the current theme or defaults
1084 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1084 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1085 #
1085 #
1086 def image_tag(source, options={})
1086 def image_tag(source, options={})
1087 if plugin = options.delete(:plugin)
1087 if plugin = options.delete(:plugin)
1088 source = "/plugin_assets/#{plugin}/images/#{source}"
1088 source = "/plugin_assets/#{plugin}/images/#{source}"
1089 elsif current_theme && current_theme.images.include?(source)
1089 elsif current_theme && current_theme.images.include?(source)
1090 source = current_theme.image_path(source)
1090 source = current_theme.image_path(source)
1091 end
1091 end
1092 super source, options
1092 super source, options
1093 end
1093 end
1094
1094
1095 # Overrides Rails' javascript_include_tag with plugins support
1095 # Overrides Rails' javascript_include_tag with plugins support
1096 # Examples:
1096 # Examples:
1097 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1097 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1098 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1098 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1099 #
1099 #
1100 def javascript_include_tag(*sources)
1100 def javascript_include_tag(*sources)
1101 options = sources.last.is_a?(Hash) ? sources.pop : {}
1101 options = sources.last.is_a?(Hash) ? sources.pop : {}
1102 if plugin = options.delete(:plugin)
1102 if plugin = options.delete(:plugin)
1103 sources = sources.map do |source|
1103 sources = sources.map do |source|
1104 if plugin
1104 if plugin
1105 "/plugin_assets/#{plugin}/javascripts/#{source}"
1105 "/plugin_assets/#{plugin}/javascripts/#{source}"
1106 else
1106 else
1107 source
1107 source
1108 end
1108 end
1109 end
1109 end
1110 end
1110 end
1111 super sources, options
1111 super sources, options
1112 end
1112 end
1113
1113
1114 def content_for(name, content = nil, &block)
1114 def content_for(name, content = nil, &block)
1115 @has_content ||= {}
1115 @has_content ||= {}
1116 @has_content[name] = true
1116 @has_content[name] = true
1117 super(name, content, &block)
1117 super(name, content, &block)
1118 end
1118 end
1119
1119
1120 def has_content?(name)
1120 def has_content?(name)
1121 (@has_content && @has_content[name]) || false
1121 (@has_content && @has_content[name]) || false
1122 end
1122 end
1123
1123
1124 def sidebar_content?
1124 def sidebar_content?
1125 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1125 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1126 end
1126 end
1127
1127
1128 def view_layouts_base_sidebar_hook_response
1128 def view_layouts_base_sidebar_hook_response
1129 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1129 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1130 end
1130 end
1131
1131
1132 def email_delivery_enabled?
1132 def email_delivery_enabled?
1133 !!ActionMailer::Base.perform_deliveries
1133 !!ActionMailer::Base.perform_deliveries
1134 end
1134 end
1135
1135
1136 # Returns the avatar image tag for the given +user+ if avatars are enabled
1136 # Returns the avatar image tag for the given +user+ if avatars are enabled
1137 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1137 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1138 def avatar(user, options = { })
1138 def avatar(user, options = { })
1139 if Setting.gravatar_enabled?
1139 if Setting.gravatar_enabled?
1140 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1140 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1141 email = nil
1141 email = nil
1142 if user.respond_to?(:mail)
1142 if user.respond_to?(:mail)
1143 email = user.mail
1143 email = user.mail
1144 elsif user.to_s =~ %r{<(.+?)>}
1144 elsif user.to_s =~ %r{<(.+?)>}
1145 email = $1
1145 email = $1
1146 end
1146 end
1147 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1147 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1148 else
1148 else
1149 ''
1149 ''
1150 end
1150 end
1151 end
1151 end
1152
1152
1153 def sanitize_anchor_name(anchor)
1153 def sanitize_anchor_name(anchor)
1154 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1154 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1155 end
1155 end
1156
1156
1157 # Returns the javascript tags that are included in the html layout head
1157 # Returns the javascript tags that are included in the html layout head
1158 def javascript_heads
1158 def javascript_heads
1159 tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
1159 tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
1160 unless User.current.pref.warn_on_leaving_unsaved == '0'
1160 unless User.current.pref.warn_on_leaving_unsaved == '0'
1161 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1161 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1162 end
1162 end
1163 tags
1163 tags
1164 end
1164 end
1165
1165
1166 def favicon
1166 def favicon
1167 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1167 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1168 end
1168 end
1169
1169
1170 def robot_exclusion_tag
1170 def robot_exclusion_tag
1171 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1171 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1172 end
1172 end
1173
1173
1174 # Returns true if arg is expected in the API response
1174 # Returns true if arg is expected in the API response
1175 def include_in_api_response?(arg)
1175 def include_in_api_response?(arg)
1176 unless @included_in_api_response
1176 unless @included_in_api_response
1177 param = params[:include]
1177 param = params[:include]
1178 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1178 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1179 @included_in_api_response.collect!(&:strip)
1179 @included_in_api_response.collect!(&:strip)
1180 end
1180 end
1181 @included_in_api_response.include?(arg.to_s)
1181 @included_in_api_response.include?(arg.to_s)
1182 end
1182 end
1183
1183
1184 # Returns options or nil if nometa param or X-Redmine-Nometa header
1184 # Returns options or nil if nometa param or X-Redmine-Nometa header
1185 # was set in the request
1185 # was set in the request
1186 def api_meta(options)
1186 def api_meta(options)
1187 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1187 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1188 # compatibility mode for activeresource clients that raise
1188 # compatibility mode for activeresource clients that raise
1189 # an error when unserializing an array with attributes
1189 # an error when unserializing an array with attributes
1190 nil
1190 nil
1191 else
1191 else
1192 options
1192 options
1193 end
1193 end
1194 end
1194 end
1195
1195
1196 private
1196 private
1197
1197
1198 def wiki_helper
1198 def wiki_helper
1199 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1199 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1200 extend helper
1200 extend helper
1201 return self
1201 return self
1202 end
1202 end
1203
1203
1204 def link_to_content_update(text, url_params = {}, html_options = {})
1204 def link_to_content_update(text, url_params = {}, html_options = {})
1205 link_to(text, url_params, html_options)
1205 link_to(text, url_params, html_options)
1206 end
1206 end
1207 end
1207 end
@@ -1,865 +1,871
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !@sortable.nil?
39 !@sortable.nil?
40 end
40 end
41
41
42 def sortable
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
44 end
45
45
46 def value(issue)
46 def value(issue)
47 issue.send name
47 issue.send name
48 end
48 end
49
49
50 def css_classes
50 def css_classes
51 name
51 name
52 end
52 end
53 end
53 end
54
54
55 class QueryCustomFieldColumn < QueryColumn
55 class QueryCustomFieldColumn < QueryColumn
56
56
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
60 if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
61 self.groupable = custom_field.order_statement
61 self.groupable = custom_field.order_statement
62 end
62 end
63 self.groupable ||= false
63 self.groupable ||= false
64 @cf = custom_field
64 @cf = custom_field
65 end
65 end
66
66
67 def caption
67 def caption
68 @cf.name
68 @cf.name
69 end
69 end
70
70
71 def custom_field
71 def custom_field
72 @cf
72 @cf
73 end
73 end
74
74
75 def value(issue)
75 def value(issue)
76 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
76 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
77 cv.size > 1 ? cv : cv.first
77 cv.size > 1 ? cv : cv.first
78 end
78 end
79
79
80 def css_classes
80 def css_classes
81 @css_classes ||= "#{name} #{@cf.field_format}"
81 @css_classes ||= "#{name} #{@cf.field_format}"
82 end
82 end
83 end
83 end
84
84
85 class Query < ActiveRecord::Base
85 class Query < ActiveRecord::Base
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
87 end
87 end
88
88
89 belongs_to :project
89 belongs_to :project
90 belongs_to :user
90 belongs_to :user
91 serialize :filters
91 serialize :filters
92 serialize :column_names
92 serialize :column_names
93 serialize :sort_criteria, Array
93 serialize :sort_criteria, Array
94
94
95 attr_protected :project_id, :user_id
95 attr_protected :project_id, :user_id
96
96
97 validates_presence_of :name
97 validates_presence_of :name
98 validates_length_of :name, :maximum => 255
98 validates_length_of :name, :maximum => 255
99 validate :validate_query_filters
99 validate :validate_query_filters
100
100
101 @@operators = { "=" => :label_equals,
101 @@operators = { "=" => :label_equals,
102 "!" => :label_not_equals,
102 "!" => :label_not_equals,
103 "o" => :label_open_issues,
103 "o" => :label_open_issues,
104 "c" => :label_closed_issues,
104 "c" => :label_closed_issues,
105 "!*" => :label_none,
105 "!*" => :label_none,
106 "*" => :label_all,
106 "*" => :label_all,
107 ">=" => :label_greater_or_equal,
107 ">=" => :label_greater_or_equal,
108 "<=" => :label_less_or_equal,
108 "<=" => :label_less_or_equal,
109 "><" => :label_between,
109 "><" => :label_between,
110 "<t+" => :label_in_less_than,
110 "<t+" => :label_in_less_than,
111 ">t+" => :label_in_more_than,
111 ">t+" => :label_in_more_than,
112 "t+" => :label_in,
112 "t+" => :label_in,
113 "t" => :label_today,
113 "t" => :label_today,
114 "w" => :label_this_week,
114 "w" => :label_this_week,
115 ">t-" => :label_less_than_ago,
115 ">t-" => :label_less_than_ago,
116 "<t-" => :label_more_than_ago,
116 "<t-" => :label_more_than_ago,
117 "t-" => :label_ago,
117 "t-" => :label_ago,
118 "~" => :label_contains,
118 "~" => :label_contains,
119 "!~" => :label_not_contains }
119 "!~" => :label_not_contains }
120
120
121 cattr_reader :operators
121 cattr_reader :operators
122
122
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
126 :list_subprojects => [ "*", "!*", "=" ],
126 :list_subprojects => [ "*", "!*", "=" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
129 :string => [ "=", "~", "!", "!~", "!*", "*" ],
129 :string => [ "=", "~", "!", "!~", "!*", "*" ],
130 :text => [ "~", "!~", "!*", "*" ],
130 :text => [ "~", "!~", "!*", "*" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
133
133
134 cattr_reader :operators_by_filter_type
134 cattr_reader :operators_by_filter_type
135
135
136 @@available_columns = [
136 @@available_columns = [
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
153 ]
153 ]
154 cattr_reader :available_columns
154 cattr_reader :available_columns
155
155
156 scope :visible, lambda {|*args|
156 scope :visible, lambda {|*args|
157 user = args.shift || User.current
157 user = args.shift || User.current
158 base = Project.allowed_to_condition(user, :view_issues, *args)
158 base = Project.allowed_to_condition(user, :view_issues, *args)
159 user_id = user.logged? ? user.id : 0
159 user_id = user.logged? ? user.id : 0
160 {
160 {
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
162 :include => :project
162 :include => :project
163 }
163 }
164 }
164 }
165
165
166 def initialize(attributes=nil, *args)
166 def initialize(attributes=nil, *args)
167 super attributes
167 super attributes
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
169 @is_for_all = project.nil?
169 @is_for_all = project.nil?
170 end
170 end
171
171
172 def validate_query_filters
172 def validate_query_filters
173 filters.each_key do |field|
173 filters.each_key do |field|
174 if values_for(field)
174 if values_for(field)
175 case type_for(field)
175 case type_for(field)
176 when :integer
176 when :integer
177 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
177 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
178 when :float
178 when :float
179 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
179 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
180 when :date, :date_past
180 when :date, :date_past
181 case operator_for(field)
181 case operator_for(field)
182 when "=", ">=", "<=", "><"
182 when "=", ">=", "<=", "><"
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
184 when ">t-", "<t-", "t-"
184 when ">t-", "<t-", "t-"
185 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
185 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
186 end
186 end
187 end
187 end
188 end
188 end
189
189
190 add_filter_error(field, :blank) unless
190 add_filter_error(field, :blank) unless
191 # filter requires one or more values
191 # filter requires one or more values
192 (values_for(field) and !values_for(field).first.blank?) or
192 (values_for(field) and !values_for(field).first.blank?) or
193 # filter doesn't require any value
193 # filter doesn't require any value
194 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
194 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
195 end if filters
195 end if filters
196 end
196 end
197
197
198 def add_filter_error(field, message)
198 def add_filter_error(field, message)
199 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
199 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
200 errors.add(:base, m)
200 errors.add(:base, m)
201 end
201 end
202
202
203 # Returns true if the query is visible to +user+ or the current user.
203 # Returns true if the query is visible to +user+ or the current user.
204 def visible?(user=User.current)
204 def visible?(user=User.current)
205 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
205 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
206 end
206 end
207
207
208 def editable_by?(user)
208 def editable_by?(user)
209 return false unless user
209 return false unless user
210 # Admin can edit them all and regular users can edit their private queries
210 # Admin can edit them all and regular users can edit their private queries
211 return true if user.admin? || (!is_public && self.user_id == user.id)
211 return true if user.admin? || (!is_public && self.user_id == user.id)
212 # Members can not edit public queries that are for all project (only admin is allowed to)
212 # Members can not edit public queries that are for all project (only admin is allowed to)
213 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
213 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
214 end
214 end
215
215
216 def available_filters
216 def available_filters
217 return @available_filters if @available_filters
217 return @available_filters if @available_filters
218
218
219 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
219 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
220
220
221 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
221 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
222 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
222 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
223 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
223 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
224 "subject" => { :type => :text, :order => 8 },
224 "subject" => { :type => :text, :order => 8 },
225 "created_on" => { :type => :date_past, :order => 9 },
225 "created_on" => { :type => :date_past, :order => 9 },
226 "updated_on" => { :type => :date_past, :order => 10 },
226 "updated_on" => { :type => :date_past, :order => 10 },
227 "start_date" => { :type => :date, :order => 11 },
227 "start_date" => { :type => :date, :order => 11 },
228 "due_date" => { :type => :date, :order => 12 },
228 "due_date" => { :type => :date, :order => 12 },
229 "estimated_hours" => { :type => :float, :order => 13 },
229 "estimated_hours" => { :type => :float, :order => 13 },
230 "done_ratio" => { :type => :integer, :order => 14 }}
230 "done_ratio" => { :type => :integer, :order => 14 }}
231
231
232 principals = []
232 principals = []
233 if project
233 if project
234 principals += project.principals.sort
234 principals += project.principals.sort
235 unless project.leaf?
235 unless project.leaf?
236 subprojects = project.descendants.visible.all
236 subprojects = project.descendants.visible.all
237 if subprojects.any?
237 if subprojects.any?
238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
239 principals += Principal.member_of(subprojects)
239 principals += Principal.member_of(subprojects)
240 end
240 end
241 end
241 end
242 else
242 else
243 all_projects = Project.visible.all
243 all_projects = Project.visible.all
244 if all_projects.any?
244 if all_projects.any?
245 # members of visible projects
245 # members of visible projects
246 principals += Principal.member_of(all_projects)
246 principals += Principal.member_of(all_projects)
247
247
248 # project filter
248 # project filter
249 project_values = []
249 project_values = []
250 if User.current.logged? && User.current.memberships.any?
250 if User.current.logged? && User.current.memberships.any?
251 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
251 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
252 end
252 end
253 Project.project_tree(all_projects) do |p, level|
253 Project.project_tree(all_projects) do |p, level|
254 prefix = (level > 0 ? ('--' * level + ' ') : '')
254 prefix = (level > 0 ? ('--' * level + ' ') : '')
255 project_values << ["#{prefix}#{p.name}", p.id.to_s]
255 project_values << ["#{prefix}#{p.name}", p.id.to_s]
256 end
256 end
257 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
257 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
258 end
258 end
259 end
259 end
260 principals.uniq!
260 principals.uniq!
261 principals.sort!
261 principals.sort!
262 users = principals.select {|p| p.is_a?(User)}
262 users = principals.select {|p| p.is_a?(User)}
263
263
264 assigned_to_values = []
264 assigned_to_values = []
265 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
265 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
266 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
266 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
267 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
267 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
268
268
269 author_values = []
269 author_values = []
270 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
270 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
271 author_values += users.collect{|s| [s.name, s.id.to_s] }
271 author_values += users.collect{|s| [s.name, s.id.to_s] }
272 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
272 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
273
273
274 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
274 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
275 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
275 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
276
276
277 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
277 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
278 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
278 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
279
279
280 if User.current.logged?
280 if User.current.logged?
281 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
281 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
282 end
282 end
283
283
284 if project
284 if project
285 # project specific filters
285 # project specific filters
286 categories = project.issue_categories.all
286 categories = project.issue_categories.all
287 unless categories.empty?
287 unless categories.empty?
288 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
288 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
289 end
289 end
290 versions = project.shared_versions.all
290 versions = project.shared_versions.all
291 unless versions.empty?
291 unless versions.empty?
292 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
292 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
293 end
293 end
294 add_custom_fields_filters(project.all_issue_custom_fields)
294 add_custom_fields_filters(project.all_issue_custom_fields)
295 else
295 else
296 # global filters for cross project issue list
296 # global filters for cross project issue list
297 system_shared_versions = Version.visible.find_all_by_sharing('system')
297 system_shared_versions = Version.visible.find_all_by_sharing('system')
298 unless system_shared_versions.empty?
298 unless system_shared_versions.empty?
299 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
299 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
300 end
300 end
301 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
301 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
302 end
302 end
303 @available_filters
303 @available_filters
304 end
304 end
305
305
306 def add_filter(field, operator, values)
306 def add_filter(field, operator, values)
307 # values must be an array
307 # values must be an array
308 return unless values.nil? || values.is_a?(Array)
308 return unless values.nil? || values.is_a?(Array)
309 # check if field is defined as an available filter
309 # check if field is defined as an available filter
310 if available_filters.has_key? field
310 if available_filters.has_key? field
311 filter_options = available_filters[field]
311 filter_options = available_filters[field]
312 # check if operator is allowed for that filter
312 # check if operator is allowed for that filter
313 #if @@operators_by_filter_type[filter_options[:type]].include? operator
313 #if @@operators_by_filter_type[filter_options[:type]].include? operator
314 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
314 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
315 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
315 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
316 #end
316 #end
317 filters[field] = {:operator => operator, :values => (values || [''])}
317 filters[field] = {:operator => operator, :values => (values || [''])}
318 end
318 end
319 end
319 end
320
320
321 def add_short_filter(field, expression)
321 def add_short_filter(field, expression)
322 return unless expression && available_filters.has_key?(field)
322 return unless expression && available_filters.has_key?(field)
323 field_type = available_filters[field][:type]
323 field_type = available_filters[field][:type]
324 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
324 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
325 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
325 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
326 add_filter field, operator, $1.present? ? $1.split('|') : ['']
326 add_filter field, operator, $1.present? ? $1.split('|') : ['']
327 end || add_filter(field, '=', expression.split('|'))
327 end || add_filter(field, '=', expression.split('|'))
328 end
328 end
329
329
330 # Add multiple filters using +add_filter+
330 # Add multiple filters using +add_filter+
331 def add_filters(fields, operators, values)
331 def add_filters(fields, operators, values)
332 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
332 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
333 fields.each do |field|
333 fields.each do |field|
334 add_filter(field, operators[field], values && values[field])
334 add_filter(field, operators[field], values && values[field])
335 end
335 end
336 end
336 end
337 end
337 end
338
338
339 def has_filter?(field)
339 def has_filter?(field)
340 filters and filters[field]
340 filters and filters[field]
341 end
341 end
342
342
343 def type_for(field)
343 def type_for(field)
344 available_filters[field][:type] if available_filters.has_key?(field)
344 available_filters[field][:type] if available_filters.has_key?(field)
345 end
345 end
346
346
347 def operator_for(field)
347 def operator_for(field)
348 has_filter?(field) ? filters[field][:operator] : nil
348 has_filter?(field) ? filters[field][:operator] : nil
349 end
349 end
350
350
351 def values_for(field)
351 def values_for(field)
352 has_filter?(field) ? filters[field][:values] : nil
352 has_filter?(field) ? filters[field][:values] : nil
353 end
353 end
354
354
355 def value_for(field, index=0)
355 def value_for(field, index=0)
356 (values_for(field) || [])[index]
356 (values_for(field) || [])[index]
357 end
357 end
358
358
359 def label_for(field)
359 def label_for(field)
360 label = available_filters[field][:name] if available_filters.has_key?(field)
360 label = available_filters[field][:name] if available_filters.has_key?(field)
361 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
361 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
362 end
362 end
363
363
364 def available_columns
364 def available_columns
365 return @available_columns if @available_columns
365 return @available_columns if @available_columns
366 @available_columns = ::Query.available_columns.dup
366 @available_columns = ::Query.available_columns.dup
367 @available_columns += (project ?
367 @available_columns += (project ?
368 project.all_issue_custom_fields :
368 project.all_issue_custom_fields :
369 IssueCustomField.find(:all)
369 IssueCustomField.find(:all)
370 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
370 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
371
371
372 if User.current.allowed_to?(:view_time_entries, project, :global => true)
372 if User.current.allowed_to?(:view_time_entries, project, :global => true)
373 index = nil
373 index = nil
374 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
374 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
375 index = (index ? index + 1 : -1)
375 index = (index ? index + 1 : -1)
376 # insert the column after estimated_hours or at the end
376 # insert the column after estimated_hours or at the end
377 @available_columns.insert index, QueryColumn.new(:spent_hours,
377 @available_columns.insert index, QueryColumn.new(:spent_hours,
378 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
378 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
379 :default_order => 'desc',
379 :default_order => 'desc',
380 :caption => :label_spent_time
380 :caption => :label_spent_time
381 )
381 )
382 end
382 end
383 @available_columns
383 @available_columns
384 end
384 end
385
385
386 def self.available_columns=(v)
386 def self.available_columns=(v)
387 self.available_columns = (v)
387 self.available_columns = (v)
388 end
388 end
389
389
390 def self.add_available_column(column)
390 def self.add_available_column(column)
391 self.available_columns << (column) if column.is_a?(QueryColumn)
391 self.available_columns << (column) if column.is_a?(QueryColumn)
392 end
392 end
393
393
394 # Returns an array of columns that can be used to group the results
394 # Returns an array of columns that can be used to group the results
395 def groupable_columns
395 def groupable_columns
396 available_columns.select {|c| c.groupable}
396 available_columns.select {|c| c.groupable}
397 end
397 end
398
398
399 # Returns a Hash of columns and the key for sorting
399 # Returns a Hash of columns and the key for sorting
400 def sortable_columns
400 def sortable_columns
401 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
401 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
402 h[column.name.to_s] = column.sortable
402 h[column.name.to_s] = column.sortable
403 h
403 h
404 })
404 })
405 end
405 end
406
406
407 def columns
407 def columns
408 # preserve the column_names order
408 # preserve the column_names order
409 (has_default_columns? ? default_columns_names : column_names).collect do |name|
409 (has_default_columns? ? default_columns_names : column_names).collect do |name|
410 available_columns.find { |col| col.name == name }
410 available_columns.find { |col| col.name == name }
411 end.compact
411 end.compact
412 end
412 end
413
413
414 def default_columns_names
414 def default_columns_names
415 @default_columns_names ||= begin
415 @default_columns_names ||= begin
416 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
416 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
417
417
418 project.present? ? default_columns : [:project] | default_columns
418 project.present? ? default_columns : [:project] | default_columns
419 end
419 end
420 end
420 end
421
421
422 def column_names=(names)
422 def column_names=(names)
423 if names
423 if names
424 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
424 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
425 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
425 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
426 # Set column_names to nil if default columns
426 # Set column_names to nil if default columns
427 if names == default_columns_names
427 if names == default_columns_names
428 names = nil
428 names = nil
429 end
429 end
430 end
430 end
431 write_attribute(:column_names, names)
431 write_attribute(:column_names, names)
432 end
432 end
433
433
434 def has_column?(column)
434 def has_column?(column)
435 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
435 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
436 end
436 end
437
437
438 def has_default_columns?
438 def has_default_columns?
439 column_names.nil? || column_names.empty?
439 column_names.nil? || column_names.empty?
440 end
440 end
441
441
442 def sort_criteria=(arg)
442 def sort_criteria=(arg)
443 c = []
443 c = []
444 if arg.is_a?(Hash)
444 if arg.is_a?(Hash)
445 arg = arg.keys.sort.collect {|k| arg[k]}
445 arg = arg.keys.sort.collect {|k| arg[k]}
446 end
446 end
447 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
447 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
448 write_attribute(:sort_criteria, c)
448 write_attribute(:sort_criteria, c)
449 end
449 end
450
450
451 def sort_criteria
451 def sort_criteria
452 read_attribute(:sort_criteria) || []
452 read_attribute(:sort_criteria) || []
453 end
453 end
454
454
455 def sort_criteria_key(arg)
455 def sort_criteria_key(arg)
456 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
456 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
457 end
457 end
458
458
459 def sort_criteria_order(arg)
459 def sort_criteria_order(arg)
460 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
460 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
461 end
461 end
462
462
463 # Returns the SQL sort order that should be prepended for grouping
463 # Returns the SQL sort order that should be prepended for grouping
464 def group_by_sort_order
464 def group_by_sort_order
465 if grouped? && (column = group_by_column)
465 if grouped? && (column = group_by_column)
466 column.sortable.is_a?(Array) ?
466 column.sortable.is_a?(Array) ?
467 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
467 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
468 "#{column.sortable} #{column.default_order}"
468 "#{column.sortable} #{column.default_order}"
469 end
469 end
470 end
470 end
471
471
472 # Returns true if the query is a grouped query
472 # Returns true if the query is a grouped query
473 def grouped?
473 def grouped?
474 !group_by_column.nil?
474 !group_by_column.nil?
475 end
475 end
476
476
477 def group_by_column
477 def group_by_column
478 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
478 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
479 end
479 end
480
480
481 def group_by_statement
481 def group_by_statement
482 group_by_column.try(:groupable)
482 group_by_column.try(:groupable)
483 end
483 end
484
484
485 def project_statement
485 def project_statement
486 project_clauses = []
486 project_clauses = []
487 if project && !project.descendants.active.empty?
487 if project && !project.descendants.active.empty?
488 ids = [project.id]
488 ids = [project.id]
489 if has_filter?("subproject_id")
489 if has_filter?("subproject_id")
490 case operator_for("subproject_id")
490 case operator_for("subproject_id")
491 when '='
491 when '='
492 # include the selected subprojects
492 # include the selected subprojects
493 ids += values_for("subproject_id").each(&:to_i)
493 ids += values_for("subproject_id").each(&:to_i)
494 when '!*'
494 when '!*'
495 # main project only
495 # main project only
496 else
496 else
497 # all subprojects
497 # all subprojects
498 ids += project.descendants.collect(&:id)
498 ids += project.descendants.collect(&:id)
499 end
499 end
500 elsif Setting.display_subprojects_issues?
500 elsif Setting.display_subprojects_issues?
501 ids += project.descendants.collect(&:id)
501 ids += project.descendants.collect(&:id)
502 end
502 end
503 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
503 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
504 elsif project
504 elsif project
505 project_clauses << "#{Project.table_name}.id = %d" % project.id
505 project_clauses << "#{Project.table_name}.id = %d" % project.id
506 end
506 end
507 project_clauses.any? ? project_clauses.join(' AND ') : nil
507 project_clauses.any? ? project_clauses.join(' AND ') : nil
508 end
508 end
509
509
510 def statement
510 def statement
511 # filters clauses
511 # filters clauses
512 filters_clauses = []
512 filters_clauses = []
513 filters.each_key do |field|
513 filters.each_key do |field|
514 next if field == "subproject_id"
514 next if field == "subproject_id"
515 v = values_for(field).clone
515 v = values_for(field).clone
516 next unless v and !v.empty?
516 next unless v and !v.empty?
517 operator = operator_for(field)
517 operator = operator_for(field)
518
518
519 # "me" value subsitution
519 # "me" value subsitution
520 if %w(assigned_to_id author_id watcher_id).include?(field)
520 if %w(assigned_to_id author_id watcher_id).include?(field)
521 if v.delete("me")
521 if v.delete("me")
522 if User.current.logged?
522 if User.current.logged?
523 v.push(User.current.id.to_s)
523 v.push(User.current.id.to_s)
524 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
524 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
525 else
525 else
526 v.push("0")
526 v.push("0")
527 end
527 end
528 end
528 end
529 end
529 end
530
530
531 if field == 'project_id'
531 if field == 'project_id'
532 if v.delete('mine')
532 if v.delete('mine')
533 v += User.current.memberships.map(&:project_id).map(&:to_s)
533 v += User.current.memberships.map(&:project_id).map(&:to_s)
534 end
534 end
535 end
535 end
536
536
537 if field =~ /^cf_(\d+)$/
537 if field =~ /^cf_(\d+)$/
538 # custom field
538 # custom field
539 filters_clauses << sql_for_custom_field(field, operator, v, $1)
539 filters_clauses << sql_for_custom_field(field, operator, v, $1)
540 elsif respond_to?("sql_for_#{field}_field")
540 elsif respond_to?("sql_for_#{field}_field")
541 # specific statement
541 # specific statement
542 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
542 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
543 else
543 else
544 # regular field
544 # regular field
545 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
545 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
546 end
546 end
547 end if filters and valid?
547 end if filters and valid?
548
548
549 filters_clauses << project_statement
549 filters_clauses << project_statement
550 filters_clauses.reject!(&:blank?)
550 filters_clauses.reject!(&:blank?)
551
551
552 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
552 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
553 end
553 end
554
554
555 # Returns the issue count
555 # Returns the issue count
556 def issue_count
556 def issue_count
557 Issue.visible.count(:include => [:status, :project], :conditions => statement)
557 Issue.visible.count(:include => [:status, :project], :conditions => statement)
558 rescue ::ActiveRecord::StatementInvalid => e
558 rescue ::ActiveRecord::StatementInvalid => e
559 raise StatementInvalid.new(e.message)
559 raise StatementInvalid.new(e.message)
560 end
560 end
561
561
562 # Returns the issue count by group or nil if query is not grouped
562 # Returns the issue count by group or nil if query is not grouped
563 def issue_count_by_group
563 def issue_count_by_group
564 r = nil
564 r = nil
565 if grouped?
565 if grouped?
566 begin
566 begin
567 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
567 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
568 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
568 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
569 rescue ActiveRecord::RecordNotFound
569 rescue ActiveRecord::RecordNotFound
570 r = {nil => issue_count}
570 r = {nil => issue_count}
571 end
571 end
572 c = group_by_column
572 c = group_by_column
573 if c.is_a?(QueryCustomFieldColumn)
573 if c.is_a?(QueryCustomFieldColumn)
574 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
574 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
575 end
575 end
576 end
576 end
577 r
577 r
578 rescue ::ActiveRecord::StatementInvalid => e
578 rescue ::ActiveRecord::StatementInvalid => e
579 raise StatementInvalid.new(e.message)
579 raise StatementInvalid.new(e.message)
580 end
580 end
581
581
582 # Returns the issues
582 # Returns the issues
583 # Valid options are :order, :offset, :limit, :include, :conditions
583 # Valid options are :order, :offset, :limit, :include, :conditions
584 def issues(options={})
584 def issues(options={})
585 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
585 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
586 order_option = nil if order_option.blank?
586 order_option = nil if order_option.blank?
587
587
588 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
588 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
589
589
590 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
590 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
591 :conditions => statement,
591 :conditions => statement,
592 :order => order_option,
592 :order => order_option,
593 :joins => joins,
593 :joins => joins,
594 :limit => options[:limit],
594 :limit => options[:limit],
595 :offset => options[:offset]
595 :offset => options[:offset]
596
596
597 if has_column?(:spent_hours)
597 if has_column?(:spent_hours)
598 Issue.load_visible_spent_hours(issues)
598 Issue.load_visible_spent_hours(issues)
599 end
599 end
600 issues
600 issues
601 rescue ::ActiveRecord::StatementInvalid => e
601 rescue ::ActiveRecord::StatementInvalid => e
602 raise StatementInvalid.new(e.message)
602 raise StatementInvalid.new(e.message)
603 end
603 end
604
604
605 # Returns the issues ids
605 # Returns the issues ids
606 def issue_ids(options={})
606 def issue_ids(options={})
607 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
607 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
608 order_option = nil if order_option.blank?
608 order_option = nil if order_option.blank?
609
609
610 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
610 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
611
611
612 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
612 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
613 :conditions => statement,
613 :conditions => statement,
614 :order => order_option,
614 :order => order_option,
615 :joins => joins,
615 :joins => joins,
616 :limit => options[:limit],
616 :limit => options[:limit],
617 :offset => options[:offset]).find_ids
617 :offset => options[:offset]).find_ids
618 rescue ::ActiveRecord::StatementInvalid => e
618 rescue ::ActiveRecord::StatementInvalid => e
619 raise StatementInvalid.new(e.message)
619 raise StatementInvalid.new(e.message)
620 end
620 end
621
621
622 # Returns the journals
622 # Returns the journals
623 # Valid options are :order, :offset, :limit
623 # Valid options are :order, :offset, :limit
624 def journals(options={})
624 def journals(options={})
625 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
625 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
626 :conditions => statement,
626 :conditions => statement,
627 :order => options[:order],
627 :order => options[:order],
628 :limit => options[:limit],
628 :limit => options[:limit],
629 :offset => options[:offset]
629 :offset => options[:offset]
630 rescue ::ActiveRecord::StatementInvalid => e
630 rescue ::ActiveRecord::StatementInvalid => e
631 raise StatementInvalid.new(e.message)
631 raise StatementInvalid.new(e.message)
632 end
632 end
633
633
634 # Returns the versions
634 # Returns the versions
635 # Valid options are :conditions
635 # Valid options are :conditions
636 def versions(options={})
636 def versions(options={})
637 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
637 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
638 rescue ::ActiveRecord::StatementInvalid => e
638 rescue ::ActiveRecord::StatementInvalid => e
639 raise StatementInvalid.new(e.message)
639 raise StatementInvalid.new(e.message)
640 end
640 end
641
641
642 def sql_for_watcher_id_field(field, operator, value)
642 def sql_for_watcher_id_field(field, operator, value)
643 db_table = Watcher.table_name
643 db_table = Watcher.table_name
644 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
644 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
645 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
645 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
646 end
646 end
647
647
648 def sql_for_member_of_group_field(field, operator, value)
648 def sql_for_member_of_group_field(field, operator, value)
649 if operator == '*' # Any group
649 if operator == '*' # Any group
650 groups = Group.all
650 groups = Group.all
651 operator = '=' # Override the operator since we want to find by assigned_to
651 operator = '=' # Override the operator since we want to find by assigned_to
652 elsif operator == "!*"
652 elsif operator == "!*"
653 groups = Group.all
653 groups = Group.all
654 operator = '!' # Override the operator since we want to find by assigned_to
654 operator = '!' # Override the operator since we want to find by assigned_to
655 else
655 else
656 groups = Group.find_all_by_id(value)
656 groups = Group.find_all_by_id(value)
657 end
657 end
658 groups ||= []
658 groups ||= []
659
659
660 members_of_groups = groups.inject([]) {|user_ids, group|
660 members_of_groups = groups.inject([]) {|user_ids, group|
661 if group && group.user_ids.present?
661 if group && group.user_ids.present?
662 user_ids << group.user_ids
662 user_ids << group.user_ids
663 end
663 end
664 user_ids.flatten.uniq.compact
664 user_ids.flatten.uniq.compact
665 }.sort.collect(&:to_s)
665 }.sort.collect(&:to_s)
666
666
667 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
667 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
668 end
668 end
669
669
670 def sql_for_assigned_to_role_field(field, operator, value)
670 def sql_for_assigned_to_role_field(field, operator, value)
671 case operator
671 case operator
672 when "*", "!*" # Member / Not member
672 when "*", "!*" # Member / Not member
673 sw = operator == "!*" ? 'NOT' : ''
673 sw = operator == "!*" ? 'NOT' : ''
674 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
674 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
675 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
675 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
676 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
676 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
677 when "=", "!"
677 when "=", "!"
678 role_cond = value.any? ?
678 role_cond = value.any? ?
679 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
679 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
680 "1=0"
680 "1=0"
681
681
682 sw = operator == "!" ? 'NOT' : ''
682 sw = operator == "!" ? 'NOT' : ''
683 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
683 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
684 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
684 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
685 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
685 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
686 end
686 end
687 end
687 end
688
688
689 private
689 private
690
690
691 def sql_for_custom_field(field, operator, value, custom_field_id)
691 def sql_for_custom_field(field, operator, value, custom_field_id)
692 db_table = CustomValue.table_name
692 db_table = CustomValue.table_name
693 db_field = 'value'
693 db_field = 'value'
694 filter = @available_filters[field]
694 filter = @available_filters[field]
695 if filter && filter[:format] == 'user'
695 if filter && filter[:format] == 'user'
696 if value.delete('me')
696 if value.delete('me')
697 value.push User.current.id.to_s
697 value.push User.current.id.to_s
698 end
698 end
699 end
699 end
700 not_in = nil
700 not_in = nil
701 if operator == '!'
701 if operator == '!'
702 # Makes ! operator work for custom fields with multiple values
702 # Makes ! operator work for custom fields with multiple values
703 operator = '='
703 operator = '='
704 not_in = 'NOT'
704 not_in = 'NOT'
705 end
705 end
706 "#{Issue.table_name}.id #{not_in} IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
706 "#{Issue.table_name}.id #{not_in} IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
707 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
707 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
708 end
708 end
709
709
710 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
710 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
711 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
711 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
712 sql = ''
712 sql = ''
713 case operator
713 case operator
714 when "="
714 when "="
715 if value.any?
715 if value.any?
716 case type_for(field)
716 case type_for(field)
717 when :date, :date_past
717 when :date, :date_past
718 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
718 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
719 when :integer
719 when :integer
720 if is_custom_filter
720 if is_custom_filter
721 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
721 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
722 else
722 else
723 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
723 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
724 end
724 end
725 when :float
725 when :float
726 if is_custom_filter
726 if is_custom_filter
727 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
727 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
728 else
728 else
729 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
729 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
730 end
730 end
731 else
731 else
732 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
732 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
733 end
733 end
734 else
734 else
735 # IN an empty set
735 # IN an empty set
736 sql = "1=0"
736 sql = "1=0"
737 end
737 end
738 when "!"
738 when "!"
739 if value.any?
739 if value.any?
740 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
740 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
741 else
741 else
742 # NOT IN an empty set
742 # NOT IN an empty set
743 sql = "1=1"
743 sql = "1=1"
744 end
744 end
745 when "!*"
745 when "!*"
746 sql = "#{db_table}.#{db_field} IS NULL"
746 sql = "#{db_table}.#{db_field} IS NULL"
747 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
747 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
748 when "*"
748 when "*"
749 sql = "#{db_table}.#{db_field} IS NOT NULL"
749 sql = "#{db_table}.#{db_field} IS NOT NULL"
750 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
750 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
751 when ">="
751 when ">="
752 if [:date, :date_past].include?(type_for(field))
752 if [:date, :date_past].include?(type_for(field))
753 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
753 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
754 else
754 else
755 if is_custom_filter
755 if is_custom_filter
756 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
756 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
757 else
757 else
758 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
758 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
759 end
759 end
760 end
760 end
761 when "<="
761 when "<="
762 if [:date, :date_past].include?(type_for(field))
762 if [:date, :date_past].include?(type_for(field))
763 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
763 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
764 else
764 else
765 if is_custom_filter
765 if is_custom_filter
766 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
766 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
767 else
767 else
768 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
768 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
769 end
769 end
770 end
770 end
771 when "><"
771 when "><"
772 if [:date, :date_past].include?(type_for(field))
772 if [:date, :date_past].include?(type_for(field))
773 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
773 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
774 else
774 else
775 if is_custom_filter
775 if is_custom_filter
776 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
776 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
777 else
777 else
778 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
778 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
779 end
779 end
780 end
780 end
781 when "o"
781 when "o"
782 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
782 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
783 when "c"
783 when "c"
784 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
784 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
785 when ">t-"
785 when ">t-"
786 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
786 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
787 when "<t-"
787 when "<t-"
788 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
788 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
789 when "t-"
789 when "t-"
790 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
790 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
791 when ">t+"
791 when ">t+"
792 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
792 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
793 when "<t+"
793 when "<t+"
794 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
794 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
795 when "t+"
795 when "t+"
796 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
796 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
797 when "t"
797 when "t"
798 sql = relative_date_clause(db_table, db_field, 0, 0)
798 sql = relative_date_clause(db_table, db_field, 0, 0)
799 when "w"
799 when "w"
800 first_day_of_week = l(:general_first_day_of_week).to_i
800 first_day_of_week = l(:general_first_day_of_week).to_i
801 day_of_week = Date.today.cwday
801 day_of_week = Date.today.cwday
802 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
802 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
803 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
803 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
804 when "~"
804 when "~"
805 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
805 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
806 when "!~"
806 when "!~"
807 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
807 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
808 else
808 else
809 raise "Unknown query operator #{operator}"
809 raise "Unknown query operator #{operator}"
810 end
810 end
811
811
812 return sql
812 return sql
813 end
813 end
814
814
815 def add_custom_fields_filters(custom_fields)
815 def add_custom_fields_filters(custom_fields)
816 @available_filters ||= {}
816 @available_filters ||= {}
817
817
818 custom_fields.select(&:is_filter?).each do |field|
818 custom_fields.select(&:is_filter?).each do |field|
819 case field.field_format
819 case field.field_format
820 when "text"
820 when "text"
821 options = { :type => :text, :order => 20 }
821 options = { :type => :text, :order => 20 }
822 when "list"
822 when "list"
823 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
823 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
824 when "date"
824 when "date"
825 options = { :type => :date, :order => 20 }
825 options = { :type => :date, :order => 20 }
826 when "bool"
826 when "bool"
827 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
827 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
828 when "int"
828 when "int"
829 options = { :type => :integer, :order => 20 }
829 options = { :type => :integer, :order => 20 }
830 when "float"
830 when "float"
831 options = { :type => :float, :order => 20 }
831 options = { :type => :float, :order => 20 }
832 when "user", "version"
832 when "user", "version"
833 next unless project
833 next unless project
834 values = field.possible_values_options(project)
834 values = field.possible_values_options(project)
835 if User.current.logged? && field.field_format == 'user'
835 if User.current.logged? && field.field_format == 'user'
836 values.unshift ["<< #{l(:label_me)} >>", "me"]
836 values.unshift ["<< #{l(:label_me)} >>", "me"]
837 end
837 end
838 options = { :type => :list_optional, :values => values, :order => 20}
838 options = { :type => :list_optional, :values => values, :order => 20}
839 else
839 else
840 options = { :type => :string, :order => 20 }
840 options = { :type => :string, :order => 20 }
841 end
841 end
842 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
842 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
843 end
843 end
844 end
844 end
845
845
846 # Returns a SQL clause for a date or datetime field.
846 # Returns a SQL clause for a date or datetime field.
847 def date_clause(table, field, from, to)
847 def date_clause(table, field, from, to)
848 s = []
848 s = []
849 if from
849 if from
850 from_yesterday = from - 1
850 from_yesterday = from - 1
851 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
851 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
852 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
852 if self.class.default_timezone == :utc
853 from_yesterday_time = from_yesterday_time.utc
854 end
855 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
853 end
856 end
854 if to
857 if to
855 to_utc = Time.gm(to.year, to.month, to.day)
858 to_time = Time.local(to.year, to.month, to.day)
856 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
859 if self.class.default_timezone == :utc
860 to_time = to_time.utc
861 end
862 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
857 end
863 end
858 s.join(' AND ')
864 s.join(' AND ')
859 end
865 end
860
866
861 # Returns a SQL clause for a date or datetime field using relative dates.
867 # Returns a SQL clause for a date or datetime field using relative dates.
862 def relative_date_clause(table, field, days_from, days_to)
868 def relative_date_clause(table, field, days_from, days_to)
863 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
869 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
864 end
870 end
865 end
871 end
@@ -1,657 +1,666
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "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 # Account statuses
23 # Account statuses
24 STATUS_ANONYMOUS = 0
24 STATUS_ANONYMOUS = 0
25 STATUS_ACTIVE = 1
25 STATUS_ACTIVE = 1
26 STATUS_REGISTERED = 2
26 STATUS_REGISTERED = 2
27 STATUS_LOCKED = 3
27 STATUS_LOCKED = 3
28
28
29 # Different ways of displaying/sorting users
29 # Different ways of displaying/sorting users
30 USER_FORMATS = {
30 USER_FORMATS = {
31 :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
31 :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
32 :firstname => {:string => '#{firstname}', :order => %w(firstname id)},
32 :firstname => {:string => '#{firstname}', :order => %w(firstname id)},
33 :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
33 :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
34 :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
34 :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
35 :username => {:string => '#{login}', :order => %w(login id)},
35 :username => {:string => '#{login}', :order => %w(login id)},
36 }
36 }
37
37
38 MAIL_NOTIFICATION_OPTIONS = [
38 MAIL_NOTIFICATION_OPTIONS = [
39 ['all', :label_user_mail_option_all],
39 ['all', :label_user_mail_option_all],
40 ['selected', :label_user_mail_option_selected],
40 ['selected', :label_user_mail_option_selected],
41 ['only_my_events', :label_user_mail_option_only_my_events],
41 ['only_my_events', :label_user_mail_option_only_my_events],
42 ['only_assigned', :label_user_mail_option_only_assigned],
42 ['only_assigned', :label_user_mail_option_only_assigned],
43 ['only_owner', :label_user_mail_option_only_owner],
43 ['only_owner', :label_user_mail_option_only_owner],
44 ['none', :label_user_mail_option_none]
44 ['none', :label_user_mail_option_none]
45 ]
45 ]
46
46
47 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
48 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 :after_remove => Proc.new {|user, group| group.user_removed(user)}
49 has_many :changesets, :dependent => :nullify
49 has_many :changesets, :dependent => :nullify
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
53 belongs_to :auth_source
53 belongs_to :auth_source
54
54
55 # Active non-anonymous users scope
55 # Active non-anonymous users scope
56 scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56 scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57 scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
57 scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
58 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
58 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
59
59
60 acts_as_customizable
60 acts_as_customizable
61
61
62 attr_accessor :password, :password_confirmation
62 attr_accessor :password, :password_confirmation
63 attr_accessor :last_before_login_on
63 attr_accessor :last_before_login_on
64 # Prevents unauthorized assignments
64 # Prevents unauthorized assignments
65 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
65 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
66
66
67 LOGIN_LENGTH_LIMIT = 60
67 LOGIN_LENGTH_LIMIT = 60
68 MAIL_LENGTH_LIMIT = 60
68 MAIL_LENGTH_LIMIT = 60
69
69
70 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
70 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
71 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
71 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
72 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
72 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
73 # Login must contain lettres, numbers, underscores only
73 # Login must contain lettres, numbers, underscores only
74 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
74 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
75 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
75 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
76 validates_length_of :firstname, :lastname, :maximum => 30
76 validates_length_of :firstname, :lastname, :maximum => 30
77 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
77 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
78 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
78 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
79 validates_confirmation_of :password, :allow_nil => true
79 validates_confirmation_of :password, :allow_nil => true
80 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
80 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
81 validate :validate_password_length
81 validate :validate_password_length
82
82
83 before_create :set_mail_notification
83 before_create :set_mail_notification
84 before_save :update_hashed_password
84 before_save :update_hashed_password
85 before_destroy :remove_references_before_destroy
85 before_destroy :remove_references_before_destroy
86
86
87 scope :in_group, lambda {|group|
87 scope :in_group, lambda {|group|
88 group_id = group.is_a?(Group) ? group.id : group.to_i
88 group_id = group.is_a?(Group) ? group.id : group.to_i
89 { :conditions => ["#{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] }
89 { :conditions => ["#{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] }
90 }
90 }
91 scope :not_in_group, lambda {|group|
91 scope :not_in_group, lambda {|group|
92 group_id = group.is_a?(Group) ? group.id : group.to_i
92 group_id = group.is_a?(Group) ? group.id : group.to_i
93 { :conditions => ["#{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] }
93 { :conditions => ["#{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] }
94 }
94 }
95
95
96 def set_mail_notification
96 def set_mail_notification
97 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
97 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
98 true
98 true
99 end
99 end
100
100
101 def update_hashed_password
101 def update_hashed_password
102 # update hashed_password if password was set
102 # update hashed_password if password was set
103 if self.password && self.auth_source_id.blank?
103 if self.password && self.auth_source_id.blank?
104 salt_password(password)
104 salt_password(password)
105 end
105 end
106 end
106 end
107
107
108 def reload(*args)
108 def reload(*args)
109 @name = nil
109 @name = nil
110 @projects_by_role = nil
110 @projects_by_role = nil
111 super
111 super
112 end
112 end
113
113
114 def mail=(arg)
114 def mail=(arg)
115 write_attribute(:mail, arg.to_s.strip)
115 write_attribute(:mail, arg.to_s.strip)
116 end
116 end
117
117
118 def identity_url=(url)
118 def identity_url=(url)
119 if url.blank?
119 if url.blank?
120 write_attribute(:identity_url, '')
120 write_attribute(:identity_url, '')
121 else
121 else
122 begin
122 begin
123 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
123 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
124 rescue OpenIdAuthentication::InvalidOpenId
124 rescue OpenIdAuthentication::InvalidOpenId
125 # Invlaid url, don't save
125 # Invlaid url, don't save
126 end
126 end
127 end
127 end
128 self.read_attribute(:identity_url)
128 self.read_attribute(:identity_url)
129 end
129 end
130
130
131 # Returns the user that matches provided login and password, or nil
131 # Returns the user that matches provided login and password, or nil
132 def self.try_to_login(login, password)
132 def self.try_to_login(login, password)
133 # Make sure no one can sign in with an empty password
133 # Make sure no one can sign in with an empty password
134 return nil if password.to_s.empty?
134 return nil if password.to_s.empty?
135 user = find_by_login(login)
135 user = find_by_login(login)
136 if user
136 if user
137 # user is already in local database
137 # user is already in local database
138 return nil if !user.active?
138 return nil if !user.active?
139 if user.auth_source
139 if user.auth_source
140 # user has an external authentication method
140 # user has an external authentication method
141 return nil unless user.auth_source.authenticate(login, password)
141 return nil unless user.auth_source.authenticate(login, password)
142 else
142 else
143 # authentication with local password
143 # authentication with local password
144 return nil unless user.check_password?(password)
144 return nil unless user.check_password?(password)
145 end
145 end
146 else
146 else
147 # user is not yet registered, try to authenticate with available sources
147 # user is not yet registered, try to authenticate with available sources
148 attrs = AuthSource.authenticate(login, password)
148 attrs = AuthSource.authenticate(login, password)
149 if attrs
149 if attrs
150 user = new(attrs)
150 user = new(attrs)
151 user.login = login
151 user.login = login
152 user.language = Setting.default_language
152 user.language = Setting.default_language
153 if user.save
153 if user.save
154 user.reload
154 user.reload
155 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
155 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
156 end
156 end
157 end
157 end
158 end
158 end
159 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
159 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
160 user
160 user
161 rescue => text
161 rescue => text
162 raise text
162 raise text
163 end
163 end
164
164
165 # Returns the user who matches the given autologin +key+ or nil
165 # Returns the user who matches the given autologin +key+ or nil
166 def self.try_to_autologin(key)
166 def self.try_to_autologin(key)
167 tokens = Token.find_all_by_action_and_value('autologin', key)
167 tokens = Token.find_all_by_action_and_value('autologin', key)
168 # Make sure there's only 1 token that matches the key
168 # Make sure there's only 1 token that matches the key
169 if tokens.size == 1
169 if tokens.size == 1
170 token = tokens.first
170 token = tokens.first
171 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
171 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
172 token.user.update_attribute(:last_login_on, Time.now)
172 token.user.update_attribute(:last_login_on, Time.now)
173 token.user
173 token.user
174 end
174 end
175 end
175 end
176 end
176 end
177
177
178 def self.name_formatter(formatter = nil)
178 def self.name_formatter(formatter = nil)
179 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
179 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
180 end
180 end
181
181
182 # Returns an array of fields names than can be used to make an order statement for users
182 # Returns an array of fields names than can be used to make an order statement for users
183 # according to how user names are displayed
183 # according to how user names are displayed
184 # Examples:
184 # Examples:
185 #
185 #
186 # User.fields_for_order_statement => ['users.login', 'users.id']
186 # User.fields_for_order_statement => ['users.login', 'users.id']
187 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
187 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
188 def self.fields_for_order_statement(table=nil)
188 def self.fields_for_order_statement(table=nil)
189 table ||= table_name
189 table ||= table_name
190 name_formatter[:order].map {|field| "#{table}.#{field}"}
190 name_formatter[:order].map {|field| "#{table}.#{field}"}
191 end
191 end
192
192
193 # Return user's full name for display
193 # Return user's full name for display
194 def name(formatter = nil)
194 def name(formatter = nil)
195 f = self.class.name_formatter(formatter)
195 f = self.class.name_formatter(formatter)
196 if formatter
196 if formatter
197 eval('"' + f[:string] + '"')
197 eval('"' + f[:string] + '"')
198 else
198 else
199 @name ||= eval('"' + f[:string] + '"')
199 @name ||= eval('"' + f[:string] + '"')
200 end
200 end
201 end
201 end
202
202
203 def active?
203 def active?
204 self.status == STATUS_ACTIVE
204 self.status == STATUS_ACTIVE
205 end
205 end
206
206
207 def registered?
207 def registered?
208 self.status == STATUS_REGISTERED
208 self.status == STATUS_REGISTERED
209 end
209 end
210
210
211 def locked?
211 def locked?
212 self.status == STATUS_LOCKED
212 self.status == STATUS_LOCKED
213 end
213 end
214
214
215 def activate
215 def activate
216 self.status = STATUS_ACTIVE
216 self.status = STATUS_ACTIVE
217 end
217 end
218
218
219 def register
219 def register
220 self.status = STATUS_REGISTERED
220 self.status = STATUS_REGISTERED
221 end
221 end
222
222
223 def lock
223 def lock
224 self.status = STATUS_LOCKED
224 self.status = STATUS_LOCKED
225 end
225 end
226
226
227 def activate!
227 def activate!
228 update_attribute(:status, STATUS_ACTIVE)
228 update_attribute(:status, STATUS_ACTIVE)
229 end
229 end
230
230
231 def register!
231 def register!
232 update_attribute(:status, STATUS_REGISTERED)
232 update_attribute(:status, STATUS_REGISTERED)
233 end
233 end
234
234
235 def lock!
235 def lock!
236 update_attribute(:status, STATUS_LOCKED)
236 update_attribute(:status, STATUS_LOCKED)
237 end
237 end
238
238
239 # Returns true if +clear_password+ is the correct user's password, otherwise false
239 # Returns true if +clear_password+ is the correct user's password, otherwise false
240 def check_password?(clear_password)
240 def check_password?(clear_password)
241 if auth_source_id.present?
241 if auth_source_id.present?
242 auth_source.authenticate(self.login, clear_password)
242 auth_source.authenticate(self.login, clear_password)
243 else
243 else
244 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
244 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
245 end
245 end
246 end
246 end
247
247
248 # Generates a random salt and computes hashed_password for +clear_password+
248 # Generates a random salt and computes hashed_password for +clear_password+
249 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
249 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
250 def salt_password(clear_password)
250 def salt_password(clear_password)
251 self.salt = User.generate_salt
251 self.salt = User.generate_salt
252 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
252 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
253 end
253 end
254
254
255 # Does the backend storage allow this user to change their password?
255 # Does the backend storage allow this user to change their password?
256 def change_password_allowed?
256 def change_password_allowed?
257 return true if auth_source.nil?
257 return true if auth_source.nil?
258 return auth_source.allow_password_changes?
258 return auth_source.allow_password_changes?
259 end
259 end
260
260
261 # Generate and set a random password. Useful for automated user creation
261 # Generate and set a random password. Useful for automated user creation
262 # Based on Token#generate_token_value
262 # Based on Token#generate_token_value
263 #
263 #
264 def random_password
264 def random_password
265 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
265 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
266 password = ''
266 password = ''
267 40.times { |i| password << chars[rand(chars.size-1)] }
267 40.times { |i| password << chars[rand(chars.size-1)] }
268 self.password = password
268 self.password = password
269 self.password_confirmation = password
269 self.password_confirmation = password
270 self
270 self
271 end
271 end
272
272
273 def pref
273 def pref
274 self.preference ||= UserPreference.new(:user => self)
274 self.preference ||= UserPreference.new(:user => self)
275 end
275 end
276
276
277 def time_zone
277 def time_zone
278 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
278 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
279 end
279 end
280
280
281 def wants_comments_in_reverse_order?
281 def wants_comments_in_reverse_order?
282 self.pref[:comments_sorting] == 'desc'
282 self.pref[:comments_sorting] == 'desc'
283 end
283 end
284
284
285 # Return user's RSS key (a 40 chars long string), used to access feeds
285 # Return user's RSS key (a 40 chars long string), used to access feeds
286 def rss_key
286 def rss_key
287 if rss_token.nil?
287 if rss_token.nil?
288 create_rss_token(:action => 'feeds')
288 create_rss_token(:action => 'feeds')
289 end
289 end
290 rss_token.value
290 rss_token.value
291 end
291 end
292
292
293 # Return user's API key (a 40 chars long string), used to access the API
293 # Return user's API key (a 40 chars long string), used to access the API
294 def api_key
294 def api_key
295 if api_token.nil?
295 if api_token.nil?
296 create_api_token(:action => 'api')
296 create_api_token(:action => 'api')
297 end
297 end
298 api_token.value
298 api_token.value
299 end
299 end
300
300
301 # Return an array of project ids for which the user has explicitly turned mail notifications on
301 # Return an array of project ids for which the user has explicitly turned mail notifications on
302 def notified_projects_ids
302 def notified_projects_ids
303 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
303 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
304 end
304 end
305
305
306 def notified_project_ids=(ids)
306 def notified_project_ids=(ids)
307 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
307 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
308 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
308 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
309 @notified_projects_ids = nil
309 @notified_projects_ids = nil
310 notified_projects_ids
310 notified_projects_ids
311 end
311 end
312
312
313 def valid_notification_options
313 def valid_notification_options
314 self.class.valid_notification_options(self)
314 self.class.valid_notification_options(self)
315 end
315 end
316
316
317 # Only users that belong to more than 1 project can select projects for which they are notified
317 # Only users that belong to more than 1 project can select projects for which they are notified
318 def self.valid_notification_options(user=nil)
318 def self.valid_notification_options(user=nil)
319 # Note that @user.membership.size would fail since AR ignores
319 # Note that @user.membership.size would fail since AR ignores
320 # :include association option when doing a count
320 # :include association option when doing a count
321 if user.nil? || user.memberships.length < 1
321 if user.nil? || user.memberships.length < 1
322 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
322 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
323 else
323 else
324 MAIL_NOTIFICATION_OPTIONS
324 MAIL_NOTIFICATION_OPTIONS
325 end
325 end
326 end
326 end
327
327
328 # Find a user account by matching the exact login and then a case-insensitive
328 # Find a user account by matching the exact login and then a case-insensitive
329 # version. Exact matches will be given priority.
329 # version. Exact matches will be given priority.
330 def self.find_by_login(login)
330 def self.find_by_login(login)
331 # First look for an exact match
331 # First look for an exact match
332 user = all(:conditions => {:login => login}).detect {|u| u.login == login}
332 user = all(:conditions => {:login => login}).detect {|u| u.login == login}
333 unless user
333 unless user
334 # Fail over to case-insensitive if none was found
334 # Fail over to case-insensitive if none was found
335 user = first(:conditions => ["LOWER(login) = ?", login.to_s.downcase])
335 user = first(:conditions => ["LOWER(login) = ?", login.to_s.downcase])
336 end
336 end
337 user
337 user
338 end
338 end
339
339
340 def self.find_by_rss_key(key)
340 def self.find_by_rss_key(key)
341 token = Token.find_by_value(key)
341 token = Token.find_by_value(key)
342 token && token.user.active? ? token.user : nil
342 token && token.user.active? ? token.user : nil
343 end
343 end
344
344
345 def self.find_by_api_key(key)
345 def self.find_by_api_key(key)
346 token = Token.find_by_action_and_value('api', key)
346 token = Token.find_by_action_and_value('api', key)
347 token && token.user.active? ? token.user : nil
347 token && token.user.active? ? token.user : nil
348 end
348 end
349
349
350 # Makes find_by_mail case-insensitive
350 # Makes find_by_mail case-insensitive
351 def self.find_by_mail(mail)
351 def self.find_by_mail(mail)
352 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
352 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
353 end
353 end
354
354
355 # Returns true if the default admin account can no longer be used
355 # Returns true if the default admin account can no longer be used
356 def self.default_admin_account_changed?
356 def self.default_admin_account_changed?
357 !User.active.find_by_login("admin").try(:check_password?, "admin")
357 !User.active.find_by_login("admin").try(:check_password?, "admin")
358 end
358 end
359
359
360 def to_s
360 def to_s
361 name
361 name
362 end
362 end
363
363
364 # Returns the current day according to user's time zone
364 # Returns the current day according to user's time zone
365 def today
365 def today
366 if time_zone.nil?
366 if time_zone.nil?
367 Date.today
367 Date.today
368 else
368 else
369 Time.now.in_time_zone(time_zone).to_date
369 Time.now.in_time_zone(time_zone).to_date
370 end
370 end
371 end
371 end
372
372
373 # Returns the day of +time+ according to user's time zone
374 def time_to_date(time)
375 if time_zone.nil?
376 time.to_date
377 else
378 time.in_time_zone(time_zone).to_date
379 end
380 end
381
373 def logged?
382 def logged?
374 true
383 true
375 end
384 end
376
385
377 def anonymous?
386 def anonymous?
378 !logged?
387 !logged?
379 end
388 end
380
389
381 # Return user's roles for project
390 # Return user's roles for project
382 def roles_for_project(project)
391 def roles_for_project(project)
383 roles = []
392 roles = []
384 # No role on archived projects
393 # No role on archived projects
385 return roles unless project && project.active?
394 return roles unless project && project.active?
386 if logged?
395 if logged?
387 # Find project membership
396 # Find project membership
388 membership = memberships.detect {|m| m.project_id == project.id}
397 membership = memberships.detect {|m| m.project_id == project.id}
389 if membership
398 if membership
390 roles = membership.roles
399 roles = membership.roles
391 else
400 else
392 @role_non_member ||= Role.non_member
401 @role_non_member ||= Role.non_member
393 roles << @role_non_member
402 roles << @role_non_member
394 end
403 end
395 else
404 else
396 @role_anonymous ||= Role.anonymous
405 @role_anonymous ||= Role.anonymous
397 roles << @role_anonymous
406 roles << @role_anonymous
398 end
407 end
399 roles
408 roles
400 end
409 end
401
410
402 # Return true if the user is a member of project
411 # Return true if the user is a member of project
403 def member_of?(project)
412 def member_of?(project)
404 !roles_for_project(project).detect {|role| role.member?}.nil?
413 !roles_for_project(project).detect {|role| role.member?}.nil?
405 end
414 end
406
415
407 # Returns a hash of user's projects grouped by roles
416 # Returns a hash of user's projects grouped by roles
408 def projects_by_role
417 def projects_by_role
409 return @projects_by_role if @projects_by_role
418 return @projects_by_role if @projects_by_role
410
419
411 @projects_by_role = Hash.new {|h,k| h[k]=[]}
420 @projects_by_role = Hash.new {|h,k| h[k]=[]}
412 memberships.each do |membership|
421 memberships.each do |membership|
413 membership.roles.each do |role|
422 membership.roles.each do |role|
414 @projects_by_role[role] << membership.project if membership.project
423 @projects_by_role[role] << membership.project if membership.project
415 end
424 end
416 end
425 end
417 @projects_by_role.each do |role, projects|
426 @projects_by_role.each do |role, projects|
418 projects.uniq!
427 projects.uniq!
419 end
428 end
420
429
421 @projects_by_role
430 @projects_by_role
422 end
431 end
423
432
424 # Returns true if user is arg or belongs to arg
433 # Returns true if user is arg or belongs to arg
425 def is_or_belongs_to?(arg)
434 def is_or_belongs_to?(arg)
426 if arg.is_a?(User)
435 if arg.is_a?(User)
427 self == arg
436 self == arg
428 elsif arg.is_a?(Group)
437 elsif arg.is_a?(Group)
429 arg.users.include?(self)
438 arg.users.include?(self)
430 else
439 else
431 false
440 false
432 end
441 end
433 end
442 end
434
443
435 # Return true if the user is allowed to do the specified action on a specific context
444 # Return true if the user is allowed to do the specified action on a specific context
436 # Action can be:
445 # Action can be:
437 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
446 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
438 # * a permission Symbol (eg. :edit_project)
447 # * a permission Symbol (eg. :edit_project)
439 # Context can be:
448 # Context can be:
440 # * a project : returns true if user is allowed to do the specified action on this project
449 # * a project : returns true if user is allowed to do the specified action on this project
441 # * an array of projects : returns true if user is allowed on every project
450 # * an array of projects : returns true if user is allowed on every project
442 # * nil with options[:global] set : check if user has at least one role allowed for this action,
451 # * nil with options[:global] set : check if user has at least one role allowed for this action,
443 # or falls back to Non Member / Anonymous permissions depending if the user is logged
452 # or falls back to Non Member / Anonymous permissions depending if the user is logged
444 def allowed_to?(action, context, options={}, &block)
453 def allowed_to?(action, context, options={}, &block)
445 if context && context.is_a?(Project)
454 if context && context.is_a?(Project)
446 # No action allowed on archived projects
455 # No action allowed on archived projects
447 return false unless context.active?
456 return false unless context.active?
448 # No action allowed on disabled modules
457 # No action allowed on disabled modules
449 return false unless context.allows_to?(action)
458 return false unless context.allows_to?(action)
450 # Admin users are authorized for anything else
459 # Admin users are authorized for anything else
451 return true if admin?
460 return true if admin?
452
461
453 roles = roles_for_project(context)
462 roles = roles_for_project(context)
454 return false unless roles
463 return false unless roles
455 roles.detect {|role|
464 roles.detect {|role|
456 (context.is_public? || role.member?) &&
465 (context.is_public? || role.member?) &&
457 role.allowed_to?(action) &&
466 role.allowed_to?(action) &&
458 (block_given? ? yield(role, self) : true)
467 (block_given? ? yield(role, self) : true)
459 }
468 }
460 elsif context && context.is_a?(Array)
469 elsif context && context.is_a?(Array)
461 # Authorize if user is authorized on every element of the array
470 # Authorize if user is authorized on every element of the array
462 context.map do |project|
471 context.map do |project|
463 allowed_to?(action, project, options, &block)
472 allowed_to?(action, project, options, &block)
464 end.inject do |memo,allowed|
473 end.inject do |memo,allowed|
465 memo && allowed
474 memo && allowed
466 end
475 end
467 elsif options[:global]
476 elsif options[:global]
468 # Admin users are always authorized
477 # Admin users are always authorized
469 return true if admin?
478 return true if admin?
470
479
471 # authorize if user has at least one role that has this permission
480 # authorize if user has at least one role that has this permission
472 roles = memberships.collect {|m| m.roles}.flatten.uniq
481 roles = memberships.collect {|m| m.roles}.flatten.uniq
473 roles << (self.logged? ? Role.non_member : Role.anonymous)
482 roles << (self.logged? ? Role.non_member : Role.anonymous)
474 roles.detect {|role|
483 roles.detect {|role|
475 role.allowed_to?(action) &&
484 role.allowed_to?(action) &&
476 (block_given? ? yield(role, self) : true)
485 (block_given? ? yield(role, self) : true)
477 }
486 }
478 else
487 else
479 false
488 false
480 end
489 end
481 end
490 end
482
491
483 # Is the user allowed to do the specified action on any project?
492 # Is the user allowed to do the specified action on any project?
484 # See allowed_to? for the actions and valid options.
493 # See allowed_to? for the actions and valid options.
485 def allowed_to_globally?(action, options, &block)
494 def allowed_to_globally?(action, options, &block)
486 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
495 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
487 end
496 end
488
497
489 # Returns true if the user is allowed to delete his own account
498 # Returns true if the user is allowed to delete his own account
490 def own_account_deletable?
499 def own_account_deletable?
491 Setting.unsubscribe? &&
500 Setting.unsubscribe? &&
492 (!admin? || User.active.first(:conditions => ["admin = ? AND id <> ?", true, id]).present?)
501 (!admin? || User.active.first(:conditions => ["admin = ? AND id <> ?", true, id]).present?)
493 end
502 end
494
503
495 safe_attributes 'login',
504 safe_attributes 'login',
496 'firstname',
505 'firstname',
497 'lastname',
506 'lastname',
498 'mail',
507 'mail',
499 'mail_notification',
508 'mail_notification',
500 'language',
509 'language',
501 'custom_field_values',
510 'custom_field_values',
502 'custom_fields',
511 'custom_fields',
503 'identity_url'
512 'identity_url'
504
513
505 safe_attributes 'status',
514 safe_attributes 'status',
506 'auth_source_id',
515 'auth_source_id',
507 :if => lambda {|user, current_user| current_user.admin?}
516 :if => lambda {|user, current_user| current_user.admin?}
508
517
509 safe_attributes 'group_ids',
518 safe_attributes 'group_ids',
510 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
519 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
511
520
512 # Utility method to help check if a user should be notified about an
521 # Utility method to help check if a user should be notified about an
513 # event.
522 # event.
514 #
523 #
515 # TODO: only supports Issue events currently
524 # TODO: only supports Issue events currently
516 def notify_about?(object)
525 def notify_about?(object)
517 case mail_notification
526 case mail_notification
518 when 'all'
527 when 'all'
519 true
528 true
520 when 'selected'
529 when 'selected'
521 # user receives notifications for created/assigned issues on unselected projects
530 # user receives notifications for created/assigned issues on unselected projects
522 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
531 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
523 true
532 true
524 else
533 else
525 false
534 false
526 end
535 end
527 when 'none'
536 when 'none'
528 false
537 false
529 when 'only_my_events'
538 when 'only_my_events'
530 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
539 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
531 true
540 true
532 else
541 else
533 false
542 false
534 end
543 end
535 when 'only_assigned'
544 when 'only_assigned'
536 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
545 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
537 true
546 true
538 else
547 else
539 false
548 false
540 end
549 end
541 when 'only_owner'
550 when 'only_owner'
542 if object.is_a?(Issue) && object.author == self
551 if object.is_a?(Issue) && object.author == self
543 true
552 true
544 else
553 else
545 false
554 false
546 end
555 end
547 else
556 else
548 false
557 false
549 end
558 end
550 end
559 end
551
560
552 def self.current=(user)
561 def self.current=(user)
553 @current_user = user
562 @current_user = user
554 end
563 end
555
564
556 def self.current
565 def self.current
557 @current_user ||= User.anonymous
566 @current_user ||= User.anonymous
558 end
567 end
559
568
560 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
569 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
561 # one anonymous user per database.
570 # one anonymous user per database.
562 def self.anonymous
571 def self.anonymous
563 anonymous_user = AnonymousUser.find(:first)
572 anonymous_user = AnonymousUser.find(:first)
564 if anonymous_user.nil?
573 if anonymous_user.nil?
565 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
574 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
566 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
575 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
567 end
576 end
568 anonymous_user
577 anonymous_user
569 end
578 end
570
579
571 # Salts all existing unsalted passwords
580 # Salts all existing unsalted passwords
572 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
581 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
573 # This method is used in the SaltPasswords migration and is to be kept as is
582 # This method is used in the SaltPasswords migration and is to be kept as is
574 def self.salt_unsalted_passwords!
583 def self.salt_unsalted_passwords!
575 transaction do
584 transaction do
576 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
585 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
577 next if user.hashed_password.blank?
586 next if user.hashed_password.blank?
578 salt = User.generate_salt
587 salt = User.generate_salt
579 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
588 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
580 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
589 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
581 end
590 end
582 end
591 end
583 end
592 end
584
593
585 protected
594 protected
586
595
587 def validate_password_length
596 def validate_password_length
588 # Password length validation based on setting
597 # Password length validation based on setting
589 if !password.nil? && password.size < Setting.password_min_length.to_i
598 if !password.nil? && password.size < Setting.password_min_length.to_i
590 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
599 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
591 end
600 end
592 end
601 end
593
602
594 private
603 private
595
604
596 # Removes references that are not handled by associations
605 # Removes references that are not handled by associations
597 # Things that are not deleted are reassociated with the anonymous user
606 # Things that are not deleted are reassociated with the anonymous user
598 def remove_references_before_destroy
607 def remove_references_before_destroy
599 return if self.id.nil?
608 return if self.id.nil?
600
609
601 substitute = User.anonymous
610 substitute = User.anonymous
602 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
611 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
603 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
612 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
604 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
613 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
605 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
614 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
606 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
615 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
607 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
616 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
608 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
617 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
609 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
618 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
610 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
619 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
611 # Remove private queries and keep public ones
620 # Remove private queries and keep public ones
612 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
621 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
613 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
622 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
614 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
623 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
615 Token.delete_all ['user_id = ?', id]
624 Token.delete_all ['user_id = ?', id]
616 Watcher.delete_all ['user_id = ?', id]
625 Watcher.delete_all ['user_id = ?', id]
617 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
626 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
618 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
627 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
619 end
628 end
620
629
621 # Return password digest
630 # Return password digest
622 def self.hash_password(clear_password)
631 def self.hash_password(clear_password)
623 Digest::SHA1.hexdigest(clear_password || "")
632 Digest::SHA1.hexdigest(clear_password || "")
624 end
633 end
625
634
626 # Returns a 128bits random salt as a hex string (32 chars long)
635 # Returns a 128bits random salt as a hex string (32 chars long)
627 def self.generate_salt
636 def self.generate_salt
628 Redmine::Utils.random_hex(16)
637 Redmine::Utils.random_hex(16)
629 end
638 end
630
639
631 end
640 end
632
641
633 class AnonymousUser < User
642 class AnonymousUser < User
634 validate :validate_anonymous_uniqueness, :on => :create
643 validate :validate_anonymous_uniqueness, :on => :create
635
644
636 def validate_anonymous_uniqueness
645 def validate_anonymous_uniqueness
637 # There should be only one AnonymousUser in the database
646 # There should be only one AnonymousUser in the database
638 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
647 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
639 end
648 end
640
649
641 def available_custom_fields
650 def available_custom_fields
642 []
651 []
643 end
652 end
644
653
645 # Overrides a few properties
654 # Overrides a few properties
646 def logged?; false end
655 def logged?; false end
647 def admin; false end
656 def admin; false end
648 def name(*args); I18n.t(:label_user_anonymous) end
657 def name(*args); I18n.t(:label_user_anonymous) end
649 def mail; nil end
658 def mail; nil end
650 def time_zone; nil end
659 def time_zone; nil end
651 def rss_key; nil end
660 def rss_key; nil end
652
661
653 # Anonymous user can not be destroyed
662 # Anonymous user can not be destroyed
654 def destroy
663 def destroy
655 false
664 false
656 end
665 end
657 end
666 end
@@ -1,234 +1,234
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'diff'
18 require 'diff'
19 require 'enumerator'
19 require 'enumerator'
20
20
21 class WikiPage < ActiveRecord::Base
21 class WikiPage < ActiveRecord::Base
22 include Redmine::SafeAttributes
22 include Redmine::SafeAttributes
23
23
24 belongs_to :wiki
24 belongs_to :wiki
25 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
25 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
26 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
26 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
27 acts_as_tree :dependent => :nullify, :order => 'title'
27 acts_as_tree :dependent => :nullify, :order => 'title'
28
28
29 acts_as_watchable
29 acts_as_watchable
30 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
30 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
31 :description => :text,
31 :description => :text,
32 :datetime => :created_on,
32 :datetime => :created_on,
33 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
33 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
34
34
35 acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
35 acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
36 :include => [{:wiki => :project}, :content],
36 :include => [{:wiki => :project}, :content],
37 :permission => :view_wiki_pages,
37 :permission => :view_wiki_pages,
38 :project_key => "#{Wiki.table_name}.project_id"
38 :project_key => "#{Wiki.table_name}.project_id"
39
39
40 attr_accessor :redirect_existing_links
40 attr_accessor :redirect_existing_links
41
41
42 validates_presence_of :title
42 validates_presence_of :title
43 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
43 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
44 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
44 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
45 validates_associated :content
45 validates_associated :content
46
46
47 validate :validate_parent_title
47 validate :validate_parent_title
48 before_destroy :remove_redirects
48 before_destroy :remove_redirects
49 before_save :handle_redirects
49 before_save :handle_redirects
50
50
51 # eager load information about last updates, without loading text
51 # eager load information about last updates, without loading text
52 scope :with_updated_on, {
52 scope :with_updated_on, {
53 :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
53 :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
54 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
54 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
55 }
55 }
56
56
57 # Wiki pages that are protected by default
57 # Wiki pages that are protected by default
58 DEFAULT_PROTECTED_PAGES = %w(sidebar)
58 DEFAULT_PROTECTED_PAGES = %w(sidebar)
59
59
60 safe_attributes 'parent_id',
60 safe_attributes 'parent_id',
61 :if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
61 :if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
62
62
63 def initialize(attributes=nil, *args)
63 def initialize(attributes=nil, *args)
64 super
64 super
65 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
65 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
66 self.protected = true
66 self.protected = true
67 end
67 end
68 end
68 end
69
69
70 def visible?(user=User.current)
70 def visible?(user=User.current)
71 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
71 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
72 end
72 end
73
73
74 def title=(value)
74 def title=(value)
75 value = Wiki.titleize(value)
75 value = Wiki.titleize(value)
76 @previous_title = read_attribute(:title) if @previous_title.blank?
76 @previous_title = read_attribute(:title) if @previous_title.blank?
77 write_attribute(:title, value)
77 write_attribute(:title, value)
78 end
78 end
79
79
80 def handle_redirects
80 def handle_redirects
81 self.title = Wiki.titleize(title)
81 self.title = Wiki.titleize(title)
82 # Manage redirects if the title has changed
82 # Manage redirects if the title has changed
83 if !@previous_title.blank? && (@previous_title != title) && !new_record?
83 if !@previous_title.blank? && (@previous_title != title) && !new_record?
84 # Update redirects that point to the old title
84 # Update redirects that point to the old title
85 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
85 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
86 r.redirects_to = title
86 r.redirects_to = title
87 r.title == r.redirects_to ? r.destroy : r.save
87 r.title == r.redirects_to ? r.destroy : r.save
88 end
88 end
89 # Remove redirects for the new title
89 # Remove redirects for the new title
90 wiki.redirects.find_all_by_title(title).each(&:destroy)
90 wiki.redirects.find_all_by_title(title).each(&:destroy)
91 # Create a redirect to the new title
91 # Create a redirect to the new title
92 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
92 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
93 @previous_title = nil
93 @previous_title = nil
94 end
94 end
95 end
95 end
96
96
97 def remove_redirects
97 def remove_redirects
98 # Remove redirects to this page
98 # Remove redirects to this page
99 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
99 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
100 end
100 end
101
101
102 def pretty_title
102 def pretty_title
103 WikiPage.pretty_title(title)
103 WikiPage.pretty_title(title)
104 end
104 end
105
105
106 def content_for_version(version=nil)
106 def content_for_version(version=nil)
107 result = content.versions.find_by_version(version.to_i) if version
107 result = content.versions.find_by_version(version.to_i) if version
108 result ||= content
108 result ||= content
109 result
109 result
110 end
110 end
111
111
112 def diff(version_to=nil, version_from=nil)
112 def diff(version_to=nil, version_from=nil)
113 version_to = version_to ? version_to.to_i : self.content.version
113 version_to = version_to ? version_to.to_i : self.content.version
114 version_from = version_from ? version_from.to_i : version_to - 1
114 version_from = version_from ? version_from.to_i : version_to - 1
115 version_to, version_from = version_from, version_to unless version_from < version_to
115 version_to, version_from = version_from, version_to unless version_from < version_to
116
116
117 content_to = content.versions.find_by_version(version_to)
117 content_to = content.versions.find_by_version(version_to)
118 content_from = content.versions.find_by_version(version_from)
118 content_from = content.versions.find_by_version(version_from)
119
119
120 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
120 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
121 end
121 end
122
122
123 def annotate(version=nil)
123 def annotate(version=nil)
124 version = version ? version.to_i : self.content.version
124 version = version ? version.to_i : self.content.version
125 c = content.versions.find_by_version(version)
125 c = content.versions.find_by_version(version)
126 c ? WikiAnnotate.new(c) : nil
126 c ? WikiAnnotate.new(c) : nil
127 end
127 end
128
128
129 def self.pretty_title(str)
129 def self.pretty_title(str)
130 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
130 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
131 end
131 end
132
132
133 def project
133 def project
134 wiki.project
134 wiki.project
135 end
135 end
136
136
137 def text
137 def text
138 content.text if content
138 content.text if content
139 end
139 end
140
140
141 def updated_on
141 def updated_on
142 unless @updated_on
142 unless @updated_on
143 if time = read_attribute(:updated_on)
143 if time = read_attribute(:updated_on)
144 # content updated_on was eager loaded with the page
144 # content updated_on was eager loaded with the page
145 begin
145 begin
146 @updated_on = Time.zone ? Time.zone.parse(time.to_s) : Time.parse(time.to_s)
146 @updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime)
147 rescue
147 rescue
148 end
148 end
149 else
149 else
150 @updated_on = content && content.updated_on
150 @updated_on = content && content.updated_on
151 end
151 end
152 end
152 end
153 @updated_on
153 @updated_on
154 end
154 end
155
155
156 # Returns true if usr is allowed to edit the page, otherwise false
156 # Returns true if usr is allowed to edit the page, otherwise false
157 def editable_by?(usr)
157 def editable_by?(usr)
158 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
158 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
159 end
159 end
160
160
161 def attachments_deletable?(usr=User.current)
161 def attachments_deletable?(usr=User.current)
162 editable_by?(usr) && super(usr)
162 editable_by?(usr) && super(usr)
163 end
163 end
164
164
165 def parent_title
165 def parent_title
166 @parent_title || (self.parent && self.parent.pretty_title)
166 @parent_title || (self.parent && self.parent.pretty_title)
167 end
167 end
168
168
169 def parent_title=(t)
169 def parent_title=(t)
170 @parent_title = t
170 @parent_title = t
171 parent_page = t.blank? ? nil : self.wiki.find_page(t)
171 parent_page = t.blank? ? nil : self.wiki.find_page(t)
172 self.parent = parent_page
172 self.parent = parent_page
173 end
173 end
174
174
175 protected
175 protected
176
176
177 def validate_parent_title
177 def validate_parent_title
178 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
178 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
179 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
179 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
180 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
180 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
181 end
181 end
182 end
182 end
183
183
184 class WikiDiff < Redmine::Helpers::Diff
184 class WikiDiff < Redmine::Helpers::Diff
185 attr_reader :content_to, :content_from
185 attr_reader :content_to, :content_from
186
186
187 def initialize(content_to, content_from)
187 def initialize(content_to, content_from)
188 @content_to = content_to
188 @content_to = content_to
189 @content_from = content_from
189 @content_from = content_from
190 super(content_to.text, content_from.text)
190 super(content_to.text, content_from.text)
191 end
191 end
192 end
192 end
193
193
194 class WikiAnnotate
194 class WikiAnnotate
195 attr_reader :lines, :content
195 attr_reader :lines, :content
196
196
197 def initialize(content)
197 def initialize(content)
198 @content = content
198 @content = content
199 current = content
199 current = content
200 current_lines = current.text.split(/\r?\n/)
200 current_lines = current.text.split(/\r?\n/)
201 @lines = current_lines.collect {|t| [nil, nil, t]}
201 @lines = current_lines.collect {|t| [nil, nil, t]}
202 positions = []
202 positions = []
203 current_lines.size.times {|i| positions << i}
203 current_lines.size.times {|i| positions << i}
204 while (current.previous)
204 while (current.previous)
205 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
205 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
206 d.each_slice(3) do |s|
206 d.each_slice(3) do |s|
207 sign, line = s[0], s[1]
207 sign, line = s[0], s[1]
208 if sign == '+' && positions[line] && positions[line] != -1
208 if sign == '+' && positions[line] && positions[line] != -1
209 if @lines[positions[line]][0].nil?
209 if @lines[positions[line]][0].nil?
210 @lines[positions[line]][0] = current.version
210 @lines[positions[line]][0] = current.version
211 @lines[positions[line]][1] = current.author
211 @lines[positions[line]][1] = current.author
212 end
212 end
213 end
213 end
214 end
214 end
215 d.each_slice(3) do |s|
215 d.each_slice(3) do |s|
216 sign, line = s[0], s[1]
216 sign, line = s[0], s[1]
217 if sign == '-'
217 if sign == '-'
218 positions.insert(line, -1)
218 positions.insert(line, -1)
219 else
219 else
220 positions[line] = nil
220 positions[line] = nil
221 end
221 end
222 end
222 end
223 positions.compact!
223 positions.compact!
224 # Stop if every line is annotated
224 # Stop if every line is annotated
225 break unless @lines.detect { |line| line[0].nil? }
225 break unless @lines.detect { |line| line[0].nil? }
226 current = current.previous
226 current = current.previous
227 end
227 end
228 @lines.each { |line|
228 @lines.each { |line|
229 line[0] ||= current.version
229 line[0] ||= current.version
230 # if the last known version is > 1 (eg. history was cleared), we don't know the author
230 # if the last known version is > 1 (eg. history was cleared), we don't know the author
231 line[1] ||= current.author if current.version == 1
231 line[1] ||= current.author if current.version == 1
232 }
232 }
233 end
233 end
234 end
234 end
@@ -1,58 +1,59
1 require File.expand_path('../boot', __FILE__)
1 require File.expand_path('../boot', __FILE__)
2
2
3 require 'rails/all'
3 require 'rails/all'
4
4
5 if defined?(Bundler)
5 if defined?(Bundler)
6 # If you precompile assets before deploying to production, use this line
6 # If you precompile assets before deploying to production, use this line
7 Bundler.require(*Rails.groups(:assets => %w(development test)))
7 Bundler.require(*Rails.groups(:assets => %w(development test)))
8 # If you want your assets lazily compiled in production, use this line
8 # If you want your assets lazily compiled in production, use this line
9 # Bundler.require(:default, :assets, Rails.env)
9 # Bundler.require(:default, :assets, Rails.env)
10 end
10 end
11
11
12 module RedmineApp
12 module RedmineApp
13 class Application < Rails::Application
13 class Application < Rails::Application
14 # Settings in config/environments/* take precedence over those specified here.
14 # Settings in config/environments/* take precedence over those specified here.
15 # Application configuration should go into files in config/initializers
15 # Application configuration should go into files in config/initializers
16 # -- all .rb files in that directory are automatically loaded.
16 # -- all .rb files in that directory are automatically loaded.
17
17
18 # Custom directories with classes and modules you want to be autoloadable.
18 # Custom directories with classes and modules you want to be autoloadable.
19 config.autoload_paths += %W(#{config.root}/lib)
19 config.autoload_paths += %W(#{config.root}/lib)
20
20
21 # Only load the plugins named here, in the order given (default is alphabetical).
21 # Only load the plugins named here, in the order given (default is alphabetical).
22 # :all can be used as a placeholder for all plugins not explicitly named.
22 # :all can be used as a placeholder for all plugins not explicitly named.
23 # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
23 # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24
24
25 # Activate observers that should always be running.
25 # Activate observers that should always be running.
26 config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer
26 config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer
27
27
28 config.active_record.store_full_sti_class = true
28 config.active_record.store_full_sti_class = true
29 config.active_record.default_timezone = :local
29
30
30 # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
31 # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
31 # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
32 # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
32 # config.time_zone = 'Central Time (US & Canada)'
33 # config.time_zone = 'Central Time (US & Canada)'
33
34
34 # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
35 # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
35 # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
36 # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
36 # config.i18n.default_locale = :de
37 # config.i18n.default_locale = :de
37
38
38 # Configure the default encoding used in templates for Ruby 1.9.
39 # Configure the default encoding used in templates for Ruby 1.9.
39 config.encoding = "utf-8"
40 config.encoding = "utf-8"
40
41
41 # Configure sensitive parameters which will be filtered from the log file.
42 # Configure sensitive parameters which will be filtered from the log file.
42 config.filter_parameters += [:password]
43 config.filter_parameters += [:password]
43
44
44 # Enable the asset pipeline
45 # Enable the asset pipeline
45 config.assets.enabled = false
46 config.assets.enabled = false
46
47
47 # Version of your assets, change this if you want to expire all your assets
48 # Version of your assets, change this if you want to expire all your assets
48 config.assets.version = '1.0'
49 config.assets.version = '1.0'
49
50
50 config.action_mailer.perform_deliveries = false
51 config.action_mailer.perform_deliveries = false
51
52
52 config.session_store :cookie_store, :key => '_redmine_session'
53 config.session_store :cookie_store, :key => '_redmine_session'
53
54
54 if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
55 if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
55 instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
56 instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
56 end
57 end
57 end
58 end
58 end
59 end
@@ -1,265 +1,265
1 ---
1 ---
2 issues_001:
2 issues_001:
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
3 created_on: <%= 3.days.ago.to_s(:db) %>
4 project_id: 1
4 project_id: 1
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
5 updated_on: <%= 1.day.ago.to_s(:db) %>
6 priority_id: 4
6 priority_id: 4
7 subject: Can't print recipes
7 subject: Can't print recipes
8 id: 1
8 id: 1
9 fixed_version_id:
9 fixed_version_id:
10 category_id: 1
10 category_id: 1
11 description: Unable to print recipes
11 description: Unable to print recipes
12 tracker_id: 1
12 tracker_id: 1
13 assigned_to_id:
13 assigned_to_id:
14 author_id: 2
14 author_id: 2
15 status_id: 1
15 status_id: 1
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
18 root_id: 1
18 root_id: 1
19 lft: 1
19 lft: 1
20 rgt: 2
20 rgt: 2
21 lock_version: 3
21 lock_version: 3
22 issues_002:
22 issues_002:
23 created_on: 2006-07-19 21:04:21 +02:00
23 created_on: 2006-07-19 21:04:21 +02:00
24 project_id: 1
24 project_id: 1
25 updated_on: 2006-07-19 21:09:50 +02:00
25 updated_on: 2006-07-19 21:09:50 +02:00
26 priority_id: 5
26 priority_id: 5
27 subject: Add ingredients categories
27 subject: Add ingredients categories
28 id: 2
28 id: 2
29 fixed_version_id: 2
29 fixed_version_id: 2
30 category_id:
30 category_id:
31 description: Ingredients of the recipe should be classified by categories
31 description: Ingredients of the recipe should be classified by categories
32 tracker_id: 2
32 tracker_id: 2
33 assigned_to_id: 3
33 assigned_to_id: 3
34 author_id: 2
34 author_id: 2
35 status_id: 2
35 status_id: 2
36 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
36 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
37 due_date:
37 due_date:
38 root_id: 2
38 root_id: 2
39 lft: 1
39 lft: 1
40 rgt: 2
40 rgt: 2
41 lock_version: 3
41 lock_version: 3
42 done_ratio: 30
42 done_ratio: 30
43 issues_003:
43 issues_003:
44 created_on: 2006-07-19 21:07:27 +02:00
44 created_on: 2006-07-19 21:07:27 +02:00
45 project_id: 1
45 project_id: 1
46 updated_on: 2006-07-19 21:07:27 +02:00
46 updated_on: 2006-07-19 21:07:27 +02:00
47 priority_id: 4
47 priority_id: 4
48 subject: Error 281 when updating a recipe
48 subject: Error 281 when updating a recipe
49 id: 3
49 id: 3
50 fixed_version_id:
50 fixed_version_id:
51 category_id:
51 category_id:
52 description: Error 281 is encountered when saving a recipe
52 description: Error 281 is encountered when saving a recipe
53 tracker_id: 1
53 tracker_id: 1
54 assigned_to_id: 3
54 assigned_to_id: 3
55 author_id: 2
55 author_id: 2
56 status_id: 1
56 status_id: 1
57 start_date: <%= 15.day.ago.to_date.to_s(:db) %>
57 start_date: <%= 15.day.ago.to_date.to_s(:db) %>
58 due_date: <%= 5.day.ago.to_date.to_s(:db) %>
58 due_date: <%= 5.day.ago.to_date.to_s(:db) %>
59 root_id: 3
59 root_id: 3
60 lft: 1
60 lft: 1
61 rgt: 2
61 rgt: 2
62 issues_004:
62 issues_004:
63 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
63 created_on: <%= 5.days.ago.to_s(:db) %>
64 project_id: 2
64 project_id: 2
65 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
65 updated_on: <%= 2.days.ago.to_s(:db) %>
66 priority_id: 4
66 priority_id: 4
67 subject: Issue on project 2
67 subject: Issue on project 2
68 id: 4
68 id: 4
69 fixed_version_id:
69 fixed_version_id:
70 category_id:
70 category_id:
71 description: Issue on project 2
71 description: Issue on project 2
72 tracker_id: 1
72 tracker_id: 1
73 assigned_to_id: 2
73 assigned_to_id: 2
74 author_id: 2
74 author_id: 2
75 status_id: 1
75 status_id: 1
76 root_id: 4
76 root_id: 4
77 lft: 1
77 lft: 1
78 rgt: 2
78 rgt: 2
79 issues_005:
79 issues_005:
80 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
80 created_on: <%= 5.days.ago.to_s(:db) %>
81 project_id: 3
81 project_id: 3
82 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
82 updated_on: <%= 2.days.ago.to_s(:db) %>
83 priority_id: 4
83 priority_id: 4
84 subject: Subproject issue
84 subject: Subproject issue
85 id: 5
85 id: 5
86 fixed_version_id:
86 fixed_version_id:
87 category_id:
87 category_id:
88 description: This is an issue on a cookbook subproject
88 description: This is an issue on a cookbook subproject
89 tracker_id: 1
89 tracker_id: 1
90 assigned_to_id:
90 assigned_to_id:
91 author_id: 2
91 author_id: 2
92 status_id: 1
92 status_id: 1
93 root_id: 5
93 root_id: 5
94 lft: 1
94 lft: 1
95 rgt: 2
95 rgt: 2
96 issues_006:
96 issues_006:
97 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
97 created_on: <%= 1.minute.ago.to_s(:db) %>
98 project_id: 5
98 project_id: 5
99 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
99 updated_on: <%= 1.minute.ago.to_s(:db) %>
100 priority_id: 4
100 priority_id: 4
101 subject: Issue of a private subproject
101 subject: Issue of a private subproject
102 id: 6
102 id: 6
103 fixed_version_id:
103 fixed_version_id:
104 category_id:
104 category_id:
105 description: This is an issue of a private subproject of cookbook
105 description: This is an issue of a private subproject of cookbook
106 tracker_id: 1
106 tracker_id: 1
107 assigned_to_id:
107 assigned_to_id:
108 author_id: 2
108 author_id: 2
109 status_id: 1
109 status_id: 1
110 start_date: <%= Date.today.to_s(:db) %>
110 start_date: <%= Date.today.to_s(:db) %>
111 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
111 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
112 root_id: 6
112 root_id: 6
113 lft: 1
113 lft: 1
114 rgt: 2
114 rgt: 2
115 issues_007:
115 issues_007:
116 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
116 created_on: <%= 10.days.ago.to_s(:db) %>
117 project_id: 1
117 project_id: 1
118 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
118 updated_on: <%= 10.days.ago.to_s(:db) %>
119 priority_id: 5
119 priority_id: 5
120 subject: Issue due today
120 subject: Issue due today
121 id: 7
121 id: 7
122 fixed_version_id:
122 fixed_version_id:
123 category_id:
123 category_id:
124 description: This is an issue that is due today
124 description: This is an issue that is due today
125 tracker_id: 1
125 tracker_id: 1
126 assigned_to_id:
126 assigned_to_id:
127 author_id: 2
127 author_id: 2
128 status_id: 1
128 status_id: 1
129 start_date: <%= 10.days.ago.to_s(:db) %>
129 start_date: <%= 10.days.ago.to_s(:db) %>
130 due_date: <%= Date.today.to_s(:db) %>
130 due_date: <%= Date.today.to_s(:db) %>
131 lock_version: 0
131 lock_version: 0
132 root_id: 7
132 root_id: 7
133 lft: 1
133 lft: 1
134 rgt: 2
134 rgt: 2
135 issues_008:
135 issues_008:
136 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
136 created_on: <%= 10.days.ago.to_s(:db) %>
137 project_id: 1
137 project_id: 1
138 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
138 updated_on: <%= 10.days.ago.to_s(:db) %>
139 priority_id: 5
139 priority_id: 5
140 subject: Closed issue
140 subject: Closed issue
141 id: 8
141 id: 8
142 fixed_version_id:
142 fixed_version_id:
143 category_id:
143 category_id:
144 description: This is a closed issue.
144 description: This is a closed issue.
145 tracker_id: 1
145 tracker_id: 1
146 assigned_to_id:
146 assigned_to_id:
147 author_id: 2
147 author_id: 2
148 status_id: 5
148 status_id: 5
149 start_date:
149 start_date:
150 due_date:
150 due_date:
151 lock_version: 0
151 lock_version: 0
152 root_id: 8
152 root_id: 8
153 lft: 1
153 lft: 1
154 rgt: 2
154 rgt: 2
155 issues_009:
155 issues_009:
156 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
156 created_on: <%= 1.minute.ago.to_s(:db) %>
157 project_id: 5
157 project_id: 5
158 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
158 updated_on: <%= 1.minute.ago.to_s(:db) %>
159 priority_id: 5
159 priority_id: 5
160 subject: Blocked Issue
160 subject: Blocked Issue
161 id: 9
161 id: 9
162 fixed_version_id:
162 fixed_version_id:
163 category_id:
163 category_id:
164 description: This is an issue that is blocked by issue #10
164 description: This is an issue that is blocked by issue #10
165 tracker_id: 1
165 tracker_id: 1
166 assigned_to_id:
166 assigned_to_id:
167 author_id: 2
167 author_id: 2
168 status_id: 1
168 status_id: 1
169 start_date: <%= Date.today.to_s(:db) %>
169 start_date: <%= Date.today.to_s(:db) %>
170 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
170 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
171 root_id: 9
171 root_id: 9
172 lft: 1
172 lft: 1
173 rgt: 2
173 rgt: 2
174 issues_010:
174 issues_010:
175 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
175 created_on: <%= 1.minute.ago.to_s(:db) %>
176 project_id: 5
176 project_id: 5
177 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
177 updated_on: <%= 1.minute.ago.to_s(:db) %>
178 priority_id: 5
178 priority_id: 5
179 subject: Issue Doing the Blocking
179 subject: Issue Doing the Blocking
180 id: 10
180 id: 10
181 fixed_version_id:
181 fixed_version_id:
182 category_id:
182 category_id:
183 description: This is an issue that blocks issue #9
183 description: This is an issue that blocks issue #9
184 tracker_id: 1
184 tracker_id: 1
185 assigned_to_id:
185 assigned_to_id:
186 author_id: 2
186 author_id: 2
187 status_id: 1
187 status_id: 1
188 start_date: <%= Date.today.to_s(:db) %>
188 start_date: <%= Date.today.to_s(:db) %>
189 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
189 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
190 root_id: 10
190 root_id: 10
191 lft: 1
191 lft: 1
192 rgt: 2
192 rgt: 2
193 issues_011:
193 issues_011:
194 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
194 created_on: <%= 3.days.ago.to_s(:db) %>
195 project_id: 1
195 project_id: 1
196 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
196 updated_on: <%= 1.day.ago.to_s(:db) %>
197 priority_id: 5
197 priority_id: 5
198 subject: Closed issue on a closed version
198 subject: Closed issue on a closed version
199 id: 11
199 id: 11
200 fixed_version_id: 1
200 fixed_version_id: 1
201 category_id: 1
201 category_id: 1
202 description:
202 description:
203 tracker_id: 1
203 tracker_id: 1
204 assigned_to_id:
204 assigned_to_id:
205 author_id: 2
205 author_id: 2
206 status_id: 5
206 status_id: 5
207 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
207 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
208 due_date:
208 due_date:
209 root_id: 11
209 root_id: 11
210 lft: 1
210 lft: 1
211 rgt: 2
211 rgt: 2
212 issues_012:
212 issues_012:
213 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
213 created_on: <%= 3.days.ago.to_s(:db) %>
214 project_id: 1
214 project_id: 1
215 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
215 updated_on: <%= 1.day.ago.to_s(:db) %>
216 priority_id: 5
216 priority_id: 5
217 subject: Closed issue on a locked version
217 subject: Closed issue on a locked version
218 id: 12
218 id: 12
219 fixed_version_id: 2
219 fixed_version_id: 2
220 category_id: 1
220 category_id: 1
221 description:
221 description:
222 tracker_id: 1
222 tracker_id: 1
223 assigned_to_id:
223 assigned_to_id:
224 author_id: 3
224 author_id: 3
225 status_id: 5
225 status_id: 5
226 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
226 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
227 due_date:
227 due_date:
228 root_id: 12
228 root_id: 12
229 lft: 1
229 lft: 1
230 rgt: 2
230 rgt: 2
231 issues_013:
231 issues_013:
232 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
232 created_on: <%= 5.days.ago.to_s(:db) %>
233 project_id: 3
233 project_id: 3
234 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
234 updated_on: <%= 2.days.ago.to_s(:db) %>
235 priority_id: 4
235 priority_id: 4
236 subject: Subproject issue two
236 subject: Subproject issue two
237 id: 13
237 id: 13
238 fixed_version_id:
238 fixed_version_id:
239 category_id:
239 category_id:
240 description: This is a second issue on a cookbook subproject
240 description: This is a second issue on a cookbook subproject
241 tracker_id: 1
241 tracker_id: 1
242 assigned_to_id:
242 assigned_to_id:
243 author_id: 2
243 author_id: 2
244 status_id: 1
244 status_id: 1
245 root_id: 13
245 root_id: 13
246 lft: 1
246 lft: 1
247 rgt: 2
247 rgt: 2
248 issues_014:
248 issues_014:
249 id: 14
249 id: 14
250 created_on: <%= 15.days.ago.to_date.to_s(:db) %>
250 created_on: <%= 15.days.ago.to_s(:db) %>
251 project_id: 3
251 project_id: 3
252 updated_on: <%= 15.days.ago.to_date.to_s(:db) %>
252 updated_on: <%= 15.days.ago.to_s(:db) %>
253 priority_id: 5
253 priority_id: 5
254 subject: Private issue on public project
254 subject: Private issue on public project
255 fixed_version_id:
255 fixed_version_id:
256 category_id:
256 category_id:
257 description: This is a private issue
257 description: This is a private issue
258 tracker_id: 1
258 tracker_id: 1
259 assigned_to_id:
259 assigned_to_id:
260 author_id: 2
260 author_id: 2
261 status_id: 1
261 status_id: 1
262 is_private: true
262 is_private: true
263 root_id: 14
263 root_id: 14
264 lft: 1
264 lft: 1
265 rgt: 2
265 rgt: 2
@@ -1,1027 +1,1058
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class 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 test 'object_daddy creation' do
37 test 'object_daddy creation' do
38 User.generate!(:firstname => 'Testing connection')
38 User.generate!(:firstname => 'Testing connection')
39 User.generate!(:firstname => 'Testing connection')
39 User.generate!(:firstname => 'Testing connection')
40 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
40 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
41 end
41 end
42
42
43 def test_truth
43 def test_truth
44 assert_kind_of User, @jsmith
44 assert_kind_of User, @jsmith
45 end
45 end
46
46
47 def test_mail_should_be_stripped
47 def test_mail_should_be_stripped
48 u = User.new
48 u = User.new
49 u.mail = " foo@bar.com "
49 u.mail = " foo@bar.com "
50 assert_equal "foo@bar.com", u.mail
50 assert_equal "foo@bar.com", u.mail
51 end
51 end
52
52
53 def test_mail_validation
53 def test_mail_validation
54 u = User.new
54 u = User.new
55 u.mail = ''
55 u.mail = ''
56 assert !u.valid?
56 assert !u.valid?
57 assert_include I18n.translate('activerecord.errors.messages.blank'), u.errors[:mail]
57 assert_include I18n.translate('activerecord.errors.messages.blank'), u.errors[:mail]
58 end
58 end
59
59
60 def test_login_length_validation
60 def test_login_length_validation
61 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
61 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
62 user.login = "x" * (User::LOGIN_LENGTH_LIMIT+1)
62 user.login = "x" * (User::LOGIN_LENGTH_LIMIT+1)
63 assert !user.valid?
63 assert !user.valid?
64
64
65 user.login = "x" * (User::LOGIN_LENGTH_LIMIT)
65 user.login = "x" * (User::LOGIN_LENGTH_LIMIT)
66 assert user.valid?
66 assert user.valid?
67 assert user.save
67 assert user.save
68 end
68 end
69
69
70 def test_create
70 def test_create
71 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
71 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
72
72
73 user.login = "jsmith"
73 user.login = "jsmith"
74 user.password, user.password_confirmation = "password", "password"
74 user.password, user.password_confirmation = "password", "password"
75 # login uniqueness
75 # login uniqueness
76 assert !user.save
76 assert !user.save
77 assert_equal 1, user.errors.count
77 assert_equal 1, user.errors.count
78
78
79 user.login = "newuser"
79 user.login = "newuser"
80 user.password, user.password_confirmation = "passwd", "password"
80 user.password, user.password_confirmation = "passwd", "password"
81 # password confirmation
81 # password confirmation
82 assert !user.save
82 assert !user.save
83 assert_equal 1, user.errors.count
83 assert_equal 1, user.errors.count
84
84
85 user.password, user.password_confirmation = "password", "password"
85 user.password, user.password_confirmation = "password", "password"
86 assert user.save
86 assert user.save
87 end
87 end
88
88
89 context "User#before_create" do
89 context "User#before_create" do
90 should "set the mail_notification to the default Setting" do
90 should "set the mail_notification to the default Setting" do
91 @user1 = User.generate!
91 @user1 = User.generate!
92 assert_equal 'only_my_events', @user1.mail_notification
92 assert_equal 'only_my_events', @user1.mail_notification
93
93
94 with_settings :default_notification_option => 'all' do
94 with_settings :default_notification_option => 'all' do
95 @user2 = User.generate!
95 @user2 = User.generate!
96 assert_equal 'all', @user2.mail_notification
96 assert_equal 'all', @user2.mail_notification
97 end
97 end
98 end
98 end
99 end
99 end
100
100
101 context "User.login" do
101 context "User.login" do
102 should "be case-insensitive." do
102 should "be case-insensitive." do
103 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
103 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
104 u.login = 'newuser'
104 u.login = 'newuser'
105 u.password, u.password_confirmation = "password", "password"
105 u.password, u.password_confirmation = "password", "password"
106 assert u.save
106 assert u.save
107
107
108 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
108 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
109 u.login = 'NewUser'
109 u.login = 'NewUser'
110 u.password, u.password_confirmation = "password", "password"
110 u.password, u.password_confirmation = "password", "password"
111 assert !u.save
111 assert !u.save
112 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:login]
112 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:login]
113 end
113 end
114 end
114 end
115
115
116 def test_mail_uniqueness_should_not_be_case_sensitive
116 def test_mail_uniqueness_should_not_be_case_sensitive
117 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
117 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
118 u.login = 'newuser1'
118 u.login = 'newuser1'
119 u.password, u.password_confirmation = "password", "password"
119 u.password, u.password_confirmation = "password", "password"
120 assert u.save
120 assert u.save
121
121
122 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
122 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
123 u.login = 'newuser2'
123 u.login = 'newuser2'
124 u.password, u.password_confirmation = "password", "password"
124 u.password, u.password_confirmation = "password", "password"
125 assert !u.save
125 assert !u.save
126 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:mail]
126 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:mail]
127 end
127 end
128
128
129 def test_update
129 def test_update
130 assert_equal "admin", @admin.login
130 assert_equal "admin", @admin.login
131 @admin.login = "john"
131 @admin.login = "john"
132 assert @admin.save, @admin.errors.full_messages.join("; ")
132 assert @admin.save, @admin.errors.full_messages.join("; ")
133 @admin.reload
133 @admin.reload
134 assert_equal "john", @admin.login
134 assert_equal "john", @admin.login
135 end
135 end
136
136
137 def test_update_should_not_fail_for_legacy_user_with_different_case_logins
137 def test_update_should_not_fail_for_legacy_user_with_different_case_logins
138 u1 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser1@somenet.foo")
138 u1 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser1@somenet.foo")
139 u1.login = 'newuser1'
139 u1.login = 'newuser1'
140 assert u1.save
140 assert u1.save
141
141
142 u2 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser2@somenet.foo")
142 u2 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser2@somenet.foo")
143 u2.login = 'newuser1'
143 u2.login = 'newuser1'
144 assert u2.save(:validate => false)
144 assert u2.save(:validate => false)
145
145
146 user = User.find(u2.id)
146 user = User.find(u2.id)
147 user.firstname = "firstname"
147 user.firstname = "firstname"
148 assert user.save, "Save failed"
148 assert user.save, "Save failed"
149 end
149 end
150
150
151 def test_destroy_should_delete_members_and_roles
151 def test_destroy_should_delete_members_and_roles
152 members = Member.find_all_by_user_id(2)
152 members = Member.find_all_by_user_id(2)
153 ms = members.size
153 ms = members.size
154 rs = members.collect(&:roles).flatten.size
154 rs = members.collect(&:roles).flatten.size
155
155
156 assert_difference 'Member.count', - ms do
156 assert_difference 'Member.count', - ms do
157 assert_difference 'MemberRole.count', - rs do
157 assert_difference 'MemberRole.count', - rs do
158 User.find(2).destroy
158 User.find(2).destroy
159 end
159 end
160 end
160 end
161
161
162 assert_nil User.find_by_id(2)
162 assert_nil User.find_by_id(2)
163 assert Member.find_all_by_user_id(2).empty?
163 assert Member.find_all_by_user_id(2).empty?
164 end
164 end
165
165
166 def test_destroy_should_update_attachments
166 def test_destroy_should_update_attachments
167 attachment = Attachment.create!(:container => Project.find(1),
167 attachment = Attachment.create!(:container => Project.find(1),
168 :file => uploaded_test_file("testfile.txt", "text/plain"),
168 :file => uploaded_test_file("testfile.txt", "text/plain"),
169 :author_id => 2)
169 :author_id => 2)
170
170
171 User.find(2).destroy
171 User.find(2).destroy
172 assert_nil User.find_by_id(2)
172 assert_nil User.find_by_id(2)
173 assert_equal User.anonymous, attachment.reload.author
173 assert_equal User.anonymous, attachment.reload.author
174 end
174 end
175
175
176 def test_destroy_should_update_comments
176 def test_destroy_should_update_comments
177 comment = Comment.create!(
177 comment = Comment.create!(
178 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
178 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
179 :author => User.find(2),
179 :author => User.find(2),
180 :comments => 'foo'
180 :comments => 'foo'
181 )
181 )
182
182
183 User.find(2).destroy
183 User.find(2).destroy
184 assert_nil User.find_by_id(2)
184 assert_nil User.find_by_id(2)
185 assert_equal User.anonymous, comment.reload.author
185 assert_equal User.anonymous, comment.reload.author
186 end
186 end
187
187
188 def test_destroy_should_update_issues
188 def test_destroy_should_update_issues
189 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
189 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
190
190
191 User.find(2).destroy
191 User.find(2).destroy
192 assert_nil User.find_by_id(2)
192 assert_nil User.find_by_id(2)
193 assert_equal User.anonymous, issue.reload.author
193 assert_equal User.anonymous, issue.reload.author
194 end
194 end
195
195
196 def test_destroy_should_unassign_issues
196 def test_destroy_should_unassign_issues
197 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
197 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
198
198
199 User.find(2).destroy
199 User.find(2).destroy
200 assert_nil User.find_by_id(2)
200 assert_nil User.find_by_id(2)
201 assert_nil issue.reload.assigned_to
201 assert_nil issue.reload.assigned_to
202 end
202 end
203
203
204 def test_destroy_should_update_journals
204 def test_destroy_should_update_journals
205 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
205 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
206 issue.init_journal(User.find(2), "update")
206 issue.init_journal(User.find(2), "update")
207 issue.save!
207 issue.save!
208
208
209 User.find(2).destroy
209 User.find(2).destroy
210 assert_nil User.find_by_id(2)
210 assert_nil User.find_by_id(2)
211 assert_equal User.anonymous, issue.journals.first.reload.user
211 assert_equal User.anonymous, issue.journals.first.reload.user
212 end
212 end
213
213
214 def test_destroy_should_update_journal_details_old_value
214 def test_destroy_should_update_journal_details_old_value
215 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
215 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
216 issue.init_journal(User.find(1), "update")
216 issue.init_journal(User.find(1), "update")
217 issue.assigned_to_id = nil
217 issue.assigned_to_id = nil
218 assert_difference 'JournalDetail.count' do
218 assert_difference 'JournalDetail.count' do
219 issue.save!
219 issue.save!
220 end
220 end
221 journal_detail = JournalDetail.first(:order => 'id DESC')
221 journal_detail = JournalDetail.first(:order => 'id DESC')
222 assert_equal '2', journal_detail.old_value
222 assert_equal '2', journal_detail.old_value
223
223
224 User.find(2).destroy
224 User.find(2).destroy
225 assert_nil User.find_by_id(2)
225 assert_nil User.find_by_id(2)
226 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
226 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
227 end
227 end
228
228
229 def test_destroy_should_update_journal_details_value
229 def test_destroy_should_update_journal_details_value
230 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
230 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
231 issue.init_journal(User.find(1), "update")
231 issue.init_journal(User.find(1), "update")
232 issue.assigned_to_id = 2
232 issue.assigned_to_id = 2
233 assert_difference 'JournalDetail.count' do
233 assert_difference 'JournalDetail.count' do
234 issue.save!
234 issue.save!
235 end
235 end
236 journal_detail = JournalDetail.first(:order => 'id DESC')
236 journal_detail = JournalDetail.first(:order => 'id DESC')
237 assert_equal '2', journal_detail.value
237 assert_equal '2', journal_detail.value
238
238
239 User.find(2).destroy
239 User.find(2).destroy
240 assert_nil User.find_by_id(2)
240 assert_nil User.find_by_id(2)
241 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
241 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
242 end
242 end
243
243
244 def test_destroy_should_update_messages
244 def test_destroy_should_update_messages
245 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
245 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
246 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
246 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
247
247
248 User.find(2).destroy
248 User.find(2).destroy
249 assert_nil User.find_by_id(2)
249 assert_nil User.find_by_id(2)
250 assert_equal User.anonymous, message.reload.author
250 assert_equal User.anonymous, message.reload.author
251 end
251 end
252
252
253 def test_destroy_should_update_news
253 def test_destroy_should_update_news
254 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
254 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
255
255
256 User.find(2).destroy
256 User.find(2).destroy
257 assert_nil User.find_by_id(2)
257 assert_nil User.find_by_id(2)
258 assert_equal User.anonymous, news.reload.author
258 assert_equal User.anonymous, news.reload.author
259 end
259 end
260
260
261 def test_destroy_should_delete_private_queries
261 def test_destroy_should_delete_private_queries
262 query = Query.new(:name => 'foo', :is_public => false)
262 query = Query.new(:name => 'foo', :is_public => false)
263 query.project_id = 1
263 query.project_id = 1
264 query.user_id = 2
264 query.user_id = 2
265 query.save!
265 query.save!
266
266
267 User.find(2).destroy
267 User.find(2).destroy
268 assert_nil User.find_by_id(2)
268 assert_nil User.find_by_id(2)
269 assert_nil Query.find_by_id(query.id)
269 assert_nil Query.find_by_id(query.id)
270 end
270 end
271
271
272 def test_destroy_should_update_public_queries
272 def test_destroy_should_update_public_queries
273 query = Query.new(:name => 'foo', :is_public => true)
273 query = Query.new(:name => 'foo', :is_public => true)
274 query.project_id = 1
274 query.project_id = 1
275 query.user_id = 2
275 query.user_id = 2
276 query.save!
276 query.save!
277
277
278 User.find(2).destroy
278 User.find(2).destroy
279 assert_nil User.find_by_id(2)
279 assert_nil User.find_by_id(2)
280 assert_equal User.anonymous, query.reload.user
280 assert_equal User.anonymous, query.reload.user
281 end
281 end
282
282
283 def test_destroy_should_update_time_entries
283 def test_destroy_should_update_time_entries
284 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
284 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
285 entry.project_id = 1
285 entry.project_id = 1
286 entry.user_id = 2
286 entry.user_id = 2
287 entry.save!
287 entry.save!
288
288
289 User.find(2).destroy
289 User.find(2).destroy
290 assert_nil User.find_by_id(2)
290 assert_nil User.find_by_id(2)
291 assert_equal User.anonymous, entry.reload.user
291 assert_equal User.anonymous, entry.reload.user
292 end
292 end
293
293
294 def test_destroy_should_delete_tokens
294 def test_destroy_should_delete_tokens
295 token = Token.create!(:user_id => 2, :value => 'foo')
295 token = Token.create!(:user_id => 2, :value => 'foo')
296
296
297 User.find(2).destroy
297 User.find(2).destroy
298 assert_nil User.find_by_id(2)
298 assert_nil User.find_by_id(2)
299 assert_nil Token.find_by_id(token.id)
299 assert_nil Token.find_by_id(token.id)
300 end
300 end
301
301
302 def test_destroy_should_delete_watchers
302 def test_destroy_should_delete_watchers
303 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
303 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
304 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
304 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
305
305
306 User.find(2).destroy
306 User.find(2).destroy
307 assert_nil User.find_by_id(2)
307 assert_nil User.find_by_id(2)
308 assert_nil Watcher.find_by_id(watcher.id)
308 assert_nil Watcher.find_by_id(watcher.id)
309 end
309 end
310
310
311 def test_destroy_should_update_wiki_contents
311 def test_destroy_should_update_wiki_contents
312 wiki_content = WikiContent.create!(
312 wiki_content = WikiContent.create!(
313 :text => 'foo',
313 :text => 'foo',
314 :author_id => 2,
314 :author_id => 2,
315 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
315 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
316 )
316 )
317 wiki_content.text = 'bar'
317 wiki_content.text = 'bar'
318 assert_difference 'WikiContent::Version.count' do
318 assert_difference 'WikiContent::Version.count' do
319 wiki_content.save!
319 wiki_content.save!
320 end
320 end
321
321
322 User.find(2).destroy
322 User.find(2).destroy
323 assert_nil User.find_by_id(2)
323 assert_nil User.find_by_id(2)
324 assert_equal User.anonymous, wiki_content.reload.author
324 assert_equal User.anonymous, wiki_content.reload.author
325 wiki_content.versions.each do |version|
325 wiki_content.versions.each do |version|
326 assert_equal User.anonymous, version.reload.author
326 assert_equal User.anonymous, version.reload.author
327 end
327 end
328 end
328 end
329
329
330 def test_destroy_should_nullify_issue_categories
330 def test_destroy_should_nullify_issue_categories
331 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
331 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
332
332
333 User.find(2).destroy
333 User.find(2).destroy
334 assert_nil User.find_by_id(2)
334 assert_nil User.find_by_id(2)
335 assert_nil category.reload.assigned_to_id
335 assert_nil category.reload.assigned_to_id
336 end
336 end
337
337
338 def test_destroy_should_nullify_changesets
338 def test_destroy_should_nullify_changesets
339 changeset = Changeset.create!(
339 changeset = Changeset.create!(
340 :repository => Repository::Subversion.create!(
340 :repository => Repository::Subversion.create!(
341 :project_id => 1,
341 :project_id => 1,
342 :url => 'file:///tmp',
342 :url => 'file:///tmp',
343 :identifier => 'tmp'
343 :identifier => 'tmp'
344 ),
344 ),
345 :revision => '12',
345 :revision => '12',
346 :committed_on => Time.now,
346 :committed_on => Time.now,
347 :committer => 'jsmith'
347 :committer => 'jsmith'
348 )
348 )
349 assert_equal 2, changeset.user_id
349 assert_equal 2, changeset.user_id
350
350
351 User.find(2).destroy
351 User.find(2).destroy
352 assert_nil User.find_by_id(2)
352 assert_nil User.find_by_id(2)
353 assert_nil changeset.reload.user_id
353 assert_nil changeset.reload.user_id
354 end
354 end
355
355
356 def test_anonymous_user_should_not_be_destroyable
356 def test_anonymous_user_should_not_be_destroyable
357 assert_no_difference 'User.count' do
357 assert_no_difference 'User.count' do
358 assert_equal false, User.anonymous.destroy
358 assert_equal false, User.anonymous.destroy
359 end
359 end
360 end
360 end
361
361
362 def test_validate_login_presence
362 def test_validate_login_presence
363 @admin.login = ""
363 @admin.login = ""
364 assert !@admin.save
364 assert !@admin.save
365 assert_equal 1, @admin.errors.count
365 assert_equal 1, @admin.errors.count
366 end
366 end
367
367
368 def test_validate_mail_notification_inclusion
368 def test_validate_mail_notification_inclusion
369 u = User.new
369 u = User.new
370 u.mail_notification = 'foo'
370 u.mail_notification = 'foo'
371 u.save
371 u.save
372 assert_not_nil u.errors[:mail_notification]
372 assert_not_nil u.errors[:mail_notification]
373 end
373 end
374
374
375 context "User#try_to_login" do
375 context "User#try_to_login" do
376 should "fall-back to case-insensitive if user login is not found as-typed." do
376 should "fall-back to case-insensitive if user login is not found as-typed." do
377 user = User.try_to_login("AdMin", "admin")
377 user = User.try_to_login("AdMin", "admin")
378 assert_kind_of User, user
378 assert_kind_of User, user
379 assert_equal "admin", user.login
379 assert_equal "admin", user.login
380 end
380 end
381
381
382 should "select the exact matching user first" do
382 should "select the exact matching user first" do
383 case_sensitive_user = User.generate! do |user|
383 case_sensitive_user = User.generate! do |user|
384 user.password = "admin"
384 user.password = "admin"
385 end
385 end
386 # bypass validations to make it appear like existing data
386 # bypass validations to make it appear like existing data
387 case_sensitive_user.update_attribute(:login, 'ADMIN')
387 case_sensitive_user.update_attribute(:login, 'ADMIN')
388
388
389 user = User.try_to_login("ADMIN", "admin")
389 user = User.try_to_login("ADMIN", "admin")
390 assert_kind_of User, user
390 assert_kind_of User, user
391 assert_equal "ADMIN", user.login
391 assert_equal "ADMIN", user.login
392
392
393 end
393 end
394 end
394 end
395
395
396 def test_password
396 def test_password
397 user = User.try_to_login("admin", "admin")
397 user = User.try_to_login("admin", "admin")
398 assert_kind_of User, user
398 assert_kind_of User, user
399 assert_equal "admin", user.login
399 assert_equal "admin", user.login
400 user.password = "hello"
400 user.password = "hello"
401 assert user.save
401 assert user.save
402
402
403 user = User.try_to_login("admin", "hello")
403 user = User.try_to_login("admin", "hello")
404 assert_kind_of User, user
404 assert_kind_of User, user
405 assert_equal "admin", user.login
405 assert_equal "admin", user.login
406 end
406 end
407
407
408 def test_validate_password_length
408 def test_validate_password_length
409 with_settings :password_min_length => '100' do
409 with_settings :password_min_length => '100' do
410 user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo")
410 user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo")
411 user.login = "newuser100"
411 user.login = "newuser100"
412 user.password, user.password_confirmation = "password100", "password100"
412 user.password, user.password_confirmation = "password100", "password100"
413 assert !user.save
413 assert !user.save
414 assert_equal 1, user.errors.count
414 assert_equal 1, user.errors.count
415 end
415 end
416 end
416 end
417
417
418 def test_name_format
418 def test_name_format
419 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
419 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
420 with_settings :user_format => :firstname_lastname do
420 with_settings :user_format => :firstname_lastname do
421 assert_equal 'John Smith', @jsmith.reload.name
421 assert_equal 'John Smith', @jsmith.reload.name
422 end
422 end
423 with_settings :user_format => :username do
423 with_settings :user_format => :username do
424 assert_equal 'jsmith', @jsmith.reload.name
424 assert_equal 'jsmith', @jsmith.reload.name
425 end
425 end
426 end
426 end
427
427
428 def test_today_should_return_the_day_according_to_user_time_zone
429 preference = User.find(1).pref
430 date = Date.new(2012, 05, 15)
431 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
432 Date.stubs(:today).returns(date)
433 Time.stubs(:now).returns(time)
434
435 preference.update_attribute :time_zone, 'Baku' # UTC+4
436 assert_equal '2012-05-16', User.find(1).today.to_s
437
438 preference.update_attribute :time_zone, 'La Paz' # UTC-4
439 assert_equal '2012-05-15', User.find(1).today.to_s
440
441 preference.update_attribute :time_zone, ''
442 assert_equal '2012-05-15', User.find(1).today.to_s
443 end
444
445 def test_time_to_date_should_return_the_date_according_to_user_time_zone
446 preference = User.find(1).pref
447 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
448
449 preference.update_attribute :time_zone, 'Baku' # UTC+4
450 assert_equal '2012-05-16', User.find(1).time_to_date(time).to_s
451
452 preference.update_attribute :time_zone, 'La Paz' # UTC-4
453 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
454
455 preference.update_attribute :time_zone, ''
456 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
457 end
458
428 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
459 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
429 with_settings :user_format => 'lastname_coma_firstname' do
460 with_settings :user_format => 'lastname_coma_firstname' do
430 assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement
461 assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement
431 end
462 end
432 end
463 end
433
464
434 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
465 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
435 with_settings :user_format => 'lastname_firstname' do
466 with_settings :user_format => 'lastname_firstname' do
436 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors')
467 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors')
437 end
468 end
438 end
469 end
439
470
440 def test_fields_for_order_statement_with_blank_format_should_return_default
471 def test_fields_for_order_statement_with_blank_format_should_return_default
441 with_settings :user_format => '' do
472 with_settings :user_format => '' do
442 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
473 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
443 end
474 end
444 end
475 end
445
476
446 def test_fields_for_order_statement_with_invalid_format_should_return_default
477 def test_fields_for_order_statement_with_invalid_format_should_return_default
447 with_settings :user_format => 'foo' do
478 with_settings :user_format => 'foo' do
448 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
479 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
449 end
480 end
450 end
481 end
451
482
452 def test_lock
483 def test_lock
453 user = User.try_to_login("jsmith", "jsmith")
484 user = User.try_to_login("jsmith", "jsmith")
454 assert_equal @jsmith, user
485 assert_equal @jsmith, user
455
486
456 @jsmith.status = User::STATUS_LOCKED
487 @jsmith.status = User::STATUS_LOCKED
457 assert @jsmith.save
488 assert @jsmith.save
458
489
459 user = User.try_to_login("jsmith", "jsmith")
490 user = User.try_to_login("jsmith", "jsmith")
460 assert_equal nil, user
491 assert_equal nil, user
461 end
492 end
462
493
463 context ".try_to_login" do
494 context ".try_to_login" do
464 context "with good credentials" do
495 context "with good credentials" do
465 should "return the user" do
496 should "return the user" do
466 user = User.try_to_login("admin", "admin")
497 user = User.try_to_login("admin", "admin")
467 assert_kind_of User, user
498 assert_kind_of User, user
468 assert_equal "admin", user.login
499 assert_equal "admin", user.login
469 end
500 end
470 end
501 end
471
502
472 context "with wrong credentials" do
503 context "with wrong credentials" do
473 should "return nil" do
504 should "return nil" do
474 assert_nil User.try_to_login("admin", "foo")
505 assert_nil User.try_to_login("admin", "foo")
475 end
506 end
476 end
507 end
477 end
508 end
478
509
479 if ldap_configured?
510 if ldap_configured?
480 context "#try_to_login using LDAP" do
511 context "#try_to_login using LDAP" do
481 context "with failed connection to the LDAP server" do
512 context "with failed connection to the LDAP server" do
482 should "return nil" do
513 should "return nil" do
483 @auth_source = AuthSourceLdap.find(1)
514 @auth_source = AuthSourceLdap.find(1)
484 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
515 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
485
516
486 assert_equal nil, User.try_to_login('edavis', 'wrong')
517 assert_equal nil, User.try_to_login('edavis', 'wrong')
487 end
518 end
488 end
519 end
489
520
490 context "with an unsuccessful authentication" do
521 context "with an unsuccessful authentication" do
491 should "return nil" do
522 should "return nil" do
492 assert_equal nil, User.try_to_login('edavis', 'wrong')
523 assert_equal nil, User.try_to_login('edavis', 'wrong')
493 end
524 end
494 end
525 end
495
526
496 context "binding with user's account" do
527 context "binding with user's account" do
497 setup do
528 setup do
498 @auth_source = AuthSourceLdap.find(1)
529 @auth_source = AuthSourceLdap.find(1)
499 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
530 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
500 @auth_source.account_password = ''
531 @auth_source.account_password = ''
501 @auth_source.save!
532 @auth_source.save!
502
533
503 @ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1)
534 @ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1)
504 @ldap_user.login = 'example1'
535 @ldap_user.login = 'example1'
505 @ldap_user.save!
536 @ldap_user.save!
506 end
537 end
507
538
508 context "with a successful authentication" do
539 context "with a successful authentication" do
509 should "return the user" do
540 should "return the user" do
510 assert_equal @ldap_user, User.try_to_login('example1', '123456')
541 assert_equal @ldap_user, User.try_to_login('example1', '123456')
511 end
542 end
512 end
543 end
513
544
514 context "with an unsuccessful authentication" do
545 context "with an unsuccessful authentication" do
515 should "return nil" do
546 should "return nil" do
516 assert_nil User.try_to_login('example1', '11111')
547 assert_nil User.try_to_login('example1', '11111')
517 end
548 end
518 end
549 end
519 end
550 end
520
551
521 context "on the fly registration" do
552 context "on the fly registration" do
522 setup do
553 setup do
523 @auth_source = AuthSourceLdap.find(1)
554 @auth_source = AuthSourceLdap.find(1)
524 @auth_source.update_attribute :onthefly_register, true
555 @auth_source.update_attribute :onthefly_register, true
525 end
556 end
526
557
527 context "with a successful authentication" do
558 context "with a successful authentication" do
528 should "create a new user account if it doesn't exist" do
559 should "create a new user account if it doesn't exist" do
529 assert_difference('User.count') do
560 assert_difference('User.count') do
530 user = User.try_to_login('edavis', '123456')
561 user = User.try_to_login('edavis', '123456')
531 assert !user.admin?
562 assert !user.admin?
532 end
563 end
533 end
564 end
534
565
535 should "retrieve existing user" do
566 should "retrieve existing user" do
536 user = User.try_to_login('edavis', '123456')
567 user = User.try_to_login('edavis', '123456')
537 user.admin = true
568 user.admin = true
538 user.save!
569 user.save!
539
570
540 assert_no_difference('User.count') do
571 assert_no_difference('User.count') do
541 user = User.try_to_login('edavis', '123456')
572 user = User.try_to_login('edavis', '123456')
542 assert user.admin?
573 assert user.admin?
543 end
574 end
544 end
575 end
545 end
576 end
546
577
547 context "binding with user's account" do
578 context "binding with user's account" do
548 setup do
579 setup do
549 @auth_source = AuthSourceLdap.find(1)
580 @auth_source = AuthSourceLdap.find(1)
550 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
581 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
551 @auth_source.account_password = ''
582 @auth_source.account_password = ''
552 @auth_source.save!
583 @auth_source.save!
553 end
584 end
554
585
555 context "with a successful authentication" do
586 context "with a successful authentication" do
556 should "create a new user account if it doesn't exist" do
587 should "create a new user account if it doesn't exist" do
557 assert_difference('User.count') do
588 assert_difference('User.count') do
558 user = User.try_to_login('example1', '123456')
589 user = User.try_to_login('example1', '123456')
559 assert_kind_of User, user
590 assert_kind_of User, user
560 end
591 end
561 end
592 end
562 end
593 end
563
594
564 context "with an unsuccessful authentication" do
595 context "with an unsuccessful authentication" do
565 should "return nil" do
596 should "return nil" do
566 assert_nil User.try_to_login('example1', '11111')
597 assert_nil User.try_to_login('example1', '11111')
567 end
598 end
568 end
599 end
569 end
600 end
570 end
601 end
571 end
602 end
572
603
573 else
604 else
574 puts "Skipping LDAP tests."
605 puts "Skipping LDAP tests."
575 end
606 end
576
607
577 def test_create_anonymous
608 def test_create_anonymous
578 AnonymousUser.delete_all
609 AnonymousUser.delete_all
579 anon = User.anonymous
610 anon = User.anonymous
580 assert !anon.new_record?
611 assert !anon.new_record?
581 assert_kind_of AnonymousUser, anon
612 assert_kind_of AnonymousUser, anon
582 end
613 end
583
614
584 def test_ensure_single_anonymous_user
615 def test_ensure_single_anonymous_user
585 AnonymousUser.delete_all
616 AnonymousUser.delete_all
586 anon1 = User.anonymous
617 anon1 = User.anonymous
587 assert !anon1.new_record?
618 assert !anon1.new_record?
588 assert_kind_of AnonymousUser, anon1
619 assert_kind_of AnonymousUser, anon1
589 anon2 = AnonymousUser.create(
620 anon2 = AnonymousUser.create(
590 :lastname => 'Anonymous', :firstname => '',
621 :lastname => 'Anonymous', :firstname => '',
591 :mail => '', :login => '', :status => 0)
622 :mail => '', :login => '', :status => 0)
592 assert_equal 1, anon2.errors.count
623 assert_equal 1, anon2.errors.count
593 end
624 end
594
625
595 def test_rss_key
626 def test_rss_key
596 assert_nil @jsmith.rss_token
627 assert_nil @jsmith.rss_token
597 key = @jsmith.rss_key
628 key = @jsmith.rss_key
598 assert_equal 40, key.length
629 assert_equal 40, key.length
599
630
600 @jsmith.reload
631 @jsmith.reload
601 assert_equal key, @jsmith.rss_key
632 assert_equal key, @jsmith.rss_key
602 end
633 end
603
634
604 def test_rss_key_should_not_be_generated_twice
635 def test_rss_key_should_not_be_generated_twice
605 assert_difference 'Token.count', 1 do
636 assert_difference 'Token.count', 1 do
606 key1 = @jsmith.rss_key
637 key1 = @jsmith.rss_key
607 key2 = @jsmith.rss_key
638 key2 = @jsmith.rss_key
608 assert_equal key1, key2
639 assert_equal key1, key2
609 end
640 end
610 end
641 end
611
642
612 def test_api_key_should_not_be_generated_twice
643 def test_api_key_should_not_be_generated_twice
613 assert_difference 'Token.count', 1 do
644 assert_difference 'Token.count', 1 do
614 key1 = @jsmith.api_key
645 key1 = @jsmith.api_key
615 key2 = @jsmith.api_key
646 key2 = @jsmith.api_key
616 assert_equal key1, key2
647 assert_equal key1, key2
617 end
648 end
618 end
649 end
619
650
620 context "User#api_key" do
651 context "User#api_key" do
621 should "generate a new one if the user doesn't have one" do
652 should "generate a new one if the user doesn't have one" do
622 user = User.generate!(:api_token => nil)
653 user = User.generate!(:api_token => nil)
623 assert_nil user.api_token
654 assert_nil user.api_token
624
655
625 key = user.api_key
656 key = user.api_key
626 assert_equal 40, key.length
657 assert_equal 40, key.length
627 user.reload
658 user.reload
628 assert_equal key, user.api_key
659 assert_equal key, user.api_key
629 end
660 end
630
661
631 should "return the existing api token value" do
662 should "return the existing api token value" do
632 user = User.generate!
663 user = User.generate!
633 token = Token.create!(:action => 'api')
664 token = Token.create!(:action => 'api')
634 user.api_token = token
665 user.api_token = token
635 assert user.save
666 assert user.save
636
667
637 assert_equal token.value, user.api_key
668 assert_equal token.value, user.api_key
638 end
669 end
639 end
670 end
640
671
641 context "User#find_by_api_key" do
672 context "User#find_by_api_key" do
642 should "return nil if no matching key is found" do
673 should "return nil if no matching key is found" do
643 assert_nil User.find_by_api_key('zzzzzzzzz')
674 assert_nil User.find_by_api_key('zzzzzzzzz')
644 end
675 end
645
676
646 should "return nil if the key is found for an inactive user" do
677 should "return nil if the key is found for an inactive user" do
647 user = User.generate!
678 user = User.generate!
648 user.status = User::STATUS_LOCKED
679 user.status = User::STATUS_LOCKED
649 token = Token.create!(:action => 'api')
680 token = Token.create!(:action => 'api')
650 user.api_token = token
681 user.api_token = token
651 user.save
682 user.save
652
683
653 assert_nil User.find_by_api_key(token.value)
684 assert_nil User.find_by_api_key(token.value)
654 end
685 end
655
686
656 should "return the user if the key is found for an active user" do
687 should "return the user if the key is found for an active user" do
657 user = User.generate!
688 user = User.generate!
658 token = Token.create!(:action => 'api')
689 token = Token.create!(:action => 'api')
659 user.api_token = token
690 user.api_token = token
660 user.save
691 user.save
661
692
662 assert_equal user, User.find_by_api_key(token.value)
693 assert_equal user, User.find_by_api_key(token.value)
663 end
694 end
664 end
695 end
665
696
666 def test_default_admin_account_changed_should_return_false_if_account_was_not_changed
697 def test_default_admin_account_changed_should_return_false_if_account_was_not_changed
667 user = User.find_by_login("admin")
698 user = User.find_by_login("admin")
668 user.password = "admin"
699 user.password = "admin"
669 user.save!
700 user.save!
670
701
671 assert_equal false, User.default_admin_account_changed?
702 assert_equal false, User.default_admin_account_changed?
672 end
703 end
673
704
674 def test_default_admin_account_changed_should_return_true_if_password_was_changed
705 def test_default_admin_account_changed_should_return_true_if_password_was_changed
675 user = User.find_by_login("admin")
706 user = User.find_by_login("admin")
676 user.password = "newpassword"
707 user.password = "newpassword"
677 user.save!
708 user.save!
678
709
679 assert_equal true, User.default_admin_account_changed?
710 assert_equal true, User.default_admin_account_changed?
680 end
711 end
681
712
682 def test_default_admin_account_changed_should_return_true_if_account_is_disabled
713 def test_default_admin_account_changed_should_return_true_if_account_is_disabled
683 user = User.find_by_login("admin")
714 user = User.find_by_login("admin")
684 user.password = "admin"
715 user.password = "admin"
685 user.status = User::STATUS_LOCKED
716 user.status = User::STATUS_LOCKED
686 user.save!
717 user.save!
687
718
688 assert_equal true, User.default_admin_account_changed?
719 assert_equal true, User.default_admin_account_changed?
689 end
720 end
690
721
691 def test_default_admin_account_changed_should_return_true_if_account_does_not_exist
722 def test_default_admin_account_changed_should_return_true_if_account_does_not_exist
692 user = User.find_by_login("admin")
723 user = User.find_by_login("admin")
693 user.destroy
724 user.destroy
694
725
695 assert_equal true, User.default_admin_account_changed?
726 assert_equal true, User.default_admin_account_changed?
696 end
727 end
697
728
698 def test_roles_for_project
729 def test_roles_for_project
699 # user with a role
730 # user with a role
700 roles = @jsmith.roles_for_project(Project.find(1))
731 roles = @jsmith.roles_for_project(Project.find(1))
701 assert_kind_of Role, roles.first
732 assert_kind_of Role, roles.first
702 assert_equal "Manager", roles.first.name
733 assert_equal "Manager", roles.first.name
703
734
704 # user with no role
735 # user with no role
705 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
736 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
706 end
737 end
707
738
708 def test_projects_by_role_for_user_with_role
739 def test_projects_by_role_for_user_with_role
709 user = User.find(2)
740 user = User.find(2)
710 assert_kind_of Hash, user.projects_by_role
741 assert_kind_of Hash, user.projects_by_role
711 assert_equal 2, user.projects_by_role.size
742 assert_equal 2, user.projects_by_role.size
712 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
743 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
713 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
744 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
714 end
745 end
715
746
716 def test_projects_by_role_for_user_with_no_role
747 def test_projects_by_role_for_user_with_no_role
717 user = User.generate!
748 user = User.generate!
718 assert_equal({}, user.projects_by_role)
749 assert_equal({}, user.projects_by_role)
719 end
750 end
720
751
721 def test_projects_by_role_for_anonymous
752 def test_projects_by_role_for_anonymous
722 assert_equal({}, User.anonymous.projects_by_role)
753 assert_equal({}, User.anonymous.projects_by_role)
723 end
754 end
724
755
725 def test_valid_notification_options
756 def test_valid_notification_options
726 # without memberships
757 # without memberships
727 assert_equal 5, User.find(7).valid_notification_options.size
758 assert_equal 5, User.find(7).valid_notification_options.size
728 # with memberships
759 # with memberships
729 assert_equal 6, User.find(2).valid_notification_options.size
760 assert_equal 6, User.find(2).valid_notification_options.size
730 end
761 end
731
762
732 def test_valid_notification_options_class_method
763 def test_valid_notification_options_class_method
733 assert_equal 5, User.valid_notification_options.size
764 assert_equal 5, User.valid_notification_options.size
734 assert_equal 5, User.valid_notification_options(User.find(7)).size
765 assert_equal 5, User.valid_notification_options(User.find(7)).size
735 assert_equal 6, User.valid_notification_options(User.find(2)).size
766 assert_equal 6, User.valid_notification_options(User.find(2)).size
736 end
767 end
737
768
738 def test_mail_notification_all
769 def test_mail_notification_all
739 @jsmith.mail_notification = 'all'
770 @jsmith.mail_notification = 'all'
740 @jsmith.notified_project_ids = []
771 @jsmith.notified_project_ids = []
741 @jsmith.save
772 @jsmith.save
742 @jsmith.reload
773 @jsmith.reload
743 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
774 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
744 end
775 end
745
776
746 def test_mail_notification_selected
777 def test_mail_notification_selected
747 @jsmith.mail_notification = 'selected'
778 @jsmith.mail_notification = 'selected'
748 @jsmith.notified_project_ids = [1]
779 @jsmith.notified_project_ids = [1]
749 @jsmith.save
780 @jsmith.save
750 @jsmith.reload
781 @jsmith.reload
751 assert Project.find(1).recipients.include?(@jsmith.mail)
782 assert Project.find(1).recipients.include?(@jsmith.mail)
752 end
783 end
753
784
754 def test_mail_notification_only_my_events
785 def test_mail_notification_only_my_events
755 @jsmith.mail_notification = 'only_my_events'
786 @jsmith.mail_notification = 'only_my_events'
756 @jsmith.notified_project_ids = []
787 @jsmith.notified_project_ids = []
757 @jsmith.save
788 @jsmith.save
758 @jsmith.reload
789 @jsmith.reload
759 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
790 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
760 end
791 end
761
792
762 def test_comments_sorting_preference
793 def test_comments_sorting_preference
763 assert !@jsmith.wants_comments_in_reverse_order?
794 assert !@jsmith.wants_comments_in_reverse_order?
764 @jsmith.pref.comments_sorting = 'asc'
795 @jsmith.pref.comments_sorting = 'asc'
765 assert !@jsmith.wants_comments_in_reverse_order?
796 assert !@jsmith.wants_comments_in_reverse_order?
766 @jsmith.pref.comments_sorting = 'desc'
797 @jsmith.pref.comments_sorting = 'desc'
767 assert @jsmith.wants_comments_in_reverse_order?
798 assert @jsmith.wants_comments_in_reverse_order?
768 end
799 end
769
800
770 def test_find_by_mail_should_be_case_insensitive
801 def test_find_by_mail_should_be_case_insensitive
771 u = User.find_by_mail('JSmith@somenet.foo')
802 u = User.find_by_mail('JSmith@somenet.foo')
772 assert_not_nil u
803 assert_not_nil u
773 assert_equal 'jsmith@somenet.foo', u.mail
804 assert_equal 'jsmith@somenet.foo', u.mail
774 end
805 end
775
806
776 def test_random_password
807 def test_random_password
777 u = User.new
808 u = User.new
778 u.random_password
809 u.random_password
779 assert !u.password.blank?
810 assert !u.password.blank?
780 assert !u.password_confirmation.blank?
811 assert !u.password_confirmation.blank?
781 end
812 end
782
813
783 context "#change_password_allowed?" do
814 context "#change_password_allowed?" do
784 should "be allowed if no auth source is set" do
815 should "be allowed if no auth source is set" do
785 user = User.generate!
816 user = User.generate!
786 assert user.change_password_allowed?
817 assert user.change_password_allowed?
787 end
818 end
788
819
789 should "delegate to the auth source" do
820 should "delegate to the auth source" do
790 user = User.generate!
821 user = User.generate!
791
822
792 allowed_auth_source = AuthSource.generate!
823 allowed_auth_source = AuthSource.generate!
793 def allowed_auth_source.allow_password_changes?; true; end
824 def allowed_auth_source.allow_password_changes?; true; end
794
825
795 denied_auth_source = AuthSource.generate!
826 denied_auth_source = AuthSource.generate!
796 def denied_auth_source.allow_password_changes?; false; end
827 def denied_auth_source.allow_password_changes?; false; end
797
828
798 assert user.change_password_allowed?
829 assert user.change_password_allowed?
799
830
800 user.auth_source = allowed_auth_source
831 user.auth_source = allowed_auth_source
801 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
832 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
802
833
803 user.auth_source = denied_auth_source
834 user.auth_source = denied_auth_source
804 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
835 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
805 end
836 end
806 end
837 end
807
838
808 def test_own_account_deletable_should_be_true_with_unsubscrive_enabled
839 def test_own_account_deletable_should_be_true_with_unsubscrive_enabled
809 with_settings :unsubscribe => '1' do
840 with_settings :unsubscribe => '1' do
810 assert_equal true, User.find(2).own_account_deletable?
841 assert_equal true, User.find(2).own_account_deletable?
811 end
842 end
812 end
843 end
813
844
814 def test_own_account_deletable_should_be_false_with_unsubscrive_disabled
845 def test_own_account_deletable_should_be_false_with_unsubscrive_disabled
815 with_settings :unsubscribe => '0' do
846 with_settings :unsubscribe => '0' do
816 assert_equal false, User.find(2).own_account_deletable?
847 assert_equal false, User.find(2).own_account_deletable?
817 end
848 end
818 end
849 end
819
850
820 def test_own_account_deletable_should_be_false_for_a_single_admin
851 def test_own_account_deletable_should_be_false_for_a_single_admin
821 User.delete_all(["admin = ? AND id <> ?", true, 1])
852 User.delete_all(["admin = ? AND id <> ?", true, 1])
822
853
823 with_settings :unsubscribe => '1' do
854 with_settings :unsubscribe => '1' do
824 assert_equal false, User.find(1).own_account_deletable?
855 assert_equal false, User.find(1).own_account_deletable?
825 end
856 end
826 end
857 end
827
858
828 def test_own_account_deletable_should_be_true_for_an_admin_if_other_admin_exists
859 def test_own_account_deletable_should_be_true_for_an_admin_if_other_admin_exists
829 User.generate! do |user|
860 User.generate! do |user|
830 user.admin = true
861 user.admin = true
831 end
862 end
832
863
833 with_settings :unsubscribe => '1' do
864 with_settings :unsubscribe => '1' do
834 assert_equal true, User.find(1).own_account_deletable?
865 assert_equal true, User.find(1).own_account_deletable?
835 end
866 end
836 end
867 end
837
868
838 context "#allowed_to?" do
869 context "#allowed_to?" do
839 context "with a unique project" do
870 context "with a unique project" do
840 should "return false if project is archived" do
871 should "return false if project is archived" do
841 project = Project.find(1)
872 project = Project.find(1)
842 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
873 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
843 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
874 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
844 end
875 end
845
876
846 should "return false if related module is disabled" do
877 should "return false if related module is disabled" do
847 project = Project.find(1)
878 project = Project.find(1)
848 project.enabled_module_names = ["issue_tracking"]
879 project.enabled_module_names = ["issue_tracking"]
849 assert @admin.allowed_to?(:add_issues, project)
880 assert @admin.allowed_to?(:add_issues, project)
850 assert ! @admin.allowed_to?(:view_wiki_pages, project)
881 assert ! @admin.allowed_to?(:view_wiki_pages, project)
851 end
882 end
852
883
853 should "authorize nearly everything for admin users" do
884 should "authorize nearly everything for admin users" do
854 project = Project.find(1)
885 project = Project.find(1)
855 assert ! @admin.member_of?(project)
886 assert ! @admin.member_of?(project)
856 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
887 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
857 assert @admin.allowed_to?(p.to_sym, project)
888 assert @admin.allowed_to?(p.to_sym, project)
858 end
889 end
859 end
890 end
860
891
861 should "authorize normal users depending on their roles" do
892 should "authorize normal users depending on their roles" do
862 project = Project.find(1)
893 project = Project.find(1)
863 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
894 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
864 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
895 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
865 end
896 end
866 end
897 end
867
898
868 context "with multiple projects" do
899 context "with multiple projects" do
869 should "return false if array is empty" do
900 should "return false if array is empty" do
870 assert ! @admin.allowed_to?(:view_project, [])
901 assert ! @admin.allowed_to?(:view_project, [])
871 end
902 end
872
903
873 should "return true only if user has permission on all these projects" do
904 should "return true only if user has permission on all these projects" do
874 assert @admin.allowed_to?(:view_project, Project.all)
905 assert @admin.allowed_to?(:view_project, Project.all)
875 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
906 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
876 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
907 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
877 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
908 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
878 end
909 end
879
910
880 should "behave correctly with arrays of 1 project" do
911 should "behave correctly with arrays of 1 project" do
881 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
912 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
882 end
913 end
883 end
914 end
884
915
885 context "with options[:global]" do
916 context "with options[:global]" do
886 should "authorize if user has at least one role that has this permission" do
917 should "authorize if user has at least one role that has this permission" do
887 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
918 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
888 @anonymous = User.find(6)
919 @anonymous = User.find(6)
889 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
920 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
890 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
921 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
891 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
922 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
892 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
923 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
893 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
924 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
894 end
925 end
895 end
926 end
896 end
927 end
897
928
898 context "User#notify_about?" do
929 context "User#notify_about?" do
899 context "Issues" do
930 context "Issues" do
900 setup do
931 setup do
901 @project = Project.find(1)
932 @project = Project.find(1)
902 @author = User.generate!
933 @author = User.generate!
903 @assignee = User.generate!
934 @assignee = User.generate!
904 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
935 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
905 end
936 end
906
937
907 should "be true for a user with :all" do
938 should "be true for a user with :all" do
908 @author.update_attribute(:mail_notification, 'all')
939 @author.update_attribute(:mail_notification, 'all')
909 assert @author.notify_about?(@issue)
940 assert @author.notify_about?(@issue)
910 end
941 end
911
942
912 should "be false for a user with :none" do
943 should "be false for a user with :none" do
913 @author.update_attribute(:mail_notification, 'none')
944 @author.update_attribute(:mail_notification, 'none')
914 assert ! @author.notify_about?(@issue)
945 assert ! @author.notify_about?(@issue)
915 end
946 end
916
947
917 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
948 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
918 @user = User.generate!(:mail_notification => 'only_my_events')
949 @user = User.generate!(:mail_notification => 'only_my_events')
919 Member.create!(:user => @user, :project => @project, :role_ids => [1])
950 Member.create!(:user => @user, :project => @project, :role_ids => [1])
920 assert ! @user.notify_about?(@issue)
951 assert ! @user.notify_about?(@issue)
921 end
952 end
922
953
923 should "be true for a user with :only_my_events and is the author" do
954 should "be true for a user with :only_my_events and is the author" do
924 @author.update_attribute(:mail_notification, 'only_my_events')
955 @author.update_attribute(:mail_notification, 'only_my_events')
925 assert @author.notify_about?(@issue)
956 assert @author.notify_about?(@issue)
926 end
957 end
927
958
928 should "be true for a user with :only_my_events and is the assignee" do
959 should "be true for a user with :only_my_events and is the assignee" do
929 @assignee.update_attribute(:mail_notification, 'only_my_events')
960 @assignee.update_attribute(:mail_notification, 'only_my_events')
930 assert @assignee.notify_about?(@issue)
961 assert @assignee.notify_about?(@issue)
931 end
962 end
932
963
933 should "be true for a user with :only_assigned and is the assignee" do
964 should "be true for a user with :only_assigned and is the assignee" do
934 @assignee.update_attribute(:mail_notification, 'only_assigned')
965 @assignee.update_attribute(:mail_notification, 'only_assigned')
935 assert @assignee.notify_about?(@issue)
966 assert @assignee.notify_about?(@issue)
936 end
967 end
937
968
938 should "be false for a user with :only_assigned and is not the assignee" do
969 should "be false for a user with :only_assigned and is not the assignee" do
939 @author.update_attribute(:mail_notification, 'only_assigned')
970 @author.update_attribute(:mail_notification, 'only_assigned')
940 assert ! @author.notify_about?(@issue)
971 assert ! @author.notify_about?(@issue)
941 end
972 end
942
973
943 should "be true for a user with :only_owner and is the author" do
974 should "be true for a user with :only_owner and is the author" do
944 @author.update_attribute(:mail_notification, 'only_owner')
975 @author.update_attribute(:mail_notification, 'only_owner')
945 assert @author.notify_about?(@issue)
976 assert @author.notify_about?(@issue)
946 end
977 end
947
978
948 should "be false for a user with :only_owner and is not the author" do
979 should "be false for a user with :only_owner and is not the author" do
949 @assignee.update_attribute(:mail_notification, 'only_owner')
980 @assignee.update_attribute(:mail_notification, 'only_owner')
950 assert ! @assignee.notify_about?(@issue)
981 assert ! @assignee.notify_about?(@issue)
951 end
982 end
952
983
953 should "be true for a user with :selected and is the author" do
984 should "be true for a user with :selected and is the author" do
954 @author.update_attribute(:mail_notification, 'selected')
985 @author.update_attribute(:mail_notification, 'selected')
955 assert @author.notify_about?(@issue)
986 assert @author.notify_about?(@issue)
956 end
987 end
957
988
958 should "be true for a user with :selected and is the assignee" do
989 should "be true for a user with :selected and is the assignee" do
959 @assignee.update_attribute(:mail_notification, 'selected')
990 @assignee.update_attribute(:mail_notification, 'selected')
960 assert @assignee.notify_about?(@issue)
991 assert @assignee.notify_about?(@issue)
961 end
992 end
962
993
963 should "be false for a user with :selected and is not the author or assignee" do
994 should "be false for a user with :selected and is not the author or assignee" do
964 @user = User.generate!(:mail_notification => 'selected')
995 @user = User.generate!(:mail_notification => 'selected')
965 Member.create!(:user => @user, :project => @project, :role_ids => [1])
996 Member.create!(:user => @user, :project => @project, :role_ids => [1])
966 assert ! @user.notify_about?(@issue)
997 assert ! @user.notify_about?(@issue)
967 end
998 end
968 end
999 end
969
1000
970 context "other events" do
1001 context "other events" do
971 should 'be added and tested'
1002 should 'be added and tested'
972 end
1003 end
973 end
1004 end
974
1005
975 def test_salt_unsalted_passwords
1006 def test_salt_unsalted_passwords
976 # Restore a user with an unsalted password
1007 # Restore a user with an unsalted password
977 user = User.find(1)
1008 user = User.find(1)
978 user.salt = nil
1009 user.salt = nil
979 user.hashed_password = User.hash_password("unsalted")
1010 user.hashed_password = User.hash_password("unsalted")
980 user.save!
1011 user.save!
981
1012
982 User.salt_unsalted_passwords!
1013 User.salt_unsalted_passwords!
983
1014
984 user.reload
1015 user.reload
985 # Salt added
1016 # Salt added
986 assert !user.salt.blank?
1017 assert !user.salt.blank?
987 # Password still valid
1018 # Password still valid
988 assert user.check_password?("unsalted")
1019 assert user.check_password?("unsalted")
989 assert_equal user, User.try_to_login(user.login, "unsalted")
1020 assert_equal user, User.try_to_login(user.login, "unsalted")
990 end
1021 end
991
1022
992 if Object.const_defined?(:OpenID)
1023 if Object.const_defined?(:OpenID)
993
1024
994 def test_setting_identity_url
1025 def test_setting_identity_url
995 normalized_open_id_url = 'http://example.com/'
1026 normalized_open_id_url = 'http://example.com/'
996 u = User.new( :identity_url => 'http://example.com/' )
1027 u = User.new( :identity_url => 'http://example.com/' )
997 assert_equal normalized_open_id_url, u.identity_url
1028 assert_equal normalized_open_id_url, u.identity_url
998 end
1029 end
999
1030
1000 def test_setting_identity_url_without_trailing_slash
1031 def test_setting_identity_url_without_trailing_slash
1001 normalized_open_id_url = 'http://example.com/'
1032 normalized_open_id_url = 'http://example.com/'
1002 u = User.new( :identity_url => 'http://example.com' )
1033 u = User.new( :identity_url => 'http://example.com' )
1003 assert_equal normalized_open_id_url, u.identity_url
1034 assert_equal normalized_open_id_url, u.identity_url
1004 end
1035 end
1005
1036
1006 def test_setting_identity_url_without_protocol
1037 def test_setting_identity_url_without_protocol
1007 normalized_open_id_url = 'http://example.com/'
1038 normalized_open_id_url = 'http://example.com/'
1008 u = User.new( :identity_url => 'example.com' )
1039 u = User.new( :identity_url => 'example.com' )
1009 assert_equal normalized_open_id_url, u.identity_url
1040 assert_equal normalized_open_id_url, u.identity_url
1010 end
1041 end
1011
1042
1012 def test_setting_blank_identity_url
1043 def test_setting_blank_identity_url
1013 u = User.new( :identity_url => 'example.com' )
1044 u = User.new( :identity_url => 'example.com' )
1014 u.identity_url = ''
1045 u.identity_url = ''
1015 assert u.identity_url.blank?
1046 assert u.identity_url.blank?
1016 end
1047 end
1017
1048
1018 def test_setting_invalid_identity_url
1049 def test_setting_invalid_identity_url
1019 u = User.new( :identity_url => 'this is not an openid url' )
1050 u = User.new( :identity_url => 'this is not an openid url' )
1020 assert u.identity_url.blank?
1051 assert u.identity_url.blank?
1021 end
1052 end
1022
1053
1023 else
1054 else
1024 puts "Skipping openid tests."
1055 puts "Skipping openid tests."
1025 end
1056 end
1026
1057
1027 end
1058 end
General Comments 0
You need to be logged in to leave comments. Login now