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