##// END OF EJS Templates
back out r12441...
Toshi MARUYAMA -
r12168:14bd57eb4772
parent child
Show More
@@ -1,1311 +1,1311
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 850 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
851 851 if attachments && attachment = Attachment.latest_attach(attachments, name)
852 852 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
853 853 end
854 854 when 'project'
855 855 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
856 856 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
857 857 end
858 858 end
859 859 end
860 860 end
861 861 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
862 862 end
863 863 end
864 864
865 865 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
866 866
867 867 def parse_sections(text, project, obj, attr, only_path, options)
868 868 return unless options[:edit_section_links]
869 869 text.gsub!(HEADING_RE) do
870 870 heading = $1
871 871 @current_section += 1
872 872 if @current_section > 1
873 873 content_tag('div',
874 874 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
875 875 :class => 'contextual',
876 876 :title => l(:button_edit_section),
877 877 :id => "section-#{@current_section}") + heading.html_safe
878 878 else
879 879 heading
880 880 end
881 881 end
882 882 end
883 883
884 884 # Headings and TOC
885 885 # Adds ids and links to headings unless options[:headings] is set to false
886 886 def parse_headings(text, project, obj, attr, only_path, options)
887 887 return if options[:headings] == false
888 888
889 889 text.gsub!(HEADING_RE) do
890 890 level, attrs, content = $2.to_i, $3, $4
891 891 item = strip_tags(content).strip
892 892 anchor = sanitize_anchor_name(item)
893 893 # used for single-file wiki export
894 894 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
895 895 @heading_anchors[anchor] ||= 0
896 896 idx = (@heading_anchors[anchor] += 1)
897 897 if idx > 1
898 898 anchor = "#{anchor}-#{idx}"
899 899 end
900 900 @parsed_headings << [level, anchor, item]
901 901 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
902 902 end
903 903 end
904 904
905 905 MACROS_RE = /(
906 906 (!)? # escaping
907 907 (
908 908 \{\{ # opening tag
909 909 ([\w]+) # macro name
910 910 (\(([^\n\r]*?)\))? # optional arguments
911 911 ([\n\r].*?[\n\r])? # optional block of text
912 912 \}\} # closing tag
913 913 )
914 914 )/mx unless const_defined?(:MACROS_RE)
915 915
916 916 MACRO_SUB_RE = /(
917 917 \{\{
918 918 macro\((\d+)\)
919 919 \}\}
920 920 )/x unless const_defined?(:MACRO_SUB_RE)
921 921
922 922 # Extracts macros from text
923 923 def catch_macros(text)
924 924 macros = {}
925 925 text.gsub!(MACROS_RE) do
926 926 all, macro = $1, $4.downcase
927 927 if macro_exists?(macro) || all =~ MACRO_SUB_RE
928 928 index = macros.size
929 929 macros[index] = all
930 930 "{{macro(#{index})}}"
931 931 else
932 932 all
933 933 end
934 934 end
935 935 macros
936 936 end
937 937
938 938 # Executes and replaces macros in text
939 939 def inject_macros(text, obj, macros, execute=true)
940 940 text.gsub!(MACRO_SUB_RE) do
941 941 all, index = $1, $2.to_i
942 942 orig = macros.delete(index)
943 943 if execute && orig && orig =~ MACROS_RE
944 944 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
945 945 if esc.nil?
946 946 h(exec_macro(macro, obj, args, block) || all)
947 947 else
948 948 h(all)
949 949 end
950 950 elsif orig
951 951 h(orig)
952 952 else
953 953 h(all)
954 954 end
955 955 end
956 956 end
957 957
958 958 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
959 959
960 960 # Renders the TOC with given headings
961 961 def replace_toc(text, headings)
962 962 text.gsub!(TOC_RE) do
963 963 # Keep only the 4 first levels
964 964 headings = headings.select{|level, anchor, item| level <= 4}
965 965 if headings.empty?
966 966 ''
967 967 else
968 968 div_class = 'toc'
969 969 div_class << ' right' if $1 == '>'
970 970 div_class << ' left' if $1 == '<'
971 971 out = "<ul class=\"#{div_class}\"><li>"
972 972 root = headings.map(&:first).min
973 973 current = root
974 974 started = false
975 975 headings.each do |level, anchor, item|
976 976 if level > current
977 977 out << '<ul><li>' * (level - current)
978 978 elsif level < current
979 979 out << "</li></ul>\n" * (current - level) + "</li><li>"
980 980 elsif started
981 981 out << '</li><li>'
982 982 end
983 983 out << "<a href=\"##{anchor}\">#{item}</a>"
984 984 current = level
985 985 started = true
986 986 end
987 987 out << '</li></ul>' * (current - root)
988 988 out << '</li></ul>'
989 989 end
990 990 end
991 991 end
992 992
993 993 # Same as Rails' simple_format helper without using paragraphs
994 994 def simple_format_without_paragraph(text)
995 995 text.to_s.
996 996 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
997 997 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
998 998 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
999 999 html_safe
1000 1000 end
1001 1001
1002 1002 def lang_options_for_select(blank=true)
1003 1003 (blank ? [["(auto)", ""]] : []) + languages_options
1004 1004 end
1005 1005
1006 1006 def label_tag_for(name, option_tags = nil, options = {})
1007 1007 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1008 1008 content_tag("label", label_text)
1009 1009 end
1010 1010
1011 1011 def labelled_form_for(*args, &proc)
1012 1012 args << {} unless args.last.is_a?(Hash)
1013 1013 options = args.last
1014 1014 if args.first.is_a?(Symbol)
1015 1015 options.merge!(:as => args.shift)
1016 1016 end
1017 1017 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1018 1018 form_for(*args, &proc)
1019 1019 end
1020 1020
1021 1021 def labelled_fields_for(*args, &proc)
1022 1022 args << {} unless args.last.is_a?(Hash)
1023 1023 options = args.last
1024 1024 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1025 1025 fields_for(*args, &proc)
1026 1026 end
1027 1027
1028 1028 def labelled_remote_form_for(*args, &proc)
1029 1029 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1030 1030 args << {} unless args.last.is_a?(Hash)
1031 1031 options = args.last
1032 1032 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1033 1033 form_for(*args, &proc)
1034 1034 end
1035 1035
1036 1036 def error_messages_for(*objects)
1037 1037 html = ""
1038 1038 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1039 1039 errors = objects.map {|o| o.errors.full_messages}.flatten
1040 1040 if errors.any?
1041 1041 html << "<div id='errorExplanation'><ul>\n"
1042 1042 errors.each do |error|
1043 1043 html << "<li>#{h error}</li>\n"
1044 1044 end
1045 1045 html << "</ul></div>\n"
1046 1046 end
1047 1047 html.html_safe
1048 1048 end
1049 1049
1050 1050 def delete_link(url, options={})
1051 1051 options = {
1052 1052 :method => :delete,
1053 1053 :data => {:confirm => l(:text_are_you_sure)},
1054 1054 :class => 'icon icon-del'
1055 1055 }.merge(options)
1056 1056
1057 1057 link_to l(:button_delete), url, options
1058 1058 end
1059 1059
1060 1060 def preview_link(url, form, target='preview', options={})
1061 1061 content_tag 'a', l(:label_preview), {
1062 1062 :href => "#",
1063 1063 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1064 1064 :accesskey => accesskey(:preview)
1065 1065 }.merge(options)
1066 1066 end
1067 1067
1068 1068 def link_to_function(name, function, html_options={})
1069 1069 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1070 1070 end
1071 1071
1072 1072 # Helper to render JSON in views
1073 1073 def raw_json(arg)
1074 1074 arg.to_json.to_s.gsub('/', '\/').html_safe
1075 1075 end
1076 1076
1077 1077 def back_url
1078 1078 url = params[:back_url]
1079 1079 if url.nil? && referer = request.env['HTTP_REFERER']
1080 1080 url = CGI.unescape(referer.to_s)
1081 1081 end
1082 1082 url
1083 1083 end
1084 1084
1085 1085 def back_url_hidden_field_tag
1086 1086 url = back_url
1087 1087 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1088 1088 end
1089 1089
1090 1090 def check_all_links(form_name)
1091 1091 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1092 1092 " | ".html_safe +
1093 1093 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1094 1094 end
1095 1095
1096 1096 def progress_bar(pcts, options={})
1097 1097 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1098 1098 pcts = pcts.collect(&:round)
1099 1099 pcts[1] = pcts[1] - pcts[0]
1100 1100 pcts << (100 - pcts[1] - pcts[0])
1101 1101 width = options[:width] || '100px;'
1102 1102 legend = options[:legend] || ''
1103 1103 content_tag('table',
1104 1104 content_tag('tr',
1105 1105 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1106 1106 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1107 1107 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1108 1108 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1109 1109 content_tag('p', legend, :class => 'percent').html_safe
1110 1110 end
1111 1111
1112 1112 def checked_image(checked=true)
1113 1113 if checked
1114 1114 image_tag 'toggle_check.png'
1115 1115 end
1116 1116 end
1117 1117
1118 1118 def context_menu(url)
1119 1119 unless @context_menu_included
1120 1120 content_for :header_tags do
1121 1121 javascript_include_tag('context_menu') +
1122 1122 stylesheet_link_tag('context_menu')
1123 1123 end
1124 1124 if l(:direction) == 'rtl'
1125 1125 content_for :header_tags do
1126 1126 stylesheet_link_tag('context_menu_rtl')
1127 1127 end
1128 1128 end
1129 1129 @context_menu_included = true
1130 1130 end
1131 1131 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1132 1132 end
1133 1133
1134 1134 def calendar_for(field_id)
1135 1135 include_calendar_headers_tags
1136 1136 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1137 1137 end
1138 1138
1139 1139 def include_calendar_headers_tags
1140 1140 unless @calendar_headers_tags_included
1141 1141 tags = javascript_include_tag("datepicker")
1142 1142 @calendar_headers_tags_included = true
1143 1143 content_for :header_tags do
1144 1144 start_of_week = Setting.start_of_week
1145 1145 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1146 1146 # Redmine uses 1..7 (monday..sunday) in settings and locales
1147 1147 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1148 1148 start_of_week = start_of_week.to_i % 7
1149 1149 tags << javascript_tag(
1150 1150 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1151 1151 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1152 1152 path_to_image('/images/calendar.png') +
1153 1153 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1154 1154 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1155 1155 "beforeShow: beforeShowDatePicker};")
1156 1156 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1157 1157 unless jquery_locale == 'en'
1158 1158 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1159 1159 end
1160 1160 tags
1161 1161 end
1162 1162 end
1163 1163 end
1164 1164
1165 1165 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1166 1166 # Examples:
1167 1167 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1168 1168 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1169 1169 #
1170 1170 def stylesheet_link_tag(*sources)
1171 1171 options = sources.last.is_a?(Hash) ? sources.pop : {}
1172 1172 plugin = options.delete(:plugin)
1173 1173 sources = sources.map do |source|
1174 1174 if plugin
1175 1175 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1176 1176 elsif current_theme && current_theme.stylesheets.include?(source)
1177 1177 current_theme.stylesheet_path(source)
1178 1178 else
1179 1179 source
1180 1180 end
1181 1181 end
1182 super *sources, options
1182 super sources, options
1183 1183 end
1184 1184
1185 1185 # Overrides Rails' image_tag with themes and plugins support.
1186 1186 # Examples:
1187 1187 # image_tag('image.png') # => picks image.png from the current theme or defaults
1188 1188 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1189 1189 #
1190 1190 def image_tag(source, options={})
1191 1191 if plugin = options.delete(:plugin)
1192 1192 source = "/plugin_assets/#{plugin}/images/#{source}"
1193 1193 elsif current_theme && current_theme.images.include?(source)
1194 1194 source = current_theme.image_path(source)
1195 1195 end
1196 1196 super source, options
1197 1197 end
1198 1198
1199 1199 # Overrides Rails' javascript_include_tag with plugins support
1200 1200 # Examples:
1201 1201 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1202 1202 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1203 1203 #
1204 1204 def javascript_include_tag(*sources)
1205 1205 options = sources.last.is_a?(Hash) ? sources.pop : {}
1206 1206 if plugin = options.delete(:plugin)
1207 1207 sources = sources.map do |source|
1208 1208 if plugin
1209 1209 "/plugin_assets/#{plugin}/javascripts/#{source}"
1210 1210 else
1211 1211 source
1212 1212 end
1213 1213 end
1214 1214 end
1215 super *sources, options
1215 super sources, options
1216 1216 end
1217 1217
1218 1218 # TODO: remove this in 2.5.0
1219 1219 def has_content?(name)
1220 1220 content_for?(name)
1221 1221 end
1222 1222
1223 1223 def sidebar_content?
1224 1224 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1225 1225 end
1226 1226
1227 1227 def view_layouts_base_sidebar_hook_response
1228 1228 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1229 1229 end
1230 1230
1231 1231 def email_delivery_enabled?
1232 1232 !!ActionMailer::Base.perform_deliveries
1233 1233 end
1234 1234
1235 1235 # Returns the avatar image tag for the given +user+ if avatars are enabled
1236 1236 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1237 1237 def avatar(user, options = { })
1238 1238 if Setting.gravatar_enabled?
1239 1239 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1240 1240 email = nil
1241 1241 if user.respond_to?(:mail)
1242 1242 email = user.mail
1243 1243 elsif user.to_s =~ %r{<(.+?)>}
1244 1244 email = $1
1245 1245 end
1246 1246 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1247 1247 else
1248 1248 ''
1249 1249 end
1250 1250 end
1251 1251
1252 1252 def sanitize_anchor_name(anchor)
1253 1253 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1254 1254 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1255 1255 else
1256 1256 # TODO: remove when ruby1.8 is no longer supported
1257 1257 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1258 1258 end
1259 1259 end
1260 1260
1261 1261 # Returns the javascript tags that are included in the html layout head
1262 1262 def javascript_heads
1263 1263 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1264 1264 unless User.current.pref.warn_on_leaving_unsaved == '0'
1265 1265 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1266 1266 end
1267 1267 tags
1268 1268 end
1269 1269
1270 1270 def favicon
1271 1271 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1272 1272 end
1273 1273
1274 1274 def robot_exclusion_tag
1275 1275 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1276 1276 end
1277 1277
1278 1278 # Returns true if arg is expected in the API response
1279 1279 def include_in_api_response?(arg)
1280 1280 unless @included_in_api_response
1281 1281 param = params[:include]
1282 1282 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1283 1283 @included_in_api_response.collect!(&:strip)
1284 1284 end
1285 1285 @included_in_api_response.include?(arg.to_s)
1286 1286 end
1287 1287
1288 1288 # Returns options or nil if nometa param or X-Redmine-Nometa header
1289 1289 # was set in the request
1290 1290 def api_meta(options)
1291 1291 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1292 1292 # compatibility mode for activeresource clients that raise
1293 1293 # an error when unserializing an array with attributes
1294 1294 nil
1295 1295 else
1296 1296 options
1297 1297 end
1298 1298 end
1299 1299
1300 1300 private
1301 1301
1302 1302 def wiki_helper
1303 1303 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1304 1304 extend helper
1305 1305 return self
1306 1306 end
1307 1307
1308 1308 def link_to_content_update(text, url_params = {}, html_options = {})
1309 1309 link_to(text, url_params, html_options)
1310 1310 end
1311 1311 end
General Comments 0
You need to be logged in to leave comments. Login now