##// END OF EJS Templates
Don't use a global variable for storing context menu URL....
Jean-Philippe Lang -
r15554:18073c971e69
parent child
Show More
@@ -1,1374 +1,1374
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28 include Redmine::SudoMode::Helper
29 29 include Redmine::Themes::Helper
30 30 include Redmine::Hook::Helper
31 31 include Redmine::Helpers::URL
32 32
33 33 extend Forwardable
34 34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
35 35
36 36 # Return true if user is authorized for controller/action, otherwise false
37 37 def authorize_for(controller, action)
38 38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
39 39 end
40 40
41 41 # Display a link if user is authorized
42 42 #
43 43 # @param [String] name Anchor text (passed to link_to)
44 44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
45 45 # @param [optional, Hash] html_options Options passed to link_to
46 46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
47 47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
48 48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
49 49 end
50 50
51 51 # Displays a link to user's account page if active
52 52 def link_to_user(user, options={})
53 53 if user.is_a?(User)
54 54 name = h(user.name(options[:format]))
55 55 if user.active? || (User.current.admin? && user.logged?)
56 56 link_to name, user_path(user), :class => user.css_classes
57 57 else
58 58 name
59 59 end
60 60 else
61 61 h(user.to_s)
62 62 end
63 63 end
64 64
65 65 # Displays a link to +issue+ with its subject.
66 66 # Examples:
67 67 #
68 68 # link_to_issue(issue) # => Defect #6: This is the subject
69 69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 70 # link_to_issue(issue, :subject => false) # => Defect #6
71 71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
78 78 if options[:subject] == false
79 79 title = issue.subject.truncate(60)
80 80 else
81 81 subject = issue.subject
82 82 if truncate_length = options[:truncate]
83 83 subject = subject.truncate(truncate_length)
84 84 end
85 85 end
86 86 only_path = options[:only_path].nil? ? true : options[:only_path]
87 87 s = link_to(text, issue_url(issue, :only_path => only_path),
88 88 :class => issue.css_classes, :title => title)
89 89 s << h(": #{subject}") if subject
90 90 s = h("#{issue.project} - ") + s if options[:project]
91 91 s
92 92 end
93 93
94 94 # Generates a link to an attachment.
95 95 # Options:
96 96 # * :text - Link text (default to attachment filename)
97 97 # * :download - Force download (default: false)
98 98 def link_to_attachment(attachment, options={})
99 99 text = options.delete(:text) || attachment.filename
100 100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
101 101 html_options = options.slice!(:only_path)
102 102 options[:only_path] = true unless options.key?(:only_path)
103 103 url = send(route_method, attachment, attachment.filename, options)
104 104 link_to text, url, html_options
105 105 end
106 106
107 107 # Generates a link to a SCM revision
108 108 # Options:
109 109 # * :text - Link text (default to the formatted revision)
110 110 def link_to_revision(revision, repository, options={})
111 111 if repository.is_a?(Project)
112 112 repository = repository.repository
113 113 end
114 114 text = options.delete(:text) || format_revision(revision)
115 115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
116 116 link_to(
117 117 h(text),
118 118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
119 119 :title => l(:label_revision_id, format_revision(revision)),
120 120 :accesskey => options[:accesskey]
121 121 )
122 122 end
123 123
124 124 # Generates a link to a message
125 125 def link_to_message(message, options={}, html_options = nil)
126 126 link_to(
127 127 message.subject.truncate(60),
128 128 board_message_url(message.board_id, message.parent_id || message.id, {
129 129 :r => (message.parent_id && message.id),
130 130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
131 131 :only_path => true
132 132 }.merge(options)),
133 133 html_options
134 134 )
135 135 end
136 136
137 137 # Generates a link to a project if active
138 138 # Examples:
139 139 #
140 140 # link_to_project(project) # => link to the specified project overview
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.archived?
146 146 h(project.name)
147 147 else
148 148 link_to project.name,
149 149 project_url(project, {:only_path => true}.merge(options)),
150 150 html_options
151 151 end
152 152 end
153 153
154 154 # Generates a link to a project settings if active
155 155 def link_to_project_settings(project, options={}, html_options=nil)
156 156 if project.active?
157 157 link_to project.name, settings_project_path(project, options), html_options
158 158 elsif project.archived?
159 159 h(project.name)
160 160 else
161 161 link_to project.name, project_path(project, options), html_options
162 162 end
163 163 end
164 164
165 165 # Generates a link to a version
166 166 def link_to_version(version, options = {})
167 167 return '' unless version && version.is_a?(Version)
168 168 options = {:title => format_date(version.effective_date)}.merge(options)
169 169 link_to_if version.visible?, format_version_name(version), version_path(version), options
170 170 end
171 171
172 172 # Helper that formats object for html or text rendering
173 173 def format_object(object, html=true, &block)
174 174 if block_given?
175 175 object = yield object
176 176 end
177 177 case object.class.name
178 178 when 'Array'
179 179 object.map {|o| format_object(o, html)}.join(', ').html_safe
180 180 when 'Time'
181 181 format_time(object)
182 182 when 'Date'
183 183 format_date(object)
184 184 when 'Fixnum'
185 185 object.to_s
186 186 when 'Float'
187 187 sprintf "%.2f", object
188 188 when 'User'
189 189 html ? link_to_user(object) : object.to_s
190 190 when 'Project'
191 191 html ? link_to_project(object) : object.to_s
192 192 when 'Version'
193 193 html ? link_to_version(object) : object.to_s
194 194 when 'TrueClass'
195 195 l(:general_text_Yes)
196 196 when 'FalseClass'
197 197 l(:general_text_No)
198 198 when 'Issue'
199 199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
200 200 when 'Attachment'
201 201 html ? link_to_attachment(object, :download => true) : object.filename
202 202 when 'CustomValue', 'CustomFieldValue'
203 203 if object.custom_field
204 204 f = object.custom_field.format.formatted_custom_value(self, object, html)
205 205 if f.nil? || f.is_a?(String)
206 206 f
207 207 else
208 208 format_object(f, html, &block)
209 209 end
210 210 else
211 211 object.value.to_s
212 212 end
213 213 else
214 214 html ? h(object) : object.to_s
215 215 end
216 216 end
217 217
218 218 def wiki_page_path(page, options={})
219 219 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
220 220 end
221 221
222 222 def thumbnail_tag(attachment)
223 223 link_to image_tag(thumbnail_path(attachment)),
224 224 named_attachment_path(attachment, attachment.filename),
225 225 :title => attachment.filename
226 226 end
227 227
228 228 def toggle_link(name, id, options={})
229 229 onclick = "$('##{id}').toggle(); "
230 230 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
231 231 onclick << "return false;"
232 232 link_to(name, "#", :onclick => onclick)
233 233 end
234 234
235 235 # Used to format item titles on the activity view
236 236 def format_activity_title(text)
237 237 text
238 238 end
239 239
240 240 def format_activity_day(date)
241 241 date == User.current.today ? l(:label_today).titleize : format_date(date)
242 242 end
243 243
244 244 def format_activity_description(text)
245 245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
246 246 ).gsub(/[\r\n]+/, "<br />").html_safe
247 247 end
248 248
249 249 def format_version_name(version)
250 250 if version.project == @project
251 251 h(version)
252 252 else
253 253 h("#{version.project} - #{version}")
254 254 end
255 255 end
256 256
257 257 def due_date_distance_in_words(date)
258 258 if date
259 259 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
260 260 end
261 261 end
262 262
263 263 # Renders a tree of projects as a nested set of unordered lists
264 264 # The given collection may be a subset of the whole project tree
265 265 # (eg. some intermediate nodes are private and can not be seen)
266 266 def render_project_nested_lists(projects, &block)
267 267 s = ''
268 268 if projects.any?
269 269 ancestors = []
270 270 original_project = @project
271 271 projects.sort_by(&:lft).each do |project|
272 272 # set the project environment to please macros.
273 273 @project = project
274 274 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
275 275 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
276 276 else
277 277 ancestors.pop
278 278 s << "</li>"
279 279 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
280 280 ancestors.pop
281 281 s << "</ul></li>\n"
282 282 end
283 283 end
284 284 classes = (ancestors.empty? ? 'root' : 'child')
285 285 s << "<li class='#{classes}'><div class='#{classes}'>"
286 286 s << h(block_given? ? capture(project, &block) : project.name)
287 287 s << "</div>\n"
288 288 ancestors << project
289 289 end
290 290 s << ("</li></ul>\n" * ancestors.size)
291 291 @project = original_project
292 292 end
293 293 s.html_safe
294 294 end
295 295
296 296 def render_page_hierarchy(pages, node=nil, options={})
297 297 content = ''
298 298 if pages[node]
299 299 content << "<ul class=\"pages-hierarchy\">\n"
300 300 pages[node].each do |page|
301 301 content << "<li>"
302 302 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
303 303 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
304 304 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
305 305 content << "</li>\n"
306 306 end
307 307 content << "</ul>\n"
308 308 end
309 309 content.html_safe
310 310 end
311 311
312 312 # Renders flash messages
313 313 def render_flash_messages
314 314 s = ''
315 315 flash.each do |k,v|
316 316 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
317 317 end
318 318 s.html_safe
319 319 end
320 320
321 321 # Renders tabs and their content
322 322 def render_tabs(tabs, selected=params[:tab])
323 323 if tabs.any?
324 324 unless tabs.detect {|tab| tab[:name] == selected}
325 325 selected = nil
326 326 end
327 327 selected ||= tabs.first[:name]
328 328 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
329 329 else
330 330 content_tag 'p', l(:label_no_data), :class => "nodata"
331 331 end
332 332 end
333 333
334 334 # Renders the project quick-jump box
335 335 def render_project_jump_box
336 336 return unless User.current.logged?
337 337 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
338 338 if projects.any?
339 339 options =
340 340 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
341 341 '<option value="" disabled="disabled">---</option>').html_safe
342 342
343 343 options << project_tree_options_for_select(projects, :selected => @project) do |p|
344 344 { :value => project_path(:id => p, :jump => current_menu_item) }
345 345 end
346 346
347 347 content_tag( :span, nil, :class => 'jump-box-arrow') +
348 348 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
349 349 end
350 350 end
351 351
352 352 def project_tree_options_for_select(projects, options = {})
353 353 s = ''.html_safe
354 354 if blank_text = options[:include_blank]
355 355 if blank_text == true
356 356 blank_text = '&nbsp;'.html_safe
357 357 end
358 358 s << content_tag('option', blank_text, :value => '')
359 359 end
360 360 project_tree(projects) do |project, level|
361 361 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
362 362 tag_options = {:value => project.id}
363 363 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
364 364 tag_options[:selected] = 'selected'
365 365 else
366 366 tag_options[:selected] = nil
367 367 end
368 368 tag_options.merge!(yield(project)) if block_given?
369 369 s << content_tag('option', name_prefix + h(project), tag_options)
370 370 end
371 371 s.html_safe
372 372 end
373 373
374 374 # Yields the given block for each project with its level in the tree
375 375 #
376 376 # Wrapper for Project#project_tree
377 377 def project_tree(projects, options={}, &block)
378 378 Project.project_tree(projects, options, &block)
379 379 end
380 380
381 381 def principals_check_box_tags(name, principals)
382 382 s = ''
383 383 principals.each do |principal|
384 384 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
385 385 end
386 386 s.html_safe
387 387 end
388 388
389 389 # Returns a string for users/groups option tags
390 390 def principals_options_for_select(collection, selected=nil)
391 391 s = ''
392 392 if collection.include?(User.current)
393 393 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
394 394 end
395 395 groups = ''
396 396 collection.sort.each do |element|
397 397 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
398 398 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
399 399 end
400 400 unless groups.empty?
401 401 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
402 402 end
403 403 s.html_safe
404 404 end
405 405
406 406 def option_tag(name, text, value, selected=nil, options={})
407 407 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
408 408 end
409 409
410 410 def truncate_single_line_raw(string, length)
411 411 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
412 412 end
413 413
414 414 # Truncates at line break after 250 characters or options[:length]
415 415 def truncate_lines(string, options={})
416 416 length = options[:length] || 250
417 417 if string.to_s =~ /\A(.{#{length}}.*?)$/m
418 418 "#{$1}..."
419 419 else
420 420 string
421 421 end
422 422 end
423 423
424 424 def anchor(text)
425 425 text.to_s.gsub(' ', '_')
426 426 end
427 427
428 428 def html_hours(text)
429 429 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
430 430 end
431 431
432 432 def authoring(created, author, options={})
433 433 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
434 434 end
435 435
436 436 def time_tag(time)
437 437 text = distance_of_time_in_words(Time.now, time)
438 438 if @project
439 439 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
440 440 else
441 441 content_tag('abbr', text, :title => format_time(time))
442 442 end
443 443 end
444 444
445 445 def syntax_highlight_lines(name, content)
446 446 lines = []
447 447 syntax_highlight(name, content).each_line { |line| lines << line }
448 448 lines
449 449 end
450 450
451 451 def syntax_highlight(name, content)
452 452 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
453 453 end
454 454
455 455 def to_path_param(path)
456 456 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
457 457 str.blank? ? nil : str
458 458 end
459 459
460 460 def reorder_links(name, url, method = :post)
461 461 # TODO: remove associated styles from application.css too
462 462 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
463 463
464 464 link_to(l(:label_sort_highest),
465 465 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
466 466 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
467 467 link_to(l(:label_sort_higher),
468 468 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
469 469 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
470 470 link_to(l(:label_sort_lower),
471 471 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
472 472 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
473 473 link_to(l(:label_sort_lowest),
474 474 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
475 475 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
476 476 end
477 477
478 478 def reorder_handle(object, options={})
479 479 data = {
480 480 :reorder_url => options[:url] || url_for(object),
481 481 :reorder_param => options[:param] || object.class.name.underscore
482 482 }
483 483 content_tag('span', '',
484 484 :class => "sort-handle",
485 485 :data => data,
486 486 :title => l(:button_sort))
487 487 end
488 488
489 489 def breadcrumb(*args)
490 490 elements = args.flatten
491 491 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
492 492 end
493 493
494 494 def other_formats_links(&block)
495 495 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
496 496 yield Redmine::Views::OtherFormatsBuilder.new(self)
497 497 concat('</p>'.html_safe)
498 498 end
499 499
500 500 def page_header_title
501 501 if @project.nil? || @project.new_record?
502 502 h(Setting.app_title)
503 503 else
504 504 b = []
505 505 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
506 506 if ancestors.any?
507 507 root = ancestors.shift
508 508 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
509 509 if ancestors.size > 2
510 510 b << "\xe2\x80\xa6"
511 511 ancestors = ancestors[-2, 2]
512 512 end
513 513 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
514 514 end
515 515 b << content_tag(:span, h(@project), class: 'current-project')
516 516 if b.size > 1
517 517 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
518 518 path = safe_join(b[0..-2], separator) + separator
519 519 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
520 520 end
521 521 safe_join b
522 522 end
523 523 end
524 524
525 525 # Returns a h2 tag and sets the html title with the given arguments
526 526 def title(*args)
527 527 strings = args.map do |arg|
528 528 if arg.is_a?(Array) && arg.size >= 2
529 529 link_to(*arg)
530 530 else
531 531 h(arg.to_s)
532 532 end
533 533 end
534 534 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
535 535 content_tag('h2', strings.join(' &#187; ').html_safe)
536 536 end
537 537
538 538 # Sets the html title
539 539 # Returns the html title when called without arguments
540 540 # Current project name and app_title and automatically appended
541 541 # Exemples:
542 542 # html_title 'Foo', 'Bar'
543 543 # html_title # => 'Foo - Bar - My Project - Redmine'
544 544 def html_title(*args)
545 545 if args.empty?
546 546 title = @html_title || []
547 547 title << @project.name if @project
548 548 title << Setting.app_title unless Setting.app_title == title.last
549 549 title.reject(&:blank?).join(' - ')
550 550 else
551 551 @html_title ||= []
552 552 @html_title += args
553 553 end
554 554 end
555 555
556 556 # Returns the theme, controller name, and action as css classes for the
557 557 # HTML body.
558 558 def body_css_classes
559 559 css = []
560 560 if theme = Redmine::Themes.theme(Setting.ui_theme)
561 561 css << 'theme-' + theme.name
562 562 end
563 563
564 564 css << 'project-' + @project.identifier if @project && @project.identifier.present?
565 565 css << 'controller-' + controller_name
566 566 css << 'action-' + action_name
567 567 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
568 568 css << "textarea-#{User.current.pref.textarea_font}"
569 569 end
570 570 css.join(' ')
571 571 end
572 572
573 573 def accesskey(s)
574 574 @used_accesskeys ||= []
575 575 key = Redmine::AccessKeys.key_for(s)
576 576 return nil if @used_accesskeys.include?(key)
577 577 @used_accesskeys << key
578 578 key
579 579 end
580 580
581 581 # Formats text according to system settings.
582 582 # 2 ways to call this method:
583 583 # * with a String: textilizable(text, options)
584 584 # * with an object and one of its attribute: textilizable(issue, :description, options)
585 585 def textilizable(*args)
586 586 options = args.last.is_a?(Hash) ? args.pop : {}
587 587 case args.size
588 588 when 1
589 589 obj = options[:object]
590 590 text = args.shift
591 591 when 2
592 592 obj = args.shift
593 593 attr = args.shift
594 594 text = obj.send(attr).to_s
595 595 else
596 596 raise ArgumentError, 'invalid arguments to textilizable'
597 597 end
598 598 return '' if text.blank?
599 599 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
600 600 @only_path = only_path = options.delete(:only_path) == false ? false : true
601 601
602 602 text = text.dup
603 603 macros = catch_macros(text)
604 604 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
605 605
606 606 @parsed_headings = []
607 607 @heading_anchors = {}
608 608 @current_section = 0 if options[:edit_section_links]
609 609
610 610 parse_sections(text, project, obj, attr, only_path, options)
611 611 text = parse_non_pre_blocks(text, obj, macros) do |text|
612 612 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
613 613 send method_name, text, project, obj, attr, only_path, options
614 614 end
615 615 end
616 616 parse_headings(text, project, obj, attr, only_path, options)
617 617
618 618 if @parsed_headings.any?
619 619 replace_toc(text, @parsed_headings)
620 620 end
621 621
622 622 text.html_safe
623 623 end
624 624
625 625 def parse_non_pre_blocks(text, obj, macros)
626 626 s = StringScanner.new(text)
627 627 tags = []
628 628 parsed = ''
629 629 while !s.eos?
630 630 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
631 631 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
632 632 if tags.empty?
633 633 yield text
634 634 inject_macros(text, obj, macros) if macros.any?
635 635 else
636 636 inject_macros(text, obj, macros, false) if macros.any?
637 637 end
638 638 parsed << text
639 639 if tag
640 640 if closing
641 641 if tags.last && tags.last.casecmp(tag) == 0
642 642 tags.pop
643 643 end
644 644 else
645 645 tags << tag.downcase
646 646 end
647 647 parsed << full_tag
648 648 end
649 649 end
650 650 # Close any non closing tags
651 651 while tag = tags.pop
652 652 parsed << "</#{tag}>"
653 653 end
654 654 parsed
655 655 end
656 656
657 657 def parse_inline_attachments(text, project, obj, attr, only_path, options)
658 658 return if options[:inline_attachments] == false
659 659
660 660 # when using an image link, try to use an attachment, if possible
661 661 attachments = options[:attachments] || []
662 662 attachments += obj.attachments if obj.respond_to?(:attachments)
663 663 if attachments.present?
664 664 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
665 665 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
666 666 # search for the picture in attachments
667 667 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
668 668 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
669 669 desc = found.description.to_s.gsub('"', '')
670 670 if !desc.blank? && alttext.blank?
671 671 alt = " title=\"#{desc}\" alt=\"#{desc}\""
672 672 end
673 673 "src=\"#{image_url}\"#{alt}"
674 674 else
675 675 m
676 676 end
677 677 end
678 678 end
679 679 end
680 680
681 681 # Wiki links
682 682 #
683 683 # Examples:
684 684 # [[mypage]]
685 685 # [[mypage|mytext]]
686 686 # wiki links can refer other project wikis, using project name or identifier:
687 687 # [[project:]] -> wiki starting page
688 688 # [[project:|mytext]]
689 689 # [[project:mypage]]
690 690 # [[project:mypage|mytext]]
691 691 def parse_wiki_links(text, project, obj, attr, only_path, options)
692 692 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
693 693 link_project = project
694 694 esc, all, page, title = $1, $2, $3, $5
695 695 if esc.nil?
696 696 if page =~ /^([^\:]+)\:(.*)$/
697 697 identifier, page = $1, $2
698 698 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
699 699 title ||= identifier if page.blank?
700 700 end
701 701
702 702 if link_project && link_project.wiki
703 703 # extract anchor
704 704 anchor = nil
705 705 if page =~ /^(.+?)\#(.+)$/
706 706 page, anchor = $1, $2
707 707 end
708 708 anchor = sanitize_anchor_name(anchor) if anchor.present?
709 709 # check if page exists
710 710 wiki_page = link_project.wiki.find_page(page)
711 711 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
712 712 "##{anchor}"
713 713 else
714 714 case options[:wiki_links]
715 715 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
716 716 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
717 717 else
718 718 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
719 719 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
720 720 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
721 721 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
722 722 end
723 723 end
724 724 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
725 725 else
726 726 # project or wiki doesn't exist
727 727 all
728 728 end
729 729 else
730 730 all
731 731 end
732 732 end
733 733 end
734 734
735 735 # Redmine links
736 736 #
737 737 # Examples:
738 738 # Issues:
739 739 # #52 -> Link to issue #52
740 740 # Changesets:
741 741 # r52 -> Link to revision 52
742 742 # commit:a85130f -> Link to scmid starting with a85130f
743 743 # Documents:
744 744 # document#17 -> Link to document with id 17
745 745 # document:Greetings -> Link to the document with title "Greetings"
746 746 # document:"Some document" -> Link to the document with title "Some document"
747 747 # Versions:
748 748 # version#3 -> Link to version with id 3
749 749 # version:1.0.0 -> Link to version named "1.0.0"
750 750 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
751 751 # Attachments:
752 752 # attachment:file.zip -> Link to the attachment of the current object named file.zip
753 753 # Source files:
754 754 # source:some/file -> Link to the file located at /some/file in the project's repository
755 755 # source:some/file@52 -> Link to the file's revision 52
756 756 # source:some/file#L120 -> Link to line 120 of the file
757 757 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
758 758 # export:some/file -> Force the download of the file
759 759 # Forum messages:
760 760 # message#1218 -> Link to message with id 1218
761 761 # Projects:
762 762 # project:someproject -> Link to project named "someproject"
763 763 # project#3 -> Link to project with id 3
764 764 #
765 765 # Links can refer other objects from other projects, using project identifier:
766 766 # identifier:r52
767 767 # identifier:document:"Some document"
768 768 # identifier:version:1.0.0
769 769 # identifier:source:some/file
770 770 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
771 771 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
772 772 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
773 773 if tag_content
774 774 $&
775 775 else
776 776 link = nil
777 777 project = default_project
778 778 if project_identifier
779 779 project = Project.visible.find_by_identifier(project_identifier)
780 780 end
781 781 if esc.nil?
782 782 if prefix.nil? && sep == 'r'
783 783 if project
784 784 repository = nil
785 785 if repo_identifier
786 786 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
787 787 else
788 788 repository = project.repository
789 789 end
790 790 # project.changesets.visible raises an SQL error because of a double join on repositories
791 791 if repository &&
792 792 (changeset = Changeset.visible.
793 793 find_by_repository_id_and_revision(repository.id, identifier))
794 794 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
795 795 {:only_path => only_path, :controller => 'repositories',
796 796 :action => 'revision', :id => project,
797 797 :repository_id => repository.identifier_param,
798 798 :rev => changeset.revision},
799 799 :class => 'changeset',
800 800 :title => truncate_single_line_raw(changeset.comments, 100))
801 801 end
802 802 end
803 803 elsif sep == '#'
804 804 oid = identifier.to_i
805 805 case prefix
806 806 when nil
807 807 if oid.to_s == identifier &&
808 808 issue = Issue.visible.find_by_id(oid)
809 809 anchor = comment_id ? "note-#{comment_id}" : nil
810 810 link = link_to("##{oid}#{comment_suffix}",
811 811 issue_url(issue, :only_path => only_path, :anchor => anchor),
812 812 :class => issue.css_classes,
813 813 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
814 814 end
815 815 when 'document'
816 816 if document = Document.visible.find_by_id(oid)
817 817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
818 818 end
819 819 when 'version'
820 820 if version = Version.visible.find_by_id(oid)
821 821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
822 822 end
823 823 when 'message'
824 824 if message = Message.visible.find_by_id(oid)
825 825 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
826 826 end
827 827 when 'forum'
828 828 if board = Board.visible.find_by_id(oid)
829 829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
830 830 end
831 831 when 'news'
832 832 if news = News.visible.find_by_id(oid)
833 833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
834 834 end
835 835 when 'project'
836 836 if p = Project.visible.find_by_id(oid)
837 837 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
838 838 end
839 839 end
840 840 elsif sep == ':'
841 841 # removes the double quotes if any
842 842 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
843 843 name = CGI.unescapeHTML(name)
844 844 case prefix
845 845 when 'document'
846 846 if project && document = project.documents.visible.find_by_title(name)
847 847 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
848 848 end
849 849 when 'version'
850 850 if project && version = project.versions.visible.find_by_name(name)
851 851 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
852 852 end
853 853 when 'forum'
854 854 if project && board = project.boards.visible.find_by_name(name)
855 855 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
856 856 end
857 857 when 'news'
858 858 if project && news = project.news.visible.find_by_title(name)
859 859 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
860 860 end
861 861 when 'commit', 'source', 'export'
862 862 if project
863 863 repository = nil
864 864 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
865 865 repo_prefix, repo_identifier, name = $1, $2, $3
866 866 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
867 867 else
868 868 repository = project.repository
869 869 end
870 870 if prefix == 'commit'
871 871 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
872 872 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},
873 873 :class => 'changeset',
874 874 :title => truncate_single_line_raw(changeset.comments, 100)
875 875 end
876 876 else
877 877 if repository && User.current.allowed_to?(:browse_repository, project)
878 878 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
879 879 path, rev, anchor = $1, $3, $5
880 880 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
881 881 :path => to_path_param(path),
882 882 :rev => rev,
883 883 :anchor => anchor},
884 884 :class => (prefix == 'export' ? 'source download' : 'source')
885 885 end
886 886 end
887 887 repo_prefix = nil
888 888 end
889 889 when 'attachment'
890 890 attachments = options[:attachments] || []
891 891 attachments += obj.attachments if obj.respond_to?(:attachments)
892 892 if attachments && attachment = Attachment.latest_attach(attachments, name)
893 893 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
894 894 end
895 895 when 'project'
896 896 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
897 897 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
898 898 end
899 899 end
900 900 end
901 901 end
902 902 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
903 903 end
904 904 end
905 905 end
906 906
907 907 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
908 908
909 909 def parse_sections(text, project, obj, attr, only_path, options)
910 910 return unless options[:edit_section_links]
911 911 text.gsub!(HEADING_RE) do
912 912 heading, level = $1, $2
913 913 @current_section += 1
914 914 if @current_section > 1
915 915 content_tag('div',
916 916 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
917 917 :class => 'icon-only icon-edit'),
918 918 :class => "contextual heading-#{level}",
919 919 :title => l(:button_edit_section),
920 920 :id => "section-#{@current_section}") + heading.html_safe
921 921 else
922 922 heading
923 923 end
924 924 end
925 925 end
926 926
927 927 # Headings and TOC
928 928 # Adds ids and links to headings unless options[:headings] is set to false
929 929 def parse_headings(text, project, obj, attr, only_path, options)
930 930 return if options[:headings] == false
931 931
932 932 text.gsub!(HEADING_RE) do
933 933 level, attrs, content = $2.to_i, $3, $4
934 934 item = strip_tags(content).strip
935 935 anchor = sanitize_anchor_name(item)
936 936 # used for single-file wiki export
937 937 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
938 938 @heading_anchors[anchor] ||= 0
939 939 idx = (@heading_anchors[anchor] += 1)
940 940 if idx > 1
941 941 anchor = "#{anchor}-#{idx}"
942 942 end
943 943 @parsed_headings << [level, anchor, item]
944 944 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
945 945 end
946 946 end
947 947
948 948 MACROS_RE = /(
949 949 (!)? # escaping
950 950 (
951 951 \{\{ # opening tag
952 952 ([\w]+) # macro name
953 953 (\(([^\n\r]*?)\))? # optional arguments
954 954 ([\n\r].*?[\n\r])? # optional block of text
955 955 \}\} # closing tag
956 956 )
957 957 )/mx unless const_defined?(:MACROS_RE)
958 958
959 959 MACRO_SUB_RE = /(
960 960 \{\{
961 961 macro\((\d+)\)
962 962 \}\}
963 963 )/x unless const_defined?(:MACRO_SUB_RE)
964 964
965 965 # Extracts macros from text
966 966 def catch_macros(text)
967 967 macros = {}
968 968 text.gsub!(MACROS_RE) do
969 969 all, macro = $1, $4.downcase
970 970 if macro_exists?(macro) || all =~ MACRO_SUB_RE
971 971 index = macros.size
972 972 macros[index] = all
973 973 "{{macro(#{index})}}"
974 974 else
975 975 all
976 976 end
977 977 end
978 978 macros
979 979 end
980 980
981 981 # Executes and replaces macros in text
982 982 def inject_macros(text, obj, macros, execute=true)
983 983 text.gsub!(MACRO_SUB_RE) do
984 984 all, index = $1, $2.to_i
985 985 orig = macros.delete(index)
986 986 if execute && orig && orig =~ MACROS_RE
987 987 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
988 988 if esc.nil?
989 989 h(exec_macro(macro, obj, args, block) || all)
990 990 else
991 991 h(all)
992 992 end
993 993 elsif orig
994 994 h(orig)
995 995 else
996 996 h(all)
997 997 end
998 998 end
999 999 end
1000 1000
1001 1001 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
1002 1002
1003 1003 # Renders the TOC with given headings
1004 1004 def replace_toc(text, headings)
1005 1005 text.gsub!(TOC_RE) do
1006 1006 left_align, right_align = $2, $3
1007 1007 # Keep only the 4 first levels
1008 1008 headings = headings.select{|level, anchor, item| level <= 4}
1009 1009 if headings.empty?
1010 1010 ''
1011 1011 else
1012 1012 div_class = 'toc'
1013 1013 div_class << ' right' if right_align
1014 1014 div_class << ' left' if left_align
1015 1015 out = "<ul class=\"#{div_class}\"><li>"
1016 1016 root = headings.map(&:first).min
1017 1017 current = root
1018 1018 started = false
1019 1019 headings.each do |level, anchor, item|
1020 1020 if level > current
1021 1021 out << '<ul><li>' * (level - current)
1022 1022 elsif level < current
1023 1023 out << "</li></ul>\n" * (current - level) + "</li><li>"
1024 1024 elsif started
1025 1025 out << '</li><li>'
1026 1026 end
1027 1027 out << "<a href=\"##{anchor}\">#{item}</a>"
1028 1028 current = level
1029 1029 started = true
1030 1030 end
1031 1031 out << '</li></ul>' * (current - root)
1032 1032 out << '</li></ul>'
1033 1033 end
1034 1034 end
1035 1035 end
1036 1036
1037 1037 # Same as Rails' simple_format helper without using paragraphs
1038 1038 def simple_format_without_paragraph(text)
1039 1039 text.to_s.
1040 1040 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1041 1041 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1042 1042 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1043 1043 html_safe
1044 1044 end
1045 1045
1046 1046 def lang_options_for_select(blank=true)
1047 1047 (blank ? [["(auto)", ""]] : []) + languages_options
1048 1048 end
1049 1049
1050 1050 def labelled_form_for(*args, &proc)
1051 1051 args << {} unless args.last.is_a?(Hash)
1052 1052 options = args.last
1053 1053 if args.first.is_a?(Symbol)
1054 1054 options.merge!(:as => args.shift)
1055 1055 end
1056 1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1057 1057 form_for(*args, &proc)
1058 1058 end
1059 1059
1060 1060 def labelled_fields_for(*args, &proc)
1061 1061 args << {} unless args.last.is_a?(Hash)
1062 1062 options = args.last
1063 1063 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1064 1064 fields_for(*args, &proc)
1065 1065 end
1066 1066
1067 1067 # Render the error messages for the given objects
1068 1068 def error_messages_for(*objects)
1069 1069 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1070 1070 errors = objects.map {|o| o.errors.full_messages}.flatten
1071 1071 render_error_messages(errors)
1072 1072 end
1073 1073
1074 1074 # Renders a list of error messages
1075 1075 def render_error_messages(errors)
1076 1076 html = ""
1077 1077 if errors.present?
1078 1078 html << "<div id='errorExplanation'><ul>\n"
1079 1079 errors.each do |error|
1080 1080 html << "<li>#{h error}</li>\n"
1081 1081 end
1082 1082 html << "</ul></div>\n"
1083 1083 end
1084 1084 html.html_safe
1085 1085 end
1086 1086
1087 1087 def delete_link(url, options={})
1088 1088 options = {
1089 1089 :method => :delete,
1090 1090 :data => {:confirm => l(:text_are_you_sure)},
1091 1091 :class => 'icon icon-del'
1092 1092 }.merge(options)
1093 1093
1094 1094 link_to l(:button_delete), url, options
1095 1095 end
1096 1096
1097 1097 def preview_link(url, form, target='preview', options={})
1098 1098 content_tag 'a', l(:label_preview), {
1099 1099 :href => "#",
1100 1100 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1101 1101 :accesskey => accesskey(:preview)
1102 1102 }.merge(options)
1103 1103 end
1104 1104
1105 1105 def link_to_function(name, function, html_options={})
1106 1106 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1107 1107 end
1108 1108
1109 1109 # Helper to render JSON in views
1110 1110 def raw_json(arg)
1111 1111 arg.to_json.to_s.gsub('/', '\/').html_safe
1112 1112 end
1113 1113
1114 1114 def back_url
1115 1115 url = params[:back_url]
1116 1116 if url.nil? && referer = request.env['HTTP_REFERER']
1117 1117 url = CGI.unescape(referer.to_s)
1118 1118 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1119 1119 # parsed as invalid by URI.parse so the redirect to the back URL would
1120 1120 # not be accepted (ApplicationController#validate_back_url would return
1121 1121 # false)
1122 1122 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1123 1123 end
1124 1124 url
1125 1125 end
1126 1126
1127 1127 def back_url_hidden_field_tag
1128 1128 url = back_url
1129 1129 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1130 1130 end
1131 1131
1132 1132 def check_all_links(form_name)
1133 1133 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1134 1134 " | ".html_safe +
1135 1135 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1136 1136 end
1137 1137
1138 1138 def toggle_checkboxes_link(selector)
1139 1139 link_to_function '',
1140 1140 "toggleCheckboxesBySelector('#{selector}')",
1141 1141 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1142 1142 :class => 'toggle-checkboxes'
1143 1143 end
1144 1144
1145 1145 def progress_bar(pcts, options={})
1146 1146 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1147 1147 pcts = pcts.collect(&:round)
1148 1148 pcts[1] = pcts[1] - pcts[0]
1149 1149 pcts << (100 - pcts[1] - pcts[0])
1150 1150 titles = options[:titles].to_a
1151 1151 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1152 1152 legend = options[:legend] || ''
1153 1153 content_tag('table',
1154 1154 content_tag('tr',
1155 1155 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1156 1156 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1157 1157 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1158 1158 ), :class => "progress progress-#{pcts[0]}").html_safe +
1159 1159 content_tag('p', legend, :class => 'percent').html_safe
1160 1160 end
1161 1161
1162 1162 def checked_image(checked=true)
1163 1163 if checked
1164 1164 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1165 1165 end
1166 1166 end
1167 1167
1168 def context_menu(url)
1168 def context_menu
1169 1169 unless @context_menu_included
1170 1170 content_for :header_tags do
1171 1171 javascript_include_tag('context_menu') +
1172 1172 stylesheet_link_tag('context_menu')
1173 1173 end
1174 1174 if l(:direction) == 'rtl'
1175 1175 content_for :header_tags do
1176 1176 stylesheet_link_tag('context_menu_rtl')
1177 1177 end
1178 1178 end
1179 1179 @context_menu_included = true
1180 1180 end
1181 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1181 nil
1182 1182 end
1183 1183
1184 1184 def calendar_for(field_id)
1185 1185 include_calendar_headers_tags
1186 1186 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1187 1187 end
1188 1188
1189 1189 def include_calendar_headers_tags
1190 1190 unless @calendar_headers_tags_included
1191 1191 tags = ''.html_safe
1192 1192 @calendar_headers_tags_included = true
1193 1193 content_for :header_tags do
1194 1194 start_of_week = Setting.start_of_week
1195 1195 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1196 1196 # Redmine uses 1..7 (monday..sunday) in settings and locales
1197 1197 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1198 1198 start_of_week = start_of_week.to_i % 7
1199 1199 tags << javascript_tag(
1200 1200 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1201 1201 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1202 1202 path_to_image('/images/calendar.png') +
1203 1203 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1204 1204 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1205 1205 "beforeShow: beforeShowDatePicker};")
1206 1206 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1207 1207 unless jquery_locale == 'en'
1208 1208 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1209 1209 end
1210 1210 tags
1211 1211 end
1212 1212 end
1213 1213 end
1214 1214
1215 1215 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1216 1216 # Examples:
1217 1217 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1218 1218 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1219 1219 #
1220 1220 def stylesheet_link_tag(*sources)
1221 1221 options = sources.last.is_a?(Hash) ? sources.pop : {}
1222 1222 plugin = options.delete(:plugin)
1223 1223 sources = sources.map do |source|
1224 1224 if plugin
1225 1225 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1226 1226 elsif current_theme && current_theme.stylesheets.include?(source)
1227 1227 current_theme.stylesheet_path(source)
1228 1228 else
1229 1229 source
1230 1230 end
1231 1231 end
1232 1232 super *sources, options
1233 1233 end
1234 1234
1235 1235 # Overrides Rails' image_tag with themes and plugins support.
1236 1236 # Examples:
1237 1237 # image_tag('image.png') # => picks image.png from the current theme or defaults
1238 1238 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1239 1239 #
1240 1240 def image_tag(source, options={})
1241 1241 if plugin = options.delete(:plugin)
1242 1242 source = "/plugin_assets/#{plugin}/images/#{source}"
1243 1243 elsif current_theme && current_theme.images.include?(source)
1244 1244 source = current_theme.image_path(source)
1245 1245 end
1246 1246 super source, options
1247 1247 end
1248 1248
1249 1249 # Overrides Rails' javascript_include_tag with plugins support
1250 1250 # Examples:
1251 1251 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1252 1252 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1253 1253 #
1254 1254 def javascript_include_tag(*sources)
1255 1255 options = sources.last.is_a?(Hash) ? sources.pop : {}
1256 1256 if plugin = options.delete(:plugin)
1257 1257 sources = sources.map do |source|
1258 1258 if plugin
1259 1259 "/plugin_assets/#{plugin}/javascripts/#{source}"
1260 1260 else
1261 1261 source
1262 1262 end
1263 1263 end
1264 1264 end
1265 1265 super *sources, options
1266 1266 end
1267 1267
1268 1268 def sidebar_content?
1269 1269 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1270 1270 end
1271 1271
1272 1272 def view_layouts_base_sidebar_hook_response
1273 1273 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1274 1274 end
1275 1275
1276 1276 def email_delivery_enabled?
1277 1277 !!ActionMailer::Base.perform_deliveries
1278 1278 end
1279 1279
1280 1280 # Returns the avatar image tag for the given +user+ if avatars are enabled
1281 1281 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1282 1282 def avatar(user, options = { })
1283 1283 if Setting.gravatar_enabled?
1284 1284 options.merge!(:default => Setting.gravatar_default)
1285 1285 email = nil
1286 1286 if user.respond_to?(:mail)
1287 1287 email = user.mail
1288 1288 elsif user.to_s =~ %r{<(.+?)>}
1289 1289 email = $1
1290 1290 end
1291 1291 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1292 1292 else
1293 1293 ''
1294 1294 end
1295 1295 end
1296 1296
1297 1297 # Returns a link to edit user's avatar if avatars are enabled
1298 1298 def avatar_edit_link(user, options={})
1299 1299 if Setting.gravatar_enabled?
1300 1300 url = "https://gravatar.com"
1301 1301 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1302 1302 end
1303 1303 end
1304 1304
1305 1305 def sanitize_anchor_name(anchor)
1306 1306 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1307 1307 end
1308 1308
1309 1309 # Returns the javascript tags that are included in the html layout head
1310 1310 def javascript_heads
1311 1311 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1312 1312 unless User.current.pref.warn_on_leaving_unsaved == '0'
1313 1313 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1314 1314 end
1315 1315 tags
1316 1316 end
1317 1317
1318 1318 def favicon
1319 1319 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1320 1320 end
1321 1321
1322 1322 # Returns the path to the favicon
1323 1323 def favicon_path
1324 1324 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1325 1325 image_path(icon)
1326 1326 end
1327 1327
1328 1328 # Returns the full URL to the favicon
1329 1329 def favicon_url
1330 1330 # TODO: use #image_url introduced in Rails4
1331 1331 path = favicon_path
1332 1332 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1333 1333 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1334 1334 end
1335 1335
1336 1336 def robot_exclusion_tag
1337 1337 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1338 1338 end
1339 1339
1340 1340 # Returns true if arg is expected in the API response
1341 1341 def include_in_api_response?(arg)
1342 1342 unless @included_in_api_response
1343 1343 param = params[:include]
1344 1344 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1345 1345 @included_in_api_response.collect!(&:strip)
1346 1346 end
1347 1347 @included_in_api_response.include?(arg.to_s)
1348 1348 end
1349 1349
1350 1350 # Returns options or nil if nometa param or X-Redmine-Nometa header
1351 1351 # was set in the request
1352 1352 def api_meta(options)
1353 1353 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1354 1354 # compatibility mode for activeresource clients that raise
1355 1355 # an error when deserializing an array with attributes
1356 1356 nil
1357 1357 else
1358 1358 options
1359 1359 end
1360 1360 end
1361 1361
1362 1362 def generate_csv(&block)
1363 1363 decimal_separator = l(:general_csv_decimal_separator)
1364 1364 encoding = l(:general_csv_encoding)
1365 1365 end
1366 1366
1367 1367 private
1368 1368
1369 1369 def wiki_helper
1370 1370 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1371 1371 extend helper
1372 1372 return self
1373 1373 end
1374 1374 end
@@ -1,484 +1,484
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2016 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 IssuesHelper
21 21 include ApplicationHelper
22 22 include Redmine::Export::PDF::IssuesPdfHelper
23 23
24 24 def issue_list(issues, &block)
25 25 ancestors = []
26 26 issues.each do |issue|
27 27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
28 28 ancestors.pop
29 29 end
30 30 yield issue, ancestors.size
31 31 ancestors << issue unless issue.leaf?
32 32 end
33 33 end
34 34
35 35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
36 36 ancestors = []
37 37 grouped_query_results(issues, query, issue_count_by_group) do |issue, group_name, group_count, group_totals|
38 38 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
39 39 ancestors.pop
40 40 end
41 41 yield issue, ancestors.size, group_name, group_count, group_totals
42 42 ancestors << issue unless issue.leaf?
43 43 end
44 44 end
45 45
46 46 # Renders a HTML/CSS tooltip
47 47 #
48 48 # To use, a trigger div is needed. This is a div with the class of "tooltip"
49 49 # that contains this method wrapped in a span with the class of "tip"
50 50 #
51 51 # <div class="tooltip"><%= link_to_issue(issue) %>
52 52 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
53 53 # </div>
54 54 #
55 55 def render_issue_tooltip(issue)
56 56 @cached_label_status ||= l(:field_status)
57 57 @cached_label_start_date ||= l(:field_start_date)
58 58 @cached_label_due_date ||= l(:field_due_date)
59 59 @cached_label_assigned_to ||= l(:field_assigned_to)
60 60 @cached_label_priority ||= l(:field_priority)
61 61 @cached_label_project ||= l(:field_project)
62 62
63 63 link_to_issue(issue) + "<br /><br />".html_safe +
64 64 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
65 65 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
66 66 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
67 67 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
68 68 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
69 69 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
70 70 end
71 71
72 72 def issue_heading(issue)
73 73 h("#{issue.tracker} ##{issue.id}")
74 74 end
75 75
76 76 def render_issue_subject_with_tree(issue)
77 77 s = ''
78 78 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
79 79 ancestors.each do |ancestor|
80 80 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
81 81 end
82 82 s << '<div>'
83 83 subject = h(issue.subject)
84 84 if issue.is_private?
85 85 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
86 86 end
87 87 s << content_tag('h3', subject)
88 88 s << '</div>' * (ancestors.size + 1)
89 89 s.html_safe
90 90 end
91 91
92 92 def render_descendants_tree(issue)
93 s = '<form><table class="list issues">'
93 s = '<table class="list issues">'
94 94 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level|
95 95 css = "issue issue-#{child.id} hascontextmenu #{child.css_classes}"
96 96 css << " idnt idnt-#{level}" if level > 0
97 97 s << content_tag('tr',
98 98 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
99 99 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
100 100 content_tag('td', h(child.status), :class => 'status') +
101 101 content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
102 102 content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio), :class=> 'done_ratio'),
103 103 :class => css)
104 104 end
105 s << '</table></form>'
105 s << '</table>'
106 106 s.html_safe
107 107 end
108 108
109 109 def issue_estimated_hours_details(issue)
110 110 if issue.total_estimated_hours.present?
111 111 if issue.total_estimated_hours == issue.estimated_hours
112 112 l_hours_short(issue.estimated_hours)
113 113 else
114 114 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
115 115 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
116 116 s.html_safe
117 117 end
118 118 end
119 119 end
120 120
121 121 def issue_spent_hours_details(issue)
122 122 if issue.total_spent_hours > 0
123 123 path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
124 124
125 125 if issue.total_spent_hours == issue.spent_hours
126 126 link_to(l_hours_short(issue.spent_hours), path)
127 127 else
128 128 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
129 129 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
130 130 s.html_safe
131 131 end
132 132 end
133 133 end
134 134
135 135 # Returns an array of error messages for bulk edited issues
136 136 def bulk_edit_error_messages(issues)
137 137 messages = {}
138 138 issues.each do |issue|
139 139 issue.errors.full_messages.each do |message|
140 140 messages[message] ||= []
141 141 messages[message] << issue
142 142 end
143 143 end
144 144 messages.map { |message, issues|
145 145 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
146 146 }
147 147 end
148 148
149 149 # Returns a link for adding a new subtask to the given issue
150 150 def link_to_new_subtask(issue)
151 151 attrs = {
152 152 :parent_issue_id => issue
153 153 }
154 154 attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
155 155 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
156 156 end
157 157
158 158 def trackers_options_for_select(issue)
159 159 trackers = issue.allowed_target_trackers
160 160 if issue.new_record? && issue.parent_issue_id.present?
161 161 trackers = trackers.reject do |tracker|
162 162 issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
163 163 end
164 164 end
165 165 trackers.collect {|t| [t.name, t.id]}
166 166 end
167 167
168 168 class IssueFieldsRows
169 169 include ActionView::Helpers::TagHelper
170 170
171 171 def initialize
172 172 @left = []
173 173 @right = []
174 174 end
175 175
176 176 def left(*args)
177 177 args.any? ? @left << cells(*args) : @left
178 178 end
179 179
180 180 def right(*args)
181 181 args.any? ? @right << cells(*args) : @right
182 182 end
183 183
184 184 def size
185 185 @left.size > @right.size ? @left.size : @right.size
186 186 end
187 187
188 188 def to_html
189 189 content =
190 190 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
191 191 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
192 192
193 193 content_tag('div', content, :class => 'splitcontent')
194 194 end
195 195
196 196 def cells(label, text, options={})
197 197 options[:class] = [options[:class] || "", 'attribute'].join(' ')
198 198 content_tag 'div',
199 199 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
200 200 options
201 201 end
202 202 end
203 203
204 204 def issue_fields_rows
205 205 r = IssueFieldsRows.new
206 206 yield r
207 207 r.to_html
208 208 end
209 209
210 210 def render_custom_fields_rows(issue)
211 211 values = issue.visible_custom_field_values
212 212 return if values.empty?
213 213 half = (values.size / 2.0).ceil
214 214 issue_fields_rows do |rows|
215 215 values.each_with_index do |value, i|
216 216 css = "cf_#{value.custom_field.id}"
217 217 m = (i < half ? :left : :right)
218 218 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
219 219 end
220 220 end
221 221 end
222 222
223 223 # Returns the path for updating the issue form
224 224 # with project as the current project
225 225 def update_issue_form_path(project, issue)
226 226 options = {:format => 'js'}
227 227 if issue.new_record?
228 228 if project
229 229 new_project_issue_path(project, options)
230 230 else
231 231 new_issue_path(options)
232 232 end
233 233 else
234 234 edit_issue_path(issue, options)
235 235 end
236 236 end
237 237
238 238 # Returns the number of descendants for an array of issues
239 239 def issues_descendant_count(issues)
240 240 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
241 241 ids -= issues.map(&:id)
242 242 ids.size
243 243 end
244 244
245 245 def issues_destroy_confirmation_message(issues)
246 246 issues = [issues] unless issues.is_a?(Array)
247 247 message = l(:text_issues_destroy_confirmation)
248 248
249 249 descendant_count = issues_descendant_count(issues)
250 250 if descendant_count > 0
251 251 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
252 252 end
253 253 message
254 254 end
255 255
256 256 # Returns an array of users that are proposed as watchers
257 257 # on the new issue form
258 258 def users_for_new_issue_watchers(issue)
259 259 users = issue.watcher_users
260 260 if issue.project.users.count <= 20
261 261 users = (users + issue.project.users.sort).uniq
262 262 end
263 263 users
264 264 end
265 265
266 266 def email_issue_attributes(issue, user)
267 267 items = []
268 268 %w(author status priority assigned_to category fixed_version).each do |attribute|
269 269 unless issue.disabled_core_fields.include?(attribute+"_id")
270 270 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
271 271 end
272 272 end
273 273 issue.visible_custom_field_values(user).each do |value|
274 274 items << "#{value.custom_field.name}: #{show_value(value, false)}"
275 275 end
276 276 items
277 277 end
278 278
279 279 def render_email_issue_attributes(issue, user, html=false)
280 280 items = email_issue_attributes(issue, user)
281 281 if html
282 282 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
283 283 else
284 284 items.map{|s| "* #{s}"}.join("\n")
285 285 end
286 286 end
287 287
288 288 # Returns the textual representation of a journal details
289 289 # as an array of strings
290 290 def details_to_strings(details, no_html=false, options={})
291 291 options[:only_path] = (options[:only_path] == false ? false : true)
292 292 strings = []
293 293 values_by_field = {}
294 294 details.each do |detail|
295 295 if detail.property == 'cf'
296 296 field = detail.custom_field
297 297 if field && field.multiple?
298 298 values_by_field[field] ||= {:added => [], :deleted => []}
299 299 if detail.old_value
300 300 values_by_field[field][:deleted] << detail.old_value
301 301 end
302 302 if detail.value
303 303 values_by_field[field][:added] << detail.value
304 304 end
305 305 next
306 306 end
307 307 end
308 308 strings << show_detail(detail, no_html, options)
309 309 end
310 310 if values_by_field.present?
311 311 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
312 312 values_by_field.each do |field, changes|
313 313 if changes[:added].any?
314 314 detail = multiple_values_detail.new('cf', field.id.to_s, field)
315 315 detail.value = changes[:added]
316 316 strings << show_detail(detail, no_html, options)
317 317 end
318 318 if changes[:deleted].any?
319 319 detail = multiple_values_detail.new('cf', field.id.to_s, field)
320 320 detail.old_value = changes[:deleted]
321 321 strings << show_detail(detail, no_html, options)
322 322 end
323 323 end
324 324 end
325 325 strings
326 326 end
327 327
328 328 # Returns the textual representation of a single journal detail
329 329 def show_detail(detail, no_html=false, options={})
330 330 multiple = false
331 331 show_diff = false
332 332 no_details = false
333 333
334 334 case detail.property
335 335 when 'attr'
336 336 field = detail.prop_key.to_s.gsub(/\_id$/, "")
337 337 label = l(("field_" + field).to_sym)
338 338 case detail.prop_key
339 339 when 'due_date', 'start_date'
340 340 value = format_date(detail.value.to_date) if detail.value
341 341 old_value = format_date(detail.old_value.to_date) if detail.old_value
342 342
343 343 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
344 344 'priority_id', 'category_id', 'fixed_version_id'
345 345 value = find_name_by_reflection(field, detail.value)
346 346 old_value = find_name_by_reflection(field, detail.old_value)
347 347
348 348 when 'estimated_hours'
349 349 value = l_hours_short(detail.value.to_f) unless detail.value.blank?
350 350 old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
351 351
352 352 when 'parent_id'
353 353 label = l(:field_parent_issue)
354 354 value = "##{detail.value}" unless detail.value.blank?
355 355 old_value = "##{detail.old_value}" unless detail.old_value.blank?
356 356
357 357 when 'is_private'
358 358 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
359 359 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
360 360
361 361 when 'description'
362 362 show_diff = true
363 363 end
364 364 when 'cf'
365 365 custom_field = detail.custom_field
366 366 if custom_field
367 367 label = custom_field.name
368 368 if custom_field.format.class.change_no_details
369 369 no_details = true
370 370 elsif custom_field.format.class.change_as_diff
371 371 show_diff = true
372 372 else
373 373 multiple = custom_field.multiple?
374 374 value = format_value(detail.value, custom_field) if detail.value
375 375 old_value = format_value(detail.old_value, custom_field) if detail.old_value
376 376 end
377 377 end
378 378 when 'attachment'
379 379 label = l(:label_attachment)
380 380 when 'relation'
381 381 if detail.value && !detail.old_value
382 382 rel_issue = Issue.visible.find_by_id(detail.value)
383 383 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
384 384 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
385 385 elsif detail.old_value && !detail.value
386 386 rel_issue = Issue.visible.find_by_id(detail.old_value)
387 387 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
388 388 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
389 389 end
390 390 relation_type = IssueRelation::TYPES[detail.prop_key]
391 391 label = l(relation_type[:name]) if relation_type
392 392 end
393 393 call_hook(:helper_issues_show_detail_after_setting,
394 394 {:detail => detail, :label => label, :value => value, :old_value => old_value })
395 395
396 396 label ||= detail.prop_key
397 397 value ||= detail.value
398 398 old_value ||= detail.old_value
399 399
400 400 unless no_html
401 401 label = content_tag('strong', label)
402 402 old_value = content_tag("i", h(old_value)) if detail.old_value
403 403 if detail.old_value && detail.value.blank? && detail.property != 'relation'
404 404 old_value = content_tag("del", old_value)
405 405 end
406 406 if detail.property == 'attachment' && value.present? &&
407 407 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
408 408 # Link to the attachment if it has not been removed
409 409 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
410 410 if options[:only_path] != false && (atta.is_text? || atta.is_image?)
411 411 value += ' '
412 412 value += link_to(l(:button_view),
413 413 { :controller => 'attachments', :action => 'show',
414 414 :id => atta, :filename => atta.filename },
415 415 :class => 'icon-only icon-magnifier',
416 416 :title => l(:button_view))
417 417 end
418 418 else
419 419 value = content_tag("i", h(value)) if value
420 420 end
421 421 end
422 422
423 423 if no_details
424 424 s = l(:text_journal_changed_no_detail, :label => label).html_safe
425 425 elsif show_diff
426 426 s = l(:text_journal_changed_no_detail, :label => label)
427 427 unless no_html
428 428 diff_link = link_to 'diff',
429 429 diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
430 430 :title => l(:label_view_diff)
431 431 s << " (#{ diff_link })"
432 432 end
433 433 s.html_safe
434 434 elsif detail.value.present?
435 435 case detail.property
436 436 when 'attr', 'cf'
437 437 if detail.old_value.present?
438 438 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
439 439 elsif multiple
440 440 l(:text_journal_added, :label => label, :value => value).html_safe
441 441 else
442 442 l(:text_journal_set_to, :label => label, :value => value).html_safe
443 443 end
444 444 when 'attachment', 'relation'
445 445 l(:text_journal_added, :label => label, :value => value).html_safe
446 446 end
447 447 else
448 448 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
449 449 end
450 450 end
451 451
452 452 # Find the name of an associated record stored in the field attribute
453 453 def find_name_by_reflection(field, id)
454 454 unless id.present?
455 455 return nil
456 456 end
457 457 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
458 458 association = Issue.reflect_on_association(key.first.to_sym)
459 459 name = nil
460 460 if association
461 461 record = association.klass.find_by_id(key.last)
462 462 if record
463 463 name = record.name.force_encoding('UTF-8')
464 464 end
465 465 end
466 466 hash[key] = name
467 467 end
468 468 @detail_value_name_by_reflection[[field, id]]
469 469 end
470 470
471 471 # Renders issue children recursively
472 472 def render_api_issue_children(issue, api)
473 473 return if issue.leaf?
474 474 api.array :children do
475 475 issue.children.each do |child|
476 476 api.issue(:id => child.id) do
477 477 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
478 478 api.subject child.subject
479 479 render_api_issue_children(child, api)
480 480 end
481 481 end
482 482 end
483 483 end
484 484 end
@@ -1,44 +1,44
1 <%= form_tag({}) do -%>
1 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
2 2 <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
3 3 <div class="autoscroll">
4 4 <table class="list issues <%= sort_css_classes %>">
5 5 <thead>
6 6 <tr>
7 7 <th class="checkbox hide-when-print">
8 8 <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
9 9 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
10 10 </th>
11 11 <% query.inline_columns.each do |column| %>
12 12 <%= column_header(column) %>
13 13 <% end %>
14 14 </tr>
15 15 </thead>
16 16 <tbody>
17 17 <% grouped_issue_list(issues, @query, @issue_count_by_group) do |issue, level, group_name, group_count, group_totals| -%>
18 18 <% if group_name %>
19 19 <% reset_cycle %>
20 20 <tr class="group open">
21 21 <td colspan="<%= query.inline_columns.size + 1 %>">
22 22 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
23 23 <span class="name"><%= group_name %></span> <span class="count"><%= group_count %></span> <span class="totals"><%= group_totals %></span>
24 24 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
25 25 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
26 26 </td>
27 27 </tr>
28 28 <% end %>
29 29 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
30 30 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
31 31 <%= raw query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
32 32 </tr>
33 33 <% @query.block_columns.each do |column|
34 34 if (text = column_content(column, issue)) && text.present? -%>
35 35 <tr class="<%= current_cycle %>">
36 36 <td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"><%= text %></td>
37 37 </tr>
38 38 <% end -%>
39 39 <% end -%>
40 40 <% end -%>
41 41 </tbody>
42 42 </table>
43 43 </div>
44 44 <% end -%>
@@ -1,29 +1,29
1 1 <% if issues && issues.any? %>
2 <%= form_tag({}) do %>
2 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
3 3 <table class="list list-simple issues">
4 4 <thead><tr>
5 5 <th class="id">#</th>
6 6 <th class="project"><%=l(:field_project)%></th>
7 7 <th class="status"><%=l(:field_status)%></th>
8 8 <th class="subject"><%=l(:field_subject)%></th>
9 9 </tr></thead>
10 10 <tbody>
11 11 <% for issue in issues %>
12 12 <tr id="issue-<%= h(issue.id) %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
13 13 <td class="id">
14 14 <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %>
15 15 <%= link_to("#{issue.tracker} ##{issue.id}", issue_path(issue)) %>
16 16 </td>
17 17 <td class="project"><%= link_to_project(issue.project) %></td>
18 18 <td class="status"><%= issue.status %></td>
19 19 <td class="subject">
20 20 <span><%= link_to(issue.subject, issue_path(issue)) %></span>
21 21 </td>
22 22 </tr>
23 23 <% end %>
24 24 </tbody>
25 25 </table>
26 26 <% end %>
27 27 <% else %>
28 28 <p class="nodata"><%= l(:label_no_data) %></p>
29 29 <% end %>
@@ -1,43 +1,43
1 1 <div class="contextual">
2 2 <% if User.current.allowed_to?(:manage_issue_relations, @project) %>
3 3 <%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
4 4 <% end %>
5 5 </div>
6 6
7 7 <p><strong><%=l(:label_related_issues)%></strong></p>
8 8
9 9 <% if @relations.present? %>
10 <form>
10 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
11 11 <table class="list issues">
12 12 <% @relations.each do |relation| %>
13 13 <% other_issue = relation.other_issue(@issue) -%>
14 14 <tr class="issue hascontextmenu <%= other_issue.css_classes %>" id="relation-<%= relation.id %>">
15 15 <td class="checkbox"><%= check_box_tag("ids[]", other_issue.id, false, :id => nil) %></td>
16 16 <td class="subject" style="width: 50%">
17 17 <%= relation.to_s(@issue) {|other| link_to_issue(other, :project => Setting.cross_project_issue_relations?)}.html_safe %>
18 18 </td>
19 19 <td class="status"><%= other_issue.status.name %></td>
20 20 <td class="start_date"><%= format_date(other_issue.start_date) %></td>
21 21 <td class="due_date"><%= format_date(other_issue.due_date) %></td>
22 22 <td class="buttons"><%= link_to(l(:label_relation_delete),
23 23 relation_path(relation),
24 24 :remote => true,
25 25 :method => :delete,
26 26 :data => {:confirm => l(:text_are_you_sure)},
27 27 :title => l(:label_relation_delete),
28 28 :class => 'icon-only icon-link-break'
29 29 ) if User.current.allowed_to?(:manage_issue_relations, @project) %></td>
30 30 </tr>
31 31 <% end %>
32 32 </table>
33 </form>
33 <% end %>
34 34 <% end %>
35 35
36 36 <%= form_for @relation, {
37 37 :as => :relation, :remote => true,
38 38 :url => issue_relations_path(@issue),
39 39 :method => :post,
40 40 :html => {:id => 'new-relation-form', :style => 'display: none;'}
41 41 } do |f| %>
42 42 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
43 43 <% end %>
@@ -1,72 +1,72
1 1 <div class="contextual">
2 2 <% if User.current.allowed_to?(:add_issues, @project, :global => true) && (@project.nil? || Issue.allowed_target_trackers(@project).any?) %>
3 3 <%= link_to l(:label_issue_new), _new_project_issue_path(@project), :class => 'icon icon-add new-issue' %>
4 4 <% end %>
5 5 </div>
6 6
7 7 <h2><%= @query.new_record? ? l(:label_issue_plural) : @query.name %></h2>
8 8 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
9 9
10 10 <%= form_tag(_project_issues_path(@project), :method => :get, :id => 'query_form') do %>
11 11 <%= render :partial => 'queries/query_form' %>
12 12 <% end %>
13 13
14 14 <% if @query.valid? %>
15 15 <% if @issues.empty? %>
16 16 <p class="nodata"><%= l(:label_no_data) %></p>
17 17 <% else %>
18 18 <%= render_query_totals(@query) %>
19 19 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
20 20 <span class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></span>
21 21 <% end %>
22 22
23 23 <% other_formats_links do |f| %>
24 24 <%= f.link_to_with_query_parameters 'Atom', :key => User.current.rss_key %>
25 25 <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
26 26 <%= f.link_to_with_query_parameters 'PDF' %>
27 27 <% end %>
28 28
29 29 <div id="csv-export-options" style="display:none;">
30 30 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
31 31 <%= form_tag(_project_issues_path(@project, :format => 'csv'), :method => :get, :id => 'csv-export-form') do %>
32 32 <%= query_as_hidden_field_tags(@query) %>
33 33 <%= hidden_field_tag 'sort', @sort_criteria.to_param, :id => nil %>
34 34 <p>
35 35 <label><%= radio_button_tag 'csv[columns]', '', true %> <%= l(:description_selected_columns) %></label><br />
36 36 <label><%= radio_button_tag 'csv[columns]', 'all' %> <%= l(:description_all_columns) %></label>
37 37 </p>
38 38 <p>
39 39 <label><%= check_box_tag 'csv[description]', '1', @query.has_column?(:description) %> <%= l(:field_description) %></label>
40 40 </p>
41 41 <% if @issue_count > Setting.issues_export_limit.to_i %>
42 42 <p class="icon icon-warning">
43 43 <%= l(:setting_issues_export_limit) %>: <%= Setting.issues_export_limit.to_i %>
44 44 </p>
45 45 <% end %>
46 46 <p class="buttons">
47 47 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
48 48 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
49 49 </p>
50 50 <% end %>
51 51 </div>
52 52
53 53 <% end %>
54 54 <%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
55 55
56 56 <% content_for :sidebar do %>
57 57 <%= render :partial => 'issues/sidebar' %>
58 58 <% end %>
59 59
60 60 <% content_for :header_tags do %>
61 61 <%= auto_discovery_link_tag(:atom,
62 62 {:query_id => @query, :format => 'atom',
63 63 :page => nil, :key => User.current.rss_key},
64 64 :title => l(:label_issue_plural)) %>
65 65 <%= auto_discovery_link_tag(:atom,
66 66 {:controller => 'journals', :action => 'index',
67 67 :query_id => @query, :format => 'atom',
68 68 :page => nil, :key => User.current.rss_key},
69 69 :title => l(:label_changes_details)) %>
70 70 <% end %>
71 71
72 <%= context_menu issues_context_menu_path %>
72 <%= context_menu %>
@@ -1,160 +1,162
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <h2><%= issue_heading(@issue) %></h2>
4 4
5 5 <div class="<%= @issue.css_classes %> details">
6 6 <% if @prev_issue_id || @next_issue_id %>
7 7 <div class="next-prev-links contextual">
8 8 <%= link_to_if @prev_issue_id,
9 9 "\xc2\xab #{l(:label_previous)}",
10 10 (@prev_issue_id ? issue_path(@prev_issue_id) : nil),
11 11 :title => "##{@prev_issue_id}",
12 12 :accesskey => accesskey(:previous) %> |
13 13 <% if @issue_position && @issue_count %>
14 14 <span class="position"><%= l(:label_item_position, :position => @issue_position, :count => @issue_count) %></span> |
15 15 <% end %>
16 16 <%= link_to_if @next_issue_id,
17 17 "#{l(:label_next)} \xc2\xbb",
18 18 (@next_issue_id ? issue_path(@next_issue_id) : nil),
19 19 :title => "##{@next_issue_id}",
20 20 :accesskey => accesskey(:next) %>
21 21 </div>
22 22 <% end %>
23 23
24 24 <%= avatar(@issue.author, :size => "50") %>
25 25
26 26 <div class="subject">
27 27 <%= render_issue_subject_with_tree(@issue) %>
28 28 </div>
29 29 <p class="author">
30 30 <%= authoring @issue.created_on, @issue.author %>.
31 31 <% if @issue.created_on != @issue.updated_on %>
32 32 <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
33 33 <% end %>
34 34 </p>
35 35
36 36 <div class="attributes">
37 37 <%= issue_fields_rows do |rows|
38 38 rows.left l(:field_status), @issue.status.name, :class => 'status'
39 39 rows.left l(:field_priority), @issue.priority.name, :class => 'priority'
40 40
41 41 unless @issue.disabled_core_fields.include?('assigned_to_id')
42 42 rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
43 43 end
44 44 unless @issue.disabled_core_fields.include?('category_id') || (@issue.category.nil? && @issue.project.issue_categories.none?)
45 45 rows.left l(:field_category), (@issue.category ? @issue.category.name : "-"), :class => 'category'
46 46 end
47 47 unless @issue.disabled_core_fields.include?('fixed_version_id') || (@issue.fixed_version.nil? && @issue.assignable_versions.none?)
48 48 rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
49 49 end
50 50
51 51 unless @issue.disabled_core_fields.include?('start_date')
52 52 rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
53 53 end
54 54 unless @issue.disabled_core_fields.include?('due_date')
55 55 rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
56 56 end
57 57 unless @issue.disabled_core_fields.include?('done_ratio')
58 58 rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :legend => "#{@issue.done_ratio}%"), :class => 'progress'
59 59 end
60 60 unless @issue.disabled_core_fields.include?('estimated_hours')
61 61 rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
62 62 end
63 63 if User.current.allowed_to_view_all_time_entries?(@project)
64 64 if @issue.total_spent_hours > 0
65 65 rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
66 66 end
67 67 end
68 68 end %>
69 69 <%= render_custom_fields_rows(@issue) %>
70 70 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
71 71 </div>
72 72
73 73 <% if @issue.description? || @issue.attachments.any? -%>
74 74 <hr />
75 75 <% if @issue.description? %>
76 76 <div class="description">
77 77 <div class="contextual">
78 78 <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if @issue.notes_addable? %>
79 79 </div>
80 80
81 81 <p><strong><%=l(:field_description)%></strong></p>
82 82 <div class="wiki">
83 83 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
84 84 </div>
85 85 </div>
86 86 <% end %>
87 87 <%= link_to_attachments @issue, :thumbnails => true %>
88 88 <% end -%>
89 89
90 90 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
91 91
92 92 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
93 93 <hr />
94 94 <div id="issue_tree">
95 95 <div class="contextual">
96 96 <%= link_to_new_subtask(@issue) if User.current.allowed_to?(:manage_subtasks, @project) %>
97 97 </div>
98 98 <p><strong><%=l(:label_subtask_plural)%></strong></p>
99 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
99 100 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
101 <% end %>
100 102 </div>
101 103 <% end %>
102 104
103 105 <% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
104 106 <hr />
105 107 <div id="relations">
106 108 <%= render :partial => 'relations' %>
107 109 </div>
108 110 <% end %>
109 111
110 112 </div>
111 113
112 114 <% if @changesets.present? %>
113 115 <div id="issue-changesets">
114 116 <h3><%=l(:label_associated_revisions)%></h3>
115 117 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
116 118 </div>
117 119 <% end %>
118 120
119 121 <% if @journals.present? %>
120 122 <div id="history">
121 123 <h3><%=l(:label_history)%></h3>
122 124 <%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
123 125 </div>
124 126 <% end %>
125 127
126 128
127 129 <div style="clear: both;"></div>
128 130 <%= render :partial => 'action_menu' %>
129 131
130 132 <div style="clear: both;"></div>
131 133 <% if @issue.editable? %>
132 134 <div id="update" style="display:none;">
133 135 <h3><%= l(:button_edit) %></h3>
134 136 <%= render :partial => 'edit' %>
135 137 </div>
136 138 <% end %>
137 139
138 140 <% other_formats_links do |f| %>
139 141 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
140 142 <%= f.link_to 'PDF' %>
141 143 <% end %>
142 144
143 145 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
144 146
145 147 <% content_for :sidebar do %>
146 148 <%= render :partial => 'issues/sidebar' %>
147 149
148 150 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
149 151 (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
150 152 <div id="watchers">
151 153 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
152 154 </div>
153 155 <% end %>
154 156 <% end %>
155 157
156 158 <% content_for :header_tags do %>
157 159 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
158 160 <% end %>
159 161
160 <%= context_menu issues_context_menu_path %>
162 <%= context_menu %>
@@ -1,21 +1,21
1 1 <div class="contextual">
2 2 <%= link_to l(:label_personalize_page), {:action => 'page_layout'}, :class => 'icon icon-edit' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_my_page)%></h2>
6 6
7 7 <div id="list-top">
8 8 <%= render_blocks(@blocks['top'], @user) %>
9 9 </div>
10 10
11 11 <div id="list-left" class="splitcontentleft">
12 12 <%= render_blocks(@blocks['left'], @user) %>
13 13 </div>
14 14
15 15 <div id="list-right" class="splitcontentright">
16 16 <%= render_blocks(@blocks['right'], @user) %>
17 17 </div>
18 18
19 <%= context_menu issues_context_menu_path %>
19 <%= context_menu %>
20 20
21 21 <% html_title(l(:label_my_page)) -%>
@@ -1,63 +1,63
1 <%= form_tag({}) do -%>
1 <%= form_tag({}, :data => {:cm_url => time_entries_context_menu_path}) do -%>
2 2 <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
3 3 <div class="autoscroll">
4 4 <table class="list time-entries">
5 5 <thead>
6 6 <tr>
7 7 <th class="checkbox hide-when-print">
8 8 <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
9 9 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
10 10 </th>
11 11 <% @query.inline_columns.each do |column| %>
12 12 <%= column_header(column) %>
13 13 <% end %>
14 14 <th></th>
15 15 </tr>
16 16 </thead>
17 17 <tbody>
18 18 <% grouped_query_results(entries, @query, @entry_count_by_group) do |entry, group_name, group_count, group_totals| -%>
19 19 <% if group_name %>
20 20 <% reset_cycle %>
21 21 <tr class="group open">
22 22 <td colspan="<%= @query.inline_columns.size + 2 %>">
23 23 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
24 24 <span class="name"><%= group_name %></span>
25 25 <% if group_count %>
26 26 <span class="count"><%= group_count %></span>
27 27 <% end %>
28 28 <span class="totals"><%= group_totals %></span>
29 29 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
30 30 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
31 31 </td>
32 32 </tr>
33 33 <% end %>
34 34 <tr id="time-entry-<%= entry.id %>" class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
35 35 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
36 36 <%= raw @query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, entry)}</td>"}.join %>
37 37 <td class="buttons">
38 38 <% if entry.editable_by?(User.current) -%>
39 39 <%= link_to l(:button_edit), edit_time_entry_path(entry),
40 40 :title => l(:button_edit),
41 41 :class => 'icon-only icon-edit' %>
42 42 <%= link_to l(:button_delete), time_entry_path(entry),
43 43 :data => {:confirm => l(:text_are_you_sure)},
44 44 :method => :delete,
45 45 :title => l(:button_delete),
46 46 :class => 'icon-only icon-del' %>
47 47 <% end -%>
48 48 </td>
49 49 </tr>
50 50 <% @query.block_columns.each do |column|
51 51 if (text = column_content(column, issue)) && text.present? -%>
52 52 <tr class="<%= current_cycle %>">
53 53 <td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"><%= text %></td>
54 54 </tr>
55 55 <% end -%>
56 56 <% end -%>
57 57 <% end -%>
58 58 </tbody>
59 59 </table>
60 60 </div>
61 61 <% end -%>
62 62
63 <%= context_menu time_entries_context_menu_path %>
63 <%= context_menu %>
@@ -1,102 +1,102
1 1 <div class="contextual">
2 2 <%= link_to(l(:label_version_new), new_project_version_path(@project),
3 3 :class => 'icon icon-add') if User.current.allowed_to?(:manage_versions, @project) %>
4 4 </div>
5 5
6 6 <h2><%=l(:label_roadmap)%></h2>
7 7
8 8 <% if @versions.empty? %>
9 9 <p class="nodata"><%= l(:label_no_data) %></p>
10 10 <% else %>
11 11 <div id="roadmap">
12 12 <% @versions.each do |version| %>
13 13 <article class="version-article <%= version.css_classes %>">
14 14 <% if User.current.allowed_to?(:manage_versions, version.project) %>
15 15 <div class="contextual">
16 16 <%= link_to l(:button_edit), edit_version_path(version), :title => l(:button_edit), :class => 'icon-only icon-edit' %>
17 17 </div>
18 18 <% end %>
19 19 <header>
20 20 <h3 class="version"><%= link_to_version version, :name => version_anchor(version) %></h3>
21 21 </header>
22 22 <%= render :partial => 'versions/overview', :locals => {:version => version} %>
23 23 <%= render(:partial => "wiki/content",
24 24 :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
25 25 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
26 <%= form_tag({}) do -%>
26 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
27 27 <table class="list related-issues">
28 28 <caption><%= l(:label_related_issues) %></caption>
29 29 <% issues.each do |issue| -%>
30 30 <tr class="hascontextmenu">
31 31 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
32 32 <td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
33 33 </tr>
34 34 <% end -%>
35 35 </table>
36 36 <% end %>
37 37 <% end %>
38 38 <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
39 39 </article>
40 40 <% end %>
41 41 </div>
42 42 <% end %>
43 43
44 44 <% content_for :sidebar do %>
45 45 <%= form_tag({}, :method => :get) do %>
46 46 <h3><%= l(:label_roadmap) %></h3>
47 47 <ul>
48 48 <% @trackers.each do |tracker| %>
49 49 <li>
50 50 <label>
51 51 <%= check_box_tag("tracker_ids[]", tracker.id,
52 52 (@selected_tracker_ids.include? tracker.id.to_s),
53 53 :id => nil) %>
54 54 <%= tracker.name %>
55 55 </label>
56 56 </li>
57 57 <% end %>
58 58 </ul>
59 59 <p></p>
60 60 <ul>
61 61 <li>
62 62 <label for="completed">
63 63 <%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %>
64 64 </label>
65 65 </li>
66 66 <% if @project.descendants.active.any? %>
67 67 <li>
68 68 <%= hidden_field_tag 'with_subprojects', 0, :id => nil %>
69 69 <label>
70 70 <%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%>
71 71 </label>
72 72 </li>
73 73 <% end %>
74 74 </ul>
75 75 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
76 76 <% end %>
77 77
78 78 <h3><%= l(:label_version_plural) %></h3>
79 79 <ul>
80 80 <% @versions.each do |version| %>
81 81 <li>
82 82 <%= link_to(format_version_name(version), "##{version_anchor(version)}") %>
83 83 </li>
84 84 <% end %>
85 85 </ul>
86 86 <% if @completed_versions.present? %>
87 87 <p>
88 88 <%= link_to_function l(:label_completed_versions),
89 89 '$("#toggle-completed-versions").toggleClass("collapsed"); $("#completed-versions").toggle()',
90 90 :id => 'toggle-completed-versions', :class => 'collapsible collapsed' %>
91 91 <ul id = "completed-versions" style = "display:none;">
92 92 <% @completed_versions.each do |version| %>
93 93 <li><%= link_to_version version %></li>
94 94 <% end %>
95 95 </ul>
96 96 </p>
97 97 <% end %>
98 98 <% end %>
99 99
100 100 <% html_title(l(:label_roadmap)) %>
101 101
102 <%= context_menu issues_context_menu_path %>
102 <%= context_menu %>
@@ -1,56 +1,56
1 1 <div class="contextual">
2 2 <%= link_to(l(:button_edit), edit_version_path(@version), :class => 'icon icon-edit') if User.current.allowed_to?(:manage_versions, @version.project) %>
3 3 <%= link_to_if_authorized(l(:button_edit_associated_wikipage, :page_title => @version.wiki_page_title), {:controller => 'wiki', :action => 'edit', :project_id => @version.project, :id => Wiki.titleize(@version.wiki_page_title)}, :class => 'icon icon-edit') unless @version.wiki_page_title.blank? || @version.project.wiki.nil? %>
4 4 <%= delete_link version_path(@version, :back_url => url_for(:controller => 'versions', :action => 'index', :project_id => @version.project)) if User.current.allowed_to?(:manage_versions, @version.project) %>
5 5 <%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
6 6 </div>
7 7
8 8 <h2><%= @version.name %></h2>
9 9
10 10 <div id="roadmap" class="<%= @version.css_classes %>">
11 11 <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
12 12 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
13 13
14 14 <div id="version-summary">
15 15 <% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
16 16 <fieldset class="time-tracking"><legend><%= l(:label_time_tracking) %></legend>
17 17 <table>
18 18 <tr>
19 19 <th><%= l(:field_estimated_hours) %></th>
20 20 <td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td>
21 21 </tr>
22 22 <% if User.current.allowed_to_view_all_time_entries?(@project) %>
23 23 <tr>
24 24 <th><%= l(:label_spent_time) %></th>
25 25 <td class="total-hours"><%= link_to html_hours(l_hours(@version.spent_hours)),
26 26 project_time_entries_path(@version.project, :set_filter => 1, :"issue.fixed_version_id" => @version.id) %></td>
27 27 </tr>
28 28 <% end %>
29 29 </table>
30 30 </fieldset>
31 31 <% end %>
32 32
33 33 <div id="status_by">
34 34 <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
35 35 </div>
36 36 </div>
37 37
38 38 <% if @issues.present? %>
39 <%= form_tag({}) do -%>
39 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
40 40 <table class="list related-issues">
41 41 <caption><%= l(:label_related_issues) %></caption>
42 42 <%- @issues.each do |issue| -%>
43 43 <tr class="issue hascontextmenu">
44 44 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
45 45 <td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
46 46 </tr>
47 47 <% end %>
48 48 </table>
49 49 <% end %>
50 <%= context_menu issues_context_menu_path %>
50 <%= context_menu %>
51 51 <% end %>
52 52 </div>
53 53
54 54 <%= call_hook :view_versions_show_bottom, :version => @version %>
55 55
56 56 <% html_title @version.name %>
@@ -1,247 +1,249
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3 3
4 4 var contextMenuObserving;
5 var contextMenuUrl;
6 5
7 6 function contextMenuRightClick(event) {
8 7 var target = $(event.target);
9 8 if (target.is('a')) {return;}
10 9 var tr = target.parents('tr').first();
11 10 if (!tr.hasClass('hascontextmenu')) {return;}
12 11 event.preventDefault();
13 12 if (!contextMenuIsSelected(tr)) {
14 13 contextMenuUnselectAll();
15 14 contextMenuAddSelection(tr);
16 15 contextMenuSetLastSelected(tr);
17 16 }
18 17 contextMenuShow(event);
19 18 }
20 19
21 20 function contextMenuClick(event) {
22 21 var target = $(event.target);
23 22 var lastSelected;
24 23
25 24 if (target.is('a') && target.hasClass('submenu')) {
26 25 event.preventDefault();
27 26 return;
28 27 }
29 28 contextMenuHide();
30 29 if (target.is('a') || target.is('img')) { return; }
31 30 if (event.which == 1 || (navigator.appVersion.match(/\bMSIE\b/))) {
32 31 var tr = target.parents('tr').first();
33 32 if (tr.length && tr.hasClass('hascontextmenu')) {
34 33 // a row was clicked, check if the click was on checkbox
35 34 if (target.is('input')) {
36 35 // a checkbox may be clicked
37 36 if (target.prop('checked')) {
38 37 tr.addClass('context-menu-selection');
39 38 } else {
40 39 tr.removeClass('context-menu-selection');
41 40 }
42 41 } else {
43 42 if (event.ctrlKey || event.metaKey) {
44 43 contextMenuToggleSelection(tr);
45 44 } else if (event.shiftKey) {
46 45 lastSelected = contextMenuLastSelected();
47 46 if (lastSelected.length) {
48 47 var toggling = false;
49 48 $('.hascontextmenu').each(function(){
50 49 if (toggling || $(this).is(tr)) {
51 50 contextMenuAddSelection($(this));
52 51 }
53 52 if ($(this).is(tr) || $(this).is(lastSelected)) {
54 53 toggling = !toggling;
55 54 }
56 55 });
57 56 } else {
58 57 contextMenuAddSelection(tr);
59 58 }
60 59 } else {
61 60 contextMenuUnselectAll();
62 61 contextMenuAddSelection(tr);
63 62 }
64 63 contextMenuSetLastSelected(tr);
65 64 }
66 65 } else {
67 66 // click is outside the rows
68 67 if (target.is('a') && (target.hasClass('disabled') || target.hasClass('submenu'))) {
69 68 event.preventDefault();
70 69 } else if (target.is('.toggle-selection') || target.is('.ui-dialog *') || $('#ajax-modal').is(':visible')) {
71 70 // nop
72 71 } else {
73 72 contextMenuUnselectAll();
74 73 }
75 74 }
76 75 }
77 76 }
78 77
79 78 function contextMenuCreate() {
80 79 if ($('#context-menu').length < 1) {
81 80 var menu = document.createElement("div");
82 81 menu.setAttribute("id", "context-menu");
83 82 menu.setAttribute("style", "display:none;");
84 83 document.getElementById("content").appendChild(menu);
85 84 }
86 85 }
87 86
88 87 function contextMenuShow(event) {
89 88 var mouse_x = event.pageX;
90 89 var mouse_y = event.pageY;
91 90 var mouse_y_c = event.clientY;
92 91 var render_x = mouse_x;
93 92 var render_y = mouse_y;
94 93 var dims;
95 94 var menu_width;
96 95 var menu_height;
97 96 var window_width;
98 97 var window_height;
99 98 var max_width;
100 99 var max_height;
100 var url;
101 101
102 102 $('#context-menu').css('left', (render_x + 'px'));
103 103 $('#context-menu').css('top', (render_y + 'px'));
104 104 $('#context-menu').html('');
105 105
106 url = $(event.target).parents('form').first().data('cm-url');
107
106 108 $.ajax({
107 url: contextMenuUrl,
109 url: url,
108 110 data: $(event.target).parents('form').first().serialize(),
109 111 success: function(data, textStatus, jqXHR) {
110 112 $('#context-menu').html(data);
111 113 menu_width = $('#context-menu').width();
112 114 menu_height = $('#context-menu').height();
113 115 max_width = mouse_x + 2*menu_width;
114 116 max_height = mouse_y_c + menu_height;
115 117
116 118 var ws = window_size();
117 119 window_width = ws.width;
118 120 window_height = ws.height;
119 121
120 122 /* display the menu above and/or to the left of the click if needed */
121 123 if (max_width > window_width) {
122 124 render_x -= menu_width;
123 125 $('#context-menu').addClass('reverse-x');
124 126 } else {
125 127 $('#context-menu').removeClass('reverse-x');
126 128 }
127 129
128 130 if (max_height > window_height) {
129 131 render_y -= menu_height;
130 132 $('#context-menu').addClass('reverse-y');
131 133 // adding class for submenu
132 134 if (mouse_y_c < 325) {
133 135 $('#context-menu .folder').addClass('down');
134 136 }
135 137 } else {
136 138 // adding class for submenu
137 139 if (window_height - mouse_y_c < 345) {
138 140 $('#context-menu .folder').addClass('up');
139 141 }
140 142 $('#context-menu').removeClass('reverse-y');
141 143 }
142 144
143 145 if (render_x <= 0) render_x = 1;
144 146 if (render_y <= 0) render_y = 1;
145 147 $('#context-menu').css('left', (render_x + 'px'));
146 148 $('#context-menu').css('top', (render_y + 'px'));
147 149 $('#context-menu').show();
148 150
149 151 //if (window.parseStylesheets) { window.parseStylesheets(); } // IE
150 152 }
151 153 });
152 154 }
153 155
154 156 function contextMenuSetLastSelected(tr) {
155 157 $('.cm-last').removeClass('cm-last');
156 158 tr.addClass('cm-last');
157 159 }
158 160
159 161 function contextMenuLastSelected() {
160 162 return $('.cm-last').first();
161 163 }
162 164
163 165 function contextMenuUnselectAll() {
164 166 $('input[type=checkbox].toggle-selection').prop('checked', false);
165 167 $('.hascontextmenu').each(function(){
166 168 contextMenuRemoveSelection($(this));
167 169 });
168 170 $('.cm-last').removeClass('cm-last');
169 171 }
170 172
171 173 function contextMenuHide() {
172 174 $('#context-menu').hide();
173 175 }
174 176
175 177 function contextMenuToggleSelection(tr) {
176 178 if (contextMenuIsSelected(tr)) {
177 179 contextMenuRemoveSelection(tr);
178 180 } else {
179 181 contextMenuAddSelection(tr);
180 182 }
181 183 }
182 184
183 185 function contextMenuAddSelection(tr) {
184 186 tr.addClass('context-menu-selection');
185 187 contextMenuCheckSelectionBox(tr, true);
186 188 contextMenuClearDocumentSelection();
187 189 }
188 190
189 191 function contextMenuRemoveSelection(tr) {
190 192 tr.removeClass('context-menu-selection');
191 193 contextMenuCheckSelectionBox(tr, false);
192 194 }
193 195
194 196 function contextMenuIsSelected(tr) {
195 197 return tr.hasClass('context-menu-selection');
196 198 }
197 199
198 200 function contextMenuCheckSelectionBox(tr, checked) {
199 201 tr.find('input[type=checkbox]').prop('checked', checked);
200 202 }
201 203
202 204 function contextMenuClearDocumentSelection() {
203 205 // TODO
204 206 if (document.selection) {
205 207 document.selection.empty(); // IE
206 208 } else {
207 209 window.getSelection().removeAllRanges();
208 210 }
209 211 }
210 212
211 function contextMenuInit(url) {
212 contextMenuUrl = url;
213 function contextMenuInit() {
213 214 contextMenuCreate();
214 215 contextMenuUnselectAll();
215 216
216 217 if (!contextMenuObserving) {
217 218 $(document).click(contextMenuClick);
218 219 $(document).contextmenu(contextMenuRightClick);
219 220 contextMenuObserving = true;
220 221 }
221 222 }
222 223
223 224 function toggleIssuesSelection(el) {
224 225 var checked = $(this).prop('checked');
225 226 var boxes = $(this).parents('table').find('input[name=ids\\[\\]]');
226 227 boxes.prop('checked', checked).parents('tr').toggleClass('context-menu-selection', checked);
227 228 }
228 229
229 230 function window_size() {
230 231 var w;
231 232 var h;
232 233 if (window.innerWidth) {
233 234 w = window.innerWidth;
234 235 h = window.innerHeight;
235 236 } else if (document.documentElement) {
236 237 w = document.documentElement.clientWidth;
237 238 h = document.documentElement.clientHeight;
238 239 } else {
239 240 w = document.body.clientWidth;
240 241 h = document.body.clientHeight;
241 242 }
242 243 return {width: w, height: h};
243 244 }
244 245
245 246 $(document).ready(function(){
247 contextMenuInit();
246 248 $('input[type=checkbox].toggle-selection').on('change', toggleIssuesSelection);
247 249 });
@@ -1,257 +1,257
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class VersionsControllerTest < Redmine::ControllerTest
21 21 fixtures :projects, :versions, :issues, :users, :roles, :members,
22 22 :member_roles, :enabled_modules, :issue_statuses,
23 23 :issue_categories, :enumerations
24 24
25 25 def setup
26 26 User.current = nil
27 27 end
28 28
29 29 def test_index
30 30 get :index, :params => {:project_id => 1}
31 31 assert_response :success
32 32
33 33 # Version with no date set appears
34 34 assert_select 'h3', :text => Version.find(3).name
35 35 # Completed version doesn't appear
36 36 assert_select 'h3', :text => Version.find(1).name, :count => 0
37 37
38 38 # Context menu on issues
39 assert_select "script", :text => Regexp.new(Regexp.escape("contextMenuInit('/issues/context_menu')"))
39 assert_select "form[data-cm-url=?]", '/issues/context_menu'
40 40 assert_select "div#sidebar" do
41 41 # Links to versions anchors
42 42 assert_select 'a[href=?]', '#2.0'
43 43 # Links to completed versions in the sidebar
44 44 assert_select 'a[href=?]', '/versions/1'
45 45 end
46 46 end
47 47
48 48 def test_index_with_completed_versions
49 49 get :index, :params => {:project_id => 1, :completed => 1}
50 50 assert_response :success
51 51
52 52 # Version with no date set appears
53 53 assert_select 'h3', :text => Version.find(3).name
54 54 # Completed version appears
55 55 assert_select 'h3', :text => Version.find(1).name
56 56 end
57 57
58 58 def test_index_with_tracker_ids
59 59 (1..3).each do |tracker_id|
60 60 Issue.generate! :project_id => 1, :fixed_version_id => 3, :tracker_id => tracker_id
61 61 end
62 62 get :index, :params => {:project_id => 1, :tracker_ids => [1, 3]}
63 63 assert_response :success
64 64 assert_select 'a.issue.tracker-1'
65 65 assert_select 'a.issue.tracker-2', 0
66 66 assert_select 'a.issue.tracker-3'
67 67 end
68 68
69 69 def test_index_showing_subprojects_versions
70 70 @subproject_version = Version.create!(:project => Project.find(3), :name => "Subproject version")
71 71 get :index, :params => {:project_id => 1, :with_subprojects => 1}
72 72 assert_response :success
73 73
74 74 # Shared version
75 75 assert_select 'h3', :text => Version.find(4).name
76 76 # Subproject version
77 77 assert_select 'h3', :text => /Subproject version/
78 78 end
79 79
80 80 def test_index_should_prepend_shared_versions
81 81 get :index, :params => {:project_id => 1}
82 82 assert_response :success
83 83
84 84 assert_select '#sidebar' do
85 85 assert_select 'a[href=?]', '#2.0', :text => '2.0'
86 86 assert_select 'a[href=?]', '#subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0'
87 87 end
88 88 assert_select '#content' do
89 89 assert_select 'a[name=?]', '2.0', :text => '2.0'
90 90 assert_select 'a[name=?]', 'subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0'
91 91 end
92 92 end
93 93
94 94 def test_show
95 95 get :show, :params => {:id => 2}
96 96 assert_response :success
97 97
98 98 assert_select 'h2', :text => /1.0/
99 99 end
100 100
101 101 def test_show_should_link_to_spent_time_on_version
102 102 version = Version.generate!
103 103 issue = Issue.generate(:fixed_version => version)
104 104 TimeEntry.generate!(:issue => issue, :hours => 7.2)
105 105
106 106 get :show, :params => {:id => version.id}
107 107 assert_response :success
108 108
109 109 assert_select '.total-hours', :text => '7.20 hours'
110 110 assert_select '.total-hours a[href=?]', "/projects/ecookbook/time_entries?issue.fixed_version_id=#{version.id}&set_filter=1"
111 111 end
112 112
113 113 def test_show_should_display_nil_counts
114 114 with_settings :default_language => 'en' do
115 115 get :show, :params => {:id => 2, :status_by => 'category'}
116 116 assert_response :success
117 117 assert_select 'div#status_by' do
118 118 assert_select 'select[name=status_by]' do
119 119 assert_select 'option[value=category][selected=selected]'
120 120 end
121 121 assert_select 'a', :text => 'none'
122 122 end
123 123 end
124 124 end
125 125
126 126 def test_new
127 127 @request.session[:user_id] = 2
128 128 get :new, :params => {:project_id => '1'}
129 129 assert_response :success
130 130 assert_select 'input[name=?]', 'version[name]'
131 131 assert_select 'select[name=?]', 'version[status]', false
132 132 end
133 133
134 134 def test_new_from_issue_form
135 135 @request.session[:user_id] = 2
136 136 xhr :get, :new, :params => {:project_id => '1'}
137 137 assert_response :success
138 138 assert_equal 'text/javascript', response.content_type
139 139 end
140 140
141 141 def test_create
142 142 @request.session[:user_id] = 2 # manager
143 143 assert_difference 'Version.count' do
144 144 post :create, :params => {:project_id => '1', :version => {:name => 'test_add_version'}}
145 145 end
146 146 assert_redirected_to '/projects/ecookbook/settings/versions'
147 147 version = Version.find_by_name('test_add_version')
148 148 assert_not_nil version
149 149 assert_equal 1, version.project_id
150 150 end
151 151
152 152 def test_create_from_issue_form
153 153 @request.session[:user_id] = 2
154 154 assert_difference 'Version.count' do
155 155 xhr :post, :create, :params => {:project_id => '1', :version => {:name => 'test_add_version_from_issue_form'}}
156 156 end
157 157 version = Version.find_by_name('test_add_version_from_issue_form')
158 158 assert_not_nil version
159 159 assert_equal 1, version.project_id
160 160
161 161 assert_response :success
162 162 assert_equal 'text/javascript', response.content_type
163 163 assert_include 'test_add_version_from_issue_form', response.body
164 164 end
165 165
166 166 def test_create_from_issue_form_with_failure
167 167 @request.session[:user_id] = 2
168 168 assert_no_difference 'Version.count' do
169 169 xhr :post, :create, :params => {:project_id => '1', :version => {:name => ''}}
170 170 end
171 171 assert_response :success
172 172 assert_equal 'text/javascript', response.content_type
173 173 end
174 174
175 175 def test_get_edit
176 176 @request.session[:user_id] = 2
177 177 get :edit, :params => {:id => 2}
178 178 assert_response :success
179 179 version = Version.find(2)
180 180
181 181 assert_select 'select[name=?]', 'version[status]' do
182 182 assert_select 'option[value=?][selected="selected"]', version.status
183 183 end
184 184 assert_select 'input[name=?][value=?]', 'version[name]', version.name
185 185 end
186 186
187 187 def test_close_completed
188 188 Version.update_all("status = 'open'")
189 189 @request.session[:user_id] = 2
190 190 put :close_completed, :params => {:project_id => 'ecookbook'}
191 191 assert_redirected_to :controller => 'projects', :action => 'settings',
192 192 :tab => 'versions', :id => 'ecookbook'
193 193 assert_not_nil Version.find_by_status('closed')
194 194 end
195 195
196 196 def test_post_update
197 197 @request.session[:user_id] = 2
198 198 put :update, :params => {
199 199 :id => 2,
200 200 :version => {
201 201 :name => 'New version name',
202 202 :effective_date => Date.today.strftime("%Y-%m-%d")
203 203 }
204 204 }
205 205 assert_redirected_to :controller => 'projects', :action => 'settings',
206 206 :tab => 'versions', :id => 'ecookbook'
207 207 version = Version.find(2)
208 208 assert_equal 'New version name', version.name
209 209 assert_equal Date.today, version.effective_date
210 210 end
211 211
212 212 def test_post_update_with_validation_failure
213 213 @request.session[:user_id] = 2
214 214 put :update, :params => {
215 215 :id => 2,
216 216 :version => {
217 217 :name => '',
218 218 :effective_date => Date.today.strftime("%Y-%m-%d")
219 219 }
220 220 }
221 221 assert_response :success
222 222 assert_select_error /Name cannot be blank/
223 223 end
224 224
225 225 def test_destroy
226 226 @request.session[:user_id] = 2
227 227 assert_difference 'Version.count', -1 do
228 228 delete :destroy, :params => {:id => 3}
229 229 end
230 230 assert_redirected_to :controller => 'projects', :action => 'settings',
231 231 :tab => 'versions', :id => 'ecookbook'
232 232 assert_nil Version.find_by_id(3)
233 233 end
234 234
235 235 def test_destroy_version_in_use_should_fail
236 236 @request.session[:user_id] = 2
237 237 assert_no_difference 'Version.count' do
238 238 delete :destroy, :params => {:id => 2}
239 239 end
240 240 assert_redirected_to :controller => 'projects', :action => 'settings',
241 241 :tab => 'versions', :id => 'ecookbook'
242 242 assert flash[:error].match(/Unable to delete version/)
243 243 assert Version.find_by_id(2)
244 244 end
245 245
246 246 def test_issue_status_by
247 247 xhr :get, :status_by, :params => {:id => 2}
248 248 assert_response :success
249 249 end
250 250
251 251 def test_issue_status_by_status
252 252 xhr :get, :status_by, :params => {:id => 2, :status_by => 'status'}
253 253 assert_response :success
254 254 assert_include 'Assigned', response.body
255 255 assert_include 'Closed', response.body
256 256 end
257 257 end
General Comments 0
You need to be logged in to leave comments. Login now