##// END OF EJS Templates
Merged r14108 (#19348)....
Jean-Philippe Lang -
r13728:7783d7557fdb
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

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