##// END OF EJS Templates
Fixed that viewing the history of a wiki page with attachments raises an error (#12801)....
Jean-Philippe Lang -
r10927:d0ffc0575a65
parent child
Show More
@@ -1,1234 +1,1234
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 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 action = options.delete(:download) ? 'download' : 'show'
95 95 opt_only_path = {}
96 96 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
97 97 options.delete(:only_path)
98 98 link_to(h(text),
99 99 {:controller => 'attachments', :action => action,
100 100 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
101 101 options)
102 102 end
103 103
104 104 # Generates a link to a SCM revision
105 105 # Options:
106 106 # * :text - Link text (default to the formatted revision)
107 107 def link_to_revision(revision, repository, options={})
108 108 if repository.is_a?(Project)
109 109 repository = repository.repository
110 110 end
111 111 text = options.delete(:text) || format_revision(revision)
112 112 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
113 113 link_to(
114 114 h(text),
115 115 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
116 116 :title => l(:label_revision_id, format_revision(revision))
117 117 )
118 118 end
119 119
120 120 # Generates a link to a message
121 121 def link_to_message(message, options={}, html_options = nil)
122 122 link_to(
123 123 h(truncate(message.subject, :length => 60)),
124 124 { :controller => 'messages', :action => 'show',
125 125 :board_id => message.board_id,
126 126 :id => (message.parent_id || message.id),
127 127 :r => (message.parent_id && message.id),
128 128 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
129 129 }.merge(options),
130 130 html_options
131 131 )
132 132 end
133 133
134 134 # Generates a link to a project if active
135 135 # Examples:
136 136 #
137 137 # link_to_project(project) # => link to the specified project overview
138 138 # link_to_project(project, :action=>'settings') # => link to project settings
139 139 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
140 140 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
141 141 #
142 142 def link_to_project(project, options={}, html_options = nil)
143 143 if project.archived?
144 144 h(project)
145 145 else
146 146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
147 147 link_to(h(project), url, html_options)
148 148 end
149 149 end
150 150
151 151 def wiki_page_path(page, options={})
152 152 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
153 153 end
154 154
155 155 def thumbnail_tag(attachment)
156 156 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
157 157 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
158 158 :title => attachment.filename
159 159 end
160 160
161 161 def toggle_link(name, id, options={})
162 162 onclick = "$('##{id}').toggle(); "
163 163 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
164 164 onclick << "return false;"
165 165 link_to(name, "#", :onclick => onclick)
166 166 end
167 167
168 168 def image_to_function(name, function, html_options = {})
169 169 html_options.symbolize_keys!
170 170 tag(:input, html_options.merge({
171 171 :type => "image", :src => image_path(name),
172 172 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
173 173 }))
174 174 end
175 175
176 176 def format_activity_title(text)
177 177 h(truncate_single_line(text, :length => 100))
178 178 end
179 179
180 180 def format_activity_day(date)
181 181 date == User.current.today ? l(:label_today).titleize : format_date(date)
182 182 end
183 183
184 184 def format_activity_description(text)
185 185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
186 186 ).gsub(/[\r\n]+/, "<br />").html_safe
187 187 end
188 188
189 189 def format_version_name(version)
190 190 if version.project == @project
191 191 h(version)
192 192 else
193 193 h("#{version.project} - #{version}")
194 194 end
195 195 end
196 196
197 197 def due_date_distance_in_words(date)
198 198 if date
199 199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
200 200 end
201 201 end
202 202
203 203 # Renders a tree of projects as a nested set of unordered lists
204 204 # The given collection may be a subset of the whole project tree
205 205 # (eg. some intermediate nodes are private and can not be seen)
206 206 def render_project_nested_lists(projects)
207 207 s = ''
208 208 if projects.any?
209 209 ancestors = []
210 210 original_project = @project
211 211 projects.sort_by(&:lft).each do |project|
212 212 # set the project environment to please macros.
213 213 @project = project
214 214 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
215 215 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
216 216 else
217 217 ancestors.pop
218 218 s << "</li>"
219 219 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
220 220 ancestors.pop
221 221 s << "</ul></li>\n"
222 222 end
223 223 end
224 224 classes = (ancestors.empty? ? 'root' : 'child')
225 225 s << "<li class='#{classes}'><div class='#{classes}'>"
226 226 s << h(block_given? ? yield(project) : project.name)
227 227 s << "</div>\n"
228 228 ancestors << project
229 229 end
230 230 s << ("</li></ul>\n" * ancestors.size)
231 231 @project = original_project
232 232 end
233 233 s.html_safe
234 234 end
235 235
236 236 def render_page_hierarchy(pages, node=nil, options={})
237 237 content = ''
238 238 if pages[node]
239 239 content << "<ul class=\"pages-hierarchy\">\n"
240 240 pages[node].each do |page|
241 241 content << "<li>"
242 242 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
243 243 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
244 244 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
245 245 content << "</li>\n"
246 246 end
247 247 content << "</ul>\n"
248 248 end
249 249 content.html_safe
250 250 end
251 251
252 252 # Renders flash messages
253 253 def render_flash_messages
254 254 s = ''
255 255 flash.each do |k,v|
256 256 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
257 257 end
258 258 s.html_safe
259 259 end
260 260
261 261 # Renders tabs and their content
262 262 def render_tabs(tabs)
263 263 if tabs.any?
264 264 render :partial => 'common/tabs', :locals => {:tabs => tabs}
265 265 else
266 266 content_tag 'p', l(:label_no_data), :class => "nodata"
267 267 end
268 268 end
269 269
270 270 # Renders the project quick-jump box
271 271 def render_project_jump_box
272 272 return unless User.current.logged?
273 273 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
274 274 if projects.any?
275 275 options =
276 276 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
277 277 '<option value="" disabled="disabled">---</option>').html_safe
278 278
279 279 options << project_tree_options_for_select(projects, :selected => @project) do |p|
280 280 { :value => project_path(:id => p, :jump => current_menu_item) }
281 281 end
282 282
283 283 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
284 284 end
285 285 end
286 286
287 287 def project_tree_options_for_select(projects, options = {})
288 288 s = ''
289 289 project_tree(projects) do |project, level|
290 290 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
291 291 tag_options = {:value => project.id}
292 292 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
293 293 tag_options[:selected] = 'selected'
294 294 else
295 295 tag_options[:selected] = nil
296 296 end
297 297 tag_options.merge!(yield(project)) if block_given?
298 298 s << content_tag('option', name_prefix + h(project), tag_options)
299 299 end
300 300 s.html_safe
301 301 end
302 302
303 303 # Yields the given block for each project with its level in the tree
304 304 #
305 305 # Wrapper for Project#project_tree
306 306 def project_tree(projects, &block)
307 307 Project.project_tree(projects, &block)
308 308 end
309 309
310 310 def principals_check_box_tags(name, principals)
311 311 s = ''
312 312 principals.sort.each do |principal|
313 313 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
314 314 end
315 315 s.html_safe
316 316 end
317 317
318 318 # Returns a string for users/groups option tags
319 319 def principals_options_for_select(collection, selected=nil)
320 320 s = ''
321 321 if collection.include?(User.current)
322 322 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
323 323 end
324 324 groups = ''
325 325 collection.sort.each do |element|
326 326 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
327 327 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
328 328 end
329 329 unless groups.empty?
330 330 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
331 331 end
332 332 s.html_safe
333 333 end
334 334
335 335 # Options for the new membership projects combo-box
336 336 def options_for_membership_project_select(principal, projects)
337 337 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
338 338 options << project_tree_options_for_select(projects) do |p|
339 339 {:disabled => principal.projects.include?(p)}
340 340 end
341 341 options
342 342 end
343 343
344 344 # Truncates and returns the string as a single line
345 345 def truncate_single_line(string, *args)
346 346 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
347 347 end
348 348
349 349 # Truncates at line break after 250 characters or options[:length]
350 350 def truncate_lines(string, options={})
351 351 length = options[:length] || 250
352 352 if string.to_s =~ /\A(.{#{length}}.*?)$/m
353 353 "#{$1}..."
354 354 else
355 355 string
356 356 end
357 357 end
358 358
359 359 def anchor(text)
360 360 text.to_s.gsub(' ', '_')
361 361 end
362 362
363 363 def html_hours(text)
364 364 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
365 365 end
366 366
367 367 def authoring(created, author, options={})
368 368 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
369 369 end
370 370
371 371 def time_tag(time)
372 372 text = distance_of_time_in_words(Time.now, time)
373 373 if @project
374 374 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
375 375 else
376 376 content_tag('acronym', text, :title => format_time(time))
377 377 end
378 378 end
379 379
380 380 def syntax_highlight_lines(name, content)
381 381 lines = []
382 382 syntax_highlight(name, content).each_line { |line| lines << line }
383 383 lines
384 384 end
385 385
386 386 def syntax_highlight(name, content)
387 387 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
388 388 end
389 389
390 390 def to_path_param(path)
391 391 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
392 392 str.blank? ? nil : str
393 393 end
394 394
395 395 def reorder_links(name, url, method = :post)
396 396 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
397 397 url.merge({"#{name}[move_to]" => 'highest'}),
398 398 :method => method, :title => l(:label_sort_highest)) +
399 399 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
400 400 url.merge({"#{name}[move_to]" => 'higher'}),
401 401 :method => method, :title => l(:label_sort_higher)) +
402 402 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
403 403 url.merge({"#{name}[move_to]" => 'lower'}),
404 404 :method => method, :title => l(:label_sort_lower)) +
405 405 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
406 406 url.merge({"#{name}[move_to]" => 'lowest'}),
407 407 :method => method, :title => l(:label_sort_lowest))
408 408 end
409 409
410 410 def breadcrumb(*args)
411 411 elements = args.flatten
412 412 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
413 413 end
414 414
415 415 def other_formats_links(&block)
416 416 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
417 417 yield Redmine::Views::OtherFormatsBuilder.new(self)
418 418 concat('</p>'.html_safe)
419 419 end
420 420
421 421 def page_header_title
422 422 if @project.nil? || @project.new_record?
423 423 h(Setting.app_title)
424 424 else
425 425 b = []
426 426 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
427 427 if ancestors.any?
428 428 root = ancestors.shift
429 429 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
430 430 if ancestors.size > 2
431 431 b << "\xe2\x80\xa6"
432 432 ancestors = ancestors[-2, 2]
433 433 end
434 434 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
435 435 end
436 436 b << h(@project)
437 437 b.join(" \xc2\xbb ").html_safe
438 438 end
439 439 end
440 440
441 441 def html_title(*args)
442 442 if args.empty?
443 443 title = @html_title || []
444 444 title << @project.name if @project
445 445 title << Setting.app_title unless Setting.app_title == title.last
446 446 title.select {|t| !t.blank? }.join(' - ')
447 447 else
448 448 @html_title ||= []
449 449 @html_title += args
450 450 end
451 451 end
452 452
453 453 # Returns the theme, controller name, and action as css classes for the
454 454 # HTML body.
455 455 def body_css_classes
456 456 css = []
457 457 if theme = Redmine::Themes.theme(Setting.ui_theme)
458 458 css << 'theme-' + theme.name
459 459 end
460 460
461 461 css << 'controller-' + controller_name
462 462 css << 'action-' + action_name
463 463 css.join(' ')
464 464 end
465 465
466 466 def accesskey(s)
467 467 Redmine::AccessKeys.key_for s
468 468 end
469 469
470 470 # Formats text according to system settings.
471 471 # 2 ways to call this method:
472 472 # * with a String: textilizable(text, options)
473 473 # * with an object and one of its attribute: textilizable(issue, :description, options)
474 474 def textilizable(*args)
475 475 options = args.last.is_a?(Hash) ? args.pop : {}
476 476 case args.size
477 477 when 1
478 478 obj = options[:object]
479 479 text = args.shift
480 480 when 2
481 481 obj = args.shift
482 482 attr = args.shift
483 483 text = obj.send(attr).to_s
484 484 else
485 485 raise ArgumentError, 'invalid arguments to textilizable'
486 486 end
487 487 return '' if text.blank?
488 488 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
489 489 only_path = options.delete(:only_path) == false ? false : true
490 490
491 491 text = text.dup
492 492 macros = catch_macros(text)
493 493 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
494 494
495 495 @parsed_headings = []
496 496 @heading_anchors = {}
497 497 @current_section = 0 if options[:edit_section_links]
498 498
499 499 parse_sections(text, project, obj, attr, only_path, options)
500 500 text = parse_non_pre_blocks(text, obj, macros) do |text|
501 501 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
502 502 send method_name, text, project, obj, attr, only_path, options
503 503 end
504 504 end
505 505 parse_headings(text, project, obj, attr, only_path, options)
506 506
507 507 if @parsed_headings.any?
508 508 replace_toc(text, @parsed_headings)
509 509 end
510 510
511 511 text.html_safe
512 512 end
513 513
514 514 def parse_non_pre_blocks(text, obj, macros)
515 515 s = StringScanner.new(text)
516 516 tags = []
517 517 parsed = ''
518 518 while !s.eos?
519 519 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
520 520 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
521 521 if tags.empty?
522 522 yield text
523 523 inject_macros(text, obj, macros) if macros.any?
524 524 else
525 525 inject_macros(text, obj, macros, false) if macros.any?
526 526 end
527 527 parsed << text
528 528 if tag
529 529 if closing
530 530 if tags.last == tag.downcase
531 531 tags.pop
532 532 end
533 533 else
534 534 tags << tag.downcase
535 535 end
536 536 parsed << full_tag
537 537 end
538 538 end
539 539 # Close any non closing tags
540 540 while tag = tags.pop
541 541 parsed << "</#{tag}>"
542 542 end
543 543 parsed
544 544 end
545 545
546 546 def parse_inline_attachments(text, project, obj, attr, only_path, options)
547 547 # when using an image link, try to use an attachment, if possible
548 if options[:attachments].present? || (obj && obj.respond_to?(:attachments))
548 if options[:attachments].present? || obj.respond_to?(:attachments)
549 549 attachments = options[:attachments] || []
550 attachments += obj.attachments if obj
550 attachments += obj.attachments if obj.respond_to?(:attachments)
551 551 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
552 552 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
553 553 # search for the picture in attachments
554 554 if found = Attachment.latest_attach(attachments, filename)
555 555 image_url = url_for :only_path => only_path, :controller => 'attachments',
556 556 :action => 'download', :id => found
557 557 desc = found.description.to_s.gsub('"', '')
558 558 if !desc.blank? && alttext.blank?
559 559 alt = " title=\"#{desc}\" alt=\"#{desc}\""
560 560 end
561 561 "src=\"#{image_url}\"#{alt}"
562 562 else
563 563 m
564 564 end
565 565 end
566 566 end
567 567 end
568 568
569 569 # Wiki links
570 570 #
571 571 # Examples:
572 572 # [[mypage]]
573 573 # [[mypage|mytext]]
574 574 # wiki links can refer other project wikis, using project name or identifier:
575 575 # [[project:]] -> wiki starting page
576 576 # [[project:|mytext]]
577 577 # [[project:mypage]]
578 578 # [[project:mypage|mytext]]
579 579 def parse_wiki_links(text, project, obj, attr, only_path, options)
580 580 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
581 581 link_project = project
582 582 esc, all, page, title = $1, $2, $3, $5
583 583 if esc.nil?
584 584 if page =~ /^([^\:]+)\:(.*)$/
585 585 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
586 586 page = $2
587 587 title ||= $1 if page.blank?
588 588 end
589 589
590 590 if link_project && link_project.wiki
591 591 # extract anchor
592 592 anchor = nil
593 593 if page =~ /^(.+?)\#(.+)$/
594 594 page, anchor = $1, $2
595 595 end
596 596 anchor = sanitize_anchor_name(anchor) if anchor.present?
597 597 # check if page exists
598 598 wiki_page = link_project.wiki.find_page(page)
599 599 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
600 600 "##{anchor}"
601 601 else
602 602 case options[:wiki_links]
603 603 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
604 604 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
605 605 else
606 606 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
607 607 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
608 608 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
609 609 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
610 610 end
611 611 end
612 612 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
613 613 else
614 614 # project or wiki doesn't exist
615 615 all
616 616 end
617 617 else
618 618 all
619 619 end
620 620 end
621 621 end
622 622
623 623 # Redmine links
624 624 #
625 625 # Examples:
626 626 # Issues:
627 627 # #52 -> Link to issue #52
628 628 # Changesets:
629 629 # r52 -> Link to revision 52
630 630 # commit:a85130f -> Link to scmid starting with a85130f
631 631 # Documents:
632 632 # document#17 -> Link to document with id 17
633 633 # document:Greetings -> Link to the document with title "Greetings"
634 634 # document:"Some document" -> Link to the document with title "Some document"
635 635 # Versions:
636 636 # version#3 -> Link to version with id 3
637 637 # version:1.0.0 -> Link to version named "1.0.0"
638 638 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
639 639 # Attachments:
640 640 # attachment:file.zip -> Link to the attachment of the current object named file.zip
641 641 # Source files:
642 642 # source:some/file -> Link to the file located at /some/file in the project's repository
643 643 # source:some/file@52 -> Link to the file's revision 52
644 644 # source:some/file#L120 -> Link to line 120 of the file
645 645 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
646 646 # export:some/file -> Force the download of the file
647 647 # Forum messages:
648 648 # message#1218 -> Link to message with id 1218
649 649 #
650 650 # Links can refer other objects from other projects, using project identifier:
651 651 # identifier:r52
652 652 # identifier:document:"Some document"
653 653 # identifier:version:1.0.0
654 654 # identifier:source:some/file
655 655 def parse_redmine_links(text, project, obj, attr, only_path, options)
656 656 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|
657 657 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
658 658 link = nil
659 659 if project_identifier
660 660 project = Project.visible.find_by_identifier(project_identifier)
661 661 end
662 662 if esc.nil?
663 663 if prefix.nil? && sep == 'r'
664 664 if project
665 665 repository = nil
666 666 if repo_identifier
667 667 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
668 668 else
669 669 repository = project.repository
670 670 end
671 671 # project.changesets.visible raises an SQL error because of a double join on repositories
672 672 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
673 673 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},
674 674 :class => 'changeset',
675 675 :title => truncate_single_line(changeset.comments, :length => 100))
676 676 end
677 677 end
678 678 elsif sep == '#'
679 679 oid = identifier.to_i
680 680 case prefix
681 681 when nil
682 682 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
683 683 anchor = comment_id ? "note-#{comment_id}" : nil
684 684 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
685 685 :class => issue.css_classes,
686 686 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
687 687 end
688 688 when 'document'
689 689 if document = Document.visible.find_by_id(oid)
690 690 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
691 691 :class => 'document'
692 692 end
693 693 when 'version'
694 694 if version = Version.visible.find_by_id(oid)
695 695 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
696 696 :class => 'version'
697 697 end
698 698 when 'message'
699 699 if message = Message.visible.find_by_id(oid, :include => :parent)
700 700 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
701 701 end
702 702 when 'forum'
703 703 if board = Board.visible.find_by_id(oid)
704 704 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
705 705 :class => 'board'
706 706 end
707 707 when 'news'
708 708 if news = News.visible.find_by_id(oid)
709 709 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
710 710 :class => 'news'
711 711 end
712 712 when 'project'
713 713 if p = Project.visible.find_by_id(oid)
714 714 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
715 715 end
716 716 end
717 717 elsif sep == ':'
718 718 # removes the double quotes if any
719 719 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
720 720 case prefix
721 721 when 'document'
722 722 if project && document = project.documents.visible.find_by_title(name)
723 723 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
724 724 :class => 'document'
725 725 end
726 726 when 'version'
727 727 if project && version = project.versions.visible.find_by_name(name)
728 728 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
729 729 :class => 'version'
730 730 end
731 731 when 'forum'
732 732 if project && board = project.boards.visible.find_by_name(name)
733 733 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
734 734 :class => 'board'
735 735 end
736 736 when 'news'
737 737 if project && news = project.news.visible.find_by_title(name)
738 738 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
739 739 :class => 'news'
740 740 end
741 741 when 'commit', 'source', 'export'
742 742 if project
743 743 repository = nil
744 744 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
745 745 repo_prefix, repo_identifier, name = $1, $2, $3
746 746 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
747 747 else
748 748 repository = project.repository
749 749 end
750 750 if prefix == 'commit'
751 751 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
752 752 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},
753 753 :class => 'changeset',
754 754 :title => truncate_single_line(h(changeset.comments), :length => 100)
755 755 end
756 756 else
757 757 if repository && User.current.allowed_to?(:browse_repository, project)
758 758 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
759 759 path, rev, anchor = $1, $3, $5
760 760 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
761 761 :path => to_path_param(path),
762 762 :rev => rev,
763 763 :anchor => anchor},
764 764 :class => (prefix == 'export' ? 'source download' : 'source')
765 765 end
766 766 end
767 767 repo_prefix = nil
768 768 end
769 769 when 'attachment'
770 770 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
771 771 if attachments && attachment = attachments.detect {|a| a.filename == name }
772 772 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
773 773 :class => 'attachment'
774 774 end
775 775 when 'project'
776 776 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
777 777 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
778 778 end
779 779 end
780 780 end
781 781 end
782 782 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
783 783 end
784 784 end
785 785
786 786 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
787 787
788 788 def parse_sections(text, project, obj, attr, only_path, options)
789 789 return unless options[:edit_section_links]
790 790 text.gsub!(HEADING_RE) do
791 791 heading = $1
792 792 @current_section += 1
793 793 if @current_section > 1
794 794 content_tag('div',
795 795 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
796 796 :class => 'contextual',
797 797 :title => l(:button_edit_section)) + heading.html_safe
798 798 else
799 799 heading
800 800 end
801 801 end
802 802 end
803 803
804 804 # Headings and TOC
805 805 # Adds ids and links to headings unless options[:headings] is set to false
806 806 def parse_headings(text, project, obj, attr, only_path, options)
807 807 return if options[:headings] == false
808 808
809 809 text.gsub!(HEADING_RE) do
810 810 level, attrs, content = $2.to_i, $3, $4
811 811 item = strip_tags(content).strip
812 812 anchor = sanitize_anchor_name(item)
813 813 # used for single-file wiki export
814 814 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
815 815 @heading_anchors[anchor] ||= 0
816 816 idx = (@heading_anchors[anchor] += 1)
817 817 if idx > 1
818 818 anchor = "#{anchor}-#{idx}"
819 819 end
820 820 @parsed_headings << [level, anchor, item]
821 821 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
822 822 end
823 823 end
824 824
825 825 MACROS_RE = /(
826 826 (!)? # escaping
827 827 (
828 828 \{\{ # opening tag
829 829 ([\w]+) # macro name
830 830 (\(([^\n\r]*?)\))? # optional arguments
831 831 ([\n\r].*?[\n\r])? # optional block of text
832 832 \}\} # closing tag
833 833 )
834 834 )/mx unless const_defined?(:MACROS_RE)
835 835
836 836 MACRO_SUB_RE = /(
837 837 \{\{
838 838 macro\((\d+)\)
839 839 \}\}
840 840 )/x unless const_defined?(:MACRO_SUB_RE)
841 841
842 842 # Extracts macros from text
843 843 def catch_macros(text)
844 844 macros = {}
845 845 text.gsub!(MACROS_RE) do
846 846 all, macro = $1, $4.downcase
847 847 if macro_exists?(macro) || all =~ MACRO_SUB_RE
848 848 index = macros.size
849 849 macros[index] = all
850 850 "{{macro(#{index})}}"
851 851 else
852 852 all
853 853 end
854 854 end
855 855 macros
856 856 end
857 857
858 858 # Executes and replaces macros in text
859 859 def inject_macros(text, obj, macros, execute=true)
860 860 text.gsub!(MACRO_SUB_RE) do
861 861 all, index = $1, $2.to_i
862 862 orig = macros.delete(index)
863 863 if execute && orig && orig =~ MACROS_RE
864 864 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
865 865 if esc.nil?
866 866 h(exec_macro(macro, obj, args, block) || all)
867 867 else
868 868 h(all)
869 869 end
870 870 elsif orig
871 871 h(orig)
872 872 else
873 873 h(all)
874 874 end
875 875 end
876 876 end
877 877
878 878 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
879 879
880 880 # Renders the TOC with given headings
881 881 def replace_toc(text, headings)
882 882 text.gsub!(TOC_RE) do
883 883 # Keep only the 4 first levels
884 884 headings = headings.select{|level, anchor, item| level <= 4}
885 885 if headings.empty?
886 886 ''
887 887 else
888 888 div_class = 'toc'
889 889 div_class << ' right' if $1 == '>'
890 890 div_class << ' left' if $1 == '<'
891 891 out = "<ul class=\"#{div_class}\"><li>"
892 892 root = headings.map(&:first).min
893 893 current = root
894 894 started = false
895 895 headings.each do |level, anchor, item|
896 896 if level > current
897 897 out << '<ul><li>' * (level - current)
898 898 elsif level < current
899 899 out << "</li></ul>\n" * (current - level) + "</li><li>"
900 900 elsif started
901 901 out << '</li><li>'
902 902 end
903 903 out << "<a href=\"##{anchor}\">#{item}</a>"
904 904 current = level
905 905 started = true
906 906 end
907 907 out << '</li></ul>' * (current - root)
908 908 out << '</li></ul>'
909 909 end
910 910 end
911 911 end
912 912
913 913 # Same as Rails' simple_format helper without using paragraphs
914 914 def simple_format_without_paragraph(text)
915 915 text.to_s.
916 916 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
917 917 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
918 918 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
919 919 html_safe
920 920 end
921 921
922 922 def lang_options_for_select(blank=true)
923 923 (blank ? [["(auto)", ""]] : []) + languages_options
924 924 end
925 925
926 926 def label_tag_for(name, option_tags = nil, options = {})
927 927 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
928 928 content_tag("label", label_text)
929 929 end
930 930
931 931 def labelled_form_for(*args, &proc)
932 932 args << {} unless args.last.is_a?(Hash)
933 933 options = args.last
934 934 if args.first.is_a?(Symbol)
935 935 options.merge!(:as => args.shift)
936 936 end
937 937 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
938 938 form_for(*args, &proc)
939 939 end
940 940
941 941 def labelled_fields_for(*args, &proc)
942 942 args << {} unless args.last.is_a?(Hash)
943 943 options = args.last
944 944 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
945 945 fields_for(*args, &proc)
946 946 end
947 947
948 948 def labelled_remote_form_for(*args, &proc)
949 949 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
950 950 args << {} unless args.last.is_a?(Hash)
951 951 options = args.last
952 952 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
953 953 form_for(*args, &proc)
954 954 end
955 955
956 956 def error_messages_for(*objects)
957 957 html = ""
958 958 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
959 959 errors = objects.map {|o| o.errors.full_messages}.flatten
960 960 if errors.any?
961 961 html << "<div id='errorExplanation'><ul>\n"
962 962 errors.each do |error|
963 963 html << "<li>#{h error}</li>\n"
964 964 end
965 965 html << "</ul></div>\n"
966 966 end
967 967 html.html_safe
968 968 end
969 969
970 970 def delete_link(url, options={})
971 971 options = {
972 972 :method => :delete,
973 973 :data => {:confirm => l(:text_are_you_sure)},
974 974 :class => 'icon icon-del'
975 975 }.merge(options)
976 976
977 977 link_to l(:button_delete), url, options
978 978 end
979 979
980 980 def preview_link(url, form, target='preview', options={})
981 981 content_tag 'a', l(:label_preview), {
982 982 :href => "#",
983 983 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
984 984 :accesskey => accesskey(:preview)
985 985 }.merge(options)
986 986 end
987 987
988 988 def link_to_function(name, function, html_options={})
989 989 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
990 990 end
991 991
992 992 # Helper to render JSON in views
993 993 def raw_json(arg)
994 994 arg.to_json.to_s.gsub('/', '\/').html_safe
995 995 end
996 996
997 997 def back_url
998 998 url = params[:back_url]
999 999 if url.nil? && referer = request.env['HTTP_REFERER']
1000 1000 url = CGI.unescape(referer.to_s)
1001 1001 end
1002 1002 url
1003 1003 end
1004 1004
1005 1005 def back_url_hidden_field_tag
1006 1006 url = back_url
1007 1007 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1008 1008 end
1009 1009
1010 1010 def check_all_links(form_name)
1011 1011 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1012 1012 " | ".html_safe +
1013 1013 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1014 1014 end
1015 1015
1016 1016 def progress_bar(pcts, options={})
1017 1017 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1018 1018 pcts = pcts.collect(&:round)
1019 1019 pcts[1] = pcts[1] - pcts[0]
1020 1020 pcts << (100 - pcts[1] - pcts[0])
1021 1021 width = options[:width] || '100px;'
1022 1022 legend = options[:legend] || ''
1023 1023 content_tag('table',
1024 1024 content_tag('tr',
1025 1025 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1026 1026 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1027 1027 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1028 1028 ), :class => 'progress', :style => "width: #{width};").html_safe +
1029 1029 content_tag('p', legend, :class => 'percent').html_safe
1030 1030 end
1031 1031
1032 1032 def checked_image(checked=true)
1033 1033 if checked
1034 1034 image_tag 'toggle_check.png'
1035 1035 end
1036 1036 end
1037 1037
1038 1038 def context_menu(url)
1039 1039 unless @context_menu_included
1040 1040 content_for :header_tags do
1041 1041 javascript_include_tag('context_menu') +
1042 1042 stylesheet_link_tag('context_menu')
1043 1043 end
1044 1044 if l(:direction) == 'rtl'
1045 1045 content_for :header_tags do
1046 1046 stylesheet_link_tag('context_menu_rtl')
1047 1047 end
1048 1048 end
1049 1049 @context_menu_included = true
1050 1050 end
1051 1051 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1052 1052 end
1053 1053
1054 1054 def calendar_for(field_id)
1055 1055 include_calendar_headers_tags
1056 1056 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1057 1057 end
1058 1058
1059 1059 def include_calendar_headers_tags
1060 1060 unless @calendar_headers_tags_included
1061 1061 @calendar_headers_tags_included = true
1062 1062 content_for :header_tags do
1063 1063 start_of_week = Setting.start_of_week
1064 1064 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1065 1065 # Redmine uses 1..7 (monday..sunday) in settings and locales
1066 1066 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1067 1067 start_of_week = start_of_week.to_i % 7
1068 1068
1069 1069 tags = javascript_tag(
1070 1070 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1071 1071 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1072 1072 path_to_image('/images/calendar.png') +
1073 1073 "', showButtonPanel: true};")
1074 1074 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1075 1075 unless jquery_locale == 'en'
1076 1076 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1077 1077 end
1078 1078 tags
1079 1079 end
1080 1080 end
1081 1081 end
1082 1082
1083 1083 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1084 1084 # Examples:
1085 1085 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1086 1086 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1087 1087 #
1088 1088 def stylesheet_link_tag(*sources)
1089 1089 options = sources.last.is_a?(Hash) ? sources.pop : {}
1090 1090 plugin = options.delete(:plugin)
1091 1091 sources = sources.map do |source|
1092 1092 if plugin
1093 1093 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1094 1094 elsif current_theme && current_theme.stylesheets.include?(source)
1095 1095 current_theme.stylesheet_path(source)
1096 1096 else
1097 1097 source
1098 1098 end
1099 1099 end
1100 1100 super sources, options
1101 1101 end
1102 1102
1103 1103 # Overrides Rails' image_tag with themes and plugins support.
1104 1104 # Examples:
1105 1105 # image_tag('image.png') # => picks image.png from the current theme or defaults
1106 1106 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1107 1107 #
1108 1108 def image_tag(source, options={})
1109 1109 if plugin = options.delete(:plugin)
1110 1110 source = "/plugin_assets/#{plugin}/images/#{source}"
1111 1111 elsif current_theme && current_theme.images.include?(source)
1112 1112 source = current_theme.image_path(source)
1113 1113 end
1114 1114 super source, options
1115 1115 end
1116 1116
1117 1117 # Overrides Rails' javascript_include_tag with plugins support
1118 1118 # Examples:
1119 1119 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1120 1120 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1121 1121 #
1122 1122 def javascript_include_tag(*sources)
1123 1123 options = sources.last.is_a?(Hash) ? sources.pop : {}
1124 1124 if plugin = options.delete(:plugin)
1125 1125 sources = sources.map do |source|
1126 1126 if plugin
1127 1127 "/plugin_assets/#{plugin}/javascripts/#{source}"
1128 1128 else
1129 1129 source
1130 1130 end
1131 1131 end
1132 1132 end
1133 1133 super sources, options
1134 1134 end
1135 1135
1136 1136 def content_for(name, content = nil, &block)
1137 1137 @has_content ||= {}
1138 1138 @has_content[name] = true
1139 1139 super(name, content, &block)
1140 1140 end
1141 1141
1142 1142 def has_content?(name)
1143 1143 (@has_content && @has_content[name]) || false
1144 1144 end
1145 1145
1146 1146 def sidebar_content?
1147 1147 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1148 1148 end
1149 1149
1150 1150 def view_layouts_base_sidebar_hook_response
1151 1151 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1152 1152 end
1153 1153
1154 1154 def email_delivery_enabled?
1155 1155 !!ActionMailer::Base.perform_deliveries
1156 1156 end
1157 1157
1158 1158 # Returns the avatar image tag for the given +user+ if avatars are enabled
1159 1159 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1160 1160 def avatar(user, options = { })
1161 1161 if Setting.gravatar_enabled?
1162 1162 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1163 1163 email = nil
1164 1164 if user.respond_to?(:mail)
1165 1165 email = user.mail
1166 1166 elsif user.to_s =~ %r{<(.+?)>}
1167 1167 email = $1
1168 1168 end
1169 1169 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1170 1170 else
1171 1171 ''
1172 1172 end
1173 1173 end
1174 1174
1175 1175 def sanitize_anchor_name(anchor)
1176 1176 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1177 1177 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1178 1178 else
1179 1179 # TODO: remove when ruby1.8 is no longer supported
1180 1180 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1181 1181 end
1182 1182 end
1183 1183
1184 1184 # Returns the javascript tags that are included in the html layout head
1185 1185 def javascript_heads
1186 1186 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1187 1187 unless User.current.pref.warn_on_leaving_unsaved == '0'
1188 1188 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1189 1189 end
1190 1190 tags
1191 1191 end
1192 1192
1193 1193 def favicon
1194 1194 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1195 1195 end
1196 1196
1197 1197 def robot_exclusion_tag
1198 1198 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1199 1199 end
1200 1200
1201 1201 # Returns true if arg is expected in the API response
1202 1202 def include_in_api_response?(arg)
1203 1203 unless @included_in_api_response
1204 1204 param = params[:include]
1205 1205 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1206 1206 @included_in_api_response.collect!(&:strip)
1207 1207 end
1208 1208 @included_in_api_response.include?(arg.to_s)
1209 1209 end
1210 1210
1211 1211 # Returns options or nil if nometa param or X-Redmine-Nometa header
1212 1212 # was set in the request
1213 1213 def api_meta(options)
1214 1214 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1215 1215 # compatibility mode for activeresource clients that raise
1216 1216 # an error when unserializing an array with attributes
1217 1217 nil
1218 1218 else
1219 1219 options
1220 1220 end
1221 1221 end
1222 1222
1223 1223 private
1224 1224
1225 1225 def wiki_helper
1226 1226 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1227 1227 extend helper
1228 1228 return self
1229 1229 end
1230 1230
1231 1231 def link_to_content_update(text, url_params = {}, html_options = {})
1232 1232 link_to(text, url_params, html_options)
1233 1233 end
1234 1234 end
@@ -1,102 +1,116
1 1 ---
2 2 wiki_content_versions_001:
3 3 updated_on: 2007-03-07 00:08:07 +01:00
4 4 page_id: 1
5 5 id: 1
6 6 version: 1
7 7 author_id: 2
8 8 comments: Page creation
9 9 wiki_content_id: 1
10 10 compression: ""
11 11 data: |-
12 12 h1. CookBook documentation
13 13
14 14
15 15
16 16 Some [[documentation]] here...
17 17 wiki_content_versions_002:
18 18 updated_on: 2007-03-07 00:08:34 +01:00
19 19 page_id: 1
20 20 id: 2
21 21 version: 2
22 22 author_id: 1
23 23 comments: Small update
24 24 wiki_content_id: 1
25 25 compression: ""
26 26 data: |-
27 27 h1. CookBook documentation
28 28
29 29
30 30
31 31 Some updated [[documentation]] here...
32 32 wiki_content_versions_003:
33 33 updated_on: 2007-03-07 00:10:51 +01:00
34 34 page_id: 1
35 35 id: 3
36 36 version: 3
37 37 author_id: 1
38 38 comments: ""
39 39 wiki_content_id: 1
40 40 compression: ""
41 41 data: |-
42 42 h1. CookBook documentation
43 43 Some updated [[documentation]] here...
44 44 wiki_content_versions_004:
45 45 data: |-
46 46 h1. Another page
47 47
48 48 This is a link to a ticket: #2
49 49 updated_on: 2007-03-08 00:18:07 +01:00
50 50 page_id: 2
51 51 wiki_content_id: 2
52 52 id: 4
53 53 version: 1
54 54 author_id: 1
55 55 comments:
56 56 wiki_content_versions_005:
57 57 data: |-
58 58 h1. Title
59 59
60 60 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
61 61
62 62 h2. Heading 1
63 63
64 64 @WHATEVER@
65 65
66 66 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
67 67
68 68 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.
69 69
70 70 h2. Heading 2
71 71
72 72 Morbi facilisis accumsan orci non pharetra.
73 73 updated_on: 2007-03-08 00:16:07 +01:00
74 74 page_id: 11
75 75 wiki_content_id: 11
76 76 id: 5
77 77 version: 2
78 78 author_id: 1
79 79 comments:
80 80 wiki_content_versions_006:
81 81 data: |-
82 82 h1. Title
83 83
84 84 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
85 85
86 86 h2. Heading 1
87 87
88 88 @WHATEVER@
89 89
90 90 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
91 91
92 92 h2. Heading 2
93 93
94 94 Morbi facilisis accumsan orci non pharetra.
95 95 updated_on: 2007-03-08 00:18:07 +01:00
96 96 page_id: 11
97 97 wiki_content_id: 11
98 98 id: 6
99 99 version: 3
100 100 author_id: 1
101 101 comments:
102 wiki_content_versions_007:
103 data: |-
104 h1. Page with an inline image
105
106 This is an inline image:
107
108 !logo.gif!
109 updated_on: 2007-03-08 00:18:07 +01:00
110 page_id: 4
111 wiki_content_id: 4
112 id: 7
113 version: 1
114 author_id: 1
115 comments:
102 116
@@ -1,920 +1,933
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class WikiControllerTest < ActionController::TestCase
21 21 fixtures :projects, :users, :roles, :members, :member_roles,
22 22 :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
23 23 :wiki_content_versions, :attachments
24 24
25 25 def setup
26 26 User.current = nil
27 27 end
28 28
29 29 def test_show_start_page
30 30 get :show, :project_id => 'ecookbook'
31 31 assert_response :success
32 32 assert_template 'show'
33 33 assert_tag :tag => 'h1', :content => /CookBook documentation/
34 34
35 35 # child_pages macro
36 36 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
37 37 :child => { :tag => 'li',
38 38 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
39 39 :content => 'Page with an inline image' } }
40 40 end
41 41
42 42 def test_export_link
43 43 Role.anonymous.add_permission! :export_wiki_pages
44 44 get :show, :project_id => 'ecookbook'
45 45 assert_response :success
46 46 assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'}
47 47 end
48 48
49 49 def test_show_page_with_name
50 50 get :show, :project_id => 1, :id => 'Another_page'
51 51 assert_response :success
52 52 assert_template 'show'
53 53 assert_tag :tag => 'h1', :content => /Another page/
54 54 # Included page with an inline image
55 55 assert_tag :tag => 'p', :content => /This is an inline image/
56 56 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
57 57 :alt => 'This is a logo' }
58 58 end
59 59
60 60 def test_show_old_version
61 61 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
62 62 assert_response :success
63 63 assert_template 'show'
64 64
65 65 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/
66 66 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/
67 67 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/
68 68 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
69 69 end
70 70
71 def test_show_old_version_with_attachments
72 page = WikiPage.find(4)
73 assert page.attachments.any?
74 content = page.content
75 content.text = "update"
76 content.save!
77
78 get :show, :project_id => 'ecookbook', :id => page.title, :version => '1'
79 assert_kind_of WikiContent::Version, assigns(:content)
80 assert_response :success
81 assert_template 'show'
82 end
83
71 84 def test_show_old_version_without_permission_should_be_denied
72 85 Role.anonymous.remove_permission! :view_wiki_edits
73 86
74 87 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
75 88 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fprojects%2Fecookbook%2Fwiki%2FCookBook_documentation%2F2'
76 89 end
77 90
78 91 def test_show_first_version
79 92 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1'
80 93 assert_response :success
81 94 assert_template 'show'
82 95
83 96 assert_select 'a', :text => /Previous/, :count => 0
84 97 assert_select 'a', :text => /diff/, :count => 0
85 98 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/
86 99 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
87 100 end
88 101
89 102 def test_show_redirected_page
90 103 WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page')
91 104
92 105 get :show, :project_id => 'ecookbook', :id => 'Old_title'
93 106 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
94 107 end
95 108
96 109 def test_show_with_sidebar
97 110 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
98 111 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
99 112 page.save!
100 113
101 114 get :show, :project_id => 1, :id => 'Another_page'
102 115 assert_response :success
103 116 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
104 117 :content => /Side bar content for test_show_with_sidebar/
105 118 end
106 119
107 120 def test_show_should_display_section_edit_links
108 121 @request.session[:user_id] = 2
109 122 get :show, :project_id => 1, :id => 'Page with sections'
110 123 assert_no_tag 'a', :attributes => {
111 124 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1'
112 125 }
113 126 assert_tag 'a', :attributes => {
114 127 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
115 128 }
116 129 assert_tag 'a', :attributes => {
117 130 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3'
118 131 }
119 132 end
120 133
121 134 def test_show_current_version_should_display_section_edit_links
122 135 @request.session[:user_id] = 2
123 136 get :show, :project_id => 1, :id => 'Page with sections', :version => 3
124 137
125 138 assert_tag 'a', :attributes => {
126 139 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
127 140 }
128 141 end
129 142
130 143 def test_show_old_version_should_not_display_section_edit_links
131 144 @request.session[:user_id] = 2
132 145 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
133 146
134 147 assert_no_tag 'a', :attributes => {
135 148 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
136 149 }
137 150 end
138 151
139 152 def test_show_unexistent_page_without_edit_right
140 153 get :show, :project_id => 1, :id => 'Unexistent page'
141 154 assert_response 404
142 155 end
143 156
144 157 def test_show_unexistent_page_with_edit_right
145 158 @request.session[:user_id] = 2
146 159 get :show, :project_id => 1, :id => 'Unexistent page'
147 160 assert_response :success
148 161 assert_template 'edit'
149 162 end
150 163
151 164 def test_show_unexistent_page_with_parent_should_preselect_parent
152 165 @request.session[:user_id] = 2
153 166 get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page'
154 167 assert_response :success
155 168 assert_template 'edit'
156 169 assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'},
157 170 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}}
158 171 end
159 172
160 173 def test_show_should_not_show_history_without_permission
161 174 Role.anonymous.remove_permission! :view_wiki_edits
162 175 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
163 176
164 177 assert_response 302
165 178 end
166 179
167 180 def test_create_page
168 181 @request.session[:user_id] = 2
169 182 assert_difference 'WikiPage.count' do
170 183 assert_difference 'WikiContent.count' do
171 184 put :update, :project_id => 1,
172 185 :id => 'New page',
173 186 :content => {:comments => 'Created the page',
174 187 :text => "h1. New page\n\nThis is a new page",
175 188 :version => 0}
176 189 end
177 190 end
178 191 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
179 192 page = Project.find(1).wiki.find_page('New page')
180 193 assert !page.new_record?
181 194 assert_not_nil page.content
182 195 assert_nil page.parent
183 196 assert_equal 'Created the page', page.content.comments
184 197 end
185 198
186 199 def test_create_page_with_attachments
187 200 @request.session[:user_id] = 2
188 201 assert_difference 'WikiPage.count' do
189 202 assert_difference 'Attachment.count' do
190 203 put :update, :project_id => 1,
191 204 :id => 'New page',
192 205 :content => {:comments => 'Created the page',
193 206 :text => "h1. New page\n\nThis is a new page",
194 207 :version => 0},
195 208 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
196 209 end
197 210 end
198 211 page = Project.find(1).wiki.find_page('New page')
199 212 assert_equal 1, page.attachments.count
200 213 assert_equal 'testfile.txt', page.attachments.first.filename
201 214 end
202 215
203 216 def test_create_page_with_parent
204 217 @request.session[:user_id] = 2
205 218 assert_difference 'WikiPage.count' do
206 219 put :update, :project_id => 1, :id => 'New page',
207 220 :content => {:text => "h1. New page\n\nThis is a new page", :version => 0},
208 221 :wiki_page => {:parent_id => 2}
209 222 end
210 223 page = Project.find(1).wiki.find_page('New page')
211 224 assert_equal WikiPage.find(2), page.parent
212 225 end
213 226
214 227 def test_edit_page
215 228 @request.session[:user_id] = 2
216 229 get :edit, :project_id => 'ecookbook', :id => 'Another_page'
217 230
218 231 assert_response :success
219 232 assert_template 'edit'
220 233
221 234 assert_tag 'textarea',
222 235 :attributes => { :name => 'content[text]' },
223 236 :content => "\n"+WikiPage.find_by_title('Another_page').content.text
224 237 end
225 238
226 239 def test_edit_section
227 240 @request.session[:user_id] = 2
228 241 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
229 242
230 243 assert_response :success
231 244 assert_template 'edit'
232 245
233 246 page = WikiPage.find_by_title('Page_with_sections')
234 247 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
235 248
236 249 assert_tag 'textarea',
237 250 :attributes => { :name => 'content[text]' },
238 251 :content => "\n"+section
239 252 assert_tag 'input',
240 253 :attributes => { :name => 'section', :type => 'hidden', :value => '2' }
241 254 assert_tag 'input',
242 255 :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
243 256 end
244 257
245 258 def test_edit_invalid_section_should_respond_with_404
246 259 @request.session[:user_id] = 2
247 260 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
248 261
249 262 assert_response 404
250 263 end
251 264
252 265 def test_update_page
253 266 @request.session[:user_id] = 2
254 267 assert_no_difference 'WikiPage.count' do
255 268 assert_no_difference 'WikiContent.count' do
256 269 assert_difference 'WikiContent::Version.count' do
257 270 put :update, :project_id => 1,
258 271 :id => 'Another_page',
259 272 :content => {
260 273 :comments => "my comments",
261 274 :text => "edited",
262 275 :version => 1
263 276 }
264 277 end
265 278 end
266 279 end
267 280 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
268 281
269 282 page = Wiki.find(1).pages.find_by_title('Another_page')
270 283 assert_equal "edited", page.content.text
271 284 assert_equal 2, page.content.version
272 285 assert_equal "my comments", page.content.comments
273 286 end
274 287
275 288 def test_update_page_with_parent
276 289 @request.session[:user_id] = 2
277 290 assert_no_difference 'WikiPage.count' do
278 291 assert_no_difference 'WikiContent.count' do
279 292 assert_difference 'WikiContent::Version.count' do
280 293 put :update, :project_id => 1,
281 294 :id => 'Another_page',
282 295 :content => {
283 296 :comments => "my comments",
284 297 :text => "edited",
285 298 :version => 1
286 299 },
287 300 :wiki_page => {:parent_id => '1'}
288 301 end
289 302 end
290 303 end
291 304 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
292 305
293 306 page = Wiki.find(1).pages.find_by_title('Another_page')
294 307 assert_equal "edited", page.content.text
295 308 assert_equal 2, page.content.version
296 309 assert_equal "my comments", page.content.comments
297 310 assert_equal WikiPage.find(1), page.parent
298 311 end
299 312
300 313 def test_update_page_with_failure
301 314 @request.session[:user_id] = 2
302 315 assert_no_difference 'WikiPage.count' do
303 316 assert_no_difference 'WikiContent.count' do
304 317 assert_no_difference 'WikiContent::Version.count' do
305 318 put :update, :project_id => 1,
306 319 :id => 'Another_page',
307 320 :content => {
308 321 :comments => 'a' * 300, # failure here, comment is too long
309 322 :text => 'edited',
310 323 :version => 1
311 324 }
312 325 end
313 326 end
314 327 end
315 328 assert_response :success
316 329 assert_template 'edit'
317 330
318 331 assert_error_tag :descendant => {:content => /Comment is too long/}
319 332 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited"
320 333 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
321 334 end
322 335
323 336 def test_update_page_with_parent_change_only_should_not_create_content_version
324 337 @request.session[:user_id] = 2
325 338 assert_no_difference 'WikiPage.count' do
326 339 assert_no_difference 'WikiContent.count' do
327 340 assert_no_difference 'WikiContent::Version.count' do
328 341 put :update, :project_id => 1,
329 342 :id => 'Another_page',
330 343 :content => {
331 344 :comments => '',
332 345 :text => Wiki.find(1).find_page('Another_page').content.text,
333 346 :version => 1
334 347 },
335 348 :wiki_page => {:parent_id => '1'}
336 349 end
337 350 end
338 351 end
339 352 page = Wiki.find(1).pages.find_by_title('Another_page')
340 353 assert_equal 1, page.content.version
341 354 assert_equal WikiPage.find(1), page.parent
342 355 end
343 356
344 357 def test_update_page_with_attachments_only_should_not_create_content_version
345 358 @request.session[:user_id] = 2
346 359 assert_no_difference 'WikiPage.count' do
347 360 assert_no_difference 'WikiContent.count' do
348 361 assert_no_difference 'WikiContent::Version.count' do
349 362 assert_difference 'Attachment.count' do
350 363 put :update, :project_id => 1,
351 364 :id => 'Another_page',
352 365 :content => {
353 366 :comments => '',
354 367 :text => Wiki.find(1).find_page('Another_page').content.text,
355 368 :version => 1
356 369 },
357 370 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
358 371 end
359 372 end
360 373 end
361 374 end
362 375 page = Wiki.find(1).pages.find_by_title('Another_page')
363 376 assert_equal 1, page.content.version
364 377 end
365 378
366 379 def test_update_stale_page_should_not_raise_an_error
367 380 @request.session[:user_id] = 2
368 381 c = Wiki.find(1).find_page('Another_page').content
369 382 c.text = 'Previous text'
370 383 c.save!
371 384 assert_equal 2, c.version
372 385
373 386 assert_no_difference 'WikiPage.count' do
374 387 assert_no_difference 'WikiContent.count' do
375 388 assert_no_difference 'WikiContent::Version.count' do
376 389 put :update, :project_id => 1,
377 390 :id => 'Another_page',
378 391 :content => {
379 392 :comments => 'My comments',
380 393 :text => 'Text should not be lost',
381 394 :version => 1
382 395 }
383 396 end
384 397 end
385 398 end
386 399 assert_response :success
387 400 assert_template 'edit'
388 401 assert_tag :div,
389 402 :attributes => { :class => /error/ },
390 403 :content => /Data has been updated by another user/
391 404 assert_tag 'textarea',
392 405 :attributes => { :name => 'content[text]' },
393 406 :content => /Text should not be lost/
394 407 assert_tag 'input',
395 408 :attributes => { :name => 'content[comments]', :value => 'My comments' }
396 409
397 410 c.reload
398 411 assert_equal 'Previous text', c.text
399 412 assert_equal 2, c.version
400 413 end
401 414
402 415 def test_update_section
403 416 @request.session[:user_id] = 2
404 417 page = WikiPage.find_by_title('Page_with_sections')
405 418 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
406 419 text = page.content.text
407 420
408 421 assert_no_difference 'WikiPage.count' do
409 422 assert_no_difference 'WikiContent.count' do
410 423 assert_difference 'WikiContent::Version.count' do
411 424 put :update, :project_id => 1, :id => 'Page_with_sections',
412 425 :content => {
413 426 :text => "New section content",
414 427 :version => 3
415 428 },
416 429 :section => 2,
417 430 :section_hash => hash
418 431 end
419 432 end
420 433 end
421 434 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
422 435 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
423 436 end
424 437
425 438 def test_update_section_should_allow_stale_page_update
426 439 @request.session[:user_id] = 2
427 440 page = WikiPage.find_by_title('Page_with_sections')
428 441 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
429 442 text = page.content.text
430 443
431 444 assert_no_difference 'WikiPage.count' do
432 445 assert_no_difference 'WikiContent.count' do
433 446 assert_difference 'WikiContent::Version.count' do
434 447 put :update, :project_id => 1, :id => 'Page_with_sections',
435 448 :content => {
436 449 :text => "New section content",
437 450 :version => 2 # Current version is 3
438 451 },
439 452 :section => 2,
440 453 :section_hash => hash
441 454 end
442 455 end
443 456 end
444 457 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
445 458 page.reload
446 459 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
447 460 assert_equal 4, page.content.version
448 461 end
449 462
450 463 def test_update_section_should_not_allow_stale_section_update
451 464 @request.session[:user_id] = 2
452 465
453 466 assert_no_difference 'WikiPage.count' do
454 467 assert_no_difference 'WikiContent.count' do
455 468 assert_no_difference 'WikiContent::Version.count' do
456 469 put :update, :project_id => 1, :id => 'Page_with_sections',
457 470 :content => {
458 471 :comments => 'My comments',
459 472 :text => "Text should not be lost",
460 473 :version => 3
461 474 },
462 475 :section => 2,
463 476 :section_hash => Digest::MD5.hexdigest("wrong hash")
464 477 end
465 478 end
466 479 end
467 480 assert_response :success
468 481 assert_template 'edit'
469 482 assert_tag :div,
470 483 :attributes => { :class => /error/ },
471 484 :content => /Data has been updated by another user/
472 485 assert_tag 'textarea',
473 486 :attributes => { :name => 'content[text]' },
474 487 :content => /Text should not be lost/
475 488 assert_tag 'input',
476 489 :attributes => { :name => 'content[comments]', :value => 'My comments' }
477 490 end
478 491
479 492 def test_preview
480 493 @request.session[:user_id] = 2
481 494 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
482 495 :content => { :comments => '',
483 496 :text => 'this is a *previewed text*',
484 497 :version => 3 }
485 498 assert_response :success
486 499 assert_template 'common/_preview'
487 500 assert_tag :tag => 'strong', :content => /previewed text/
488 501 end
489 502
490 503 def test_preview_new_page
491 504 @request.session[:user_id] = 2
492 505 xhr :post, :preview, :project_id => 1, :id => 'New page',
493 506 :content => { :text => 'h1. New page',
494 507 :comments => '',
495 508 :version => 0 }
496 509 assert_response :success
497 510 assert_template 'common/_preview'
498 511 assert_tag :tag => 'h1', :content => /New page/
499 512 end
500 513
501 514 def test_history
502 515 @request.session[:user_id] = 2
503 516 get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation'
504 517 assert_response :success
505 518 assert_template 'history'
506 519 assert_not_nil assigns(:versions)
507 520 assert_equal 3, assigns(:versions).size
508 521
509 522 assert_select "input[type=submit][name=commit]"
510 523 assert_select 'td' do
511 524 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2'
512 525 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate', :text => 'Annotate'
513 526 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => 'Delete'
514 527 end
515 528 end
516 529
517 530 def test_history_with_one_version
518 531 @request.session[:user_id] = 2
519 532 get :history, :project_id => 'ecookbook', :id => 'Another_page'
520 533 assert_response :success
521 534 assert_template 'history'
522 535 assert_not_nil assigns(:versions)
523 536 assert_equal 1, assigns(:versions).size
524 537 assert_select "input[type=submit][name=commit]", false
525 538 assert_select 'td' do
526 539 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => '1'
527 540 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1/annotate', :text => 'Annotate'
528 541 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => 'Delete', :count => 0
529 542 end
530 543 end
531 544
532 545 def test_diff
533 546 content = WikiPage.find(1).content
534 547 assert_difference 'WikiContent::Version.count', 2 do
535 548 content.text = "Line removed\nThis is a sample text for testing diffs"
536 549 content.save!
537 550 content.text = "This is a sample text for testing diffs\nLine added"
538 551 content.save!
539 552 end
540 553
541 554 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1)
542 555 assert_response :success
543 556 assert_template 'diff'
544 557 assert_select 'span.diff_out', :text => 'Line removed'
545 558 assert_select 'span.diff_in', :text => 'Line added'
546 559 end
547 560
548 561 def test_diff_with_invalid_version_should_respond_with_404
549 562 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
550 563 assert_response 404
551 564 end
552 565
553 566 def test_diff_with_invalid_version_from_should_respond_with_404
554 567 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99', :version_from => '98'
555 568 assert_response 404
556 569 end
557 570
558 571 def test_annotate
559 572 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
560 573 assert_response :success
561 574 assert_template 'annotate'
562 575
563 576 # Line 1
564 577 assert_tag :tag => 'tr', :child => {
565 578 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => {
566 579 :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => {
567 580 :tag => 'td', :content => /h1\. CookBook documentation/
568 581 }
569 582 }
570 583 }
571 584
572 585 # Line 5
573 586 assert_tag :tag => 'tr', :child => {
574 587 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => {
575 588 :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/, :sibling => {
576 589 :tag => 'td', :content => /Some updated \[\[documentation\]\] here/
577 590 }
578 591 }
579 592 }
580 593 end
581 594
582 595 def test_annotate_with_invalid_version_should_respond_with_404
583 596 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
584 597 assert_response 404
585 598 end
586 599
587 600 def test_get_rename
588 601 @request.session[:user_id] = 2
589 602 get :rename, :project_id => 1, :id => 'Another_page'
590 603 assert_response :success
591 604 assert_template 'rename'
592 605 assert_tag 'option',
593 606 :attributes => {:value => ''},
594 607 :content => '',
595 608 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
596 609 assert_no_tag 'option',
597 610 :attributes => {:selected => 'selected'},
598 611 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
599 612 end
600 613
601 614 def test_get_rename_child_page
602 615 @request.session[:user_id] = 2
603 616 get :rename, :project_id => 1, :id => 'Child_1'
604 617 assert_response :success
605 618 assert_template 'rename'
606 619 assert_tag 'option',
607 620 :attributes => {:value => ''},
608 621 :content => '',
609 622 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
610 623 assert_tag 'option',
611 624 :attributes => {:value => '2', :selected => 'selected'},
612 625 :content => /Another page/,
613 626 :parent => {
614 627 :tag => 'select',
615 628 :attributes => {:name => 'wiki_page[parent_id]'}
616 629 }
617 630 end
618 631
619 632 def test_rename_with_redirect
620 633 @request.session[:user_id] = 2
621 634 post :rename, :project_id => 1, :id => 'Another_page',
622 635 :wiki_page => { :title => 'Another renamed page',
623 636 :redirect_existing_links => 1 }
624 637 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
625 638 wiki = Project.find(1).wiki
626 639 # Check redirects
627 640 assert_not_nil wiki.find_page('Another page')
628 641 assert_nil wiki.find_page('Another page', :with_redirect => false)
629 642 end
630 643
631 644 def test_rename_without_redirect
632 645 @request.session[:user_id] = 2
633 646 post :rename, :project_id => 1, :id => 'Another_page',
634 647 :wiki_page => { :title => 'Another renamed page',
635 648 :redirect_existing_links => "0" }
636 649 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
637 650 wiki = Project.find(1).wiki
638 651 # Check that there's no redirects
639 652 assert_nil wiki.find_page('Another page')
640 653 end
641 654
642 655 def test_rename_with_parent_assignment
643 656 @request.session[:user_id] = 2
644 657 post :rename, :project_id => 1, :id => 'Another_page',
645 658 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
646 659 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
647 660 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
648 661 end
649 662
650 663 def test_rename_with_parent_unassignment
651 664 @request.session[:user_id] = 2
652 665 post :rename, :project_id => 1, :id => 'Child_1',
653 666 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
654 667 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
655 668 assert_nil WikiPage.find_by_title('Child_1').parent
656 669 end
657 670
658 671 def test_destroy_a_page_without_children_should_not_ask_confirmation
659 672 @request.session[:user_id] = 2
660 673 delete :destroy, :project_id => 1, :id => 'Child_2'
661 674 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
662 675 end
663 676
664 677 def test_destroy_parent_should_ask_confirmation
665 678 @request.session[:user_id] = 2
666 679 assert_no_difference('WikiPage.count') do
667 680 delete :destroy, :project_id => 1, :id => 'Another_page'
668 681 end
669 682 assert_response :success
670 683 assert_template 'destroy'
671 684 assert_select 'form' do
672 685 assert_select 'input[name=todo][value=nullify]'
673 686 assert_select 'input[name=todo][value=destroy]'
674 687 assert_select 'input[name=todo][value=reassign]'
675 688 end
676 689 end
677 690
678 691 def test_destroy_parent_with_nullify_should_delete_parent_only
679 692 @request.session[:user_id] = 2
680 693 assert_difference('WikiPage.count', -1) do
681 694 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
682 695 end
683 696 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
684 697 assert_nil WikiPage.find_by_id(2)
685 698 end
686 699
687 700 def test_destroy_parent_with_cascade_should_delete_descendants
688 701 @request.session[:user_id] = 2
689 702 assert_difference('WikiPage.count', -4) do
690 703 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
691 704 end
692 705 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
693 706 assert_nil WikiPage.find_by_id(2)
694 707 assert_nil WikiPage.find_by_id(5)
695 708 end
696 709
697 710 def test_destroy_parent_with_reassign
698 711 @request.session[:user_id] = 2
699 712 assert_difference('WikiPage.count', -1) do
700 713 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
701 714 end
702 715 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
703 716 assert_nil WikiPage.find_by_id(2)
704 717 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
705 718 end
706 719
707 720 def test_destroy_version
708 721 @request.session[:user_id] = 2
709 722 assert_difference 'WikiContent::Version.count', -1 do
710 723 assert_no_difference 'WikiContent.count' do
711 724 assert_no_difference 'WikiPage.count' do
712 725 delete :destroy_version, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => 2
713 726 assert_redirected_to '/projects/ecookbook/wiki/CookBook_documentation/history'
714 727 end
715 728 end
716 729 end
717 730 end
718 731
719 732 def test_index
720 733 get :index, :project_id => 'ecookbook'
721 734 assert_response :success
722 735 assert_template 'index'
723 736 pages = assigns(:pages)
724 737 assert_not_nil pages
725 738 assert_equal Project.find(1).wiki.pages.size, pages.size
726 739 assert_equal pages.first.content.updated_on, pages.first.updated_on
727 740
728 741 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
729 742 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
730 743 :content => 'CookBook documentation' },
731 744 :child => { :tag => 'ul',
732 745 :child => { :tag => 'li',
733 746 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
734 747 :content => 'Page with an inline image' } } } },
735 748 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
736 749 :content => 'Another page' } }
737 750 end
738 751
739 752 def test_index_should_include_atom_link
740 753 get :index, :project_id => 'ecookbook'
741 754 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
742 755 end
743 756
744 757 def test_export_to_html
745 758 @request.session[:user_id] = 2
746 759 get :export, :project_id => 'ecookbook'
747 760
748 761 assert_response :success
749 762 assert_not_nil assigns(:pages)
750 763 assert assigns(:pages).any?
751 764 assert_equal "text/html", @response.content_type
752 765
753 766 assert_select "a[name=?]", "CookBook_documentation"
754 767 assert_select "a[name=?]", "Another_page"
755 768 assert_select "a[name=?]", "Page_with_an_inline_image"
756 769 end
757 770
758 771 def test_export_to_pdf
759 772 @request.session[:user_id] = 2
760 773 get :export, :project_id => 'ecookbook', :format => 'pdf'
761 774
762 775 assert_response :success
763 776 assert_not_nil assigns(:pages)
764 777 assert assigns(:pages).any?
765 778 assert_equal 'application/pdf', @response.content_type
766 779 assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition']
767 780 assert @response.body.starts_with?('%PDF')
768 781 end
769 782
770 783 def test_export_without_permission_should_be_denied
771 784 @request.session[:user_id] = 2
772 785 Role.find_by_name('Manager').remove_permission! :export_wiki_pages
773 786 get :export, :project_id => 'ecookbook'
774 787
775 788 assert_response 403
776 789 end
777 790
778 791 def test_date_index
779 792 get :date_index, :project_id => 'ecookbook'
780 793
781 794 assert_response :success
782 795 assert_template 'date_index'
783 796 assert_not_nil assigns(:pages)
784 797 assert_not_nil assigns(:pages_by_date)
785 798
786 799 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
787 800 end
788 801
789 802 def test_not_found
790 803 get :show, :project_id => 999
791 804 assert_response 404
792 805 end
793 806
794 807 def test_protect_page
795 808 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
796 809 assert !page.protected?
797 810 @request.session[:user_id] = 2
798 811 post :protect, :project_id => 1, :id => page.title, :protected => '1'
799 812 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
800 813 assert page.reload.protected?
801 814 end
802 815
803 816 def test_unprotect_page
804 817 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
805 818 assert page.protected?
806 819 @request.session[:user_id] = 2
807 820 post :protect, :project_id => 1, :id => page.title, :protected => '0'
808 821 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
809 822 assert !page.reload.protected?
810 823 end
811 824
812 825 def test_show_page_with_edit_link
813 826 @request.session[:user_id] = 2
814 827 get :show, :project_id => 1
815 828 assert_response :success
816 829 assert_template 'show'
817 830 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
818 831 end
819 832
820 833 def test_show_page_without_edit_link
821 834 @request.session[:user_id] = 4
822 835 get :show, :project_id => 1
823 836 assert_response :success
824 837 assert_template 'show'
825 838 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
826 839 end
827 840
828 841 def test_show_pdf
829 842 @request.session[:user_id] = 2
830 843 get :show, :project_id => 1, :format => 'pdf'
831 844 assert_response :success
832 845 assert_not_nil assigns(:page)
833 846 assert_equal 'application/pdf', @response.content_type
834 847 assert_equal 'attachment; filename="CookBook_documentation.pdf"',
835 848 @response.headers['Content-Disposition']
836 849 end
837 850
838 851 def test_show_html
839 852 @request.session[:user_id] = 2
840 853 get :show, :project_id => 1, :format => 'html'
841 854 assert_response :success
842 855 assert_not_nil assigns(:page)
843 856 assert_equal 'text/html', @response.content_type
844 857 assert_equal 'attachment; filename="CookBook_documentation.html"',
845 858 @response.headers['Content-Disposition']
846 859 assert_tag 'h1', :content => 'CookBook documentation'
847 860 end
848 861
849 862 def test_show_versioned_html
850 863 @request.session[:user_id] = 2
851 864 get :show, :project_id => 1, :format => 'html', :version => 2
852 865 assert_response :success
853 866 assert_not_nil assigns(:content)
854 867 assert_equal 2, assigns(:content).version
855 868 assert_equal 'text/html', @response.content_type
856 869 assert_equal 'attachment; filename="CookBook_documentation.html"',
857 870 @response.headers['Content-Disposition']
858 871 assert_tag 'h1', :content => 'CookBook documentation'
859 872 end
860 873
861 874 def test_show_txt
862 875 @request.session[:user_id] = 2
863 876 get :show, :project_id => 1, :format => 'txt'
864 877 assert_response :success
865 878 assert_not_nil assigns(:page)
866 879 assert_equal 'text/plain', @response.content_type
867 880 assert_equal 'attachment; filename="CookBook_documentation.txt"',
868 881 @response.headers['Content-Disposition']
869 882 assert_include 'h1. CookBook documentation', @response.body
870 883 end
871 884
872 885 def test_show_versioned_txt
873 886 @request.session[:user_id] = 2
874 887 get :show, :project_id => 1, :format => 'txt', :version => 2
875 888 assert_response :success
876 889 assert_not_nil assigns(:content)
877 890 assert_equal 2, assigns(:content).version
878 891 assert_equal 'text/plain', @response.content_type
879 892 assert_equal 'attachment; filename="CookBook_documentation.txt"',
880 893 @response.headers['Content-Disposition']
881 894 assert_include 'h1. CookBook documentation', @response.body
882 895 end
883 896
884 897 def test_edit_unprotected_page
885 898 # Non members can edit unprotected wiki pages
886 899 @request.session[:user_id] = 4
887 900 get :edit, :project_id => 1, :id => 'Another_page'
888 901 assert_response :success
889 902 assert_template 'edit'
890 903 end
891 904
892 905 def test_edit_protected_page_by_nonmember
893 906 # Non members can't edit protected wiki pages
894 907 @request.session[:user_id] = 4
895 908 get :edit, :project_id => 1, :id => 'CookBook_documentation'
896 909 assert_response 403
897 910 end
898 911
899 912 def test_edit_protected_page_by_member
900 913 @request.session[:user_id] = 2
901 914 get :edit, :project_id => 1, :id => 'CookBook_documentation'
902 915 assert_response :success
903 916 assert_template 'edit'
904 917 end
905 918
906 919 def test_history_of_non_existing_page_should_return_404
907 920 get :history, :project_id => 1, :id => 'Unknown_page'
908 921 assert_response 404
909 922 end
910 923
911 924 def test_add_attachment
912 925 @request.session[:user_id] = 2
913 926 assert_difference 'Attachment.count' do
914 927 post :add_attachment, :project_id => 1, :id => 'CookBook_documentation',
915 928 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
916 929 end
917 930 attachment = Attachment.first(:order => 'id DESC')
918 931 assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container
919 932 end
920 933 end
General Comments 0
You need to be logged in to leave comments. Login now