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