##// END OF EJS Templates
Template error when user's timezone isn't set and UTC timestamps are used (#1889)....
Jean-Philippe Lang -
r1799:8d6b32645c7b
parent child
Show More
@@ -1,558 +1,558
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20
21 21 module ApplicationHelper
22 22 include Redmine::WikiFormatting::Macros::Definitions
23 23
24 24 def current_role
25 25 @current_role ||= User.current.role_for_project(@project)
26 26 end
27 27
28 28 # Return true if user is authorized for controller/action, otherwise false
29 29 def authorize_for(controller, action)
30 30 User.current.allowed_to?({:controller => controller, :action => action}, @project)
31 31 end
32 32
33 33 # Display a link if user is authorized
34 34 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
35 35 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
36 36 end
37 37
38 38 # Display a link to remote if user is authorized
39 39 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
40 40 url = options[:url] || {}
41 41 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
42 42 end
43 43
44 44 # Display a link to user's account page
45 45 def link_to_user(user)
46 46 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
47 47 end
48 48
49 49 def link_to_issue(issue, options={})
50 50 options[:class] ||= ''
51 51 options[:class] << ' issue'
52 52 options[:class] << ' closed' if issue.closed?
53 53 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
54 54 end
55 55
56 56 # Generates a link to an attachment.
57 57 # Options:
58 58 # * :text - Link text (default to attachment filename)
59 59 # * :download - Force download (default: false)
60 60 def link_to_attachment(attachment, options={})
61 61 text = options.delete(:text) || attachment.filename
62 62 action = options.delete(:download) ? 'download' : 'show'
63 63
64 64 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
65 65 end
66 66
67 67 def toggle_link(name, id, options={})
68 68 onclick = "Element.toggle('#{id}'); "
69 69 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
70 70 onclick << "return false;"
71 71 link_to(name, "#", :onclick => onclick)
72 72 end
73 73
74 74 def image_to_function(name, function, html_options = {})
75 75 html_options.symbolize_keys!
76 76 tag(:input, html_options.merge({
77 77 :type => "image", :src => image_path(name),
78 78 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
79 79 }))
80 80 end
81 81
82 82 def prompt_to_remote(name, text, param, url, html_options = {})
83 83 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
84 84 link_to name, {}, html_options
85 85 end
86 86
87 87 def format_date(date)
88 88 return nil unless date
89 89 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
90 90 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
91 91 date.strftime(@date_format)
92 92 end
93 93
94 94 def format_time(time, include_date = true)
95 95 return nil unless time
96 96 time = time.to_time if time.is_a?(String)
97 97 zone = User.current.time_zone
98 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.utc_to_local : time)
98 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
99 99 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
100 100 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
101 101 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
102 102 end
103 103
104 104 # Truncates and returns the string as a single line
105 105 def truncate_single_line(string, *args)
106 106 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
107 107 end
108 108
109 109 def html_hours(text)
110 110 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
111 111 end
112 112
113 113 def authoring(created, author)
114 114 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
115 115 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
116 116 l(:label_added_time_by, author_tag, time_tag)
117 117 end
118 118
119 119 def l_or_humanize(s)
120 120 l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
121 121 end
122 122
123 123 def day_name(day)
124 124 l(:general_day_names).split(',')[day-1]
125 125 end
126 126
127 127 def month_name(month)
128 128 l(:actionview_datehelper_select_month_names).split(',')[month-1]
129 129 end
130 130
131 131 def syntax_highlight(name, content)
132 132 type = CodeRay::FileType[name]
133 133 type ? CodeRay.scan(content, type).html : h(content)
134 134 end
135 135
136 136 def to_path_param(path)
137 137 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
138 138 end
139 139
140 140 def pagination_links_full(paginator, count=nil, options={})
141 141 page_param = options.delete(:page_param) || :page
142 142 url_param = params.dup
143 143 # don't reuse params if filters are present
144 144 url_param.clear if url_param.has_key?(:set_filter)
145 145
146 146 html = ''
147 147 html << link_to_remote(('&#171; ' + l(:label_previous)),
148 148 {:update => 'content',
149 149 :url => url_param.merge(page_param => paginator.current.previous),
150 150 :complete => 'window.scrollTo(0,0)'},
151 151 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
152 152
153 153 html << (pagination_links_each(paginator, options) do |n|
154 154 link_to_remote(n.to_s,
155 155 {:url => {:params => url_param.merge(page_param => n)},
156 156 :update => 'content',
157 157 :complete => 'window.scrollTo(0,0)'},
158 158 {:href => url_for(:params => url_param.merge(page_param => n))})
159 159 end || '')
160 160
161 161 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
162 162 {:update => 'content',
163 163 :url => url_param.merge(page_param => paginator.current.next),
164 164 :complete => 'window.scrollTo(0,0)'},
165 165 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
166 166
167 167 unless count.nil?
168 168 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
169 169 end
170 170
171 171 html
172 172 end
173 173
174 174 def per_page_links(selected=nil)
175 175 url_param = params.dup
176 176 url_param.clear if url_param.has_key?(:set_filter)
177 177
178 178 links = Setting.per_page_options_array.collect do |n|
179 179 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
180 180 {:href => url_for(url_param.merge(:per_page => n))})
181 181 end
182 182 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
183 183 end
184 184
185 185 def breadcrumb(*args)
186 186 elements = args.flatten
187 187 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
188 188 end
189 189
190 190 def html_title(*args)
191 191 if args.empty?
192 192 title = []
193 193 title << @project.name if @project
194 194 title += @html_title if @html_title
195 195 title << Setting.app_title
196 196 title.compact.join(' - ')
197 197 else
198 198 @html_title ||= []
199 199 @html_title += args
200 200 end
201 201 end
202 202
203 203 def accesskey(s)
204 204 Redmine::AccessKeys.key_for s
205 205 end
206 206
207 207 # Formats text according to system settings.
208 208 # 2 ways to call this method:
209 209 # * with a String: textilizable(text, options)
210 210 # * with an object and one of its attribute: textilizable(issue, :description, options)
211 211 def textilizable(*args)
212 212 options = args.last.is_a?(Hash) ? args.pop : {}
213 213 case args.size
214 214 when 1
215 215 obj = options[:object]
216 216 text = args.shift
217 217 when 2
218 218 obj = args.shift
219 219 text = obj.send(args.shift).to_s
220 220 else
221 221 raise ArgumentError, 'invalid arguments to textilizable'
222 222 end
223 223 return '' if text.blank?
224 224
225 225 only_path = options.delete(:only_path) == false ? false : true
226 226
227 227 # when using an image link, try to use an attachment, if possible
228 228 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
229 229
230 230 if attachments
231 231 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
232 232 style = $1
233 233 filename = $6
234 234 rf = Regexp.new(filename, Regexp::IGNORECASE)
235 235 # search for the picture in attachments
236 236 if found = attachments.detect { |att| att.filename =~ rf }
237 237 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
238 238 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
239 239 alt = desc.blank? ? nil : "(#{desc})"
240 240 "!#{style}#{image_url}#{alt}!"
241 241 else
242 242 "!#{style}#{filename}!"
243 243 end
244 244 end
245 245 end
246 246
247 247 text = (Setting.text_formatting == 'textile') ?
248 248 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
249 249 simple_format(auto_link(h(text)))
250 250
251 251 # different methods for formatting wiki links
252 252 case options[:wiki_links]
253 253 when :local
254 254 # used for local links to html files
255 255 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
256 256 when :anchor
257 257 # used for single-file wiki export
258 258 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
259 259 else
260 260 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
261 261 end
262 262
263 263 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
264 264
265 265 # Wiki links
266 266 #
267 267 # Examples:
268 268 # [[mypage]]
269 269 # [[mypage|mytext]]
270 270 # wiki links can refer other project wikis, using project name or identifier:
271 271 # [[project:]] -> wiki starting page
272 272 # [[project:|mytext]]
273 273 # [[project:mypage]]
274 274 # [[project:mypage|mytext]]
275 275 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
276 276 link_project = project
277 277 esc, all, page, title = $1, $2, $3, $5
278 278 if esc.nil?
279 279 if page =~ /^([^\:]+)\:(.*)$/
280 280 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
281 281 page = $2
282 282 title ||= $1 if page.blank?
283 283 end
284 284
285 285 if link_project && link_project.wiki
286 286 # extract anchor
287 287 anchor = nil
288 288 if page =~ /^(.+?)\#(.+)$/
289 289 page, anchor = $1, $2
290 290 end
291 291 # check if page exists
292 292 wiki_page = link_project.wiki.find_page(page)
293 293 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
294 294 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
295 295 else
296 296 # project or wiki doesn't exist
297 297 title || page
298 298 end
299 299 else
300 300 all
301 301 end
302 302 end
303 303
304 304 # Redmine links
305 305 #
306 306 # Examples:
307 307 # Issues:
308 308 # #52 -> Link to issue #52
309 309 # Changesets:
310 310 # r52 -> Link to revision 52
311 311 # commit:a85130f -> Link to scmid starting with a85130f
312 312 # Documents:
313 313 # document#17 -> Link to document with id 17
314 314 # document:Greetings -> Link to the document with title "Greetings"
315 315 # document:"Some document" -> Link to the document with title "Some document"
316 316 # Versions:
317 317 # version#3 -> Link to version with id 3
318 318 # version:1.0.0 -> Link to version named "1.0.0"
319 319 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
320 320 # Attachments:
321 321 # attachment:file.zip -> Link to the attachment of the current object named file.zip
322 322 # Source files:
323 323 # source:some/file -> Link to the file located at /some/file in the project's repository
324 324 # source:some/file@52 -> Link to the file's revision 52
325 325 # source:some/file#L120 -> Link to line 120 of the file
326 326 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
327 327 # export:some/file -> Force the download of the file
328 328 # Forum messages:
329 329 # message#1218 -> Link to message with id 1218
330 330 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
331 331 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
332 332 link = nil
333 333 if esc.nil?
334 334 if prefix.nil? && sep == 'r'
335 335 if project && (changeset = project.changesets.find_by_revision(oid))
336 336 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
337 337 :class => 'changeset',
338 338 :title => truncate_single_line(changeset.comments, 100))
339 339 end
340 340 elsif sep == '#'
341 341 oid = oid.to_i
342 342 case prefix
343 343 when nil
344 344 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
345 345 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
346 346 :class => (issue.closed? ? 'issue closed' : 'issue'),
347 347 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
348 348 link = content_tag('del', link) if issue.closed?
349 349 end
350 350 when 'document'
351 351 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
352 352 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
353 353 :class => 'document'
354 354 end
355 355 when 'version'
356 356 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
357 357 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
358 358 :class => 'version'
359 359 end
360 360 when 'message'
361 361 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
362 362 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
363 363 :controller => 'messages',
364 364 :action => 'show',
365 365 :board_id => message.board,
366 366 :id => message.root,
367 367 :anchor => (message.parent ? "message-#{message.id}" : nil)},
368 368 :class => 'message'
369 369 end
370 370 end
371 371 elsif sep == ':'
372 372 # removes the double quotes if any
373 373 name = oid.gsub(%r{^"(.*)"$}, "\\1")
374 374 case prefix
375 375 when 'document'
376 376 if project && document = project.documents.find_by_title(name)
377 377 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
378 378 :class => 'document'
379 379 end
380 380 when 'version'
381 381 if project && version = project.versions.find_by_name(name)
382 382 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
383 383 :class => 'version'
384 384 end
385 385 when 'commit'
386 386 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
387 387 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
388 388 :class => 'changeset',
389 389 :title => truncate_single_line(changeset.comments, 100)
390 390 end
391 391 when 'source', 'export'
392 392 if project && project.repository
393 393 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
394 394 path, rev, anchor = $1, $3, $5
395 395 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
396 396 :path => to_path_param(path),
397 397 :rev => rev,
398 398 :anchor => anchor,
399 399 :format => (prefix == 'export' ? 'raw' : nil)},
400 400 :class => (prefix == 'export' ? 'source download' : 'source')
401 401 end
402 402 when 'attachment'
403 403 if attachments && attachment = attachments.detect {|a| a.filename == name }
404 404 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
405 405 :class => 'attachment'
406 406 end
407 407 end
408 408 end
409 409 end
410 410 leading + (link || "#{prefix}#{sep}#{oid}")
411 411 end
412 412
413 413 text
414 414 end
415 415
416 416 # Same as Rails' simple_format helper without using paragraphs
417 417 def simple_format_without_paragraph(text)
418 418 text.to_s.
419 419 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
420 420 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
421 421 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
422 422 end
423 423
424 424 def error_messages_for(object_name, options = {})
425 425 options = options.symbolize_keys
426 426 object = instance_variable_get("@#{object_name}")
427 427 if object && !object.errors.empty?
428 428 # build full_messages here with controller current language
429 429 full_messages = []
430 430 object.errors.each do |attr, msg|
431 431 next if msg.nil?
432 432 msg = msg.first if msg.is_a? Array
433 433 if attr == "base"
434 434 full_messages << l(msg)
435 435 else
436 436 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
437 437 end
438 438 end
439 439 # retrieve custom values error messages
440 440 if object.errors[:custom_values]
441 441 object.custom_values.each do |v|
442 442 v.errors.each do |attr, msg|
443 443 next if msg.nil?
444 444 msg = msg.first if msg.is_a? Array
445 445 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
446 446 end
447 447 end
448 448 end
449 449 content_tag("div",
450 450 content_tag(
451 451 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
452 452 ) +
453 453 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
454 454 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
455 455 )
456 456 else
457 457 ""
458 458 end
459 459 end
460 460
461 461 def lang_options_for_select(blank=true)
462 462 (blank ? [["(auto)", ""]] : []) +
463 463 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
464 464 end
465 465
466 466 def label_tag_for(name, option_tags = nil, options = {})
467 467 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
468 468 content_tag("label", label_text)
469 469 end
470 470
471 471 def labelled_tabular_form_for(name, object, options, &proc)
472 472 options[:html] ||= {}
473 473 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
474 474 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
475 475 end
476 476
477 477 def back_url_hidden_field_tag
478 478 back_url = params[:back_url] || request.env['HTTP_REFERER']
479 479 hidden_field_tag('back_url', back_url) unless back_url.blank?
480 480 end
481 481
482 482 def check_all_links(form_name)
483 483 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
484 484 " | " +
485 485 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
486 486 end
487 487
488 488 def progress_bar(pcts, options={})
489 489 pcts = [pcts, pcts] unless pcts.is_a?(Array)
490 490 pcts[1] = pcts[1] - pcts[0]
491 491 pcts << (100 - pcts[1] - pcts[0])
492 492 width = options[:width] || '100px;'
493 493 legend = options[:legend] || ''
494 494 content_tag('table',
495 495 content_tag('tr',
496 496 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
497 497 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
498 498 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
499 499 ), :class => 'progress', :style => "width: #{width};") +
500 500 content_tag('p', legend, :class => 'pourcent')
501 501 end
502 502
503 503 def context_menu_link(name, url, options={})
504 504 options[:class] ||= ''
505 505 if options.delete(:selected)
506 506 options[:class] << ' icon-checked disabled'
507 507 options[:disabled] = true
508 508 end
509 509 if options.delete(:disabled)
510 510 options.delete(:method)
511 511 options.delete(:confirm)
512 512 options.delete(:onclick)
513 513 options[:class] << ' disabled'
514 514 url = '#'
515 515 end
516 516 link_to name, url, options
517 517 end
518 518
519 519 def calendar_for(field_id)
520 520 include_calendar_headers_tags
521 521 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
522 522 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
523 523 end
524 524
525 525 def include_calendar_headers_tags
526 526 unless @calendar_headers_tags_included
527 527 @calendar_headers_tags_included = true
528 528 content_for :header_tags do
529 529 javascript_include_tag('calendar/calendar') +
530 530 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
531 531 javascript_include_tag('calendar/calendar-setup') +
532 532 stylesheet_link_tag('calendar')
533 533 end
534 534 end
535 535 end
536 536
537 537 def wikitoolbar_for(field_id)
538 538 return '' unless Setting.text_formatting == 'textile'
539 539
540 540 help_link = l(:setting_text_formatting) + ': ' +
541 541 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
542 542 :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
543 543
544 544 javascript_include_tag('jstoolbar/jstoolbar') +
545 545 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
546 546 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
547 547 end
548 548
549 549 def content_for(name, content = nil, &block)
550 550 @has_content ||= {}
551 551 @has_content[name] = true
552 552 super(name, content, &block)
553 553 end
554 554
555 555 def has_content?(name)
556 556 (@has_content && @has_content[name]) || false
557 557 end
558 558 end
@@ -1,294 +1,294
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 < ActiveRecord::Base
21 21
22 22 # Account statuses
23 23 STATUS_ANONYMOUS = 0
24 24 STATUS_ACTIVE = 1
25 25 STATUS_REGISTERED = 2
26 26 STATUS_LOCKED = 3
27 27
28 28 USER_FORMATS = {
29 29 :firstname_lastname => '#{firstname} #{lastname}',
30 30 :firstname => '#{firstname}',
31 31 :lastname_firstname => '#{lastname} #{firstname}',
32 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 33 :username => '#{login}'
34 34 }
35 35
36 36 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
37 37 has_many :members, :dependent => :delete_all
38 38 has_many :projects, :through => :memberships
39 39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
41 41 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
42 42 belongs_to :auth_source
43 43
44 44 acts_as_customizable
45 45
46 46 attr_accessor :password, :password_confirmation
47 47 attr_accessor :last_before_login_on
48 48 # Prevents unauthorized assignments
49 49 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
50 50
51 51 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
52 52 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
53 53 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
54 54 # Login must contain lettres, numbers, underscores only
55 55 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
56 56 validates_length_of :login, :maximum => 30
57 57 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
58 58 validates_length_of :firstname, :lastname, :maximum => 30
59 59 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
60 60 validates_length_of :mail, :maximum => 60, :allow_nil => true
61 61 validates_length_of :password, :minimum => 4, :allow_nil => true
62 62 validates_confirmation_of :password, :allow_nil => true
63 63
64 64 def before_create
65 65 self.mail_notification = false
66 66 true
67 67 end
68 68
69 69 def before_save
70 70 # update hashed_password if password was set
71 71 self.hashed_password = User.hash_password(self.password) if self.password
72 72 end
73 73
74 74 def self.active
75 75 with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
76 76 yield
77 77 end
78 78 end
79 79
80 80 def self.find_active(*args)
81 81 active do
82 82 find(*args)
83 83 end
84 84 end
85 85
86 86 # Returns the user that matches provided login and password, or nil
87 87 def self.try_to_login(login, password)
88 88 # Make sure no one can sign in with an empty password
89 89 return nil if password.to_s.empty?
90 90 user = find(:first, :conditions => ["login=?", login])
91 91 if user
92 92 # user is already in local database
93 93 return nil if !user.active?
94 94 if user.auth_source
95 95 # user has an external authentication method
96 96 return nil unless user.auth_source.authenticate(login, password)
97 97 else
98 98 # authentication with local password
99 99 return nil unless User.hash_password(password) == user.hashed_password
100 100 end
101 101 else
102 102 # user is not yet registered, try to authenticate with available sources
103 103 attrs = AuthSource.authenticate(login, password)
104 104 if attrs
105 105 user = new(*attrs)
106 106 user.login = login
107 107 user.language = Setting.default_language
108 108 if user.save
109 109 user.reload
110 110 logger.info("User '#{user.login}' created from the LDAP") if logger
111 111 end
112 112 end
113 113 end
114 114 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
115 115 user
116 116 rescue => text
117 117 raise text
118 118 end
119 119
120 120 # Return user's full name for display
121 121 def name(formatter = nil)
122 122 f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
123 123 eval '"' + f + '"'
124 124 end
125 125
126 126 def active?
127 127 self.status == STATUS_ACTIVE
128 128 end
129 129
130 130 def registered?
131 131 self.status == STATUS_REGISTERED
132 132 end
133 133
134 134 def locked?
135 135 self.status == STATUS_LOCKED
136 136 end
137 137
138 138 def check_password?(clear_password)
139 139 User.hash_password(clear_password) == self.hashed_password
140 140 end
141 141
142 142 def pref
143 143 self.preference ||= UserPreference.new(:user => self)
144 144 end
145 145
146 146 def time_zone
147 self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
147 @time_zone ||= (self.pref.time_zone.blank? ? nil : TimeZone[self.pref.time_zone])
148 148 end
149 149
150 150 def wants_comments_in_reverse_order?
151 151 self.pref[:comments_sorting] == 'desc'
152 152 end
153 153
154 154 # Return user's RSS key (a 40 chars long string), used to access feeds
155 155 def rss_key
156 156 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
157 157 token.value
158 158 end
159 159
160 160 # Return an array of project ids for which the user has explicitly turned mail notifications on
161 161 def notified_projects_ids
162 162 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
163 163 end
164 164
165 165 def notified_project_ids=(ids)
166 166 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
167 167 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
168 168 @notified_projects_ids = nil
169 169 notified_projects_ids
170 170 end
171 171
172 172 def self.find_by_rss_key(key)
173 173 token = Token.find_by_value(key)
174 174 token && token.user.active? ? token.user : nil
175 175 end
176 176
177 177 def self.find_by_autologin_key(key)
178 178 token = Token.find_by_action_and_value('autologin', key)
179 179 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
180 180 end
181 181
182 182 def <=>(user)
183 183 if user.nil?
184 184 -1
185 185 elsif lastname.to_s.downcase == user.lastname.to_s.downcase
186 186 firstname.to_s.downcase <=> user.firstname.to_s.downcase
187 187 else
188 188 lastname.to_s.downcase <=> user.lastname.to_s.downcase
189 189 end
190 190 end
191 191
192 192 def to_s
193 193 name
194 194 end
195 195
196 196 def logged?
197 197 true
198 198 end
199 199
200 200 def anonymous?
201 201 !logged?
202 202 end
203 203
204 204 # Return user's role for project
205 205 def role_for_project(project)
206 206 # No role on archived projects
207 207 return nil unless project && project.active?
208 208 if logged?
209 209 # Find project membership
210 210 membership = memberships.detect {|m| m.project_id == project.id}
211 211 if membership
212 212 membership.role
213 213 else
214 214 @role_non_member ||= Role.non_member
215 215 end
216 216 else
217 217 @role_anonymous ||= Role.anonymous
218 218 end
219 219 end
220 220
221 221 # Return true if the user is a member of project
222 222 def member_of?(project)
223 223 role_for_project(project).member?
224 224 end
225 225
226 226 # Return true if the user is allowed to do the specified action on project
227 227 # action can be:
228 228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
229 229 # * a permission Symbol (eg. :edit_project)
230 230 def allowed_to?(action, project, options={})
231 231 if project
232 232 # No action allowed on archived projects
233 233 return false unless project.active?
234 234 # No action allowed on disabled modules
235 235 return false unless project.allows_to?(action)
236 236 # Admin users are authorized for anything else
237 237 return true if admin?
238 238
239 239 role = role_for_project(project)
240 240 return false unless role
241 241 role.allowed_to?(action) && (project.is_public? || role.member?)
242 242
243 243 elsif options[:global]
244 244 # authorize if user has at least one role that has this permission
245 245 roles = memberships.collect {|m| m.role}.uniq
246 246 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
247 247 else
248 248 false
249 249 end
250 250 end
251 251
252 252 def self.current=(user)
253 253 @current_user = user
254 254 end
255 255
256 256 def self.current
257 257 @current_user ||= User.anonymous
258 258 end
259 259
260 260 def self.anonymous
261 261 anonymous_user = AnonymousUser.find(:first)
262 262 if anonymous_user.nil?
263 263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
264 264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
265 265 end
266 266 anonymous_user
267 267 end
268 268
269 269 private
270 270 # Return password digest
271 271 def self.hash_password(clear_password)
272 272 Digest::SHA1.hexdigest(clear_password || "")
273 273 end
274 274 end
275 275
276 276 class AnonymousUser < User
277 277
278 278 def validate_on_create
279 279 # There should be only one AnonymousUser in the database
280 280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
281 281 end
282 282
283 283 def available_custom_fields
284 284 []
285 285 end
286 286
287 287 # Overrides a few properties
288 288 def logged?; false end
289 289 def admin; false end
290 290 def name; 'Anonymous' end
291 291 def mail; nil end
292 292 def time_zone; nil end
293 293 def rss_key; nil end
294 294 end
@@ -1,354 +1,362
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../../test_helper'
19 19
20 20 class ApplicationHelperTest < HelperTestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 23 fixtures :projects, :roles, :enabled_modules,
24 24 :repositories, :changesets,
25 25 :trackers, :issue_statuses, :issues, :versions, :documents,
26 26 :wikis, :wiki_pages, :wiki_contents,
27 27 :boards, :messages
28 28
29 29 def setup
30 30 super
31 31 end
32 32
33 33 def test_auto_links
34 34 to_test = {
35 35 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
36 36 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
37 37 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
38 38 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
39 39 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
40 40 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
41 41 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
42 42 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
43 43 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
44 44 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
45 45 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
46 46 }
47 47 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
48 48 end
49 49
50 50 def test_auto_mailto
51 51 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
52 52 textilizable('test@foo.bar')
53 53 end
54 54
55 55 def test_inline_images
56 56 to_test = {
57 57 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
58 58 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
59 59 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
60 60 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
61 61 }
62 62 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
63 63 end
64 64
65 65 def test_textile_external_links
66 66 to_test = {
67 67 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
68 68 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
69 69 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
70 70 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
71 71 # no multiline link text
72 72 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
73 73 }
74 74 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
75 75 end
76 76
77 77 def test_redmine_links
78 78 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
79 79 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
80 80
81 81 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
82 82 :class => 'changeset', :title => 'My very first commit')
83 83
84 84 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
85 85 :class => 'document')
86 86
87 87 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
88 88 :class => 'version')
89 89
90 90 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
91 91
92 92 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
93 93 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
94 94
95 95 to_test = {
96 96 # tickets
97 97 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
98 98 # changesets
99 99 'r1' => changeset_link,
100 100 # documents
101 101 'document#1' => document_link,
102 102 'document:"Test document"' => document_link,
103 103 # versions
104 104 'version#2' => version_link,
105 105 'version:1.0' => version_link,
106 106 'version:"1.0"' => version_link,
107 107 # source
108 108 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
109 109 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
110 110 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
111 111 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
112 112 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
113 113 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
114 114 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
115 115 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
116 116 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
117 117 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
118 118 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
119 119 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
120 120 # message
121 121 'message#4' => link_to('Post 2', message_url, :class => 'message'),
122 122 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
123 123 # escaping
124 124 '!#3.' => '#3.',
125 125 '!r1' => 'r1',
126 126 '!document#1' => 'document#1',
127 127 '!document:"Test document"' => 'document:"Test document"',
128 128 '!version#2' => 'version#2',
129 129 '!version:1.0' => 'version:1.0',
130 130 '!version:"1.0"' => 'version:"1.0"',
131 131 '!source:/some/file' => 'source:/some/file',
132 132 # invalid expressions
133 133 'source:' => 'source:',
134 134 # url hash
135 135 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
136 136 }
137 137 @project = Project.find(1)
138 138 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
139 139 end
140 140
141 141 def test_wiki_links
142 142 to_test = {
143 143 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
144 144 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
145 145 # link with anchor
146 146 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
147 147 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
148 148 # page that doesn't exist
149 149 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
150 150 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
151 151 # link to another project wiki
152 152 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
153 153 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
154 154 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
155 155 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
156 156 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
157 157 # striked through link
158 158 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
159 159 # escaping
160 160 '![[Another page|Page]]' => '[[Another page|Page]]',
161 161 }
162 162 @project = Project.find(1)
163 163 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
164 164 end
165 165
166 166 def test_html_tags
167 167 to_test = {
168 168 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
169 169 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
170 170 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
171 171 # do not escape pre/code tags
172 172 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
173 173 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
174 174 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
175 175 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
176 176 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>"
177 177 }
178 178 to_test.each { |text, result| assert_equal result, textilizable(text) }
179 179 end
180 180
181 181 def test_allowed_html_tags
182 182 to_test = {
183 183 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
184 184 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
185 185 }
186 186 to_test.each { |text, result| assert_equal result, textilizable(text) }
187 187 end
188 188
189 189 def test_wiki_links_in_tables
190 190 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
191 191 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
192 192 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
193 193 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
194 194 }
195 195 @project = Project.find(1)
196 196 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
197 197 end
198 198
199 199 def test_text_formatting
200 200 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
201 201 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
202 202 }
203 203 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
204 204 end
205 205
206 206 def test_wiki_horizontal_rule
207 207 assert_equal '<hr />', textilizable('---')
208 208 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
209 209 end
210 210
211 211 def test_table_of_content
212 212 raw = <<-RAW
213 213 {{toc}}
214 214
215 215 h1. Title
216 216
217 217 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
218 218
219 219 h2. Subtitle
220 220
221 221 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
222 222
223 223 h2. Subtitle with %{color:red}red text%
224 224
225 225 h1. Another title
226 226
227 227 RAW
228 228
229 229 expected = '<ul class="toc">' +
230 230 '<li class="heading1"><a href="#Title">Title</a></li>' +
231 231 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
232 232 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
233 233 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
234 234 '</ul>'
235 235
236 236 assert textilizable(raw).gsub("\n", "").include?(expected)
237 237 end
238 238
239 239 def test_blockquote
240 240 # orig raw text
241 241 raw = <<-RAW
242 242 John said:
243 243 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
244 244 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
245 245 > * Donec odio lorem,
246 246 > * sagittis ac,
247 247 > * malesuada in,
248 248 > * adipiscing eu, dolor.
249 249 >
250 250 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
251 251 > Proin a tellus. Nam vel neque.
252 252
253 253 He's right.
254 254 RAW
255 255
256 256 # expected html
257 257 expected = <<-EXPECTED
258 258 <p>John said:</p>
259 259 <blockquote>
260 260 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
261 261 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
262 262 <ul>
263 263 <li>Donec odio lorem,</li>
264 264 <li>sagittis ac,</li>
265 265 <li>malesuada in,</li>
266 266 <li>adipiscing eu, dolor.</li>
267 267 </ul>
268 268 <blockquote>
269 269 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
270 270 </blockquote>
271 271 <p>Proin a tellus. Nam vel neque.</p>
272 272 </blockquote>
273 273 <p>He's right.</p>
274 274 EXPECTED
275 275
276 276 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
277 277 end
278 278
279 279 def test_table
280 280 raw = <<-RAW
281 281 This is a table with empty cells:
282 282
283 283 |cell11|cell12||
284 284 |cell21||cell23|
285 285 |cell31|cell32|cell33|
286 286 RAW
287 287
288 288 expected = <<-EXPECTED
289 289 <p>This is a table with empty cells:</p>
290 290
291 291 <table>
292 292 <tr><td>cell11</td><td>cell12</td><td></td></tr>
293 293 <tr><td>cell21</td><td></td><td>cell23</td></tr>
294 294 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
295 295 </table>
296 296 EXPECTED
297 297
298 298 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
299 299 end
300 300
301 301 def test_macro_hello_world
302 302 text = "{{hello_world}}"
303 303 assert textilizable(text).match(/Hello world!/)
304 304 # escaping
305 305 text = "!{{hello_world}}"
306 306 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
307 307 end
308 308
309 309 def test_macro_include
310 310 @project = Project.find(1)
311 311 # include a page of the current project wiki
312 312 text = "{{include(Another page)}}"
313 313 assert textilizable(text).match(/This is a link to a ticket/)
314 314
315 315 @project = nil
316 316 # include a page of a specific project wiki
317 317 text = "{{include(ecookbook:Another page)}}"
318 318 assert textilizable(text).match(/This is a link to a ticket/)
319 319
320 320 text = "{{include(ecookbook:)}}"
321 321 assert textilizable(text).match(/CookBook documentation/)
322 322
323 323 text = "{{include(unknowidentifier:somepage)}}"
324 324 assert textilizable(text).match(/Unknow project/)
325 325 end
326 326
327 327 def test_date_format_default
328 328 today = Date.today
329 329 Setting.date_format = ''
330 330 assert_equal l_date(today), format_date(today)
331 331 end
332 332
333 333 def test_date_format
334 334 today = Date.today
335 335 Setting.date_format = '%d %m %Y'
336 336 assert_equal today.strftime('%d %m %Y'), format_date(today)
337 337 end
338 338
339 339 def test_time_format_default
340 340 now = Time.now
341 341 Setting.date_format = ''
342 342 Setting.time_format = ''
343 343 assert_equal l_datetime(now), format_time(now)
344 344 assert_equal l_time(now), format_time(now, false)
345 345 end
346 346
347 347 def test_time_format
348 348 now = Time.now
349 349 Setting.date_format = '%d %m %Y'
350 350 Setting.time_format = '%H %M'
351 351 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
352 352 assert_equal now.strftime('%H %M'), format_time(now, false)
353 353 end
354
355 def test_utc_time_format
356 now = Time.now.utc
357 Setting.date_format = '%d %m %Y'
358 Setting.time_format = '%H %M'
359 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
360 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
361 end
354 362 end
General Comments 0
You need to be logged in to leave comments. Login now