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