Index: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 4761) +++ app/models/issue.rb (working copy) @@ -17,7 +17,7 @@ class Issue < ActiveRecord::Base include Redmine::SafeAttributes - + belongs_to :project belongs_to :tracker belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' @@ -68,11 +68,6 @@ named_scope :with_limit, lambda { |limit| { :limit => limit} } named_scope :on_active_project, :include => [:status, :project, :tracker], :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] - named_scope :for_gantt, lambda { - { - :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version] - } - } named_scope :without_version, lambda { { @@ -80,11 +75,6 @@ } } - named_scope :with_query, lambda {|query| - { - :conditions => Query.merge_conditions(query.statement) - } - } before_create :default_assign before_save :close_duplicates, :update_done_ratio_from_issue_status Index: app/models/query.rb =================================================================== --- app/models/query.rb (revision 4761) +++ app/models/query.rb (working copy) @@ -388,29 +388,44 @@ def group_by_statement group_by_column.try(:groupable) end - - def project_statement + + def project_statement(db_table) project_clauses = [] - if project && !@project.descendants.active.empty? - ids = [project.id] - if has_filter?("subproject_id") - case operator_for("subproject_id") - when '=' - # include the selected subprojects - ids += values_for("subproject_id").each(&:to_i) - when '!*' - # main project only + + if valid? + if project + + if !project.descendants.active.empty? + + if has_filter?('subproject_id') + case operator_for('subproject_id') + when '=' + # include the selected subprojects + ids = values_for('subproject_id').each(&:to_i) + when '!*' + # main project only + ids = [project.id] + else + # all subprojects + ids = project.descendants.collect(&:id) + end + else + ids = [project.id] + ids << project.descendants.collect(&:id) if Setting.display_subprojects_issues? + end + project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') else - # all subprojects - ids += project.descendants.collect(&:id) + project_clauses << "#{Project.table_name}.id = %d" % project.id end - elsif Setting.display_subprojects_issues? - ids += project.descendants.collect(&:id) + + elsif has_filter?('project_id') + # project filter + db_field = db_table == Project.table_name ? 'id' : 'project_id' + project_clauses << '(' + sql_for_field('project_id', operator_for('project_id'), values_for('project_id').each(&:to_i), db_table, db_field) + ')' end - project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') - elsif project - project_clauses << "#{Project.table_name}.id = %d" % project.id + end + project_clauses << Project.allowed_to_condition(User.current, :view_issues) project_clauses.join(' AND ') end @@ -419,7 +434,7 @@ # filters clauses filters_clauses = [] filters.each_key do |field| - next if field == "subproject_id" + next if ['subproject_id', 'project_id'].include?(field) v = values_for(field).clone next unless v and !v.empty? operator = operator_for(field) @@ -428,7 +443,7 @@ if %w(assigned_to_id author_id watcher_id).include?(field) v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") end - + sql = '' if field =~ /^cf_(\d+)$/ # custom field @@ -481,7 +496,7 @@ end user_ids.flatten.uniq.compact }.sort.collect(&:to_s) - + sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')' else # regular field @@ -493,7 +508,7 @@ end if filters and valid? - (filters_clauses << project_statement).join(' AND ') + (filters_clauses << project_statement(Issue.table_name)).join(' AND ') end # Returns the issue count @@ -522,7 +537,7 @@ rescue ::ActiveRecord::StatementInvalid => e raise StatementInvalid.new(e.message) end - + # Returns the issues # Valid options are :order, :offset, :limit, :include, :conditions def issues(options={}) @@ -554,11 +569,11 @@ # Valid options are :conditions def versions(options={}) Version.find :all, :include => :project, - :conditions => Query.merge_conditions(project_statement, options[:conditions]) + :conditions => Query.merge_conditions(project_statement(Version.table_name), options[:conditions]) rescue ::ActiveRecord::StatementInvalid => e raise StatementInvalid.new(e.message) end - + private # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ Index: lib/redmine/helpers/gantt.rb =================================================================== --- lib/redmine/helpers/gantt.rb (revision 4761) +++ lib/redmine/helpers/gantt.rb (working copy) @@ -38,10 +38,10 @@ attr_accessor :query attr_accessor :project attr_accessor :view - + def initialize(options={}) options = options.dup - + if options[:year] && options[:year].to_i >0 @year_from = options[:year].to_i if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12 @@ -53,18 +53,18 @@ @month_from ||= Date.today.month @year_from ||= Date.today.year end - + zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i @zoom = (zoom > 0 && zoom < 5) ? zoom : 2 months = (options[:months] || User.current.pref[:gantt_months]).to_i @months = (months > 0 && months < 25) ? months : 6 - + # Save gantt parameters as user preference (zoom and months count) if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months])) User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months User.current.preference.save end - + @date_from = Date.civil(@year_from, @month_from, 1) @date_to = (@date_from >> @months) - 1 @@ -98,51 +98,12 @@ common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months }) end - ### Extracted from the HTML view/helpers + ### Extracted from the HTML view/helpers # Returns the number of rows that will be rendered on the Gantt chart def number_of_rows - return @number_of_rows if @number_of_rows - - rows = if @project - number_of_rows_on_project(@project) - else - Project.roots.visible.has_module('issue_tracking').inject(0) do |total, project| - total += number_of_rows_on_project(project) - end - end - - rows > @max_rows ? @max_rows : rows + @number_of_rows end - # Returns the number of rows that will be used to list a project on - # the Gantt chart. This will recurse for each subproject. - def number_of_rows_on_project(project) - # Remove the project requirement for Versions because it will - # restrict issues to only be on the current project. This - # ends up missing issues which are assigned to shared versions. - @query.project = nil if @query.project - - # One Root project - count = 1 - # Issues without a Version - count += project.issues.for_gantt.without_version.with_query(@query).count - - # Versions - count += project.versions.count - - # Issues on the Versions - project.versions.each do |version| - count += version.fixed_issues.for_gantt.with_query(@query).count - end - - # Subprojects - project.children.visible.has_module('issue_tracking').each do |subproject| - count += number_of_rows_on_project(subproject) - end - - count - end - # Renders the subjects of the Gantt chart, the left side. def subjects(options={}) render(options.merge(:only => :subjects)) unless @subjects_rendered @@ -154,105 +115,255 @@ render(options.merge(:only => :lines)) unless @lines_rendered @lines end - + def render(options={}) options = {:indent => 4, :render => :subject, :format => :html}.merge(options) - + @subjects = '' unless options[:only] == :lines @lines = '' unless options[:only] == :subjects @number_of_rows = 0 - - if @project - render_project(@project, options) - else - Project.roots.visible.has_module('issue_tracking').each do |project| - render_project(project, options) + + options[:render_issues_without_version_first] = true unless options.key? :render_issues_without_version_first + + options[:top] = 0 unless options.key? :top + options[:indent_increment] = 20 unless options.key? :indent_increment + options[:top_increment] = 20 unless options.key? :top_increment + + options[:gantt_indent] = options[:indent] + + # We sort by project.lft so that projects are listed before their children + top_projects = Project.find( :all, + :conditions => @query.project_statement(Project.table_name), + :order => ["#{Project.table_name}.lft ASC"]) + + # Purge descendants to be sure to have only top projects left + prev_top = nil + top_projects.delete_if do |top| + remove_descendant = prev_top && top.is_descendant_of?(prev_top) + prev_top = top + remove_descendant + end + + # Get issues with theirs versions and subprojects as filtered by query criterias + issues = Issue.find( + :all, + :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version], + :conditions => @query.statement, + :joins => [ + "LEFT OUTER JOIN #{Project.table_name} AS top_project ON top_project.id IN (#{top_projects.collect(&:id).join(', ')}) AND top_project.lft <= #{Project.table_name}.lft AND top_project.rgt >= #{Project.table_name}.rgt " + # Top project + "LEFT OUTER JOIN #{Project.table_name} AS version_project ON version_project.id = #{Version.table_name}.project_id " + # Project of the fixed version + "LEFT OUTER JOIN #{Project.table_name} AS version_top_project ON " + + "(version_top_project.id = #{Project.table_name}.id AND #{Version.table_name}.sharing = 'none')" + + " OR (version_top_project.id = top_project.id AND #{Version.table_name}.sharing IN ('hierarchy', 'system', 'tree'))" + + " OR (version_top_project.id = CASE WHEN top_project.lft < version_project.lft THEN version_project.id ELSE top_project.id END AND #{Version.table_name}.sharing = 'descendants')"], + :order => [ + "top_project.lft ASC, top_project.name ASC, " + # Top project + "COALESCE(version_top_project.lft, #{Project.table_name}.lft) ASC, " + # Version top project or project if none + "CASE WHEN #{Issue.table_name}.fixed_version_id IS NULL THEN #{ options[:render_issues_without_version_first] ? '0 ELSE 1' : '1 ELSE 0' } END ASC, " + # Issues without version first (or last) + "CASE WHEN #{Version.table_name}.effective_date IS NULL THEN 0 ELSE 1 END DESC, " + # Version with effective_date first + "#{Version.table_name}.effective_date ASC, #{Version.table_name}.name ASC, " + + "#{Project.table_name}.lft ASC"] ) + + # TODO : at this point we could have been returned 3 extra Project objects useful for building project path : top_project, version_project and top_version_project + # There is no way ActiveRecord can map sql query results to more than one model (hibernate does), so we have to compute them again in update_and_render_project_path(), adding a dummy time & cpu overhead to the whole rendering process + # Idea : use an IssueForGantt class with association to issue, top_project, version_project and top_version_project ? + + prev_issue = nil + path_issues = [] + + # TODO : use a Hash rather than an Array when switching to Ruby 1.9, as Hash is ordered in 1.9 + issue_path = [] + + # Render Gantt by looping on issues + issues.each do |i| + + # Stack issues within the same project path to allow them to be sorted before rendering + if prev_issue && ( + prev_issue.project != i.project || + prev_issue.fixed_version != i.fixed_version) + + render_path_issues(path_issues, options) unless path_issues.empty? break if abort? + + path_issues.clear end + + issue_path = update_and_render_project_path(top_projects, i, prev_issue, issue_path, options) + break if abort? + + path_issues << i + + prev_issue = i end - + + render_path_issues(path_issues, options) unless abort? + @subjects_rendered = true unless options[:only] == :lines @lines_rendered = true unless options[:only] == :subjects - + render_end(options) end - def render_project(project, options={}) - options[:top] = 0 unless options.key? :top - options[:indent_increment] = 20 unless options.key? :indent_increment - options[:top_increment] = 20 unless options.key? :top_increment + # Renders issues in a version/project path + def render_path_issues(issues, options) - subject_for_project(project, options) unless options[:only] == :lines - line_for_project(project, options) unless options[:only] == :subjects - - options[:top] += options[:top_increment] - options[:indent] += options[:indent_increment] - @number_of_rows += 1 - return if abort? - - # Second, Issues without a version - issues = project.issues.for_gantt.without_version.with_query(@query).all(:limit => current_limit) - sort_issues!(issues) - if issues - render_issues(issues, options) - return if abort? - end + sort_issues! issues - # Third, Versions - project.versions.sort.each do |version| - render_version(version, options) - return if abort? - end + base_indent = options[:indent] - # Fourth, subprojects - project.children.visible.has_module('issue_tracking').each do |project| - render_project(project, options) - return if abort? - end unless project.leaf? + prev_issue = nil + issue_hierarchy = [] - # Remove indent to hit the next sibling - options[:indent] -= options[:indent_increment] - end + issues.each do |i| - def render_issues(issues, options={}) - @issue_ancestors = [] - - issues.each do |i| + # Indent issue hierarchy (issue in the same hierarchy tree should follow thanks to the sorting order) + ancestor_idx = -1 + (issue_hierarchy.length - 1).downto(0) do |h_i| + if issue_hierarchy[h_i].is_ancestor_of?(i) + ancestor_idx = h_i + break + end + end + issue_hierarchy.slice!((ancestor_idx + 1)..-1) + + issue_hierarchy << i + + options[:indent] = base_indent + (issue_hierarchy.length - 1) * options[:indent_increment] + + # Render issue node subject_for_issue(i, options) unless options[:only] == :lines line_for_issue(i, options) unless options[:only] == :subjects - - options[:top] += options[:top_increment] @number_of_rows += 1 break if abort? + options[:top] += options[:top_increment] + + prev_issue = i end - - options[:indent] -= (options[:indent_increment] * @issue_ancestors.size) + end + def update_and_render_project_path(top_projects, i, prev_issue, issue_path, options) + + prev_version = prev_issue.nil? ? nil : prev_issue.fixed_version + project_change = prev_issue.nil? || i.project != prev_issue.project + + first_item_to_render_idx = nil + + # Render the project path to the project the issue belongs to + if project_change + + top_project = issue_path.empty? ? nil : issue_path.first + + # Remove invalid path queue + if top_project.nil? || !i.project.is_or_is_descendant_of?(top_project) + # New branch + top_project = top_projects.detect { |p| p.is_or_is_ancestor_of?(i.project) } + top_projects.delete(top_project) # Lighten the top_projects map as a top project should not be rendered twice + issue_path = [top_project] + first_item_to_render_idx = 0 + prev_version = nil + elsif !prev_issue.nil? && !i.project.is_descendant_of?(prev_issue.project) + # Truncate actual path (in any way, we keep the top project node) + # TODO : use index { } with Ruby 1.8.7+ + first_distinct_project_idx = nil + 1.upto(issue_path.length - 1) do |pi_idx| + if issue_path[pi_idx].is_a?(Project) && !issue_path[pi_idx].is_ancestor_of?(i.project) + first_distinct_project_idx = pi_idx + break + end + end + issue_path.slice!(first_distinct_project_idx..-1) + end + + # TODO : use rindex { } with Ruby 1.9 + last_common_project_idx = nil + (issue_path.length - 1).downto(0) do |pi_idx| + if issue_path[pi_idx].is_a?(Project) + last_common_project_idx = pi_idx + break + end + end + + # Complete path + rpath_queue = [] + subproject = i.project + until subproject == issue_path.at(last_common_project_idx) + rpath_queue << subproject + subproject = subproject.parent + end + + unless rpath_queue.empty? + first_item_to_render_idx = issue_path.length if first_item_to_render_idx.nil? + issue_path += rpath_queue.reverse + end + end + + version_change = i.fixed_version != prev_version + + if version_change + # Remove previous Version node from path + unless prev_version.nil? + prev_version_idx = issue_path.index(prev_version) + unless prev_version_idx.nil? # May have been removed with the project path truncation + issue_path.delete_at(prev_version_idx) + first_item_to_render_idx = prev_version_idx if prev_version_idx < issue_path.length || (!first_item_to_render_idx.nil? && prev_version_idx < first_item_to_render_idx) + end + end + + # Place new Version node within the path + unless i.fixed_version.nil? + # TODO : use index { } with Ruby 1.8.7+ + new_version_idx = nil + 0.upto(issue_path.length - 1) do |pi_idx| + # TODO : p.shared_versions() can slow down rendering, maybe add a has_shared_version?(v) in Project ? + if issue_path[pi_idx].shared_versions.include?(i.fixed_version) + new_version_idx = pi_idx + 1 + break + end + end + issue_path.insert(new_version_idx, i.fixed_version) + first_item_to_render_idx = new_version_idx if first_item_to_render_idx.nil? || new_version_idx < first_item_to_render_idx + end + end + + # Render new path part + unless first_item_to_render_idx.nil? + options[:indent] = options[:gantt_indent] + first_item_to_render_idx * options[:indent_increment] + + issue_path.slice(first_item_to_render_idx..-1).each do |pi| + + if pi.is_a?(Version) + # Render version node + render_version(pi, options) + else + # Render project node + render_subproject(pi, options) + end + break if abort? + options[:indent] += options[:indent_increment] + options[:top] += options[:top_increment] + end + else + options[:indent] = options[:gantt_indent] + issue_path.length * options[:indent_increment] + end + + issue_path + end + def render_version(version, options={}) # Version header subject_for_version(version, options) unless options[:only] == :lines line_for_version(version, options) unless options[:only] == :subjects - - options[:top] += options[:top_increment] @number_of_rows += 1 - return if abort? - - # Remove the project requirement for Versions because it will - # restrict issues to only be on the current project. This - # ends up missing issues which are assigned to shared versions. - @query.project = nil if @query.project - - issues = version.fixed_issues.for_gantt.with_query(@query).all(:limit => current_limit) - if issues - sort_issues!(issues) - # Indent issues - options[:indent] += options[:indent_increment] - render_issues(issues, options) - options[:indent] -= options[:indent_increment] - end end - + + def render_subproject(subproject, options={}) + # Subproject header + subject_for_project(subproject, options) unless options[:only] == :lines + line_for_project(subproject, options) unless options[:only] == :subjects + @number_of_rows += 1 + end + def render_end(options={}) case options[:format] when :pdf @@ -318,7 +429,7 @@ if version.is_a?(Version) && version.start_date && version.due_date options[:zoom] ||= 1 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] - + coords = coordinates(version.start_date, version.due_date, version.completed_pourcent, options[:zoom]) label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%" label = h("#{version.project} -") + label unless @project && @project == version.project @@ -338,18 +449,13 @@ end def subject_for_issue(issue, options) - while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last) - @issue_ancestors.pop - options[:indent] -= options[:indent_increment] - end - output = case options[:format] when :html css_classes = '' css_classes << ' issue-overdue' if issue.overdue? css_classes << ' issue-behind-schedule' if issue.behind_schedule? css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to - + subject = "" if issue.assigned_to.present? assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name @@ -365,11 +471,6 @@ pdf_subject(options, issue.subject) end - unless issue.leaf? - @issue_ancestors << issue - options[:indent] += options[:indent_increment] - end - output end @@ -405,18 +506,22 @@ # width of one day in pixels zoom = @zoom*2 g_width = (@date_to - @date_from + 1)*zoom - g_height = 20 * number_of_rows + 30 + g_height = 20 * 5 + 30 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) height = g_height + headers_heigth - + width = subject_width+g_width+1 + imgl = Magick::ImageList.new - imgl.new_image(subject_width+g_width+1, height) + imgl.new_image(width, height) gc = Magick::Draw.new - + # Subjects gc.stroke('transparent') subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image) - + g_height = 20 * number_of_rows + 30 + height = g_height + headers_heigth + imgl.last.resize!(width, height) + # Months headers month_f = @date_from left = subject_width @@ -433,7 +538,7 @@ left = left + width month_f = month_f >> 1 end - + # Weeks headers if show_weeks left = subject_width @@ -465,7 +570,7 @@ week_f = week_f+7 end end - + # Days details (week-end in grey) if show_days left = subject_width @@ -482,7 +587,7 @@ wday = 1 if wday > 7 end end - + # border gc.fill('transparent') gc.stroke('grey') @@ -490,7 +595,7 @@ gc.rectangle(0, 0, subject_width+g_width, headers_heigth) gc.stroke('black') gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1) - + # content top = headers_heigth + 20 @@ -502,8 +607,8 @@ gc.stroke('red') x = (Date.today-@date_from+1)*zoom + subject_width gc.line(x, headers_heigth, x, headers_heigth + g_height-1) - end - + end + gc.draw(imgl) imgl.format = format imgl.to_blob @@ -520,14 +625,14 @@ pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s) pdf.Ln pdf.SetFontStyle('B',9) - + subject_width = PDF::LeftPaneWidth header_heigth = 5 - + headers_heigth = header_heigth show_weeks = false show_days = false - + if self.months < 7 show_weeks = true headers_heigth = 2*header_heigth @@ -536,14 +641,14 @@ headers_heigth = 3*header_heigth end end - + g_width = PDF.right_pane_width zoom = (g_width) / (self.date_to - self.date_from + 1) g_height = 120 t_height = g_height + headers_heigth - + y_start = pdf.GetY - + # Months headers month_f = self.date_from left = subject_width @@ -556,7 +661,7 @@ left = left + width month_f = month_f >> 1 end - + # Weeks headers if show_weeks left = subject_width @@ -582,7 +687,7 @@ week_f = week_f+7 end end - + # Days headers if show_days left = subject_width @@ -599,11 +704,11 @@ wday = 1 if wday > 7 end end - + pdf.SetY(y_start) pdf.SetX(15) pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1) - + # Tasks top = headers_heigth + y_start options = { @@ -620,12 +725,12 @@ render(options) pdf.Output end - + private - + def coordinates(start_date, end_date, progress, zoom=nil) zoom ||= @zoom - + coords = {} if start_date && end_date && start_date < self.date_to && end_date > self.date_from if start_date > self.date_from @@ -640,7 +745,7 @@ else coords[:bar_end] = self.date_to - self.date_from + 1 end - + if progress progress_date = start_date + (end_date - start_date) * (progress / 100.0) if progress_date > self.date_from && progress_date > start_date @@ -650,7 +755,7 @@ coords[:bar_progress_end] = self.date_to - self.date_from + 1 end end - + if progress_date < Date.today late_date = [Date.today, end_date].min if late_date > self.date_from && late_date > start_date @@ -663,7 +768,7 @@ end end end - + # Transforms dates into pixels witdh coords.keys.each do |key| coords[key] = (coords[key] * zoom).floor @@ -671,20 +776,39 @@ coords end - # Sorts a collection of issues by start_date, due_date, id for gantt rendering + # Sorts a collection of issues for gantt rendering def sort_issues!(issues) - issues.sort! { |a, b| gantt_issue_compare(a, b, issues) } + issues.sort! { |a, b| gantt_issue_compare(a, b) } end - - # TODO: top level issues should be sorted by start date - def gantt_issue_compare(x, y, issues) - if x.root_id == y.root_id + + # Compare issues for rendering order + def gantt_issue_compare(x, y) + + if x.parent_id == y.parent_id + # Same level in issue hierarchy + basic_gantt_issue_compare(x, y) + elsif x.root_id == y.root_id || x.root_id == y.id || y.root_id == x.id + # Same issue hierarchy x.lft <=> y.lft else + # Distinct hierarchies x.root_id <=> y.root_id + # TODO : advanced sort between issues when ancestors have been filtered out by query end end - + + def basic_gantt_issue_compare(x, y) + if x.start_date == y.start_date + x.id <=> y.id + elsif x.start_date.nil? + 1 # null date appears at the end + elsif y.start_date.nil? + -1 + else + x.start_date <=> y.start_date + end + end + def current_limit if @max_rows @max_rows - @number_of_rows @@ -692,13 +816,13 @@ nil end end - + def abort? if @max_rows && @number_of_rows >= @max_rows @truncated = true end end - + def pdf_new_page?(options) if options[:top] > 180 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) @@ -707,7 +831,7 @@ options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1) end end - + def html_subject(params, subject, options={}) output = "
" output << subject @@ -715,26 +839,26 @@ @subjects << output output end - + def pdf_subject(params, subject, options={}) params[:pdf].SetY(params[:top]) params[:pdf].SetX(15) - + char_limit = PDF::MaxCharactorsForSubject - params[:indent] params[:pdf].Cell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR") - + params[:pdf].SetY(params[:top]) params[:pdf].SetX(params[:subject_width]) params[:pdf].Cell(params[:g_width], 5, "", "LR") end - + def image_subject(params, subject, options={}) params[:image].fill('black') params[:image].stroke('transparent') params[:image].stroke_width(1) params[:image].text(params[:indent], params[:top] + 2, subject) end - + def html_task(params, coords, options={}) output = '' # Renders the task bar, with progress and late @@ -773,7 +897,7 @@ @lines << output output end - + def pdf_task(params, coords, options={}) height = options[:height] || 2 Index: test/unit/lib/redmine/helpers/gantt_test.rb =================================================================== --- test/unit/lib/redmine/helpers/gantt_test.rb (revision 4761) +++ test/unit/lib/redmine/helpers/gantt_test.rb (working copy) @@ -91,56 +91,6 @@ end end - context "#number_of_rows_on_project" do - setup do - create_gantt - end - - should "clear the @query.project so cross-project issues and versions can be counted" do - assert @gantt.query.project - @gantt.number_of_rows_on_project(@project) - assert_nil @gantt.query.project - end - - should "count 1 for the project itself" do - assert_equal 1, @gantt.number_of_rows_on_project(@project) - end - - should "count the number of issues without a version" do - @project.issues << Issue.generate_for_project!(@project, :fixed_version => nil) - assert_equal 2, @gantt.number_of_rows_on_project(@project) - end - - should "count the number of versions" do - @project.versions << Version.generate! - @project.versions << Version.generate! - assert_equal 3, @gantt.number_of_rows_on_project(@project) - end - - should "count the number of issues on versions, including cross-project" do - version = Version.generate! - @project.versions << version - @project.issues << Issue.generate_for_project!(@project, :fixed_version => version) - - assert_equal 3, @gantt.number_of_rows_on_project(@project) - end - - should "recursive and count the number of rows on each subproject" do - @project.versions << Version.generate! # +1 - - @subproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1 - @subproject.set_parent!(@project) - @subproject.issues << Issue.generate_for_project!(@subproject) # +1 - @subproject.issues << Issue.generate_for_project!(@subproject) # +1 - - @subsubproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1 - @subsubproject.set_parent!(@subproject) - @subsubproject.issues << Issue.generate_for_project!(@subsubproject) # +1 - - assert_equal 7, @gantt.number_of_rows_on_project(@project) # +1 for self - end - end - # TODO: more of an integration test context "#subjects" do setup do