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