##// END OF EJS Templates
Merged r14108 (#19348)....
Jean-Philippe Lang -
r13727:767064c23a1c
parent child
Show More

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

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