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