"Fossies" - the Fresh Open Source Software Archive

Member "redmine-4.1.1/app/helpers/issues_helper.rb" (6 Apr 2020, 24029 Bytes) of package /linux/www/redmine-4.1.1.tar.gz:


As a special service "Fossies" has tried to format the requested text file into HTML format (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "issues_helper.rb": 4.1.0_vs_4.1.1.

    1 # frozen_string_literal: true
    2 
    3 # Redmine - project management software
    4 # Copyright (C) 2006-2019  Jean-Philippe Lang
    5 #
    6 # This program is free software; you can redistribute it and/or
    7 # modify it under the terms of the GNU General Public License
    8 # as published by the Free Software Foundation; either version 2
    9 # of the License, or (at your option) any later version.
   10 #
   11 # This program is distributed in the hope that it will be useful,
   12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   14 # GNU General Public License for more details.
   15 #
   16 # You should have received a copy of the GNU General Public License
   17 # along with this program; if not, write to the Free Software
   18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
   19 
   20 module IssuesHelper
   21   include ApplicationHelper
   22   include Redmine::Export::PDF::IssuesPdfHelper
   23 
   24   def issue_list(issues, &block)
   25     ancestors = []
   26     issues.each do |issue|
   27       while ancestors.any? &&
   28             !issue.is_descendant_of?(ancestors.last)
   29         ancestors.pop
   30       end
   31       yield issue, ancestors.size
   32       ancestors << issue unless issue.leaf?
   33     end
   34   end
   35 
   36   def grouped_issue_list(issues, query, &block)
   37     ancestors = []
   38     grouped_query_results(issues, query) do |issue, group_name, group_count, group_totals|
   39       while ancestors.any? &&
   40             !issue.is_descendant_of?(ancestors.last)
   41         ancestors.pop
   42       end
   43       yield issue, ancestors.size, group_name, group_count, group_totals
   44       ancestors << issue unless issue.leaf?
   45     end
   46   end
   47 
   48   # Renders a HTML/CSS tooltip
   49   #
   50   # To use, a trigger div is needed.  This is a div with the class of "tooltip"
   51   # that contains this method wrapped in a span with the class of "tip"
   52   #
   53   #    <div class="tooltip"><%= link_to_issue(issue) %>
   54   #      <span class="tip"><%= render_issue_tooltip(issue) %></span>
   55   #    </div>
   56   #
   57   def render_issue_tooltip(issue)
   58     @cached_label_status ||= l(:field_status)
   59     @cached_label_start_date ||= l(:field_start_date)
   60     @cached_label_due_date ||= l(:field_due_date)
   61     @cached_label_assigned_to ||= l(:field_assigned_to)
   62     @cached_label_priority ||= l(:field_priority)
   63     @cached_label_project ||= l(:field_project)
   64 
   65     link_to_issue(issue) + "<br /><br />".html_safe +
   66       "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
   67       "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name) + (" (#{format_date(issue.closed_on)})" if issue.closed?)}<br />".html_safe +
   68       "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
   69       "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
   70       "<strong>#{@cached_label_assigned_to}</strong>: #{avatar(issue.assigned_to, :size => '13', :title => l(:field_assigned_to)) if issue.assigned_to} #{h(issue.assigned_to)}<br />".html_safe +
   71       "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
   72   end
   73 
   74   def issue_heading(issue)
   75     h("#{issue.tracker} ##{issue.id}")
   76   end
   77 
   78   def render_issue_subject_with_tree(issue)
   79     s = +''
   80     ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
   81     ancestors.each do |ancestor|
   82       s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
   83     end
   84     s << '<div>'
   85     subject = h(issue.subject)
   86     if issue.is_private?
   87       subject = subject + ' ' + content_tag('span', l(:field_is_private), :class => 'badge badge-private private')
   88     end
   89     s << content_tag('h3', subject)
   90     s << '</div>' * (ancestors.size + 1)
   91     s.html_safe
   92   end
   93 
   94   def render_descendants_tree(issue)
   95     manage_relations = User.current.allowed_to?(:manage_subtasks, issue.project)
   96     s = +'<table class="list issues odd-even">'
   97     issue_list(
   98       issue.descendants.visible.
   99         preload(:status, :priority, :tracker,
  100                 :assigned_to).sort_by(&:lft)) do |child, level|
  101       css = +"issue issue-#{child.id} hascontextmenu #{child.css_classes}"
  102       css << " idnt idnt-#{level}" if level > 0
  103       buttons =
  104         if manage_relations
  105           link_to(l(:label_delete_link_to_subtask),
  106                   issue_path(
  107                     {:id => child.id, :issue => {:parent_issue_id => ''},
  108                      :back_url => issue_path(issue.id), :no_flash => '1'}),
  109                   :method => :put,
  110                   :data => {:confirm => l(:text_are_you_sure)},
  111                   :title => l(:label_delete_link_to_subtask),
  112                   :class => 'icon-only icon-link-break'
  113                   )
  114         else
  115           "".html_safe
  116         end
  117       buttons << link_to_context_menu
  118       s <<
  119         content_tag(
  120           'tr',
  121           content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil),
  122                       :class => 'checkbox') +
  123              content_tag('td',
  124                          link_to_issue(
  125                            child,
  126                            :project => (issue.project_id != child.project_id)),
  127                          :class => 'subject') +
  128              content_tag('td', h(child.status), :class => 'status') +
  129              content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
  130              content_tag('td', format_date(child.start_date), :class => 'start_date') +
  131              content_tag('td', format_date(child.due_date), :class => 'due_date') +
  132              content_tag('td',
  133                          (if child.disabled_core_fields.include?('done_ratio')
  134                             ''
  135                           else
  136                              progress_bar(child.done_ratio)
  137                           end),
  138                          :class=> 'done_ratio') +
  139              content_tag('td', buttons, :class => 'buttons'),
  140           :class => css)
  141     end
  142     s << '</table>'
  143     s.html_safe
  144   end
  145 
  146   # Renders the list of related issues on the issue details view
  147   def render_issue_relations(issue, relations)
  148     manage_relations = User.current.allowed_to?(:manage_issue_relations, issue.project)
  149     s = ''.html_safe
  150     relations.each do |relation|
  151       other_issue = relation.other_issue(issue)
  152       css = "issue hascontextmenu #{other_issue.css_classes}"
  153       buttons =
  154         if manage_relations
  155           link_to(
  156             l(:label_relation_delete),
  157             relation_path(relation),
  158             :remote => true,
  159             :method => :delete,
  160             :data => {:confirm => l(:text_are_you_sure)},
  161             :title => l(:label_relation_delete),
  162             :class => 'icon-only icon-link-break'
  163           )
  164         else
  165           "".html_safe
  166         end
  167       buttons << link_to_context_menu
  168       s <<
  169         content_tag(
  170           'tr',
  171           content_tag('td',
  172                       check_box_tag(
  173                         "ids[]", other_issue.id,
  174                         false, :id => nil),
  175                       :class => 'checkbox') +
  176              content_tag('td',
  177                          relation.to_s(@issue) {|other|
  178                            link_to_issue(
  179                              other,
  180                              :project => Setting.cross_project_issue_relations?)
  181                          }.html_safe,
  182                          :class => 'subject') +
  183              content_tag('td', other_issue.status, :class => 'status') +
  184              content_tag('td', link_to_user(other_issue.assigned_to), :class => 'assigned_to') +
  185              content_tag('td', format_date(other_issue.start_date), :class => 'start_date') +
  186              content_tag('td', format_date(other_issue.due_date), :class => 'due_date') +
  187              content_tag('td',
  188                          (if other_issue.disabled_core_fields.include?('done_ratio')
  189                             ''
  190                           else
  191                             progress_bar(other_issue.done_ratio)
  192                           end),
  193                          :class=> 'done_ratio') +
  194              content_tag('td', buttons, :class => 'buttons'),
  195           :id => "relation-#{relation.id}",
  196           :class => css)
  197     end
  198     content_tag('table', s, :class => 'list issues odd-even')
  199   end
  200 
  201   def issue_estimated_hours_details(issue)
  202     if issue.total_estimated_hours.present?
  203       if issue.total_estimated_hours == issue.estimated_hours
  204         l_hours_short(issue.estimated_hours)
  205       else
  206         s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
  207         s += " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
  208         s.html_safe
  209       end
  210     end
  211   end
  212 
  213   def issue_spent_hours_details(issue)
  214     if issue.total_spent_hours > 0
  215       path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
  216 
  217       if issue.total_spent_hours == issue.spent_hours
  218         link_to(l_hours_short(issue.spent_hours), path)
  219       else
  220         s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
  221         s += " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
  222         s.html_safe
  223       end
  224     end
  225   end
  226 
  227   def issue_due_date_details(issue)
  228     return if issue&.due_date.nil?
  229     s = format_date(issue.due_date)
  230     s += " (#{due_date_distance_in_words(issue.due_date)})" unless issue.closed?
  231     s
  232   end
  233 
  234   # Returns a link for adding a new subtask to the given issue
  235   def link_to_new_subtask(issue)
  236     attrs = {
  237       :parent_issue_id => issue
  238     }
  239     attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
  240     link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs, :back_url => issue_path(issue)))
  241   end
  242 
  243   def trackers_options_for_select(issue)
  244     trackers = trackers_for_select(issue)
  245     trackers.collect {|t| [t.name, t.id]}
  246   end
  247 
  248   def trackers_for_select(issue)
  249     trackers = issue.allowed_target_trackers
  250     if issue.new_record? && issue.parent_issue_id.present?
  251       trackers = trackers.reject do |tracker|
  252         issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
  253       end
  254     end
  255     trackers
  256   end
  257 
  258   class IssueFieldsRows
  259     include ActionView::Helpers::TagHelper
  260 
  261     def initialize
  262       @left = []
  263       @right = []
  264     end
  265 
  266     def left(*args)
  267       args.any? ? @left << cells(*args) : @left
  268     end
  269 
  270     def right(*args)
  271       args.any? ? @right << cells(*args) : @right
  272     end
  273 
  274     def size
  275       @left.size > @right.size ? @left.size : @right.size
  276     end
  277 
  278     def to_html
  279       content =
  280         content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
  281         content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
  282 
  283       content_tag('div', content, :class => 'splitcontent')
  284     end
  285 
  286     def cells(label, text, options={})
  287       options[:class] = [options[:class] || "", 'attribute'].join(' ')
  288       content_tag(
  289         'div',
  290         content_tag('div', label + ":", :class => 'label') +
  291           content_tag('div', text, :class => 'value'),
  292         options)
  293     end
  294   end
  295 
  296   def issue_fields_rows
  297     r = IssueFieldsRows.new
  298     yield r
  299     r.to_html
  300   end
  301 
  302   def render_half_width_custom_fields_rows(issue)
  303     values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?}
  304     return if values.empty?
  305     half = (values.size / 2.0).ceil
  306     issue_fields_rows do |rows|
  307       values.each_with_index do |value, i|
  308         m = (i < half ? :left : :right)
  309         rows.send m, custom_field_name_tag(value.custom_field), custom_field_value_tag(value), :class => value.custom_field.css_classes
  310       end
  311     end
  312   end
  313 
  314   def render_full_width_custom_fields_rows(issue)
  315     values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?}
  316     return if values.empty?
  317     s = ''.html_safe
  318     values.each_with_index do |value, i|
  319       attr_value_tag = custom_field_value_tag(value)
  320       next if attr_value_tag.blank?
  321       content =
  322         content_tag('hr') +
  323         content_tag('p', content_tag('strong', custom_field_name_tag(value.custom_field) )) +
  324         content_tag('div', attr_value_tag, class: 'value')
  325       s << content_tag('div', content, class: "#{value.custom_field.css_classes} attribute")
  326     end
  327     s
  328   end
  329 
  330   # Returns the path for updating the issue form
  331   # with project as the current project
  332   def update_issue_form_path(project, issue)
  333     options = {:format => 'js'}
  334     if issue.new_record?
  335       if project
  336         new_project_issue_path(project, options)
  337       else
  338         new_issue_path(options)
  339       end
  340     else
  341       edit_issue_path(issue, options)
  342     end
  343   end
  344 
  345   # Returns the number of descendants for an array of issues
  346   def issues_descendant_count(issues)
  347     ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
  348     ids -= issues.map(&:id)
  349     ids.size
  350   end
  351 
  352   def issues_destroy_confirmation_message(issues)
  353     issues = [issues] unless issues.is_a?(Array)
  354     message = l(:text_issues_destroy_confirmation)
  355 
  356     descendant_count = issues_descendant_count(issues)
  357     if descendant_count > 0
  358       message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
  359     end
  360     message
  361   end
  362 
  363   # Returns an array of users that are proposed as watchers
  364   # on the new issue form
  365   def users_for_new_issue_watchers(issue)
  366     users = issue.watcher_users.select{|u| u.status == User::STATUS_ACTIVE}
  367     if issue.project.users.count <= 20
  368       users = (users + issue.project.users.sort).uniq
  369     end
  370     users
  371   end
  372 
  373   def email_issue_attributes(issue, user, html)
  374     items = []
  375     %w(author status priority assigned_to category fixed_version start_date due_date).each do |attribute|
  376       if issue.disabled_core_fields.grep(/^#{attribute}(_id)?$/).empty?
  377         attr_value = (issue.send attribute).to_s
  378         next if attr_value.blank?
  379         if html
  380           items << content_tag('strong', "#{l("field_#{attribute}")}: ") + attr_value
  381         else
  382           items << "#{l("field_#{attribute}")}: #{attr_value}"
  383         end
  384       end
  385     end
  386     issue.visible_custom_field_values(user).each do |value|
  387       cf_value = show_value(value, false)
  388       next if cf_value.blank?
  389       if html
  390         items << content_tag('strong', "#{value.custom_field.name}: ") + cf_value
  391       else
  392         items << "#{value.custom_field.name}: #{cf_value}"
  393       end
  394     end
  395     items
  396   end
  397 
  398   def render_email_issue_attributes(issue, user, html=false)
  399     items = email_issue_attributes(issue, user, html)
  400     if html
  401       content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe, :class => "details")
  402     else
  403       items.map{|s| "* #{s}"}.join("\n")
  404     end
  405   end
  406 
  407   MultipleValuesDetail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
  408 
  409   # Returns the textual representation of a journal details
  410   # as an array of strings
  411   def details_to_strings(details, no_html=false, options={})
  412     options[:only_path] = !(options[:only_path] == false)
  413     strings = []
  414     values_by_field = {}
  415     details.each do |detail|
  416       if detail.property == 'cf'
  417         field = detail.custom_field
  418         if field && field.multiple?
  419           values_by_field[field] ||= {:added => [], :deleted => []}
  420           if detail.old_value
  421             values_by_field[field][:deleted] << detail.old_value
  422           end
  423           if detail.value
  424             values_by_field[field][:added] << detail.value
  425           end
  426           next
  427         end
  428       end
  429       strings << show_detail(detail, no_html, options)
  430     end
  431     if values_by_field.present?
  432       values_by_field.each do |field, changes|
  433         if changes[:added].any?
  434           detail = MultipleValuesDetail.new('cf', field.id.to_s, field)
  435           detail.value = changes[:added]
  436           strings << show_detail(detail, no_html, options)
  437         end
  438         if changes[:deleted].any?
  439           detail = MultipleValuesDetail.new('cf', field.id.to_s, field)
  440           detail.old_value = changes[:deleted]
  441           strings << show_detail(detail, no_html, options)
  442         end
  443       end
  444     end
  445     strings
  446   end
  447 
  448   # Returns the textual representation of a single journal detail
  449   def show_detail(detail, no_html=false, options={})
  450     multiple = false
  451     show_diff = false
  452     no_details = false
  453 
  454     case detail.property
  455     when 'attr'
  456       field = detail.prop_key.to_s.gsub(/\_id$/, "")
  457       label = l(("field_" + field).to_sym)
  458       case detail.prop_key
  459       when 'due_date', 'start_date'
  460         value = format_date(detail.value.to_date) if detail.value
  461         old_value = format_date(detail.old_value.to_date) if detail.old_value
  462 
  463       when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
  464             'priority_id', 'category_id', 'fixed_version_id'
  465         value = find_name_by_reflection(field, detail.value)
  466         old_value = find_name_by_reflection(field, detail.old_value)
  467 
  468       when 'estimated_hours'
  469         value = l_hours_short(detail.value.to_f) unless detail.value.blank?
  470         old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
  471 
  472       when 'parent_id'
  473         label = l(:field_parent_issue)
  474         value = "##{detail.value}" unless detail.value.blank?
  475         old_value = "##{detail.old_value}" unless detail.old_value.blank?
  476 
  477       when 'is_private'
  478         value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
  479         old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
  480 
  481       when 'description'
  482         show_diff = true
  483       end
  484     when 'cf'
  485       custom_field = detail.custom_field
  486       if custom_field
  487         label = custom_field.name
  488         if custom_field.format.class.change_no_details
  489           no_details = true
  490         elsif custom_field.format.class.change_as_diff
  491           show_diff = true
  492         else
  493           multiple = custom_field.multiple?
  494           value = format_value(detail.value, custom_field) if detail.value
  495           old_value = format_value(detail.old_value, custom_field) if detail.old_value
  496         end
  497       end
  498     when 'attachment'
  499       label = l(:label_attachment)
  500     when 'relation'
  501       if detail.value && !detail.old_value
  502         rel_issue = Issue.visible.find_by_id(detail.value)
  503         value =
  504           if rel_issue.nil?
  505             "#{l(:label_issue)} ##{detail.value}"
  506           else
  507             (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
  508           end
  509       elsif detail.old_value && !detail.value
  510         rel_issue = Issue.visible.find_by_id(detail.old_value)
  511         old_value =
  512           if rel_issue.nil?
  513             "#{l(:label_issue)} ##{detail.old_value}"
  514           else
  515             (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
  516           end
  517       end
  518       relation_type = IssueRelation::TYPES[detail.prop_key]
  519       label = l(relation_type[:name]) if relation_type
  520     end
  521     call_hook(:helper_issues_show_detail_after_setting,
  522               {:detail => detail, :label => label, :value => value, :old_value => old_value })
  523 
  524     label ||= detail.prop_key
  525     value ||= detail.value
  526     old_value ||= detail.old_value
  527 
  528     unless no_html
  529       label = content_tag('strong', label)
  530       old_value = content_tag("i", h(old_value)) if detail.old_value
  531       if detail.old_value && detail.value.blank? && detail.property != 'relation'
  532         old_value = content_tag("del", old_value)
  533       end
  534       if detail.property == 'attachment' && value.present? &&
  535           atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
  536         # Link to the attachment if it has not been removed
  537         value = link_to_attachment(atta, only_path: options[:only_path])
  538         if options[:only_path] != false
  539           value += ' '
  540           value += link_to_attachment atta, class: 'icon-only icon-download', title: l(:button_download), download: true
  541         end
  542       else
  543         value = content_tag("i", h(value)) if value
  544       end
  545     end
  546 
  547     if no_details
  548       s = l(:text_journal_changed_no_detail, :label => label).html_safe
  549     elsif show_diff
  550       s = l(:text_journal_changed_no_detail, :label => label)
  551       unless no_html
  552         diff_link =
  553           link_to(
  554             'diff',
  555             diff_journal_url(detail.journal_id, :detail_id => detail.id,
  556                              :only_path => options[:only_path]),
  557             :title => l(:label_view_diff))
  558         s << " (#{diff_link})"
  559       end
  560       s.html_safe
  561     elsif detail.value.present?
  562       case detail.property
  563       when 'attr', 'cf'
  564         if detail.old_value.present?
  565           l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
  566         elsif multiple
  567           l(:text_journal_added, :label => label, :value => value).html_safe
  568         else
  569           l(:text_journal_set_to, :label => label, :value => value).html_safe
  570         end
  571       when 'attachment', 'relation'
  572         l(:text_journal_added, :label => label, :value => value).html_safe
  573       end
  574     else
  575       l(:text_journal_deleted, :label => label, :old => old_value).html_safe
  576     end
  577   end
  578 
  579   # Find the name of an associated record stored in the field attribute
  580   def find_name_by_reflection(field, id)
  581     return nil if id.blank?
  582     @detail_value_name_by_reflection ||= Hash.new do |hash, key|
  583       association = Issue.reflect_on_association(key.first.to_sym)
  584       name = nil
  585       if association
  586         record = association.klass.find_by_id(key.last)
  587         if record
  588           name = record.name.force_encoding('UTF-8')
  589         end
  590       end
  591       hash[key] = name
  592     end
  593     @detail_value_name_by_reflection[[field, id]]
  594   end
  595 
  596   # Renders issue children recursively
  597   def render_api_issue_children(issue, api)
  598     return if issue.leaf?
  599     api.array :children do
  600       issue.children.each do |child|
  601         api.issue(:id => child.id) do
  602           api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
  603           api.subject child.subject
  604           render_api_issue_children(child, api)
  605         end
  606       end
  607     end
  608   end
  609 
  610   # Issue history tabs
  611   def issue_history_tabs
  612     tabs = []
  613     if @journals.present?
  614       journals_without_notes = @journals.select{|value| value.notes.blank?}
  615       journals_with_notes = @journals.reject{|value| value.notes.blank?}
  616 
  617       tabs << {:name => 'history', :label => :label_history, :onclick => 'showIssueHistory("history", this.href)', :partial => 'issues/tabs/history', :locals => {:issue => @issue, :journals => @journals}}
  618       tabs << {:name => 'notes', :label => :label_issue_history_notes, :onclick => 'showIssueHistory("notes", this.href)'} if journals_with_notes.any?
  619       tabs << {:name => 'properties', :label => :label_issue_history_properties, :onclick => 'showIssueHistory("properties", this.href)'} if journals_without_notes.any?
  620     end
  621     tabs << {:name => 'time_entries', :label => :label_time_entry_plural, :remote => true, :onclick => "getRemoteTab('time_entries', '#{tab_issue_path(@issue, :name => 'time_entries')}', '#{issue_path(@issue, :tab => 'time_entries')}')"} if User.current.allowed_to?(:view_time_entries, @project) && @issue.spent_hours > 0
  622     tabs << {:name => 'changesets', :label => :label_associated_revisions, :remote => true, :onclick => "getRemoteTab('changesets', '#{tab_issue_path(@issue, :name => 'changesets')}', '#{issue_path(@issue, :tab => 'changesets')}')"} if @has_changesets
  623     tabs
  624   end
  625 
  626   def issue_history_default_tab
  627     # tab params overrides user default tab preference
  628     return params[:tab] if params[:tab].present?
  629     user_default_tab = User.current.pref.history_default_tab
  630 
  631     case user_default_tab
  632     when 'last_tab_visited'
  633       cookies['history_last_tab'].presence || 'notes'
  634     when ''
  635       'notes'
  636     else
  637       user_default_tab
  638     end
  639   end
  640 
  641 end