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