Mercurial > hg > soundsoftware-site
diff .svn/pristine/02/026bb2d46eaa121e79862de3515bb99ff0827ca3.svn-base @ 1298:4f746d8966dd redmine_2.3_integration
Merge from redmine-2.3 branch to create new branch redmine-2.3-integration
author | Chris Cannam |
---|---|
date | Fri, 14 Jun 2013 09:28:30 +0100 |
parents | 622f24f53b42 |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/026bb2d46eaa121e79862de3515bb99ff0827ca3.svn-base Fri Jun 14 09:28:30 2013 +0100 @@ -0,0 +1,943 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Helpers + # Simple class to handle gantt chart data + class Gantt + include ERB::Util + include Redmine::I18n + include Redmine::Utils::DateCalculation + + # Relation types that are rendered + DRAW_TYPES = { + IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' }, + IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' } + }.freeze + + # :nodoc: + # Some utility methods for the PDF export + class PDF + MaxCharactorsForSubject = 45 + TotalWidth = 280 + LeftPaneWidth = 100 + + def self.right_pane_width + TotalWidth - LeftPaneWidth + end + end + + attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows + 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 + @month_from = options[:month].to_i + else + @month_from = 1 + end + else + @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 + @subjects = '' + @lines = '' + @number_of_rows = nil + @issue_ancestors = [] + @truncated = false + if options.has_key?(:max_rows) + @max_rows = options[:max_rows] + else + @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i + end + end + + def common_params + { :controller => 'gantts', :action => 'show', :project_id => @project } + end + + def params + common_params.merge({:zoom => zoom, :year => year_from, + :month => month_from, :months => months}) + end + + def params_previous + common_params.merge({:year => (date_from << months).year, + :month => (date_from << months).month, + :zoom => zoom, :months => months}) + end + + def params_next + common_params.merge({:year => (date_from >> months).year, + :month => (date_from >> months).month, + :zoom => zoom, :months => months}) + end + + # 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 = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)} + rows > @max_rows ? @max_rows : 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) + return 0 unless projects.include?(project) + count = 1 + count += project_issues(project).size + count += project_versions(project).size + count + end + + # Renders the subjects of the Gantt chart, the left side. + def subjects(options={}) + render(options.merge(:only => :subjects)) unless @subjects_rendered + @subjects + end + + # Renders the lines of the Gantt chart, the right side + def lines(options={}) + render(options.merge(:only => :lines)) unless @lines_rendered + @lines + end + + # Returns issues that will be rendered + def issues + @issues ||= @query.issues( + :include => [:assigned_to, :tracker, :priority, :category, :fixed_version], + :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC", + :limit => @max_rows + ) + end + + # Returns a hash of the relations between the issues that are present on the gantt + # and that should be displayed, grouped by issue ids. + def relations + return @relations if @relations + if issues.any? + issue_ids = issues.map(&:id) + @relations = IssueRelation. + where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys). + group_by(&:issue_from_id) + else + @relations = {} + end + end + + # Return all the project nodes that will be displayed + def projects + return @projects if @projects + ids = issues.collect(&:project).uniq.collect(&:id) + if ids.any? + # All issues projects and their visible ancestors + @projects = Project.visible.all( + :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt", + :conditions => ["child.id IN (?)", ids], + :order => "#{Project.table_name}.lft ASC" + ).uniq + else + @projects = [] + end + end + + # Returns the issues that belong to +project+ + def project_issues(project) + @issues_by_project ||= issues.group_by(&:project) + @issues_by_project[project] || [] + end + + # Returns the distinct versions of the issues that belong to +project+ + def project_versions(project) + project_issues(project).collect(&:fixed_version).compact.uniq + end + + # Returns the issues that belong to +project+ and are assigned to +version+ + def version_issues(project, version) + project_issues(project).select {|issue| issue.fixed_version == version} + end + + def render(options={}) + options = {:top => 0, :top_increment => 20, + :indent_increment => 20, :render => :subject, + :format => :html}.merge(options) + indent = options[:indent] || 4 + @subjects = '' unless options[:only] == :lines + @lines = '' unless options[:only] == :subjects + @number_of_rows = 0 + Project.project_tree(projects) do |project, level| + options[:indent] = indent + level * options[:indent_increment] + render_project(project, options) + break if abort? + end + @subjects_rendered = true unless options[:only] == :lines + @lines_rendered = true unless options[:only] == :subjects + render_end(options) + end + + def render_project(project, 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? + issues = project_issues(project).select {|i| i.fixed_version.nil?} + sort_issues!(issues) + if issues + render_issues(issues, options) + return if abort? + end + versions = project_versions(project) + versions.each do |version| + render_version(project, version, options) + end + # Remove indent to hit the next sibling + options[:indent] -= options[:indent_increment] + end + + def render_issues(issues, options={}) + @issue_ancestors = [] + issues.each do |i| + 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? + end + options[:indent] -= (options[:indent_increment] * @issue_ancestors.size) + end + + def render_version(project, 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? + issues = version_issues(project, version) + 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_end(options={}) + case options[:format] + when :pdf + options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) + end + end + + def subject_for_project(project, options) + case options[:format] + when :html + html_class = "" + html_class << 'icon icon-projects ' + html_class << (project.overdue? ? 'project-overdue' : '') + s = view.link_to_project(project).html_safe + subject = view.content_tag(:span, s, + :class => html_class).html_safe + html_subject(options, subject, :css => "project-name") + when :image + image_subject(options, project.name) + when :pdf + pdf_new_page?(options) + pdf_subject(options, project.name) + end + end + + def line_for_project(project, options) + # Skip versions that don't have a start_date or due date + if project.is_a?(Project) && project.start_date && project.due_date + options[:zoom] ||= 1 + options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] + coords = coordinates(project.start_date, project.due_date, nil, options[:zoom]) + label = h(project) + case options[:format] + when :html + html_task(options, coords, :css => "project task", :label => label, :markers => true) + when :image + image_task(options, coords, :label => label, :markers => true, :height => 3) + when :pdf + pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) + end + else + '' + end + end + + def subject_for_version(version, options) + case options[:format] + when :html + html_class = "" + html_class << 'icon icon-package ' + html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " + html_class << (version.overdue? ? 'version-overdue' : '') + html_class << ' version-closed' unless version.open? + if version.start_date && version.due_date && version.completed_pourcent + progress_date = calc_progress_date(version.start_date, + version.due_date, version.completed_pourcent) + html_class << ' behind-start-date' if progress_date < self.date_from + html_class << ' over-end-date' if progress_date > self.date_to + end + s = view.link_to_version(version).html_safe + subject = view.content_tag(:span, s, + :class => html_class).html_safe + html_subject(options, subject, :css => "version-name", + :id => "version-#{version.id}") + when :image + image_subject(options, version.to_s_with_project) + when :pdf + pdf_new_page?(options) + pdf_subject(options, version.to_s_with_project) + end + end + + def line_for_version(version, options) + # Skip versions that don't have a start_date + if version.is_a?(Version) && version.due_date && version.start_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_percent, + options[:zoom]) + label = "#{h version} #{h version.completed_percent.to_i.to_s}%" + label = h("#{version.project} -") + label unless @project && @project == version.project + case options[:format] + when :html + html_task(options, coords, :css => "version task", + :label => label, :markers => true, :version => version) + when :image + image_task(options, coords, :label => label, :markers => true, :height => 3) + when :pdf + pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) + end + else + '' + end + 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 + css_classes << ' issue-closed' if issue.closed? + if issue.start_date && issue.due_before && issue.done_ratio + progress_date = calc_progress_date(issue.start_date, + issue.due_before, issue.done_ratio) + css_classes << ' behind-start-date' if progress_date < self.date_from + css_classes << ' over-end-date' if progress_date > self.date_to + end + s = "".html_safe + if issue.assigned_to.present? + assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name + s << view.avatar(issue.assigned_to, + :class => 'gravatar icon-gravatar', + :size => 10, + :title => assigned_string).to_s.html_safe + end + s << view.link_to_issue(issue).html_safe + subject = view.content_tag(:span, s, :class => css_classes).html_safe + html_subject(options, subject, :css => "issue-subject", + :title => issue.subject, :id => "issue-#{issue.id}") + "\n" + when :image + image_subject(options, issue.subject) + when :pdf + pdf_new_page?(options) + pdf_subject(options, issue.subject) + end + unless issue.leaf? + @issue_ancestors << issue + options[:indent] += options[:indent_increment] + end + output + end + + def line_for_issue(issue, options) + # Skip issues that don't have a due_before (due_date or version's due_date) + if issue.is_a?(Issue) && issue.due_before + coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom]) + label = "#{issue.status.name} #{issue.done_ratio}%" + case options[:format] + when :html + html_task(options, coords, + :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), + :label => label, :issue => issue, + :markers => !issue.leaf?) + when :image + image_task(options, coords, :label => label) + when :pdf + pdf_task(options, coords, :label => label) + end + else + '' + end + end + + # Generates a gantt image + # Only defined if RMagick is avalaible + def to_image(format='PNG') + date_to = (@date_from >> @months) - 1 + show_weeks = @zoom > 1 + show_days = @zoom > 2 + subject_width = 400 + header_height = 18 + # width of one day in pixels + zoom = @zoom * 2 + g_width = (@date_to - @date_from + 1) * zoom + g_height = 20 * number_of_rows + 30 + headers_height = (show_weeks ? 2 * header_height : header_height) + height = g_height + headers_height + imgl = Magick::ImageList.new + imgl.new_image(subject_width + g_width + 1, height) + gc = Magick::Draw.new + gc.font = Redmine::Configuration['rmagick_font_path'] || "" + # Subjects + gc.stroke('transparent') + subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image) + # Months headers + month_f = @date_from + left = subject_width + @months.times do + width = ((month_f >> 1) - month_f) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, 0, left + width, height) + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}") + left = left + width + month_f = month_f >> 1 + end + # Weeks headers + if show_weeks + left = subject_width + height = header_height + if @date_from.cwday == 1 + # date_from is monday + week_f = date_from + else + # find next monday after date_from + week_f = @date_from + (7 - @date_from.cwday + 1) + width = (7 - @date_from.cwday + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1) + left = left + width + end + while week_f <= date_to + width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1) + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s) + left = left + width + week_f = week_f + 7 + end + end + # Days details (week-end in grey) + if show_days + left = subject_width + height = g_height + header_height - 1 + wday = @date_from.cwday + (date_to - @date_from + 1).to_i.times do + width = zoom + gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white') + gc.stroke('#ddd') + gc.stroke_width(1) + gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1) + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end + end + # border + gc.fill('transparent') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(0, 0, subject_width + g_width, headers_height) + gc.stroke('black') + gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1) + # content + top = headers_height + 20 + gc.stroke('transparent') + lines(:image => gc, :top => top, :zoom => zoom, + :subject_width => subject_width, :format => :image) + # today red line + if Date.today >= @date_from and Date.today <= date_to + gc.stroke('red') + x = (Date.today - @date_from + 1) * zoom + subject_width + gc.line(x, headers_height, x, headers_height + g_height - 1) + end + gc.draw(imgl) + imgl.format = format + imgl.to_blob + end if Object.const_defined?(:Magick) + + def to_pdf + pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language) + pdf.SetTitle("#{l(:label_gantt)} #{project}") + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage("L") + pdf.SetFontStyle('B', 12) + pdf.SetX(15) + pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s) + pdf.Ln + pdf.SetFontStyle('B', 9) + subject_width = PDF::LeftPaneWidth + header_height = 5 + headers_height = header_height + show_weeks = false + show_days = false + if self.months < 7 + show_weeks = true + headers_height = 2 * header_height + if self.months < 3 + show_days = true + headers_height = 3 * header_height + 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_height + y_start = pdf.GetY + # Months headers + month_f = self.date_from + left = subject_width + height = header_height + self.months.times do + width = ((month_f >> 1) - month_f) * zoom + pdf.SetY(y_start) + pdf.SetX(left) + pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C") + left = left + width + month_f = month_f >> 1 + end + # Weeks headers + if show_weeks + left = subject_width + height = header_height + if self.date_from.cwday == 1 + # self.date_from is monday + week_f = self.date_from + else + # find next monday after self.date_from + week_f = self.date_from + (7 - self.date_from.cwday + 1) + width = (7 - self.date_from.cwday + 1) * zoom-1 + pdf.SetY(y_start + header_height) + pdf.SetX(left) + pdf.RDMCell(width + 1, height, "", "LTR") + left = left + width + 1 + end + while week_f <= self.date_to + width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom + pdf.SetY(y_start + header_height) + pdf.SetX(left) + pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") + left = left + width + week_f = week_f + 7 + end + end + # Days headers + if show_days + left = subject_width + height = header_height + wday = self.date_from.cwday + pdf.SetFontStyle('B', 7) + (self.date_to - self.date_from + 1).to_i.times do + width = zoom + pdf.SetY(y_start + 2 * header_height) + pdf.SetX(left) + pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C") + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end + end + pdf.SetY(y_start) + pdf.SetX(15) + pdf.RDMCell(subject_width + g_width - 15, headers_height, "", 1) + # Tasks + top = headers_height + y_start + options = { + :top => top, + :zoom => zoom, + :subject_width => subject_width, + :g_width => g_width, + :indent => 0, + :indent_increment => 5, + :top_increment => 5, + :format => :pdf, + :pdf => pdf + } + 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 + coords[:start] = start_date - self.date_from + coords[:bar_start] = start_date - self.date_from + else + coords[:bar_start] = 0 + end + if end_date < self.date_to + coords[:end] = end_date - self.date_from + coords[:bar_end] = end_date - self.date_from + 1 + else + coords[:bar_end] = self.date_to - self.date_from + 1 + end + if progress + progress_date = calc_progress_date(start_date, end_date, progress) + if progress_date > self.date_from && progress_date > start_date + if progress_date < self.date_to + coords[:bar_progress_end] = progress_date - self.date_from + else + 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 + if late_date < self.date_to + coords[:bar_late_end] = late_date - self.date_from + 1 + else + coords[:bar_late_end] = self.date_to - self.date_from + 1 + end + end + end + end + end + # Transforms dates into pixels witdh + coords.keys.each do |key| + coords[key] = (coords[key] * zoom).floor + end + coords + end + + def calc_progress_date(start_date, end_date, progress) + start_date + (end_date - start_date + 1) * (progress / 100.0) + end + + # TODO: Sorts a collection of issues by start_date, due_date, id for gantt rendering + def sort_issues!(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) + if x.root_id == y.root_id + x.lft <=> y.lft + else + x.root_id <=> y.root_id + end + end + + def current_limit + if @max_rows + @max_rows - @number_of_rows + else + 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]) + options[:pdf].AddPage("L") + options[:top] = 15 + options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1) + end + end + + def html_subject(params, subject, options={}) + style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;" + style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width] + output = view.content_tag(:div, subject, + :class => options[:css], :style => style, + :title => options[:title], + :id => options[:id]) + @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].RDMCell(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].RDMCell(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 issue_relations(issue) + rels = {} + if relations[issue.id] + relations[issue.id].each do |relation| + (rels[relation.relation_type] ||= []) << relation.issue_to_id + end + end + rels + end + + def html_task(params, coords, options={}) + output = '' + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + width = coords[:bar_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue] + html_id = "task-todo-version-#{options[:version].id}" if options[:version] + content_opt = {:style => style, + :class => "#{options[:css]} task_todo", + :id => html_id} + if options[:issue] + rels = issue_relations(options[:issue]) + if rels.present? + content_opt[:data] = {"rels" => rels.to_json} + end + end + output << view.content_tag(:div, ' '.html_safe, content_opt) + if coords[:bar_late_end] + width = coords[:bar_late_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} task_late") + end + if coords[:bar_progress_end] + width = coords[:bar_progress_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + html_id = "task-done-issue-#{options[:issue].id}" if options[:issue] + html_id = "task-done-version-#{options[:version].id}" if options[:version] + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} task_done", + :id => html_id) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:start]}px;" + style << "width:15px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} marker starting") + end + if coords[:end] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:end] + params[:zoom]}px;" + style << "width:15px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} marker ending") + end + end + # Renders the label on the right + if options[:label] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{(coords[:bar_end] || 0) + 8}px;" + style << "width:15px;" + output << view.content_tag(:div, options[:label], + :style => style, + :class => "#{options[:css]} label") + end + # Renders the tooltip + if options[:issue] && coords[:bar_start] && coords[:bar_end] + s = view.content_tag(:span, + view.render_issue_tooltip(options[:issue]).html_safe, + :class => "tip") + style = "" + style << "position: absolute;" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" + style << "height:12px;" + output << view.content_tag(:div, s.html_safe, + :style => style, + :class => "tooltip") + end + @lines << output + output + end + + def pdf_task(params, coords, options={}) + height = options[:height] || 2 + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(200, 200, 200) + params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1) + if coords[:bar_late_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(255, 100, 100) + params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1) + end + if coords[:bar_progress_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(90, 200, 90) + params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + params[:pdf].SetY(params[:top] + 1) + params[:pdf].SetX(params[:subject_width] + coords[:start] - 1) + params[:pdf].SetFillColor(50, 50, 200) + params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) + end + if coords[:end] + params[:pdf].SetY(params[:top] + 1) + params[:pdf].SetX(params[:subject_width] + coords[:end] - 1) + params[:pdf].SetFillColor(50, 50, 200) + params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) + end + end + # Renders the label on the right + if options[:label] + params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5) + params[:pdf].RDMCell(30, 2, options[:label]) + end + end + + def image_task(params, coords, options={}) + height = options[:height] || 6 + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + params[:image].fill('#aaa') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_end], + params[:top] - height) + if coords[:bar_late_end] + params[:image].fill('#f66') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_late_end], + params[:top] - height) + end + if coords[:bar_progress_end] + params[:image].fill('#00c600') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_progress_end], + params[:top] - height) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + x = params[:subject_width] + coords[:start] + y = params[:top] - height / 2 + params[:image].fill('blue') + params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) + end + if coords[:end] + x = params[:subject_width] + coords[:end] + params[:zoom] + y = params[:top] - height / 2 + params[:image].fill('blue') + params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) + end + end + # Renders the label on the right + if options[:label] + params[:image].fill('black') + params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5, + params[:top] + 1, + options[:label]) + end + end + end + end +end