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