12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058 |
- # frozen_string_literal: true
-
- # Redmine - project management software
- # Copyright (C) 2006-2020 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
- class MaxLinesLimitReached < StandardError
- end
-
- 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
-
- UNAVAILABLE_COLUMNS = [:tracker, :id, :subject]
-
- # Some utility methods for the PDF export
- # @private
- 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 ||= User.current.today.month
- @year_from ||= User.current.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 < Setting.gantt_months_limit.to_i + 1) ? 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 = +''
- @columns ||= {}
- @number_of_rows = nil
- @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
-
- # Renders the selected column of the Gantt chart, the right side of subjects.
- def selected_column_content(options={})
- render(options.merge(:only => :selected_columns)) unless @columns.has_key?(options[:column].name)
- @columns[options[:column].name]
- 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.
- joins("LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt").
- where("child.id IN (?)", ids).
- order("#{Project.table_name}.lft ASC").
- distinct.
- to_a
- 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 || options[:only] == :selected_columns
- @lines = +'' unless options[:only] == :subjects || options[:only] == :selected_columns
- @columns[options[:column].name] = +'' if options[:only] == :selected_columns && @columns.has_key?(options[:column]) == false
- @number_of_rows = 0
- begin
- Project.project_tree(projects) do |project, level|
- options[:indent] = indent + level * options[:indent_increment]
- render_project(project, options)
- end
- rescue MaxLinesLimitReached
- @truncated = true
- end
- @subjects_rendered = true unless options[:only] == :lines || options[:only] == :selected_columns
- @lines_rendered = true unless options[:only] == :subjects || options[:only] == :selected_columns
- render_end(options)
- end
-
- def render_project(project, options={})
- render_object_row(project, options)
- increment_indent(options) do
- # render issue that are not assigned to a version
- issues = project_issues(project).select {|i| i.fixed_version.nil?}
- render_issues(issues, options)
- # then render project versions and their issues
- versions = project_versions(project)
- self.class.sort_versions!(versions)
- versions.each do |version|
- render_version(project, version, options)
- end
- end
- end
-
- def render_version(project, version, options={})
- render_object_row(version, options)
- increment_indent(options) do
- issues = version_issues(project, version)
- render_issues(issues, options)
- end
- end
-
- def render_issues(issues, options={})
- self.class.sort_issues!(issues)
- ancestors = []
- issues.each do |issue|
- while ancestors.any? && !issue.is_descendant_of?(ancestors.last)
- ancestors.pop
- decrement_indent(options)
- end
- render_object_row(issue, options)
- unless issue.leaf?
- ancestors << issue
- increment_indent(options)
- end
- end
- decrement_indent(options, ancestors.size)
- end
-
- def render_object_row(object, options)
- class_name = object.class.name.downcase
- send("subject_for_#{class_name}", object, options) unless options[:only] == :lines || options[:only] == :selected_columns
- send("line_for_#{class_name}", object, options) unless options[:only] == :subjects || options[:only] == :selected_columns
- column_content_for_issue(object, options) if options[:only] == :selected_columns && options[:column].present? && object.is_a?(Issue)
- options[:top] += options[:top_increment]
- @number_of_rows += 1
- if @max_rows && @number_of_rows >= @max_rows
- raise MaxLinesLimitReached
- end
- end
-
- def render_end(options={})
- case options[:format]
- when :pdf
- options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
- end
- end
-
- def increment_indent(options, factor=1)
- options[:indent] += options[:indent_increment] * factor
- if block_given?
- yield
- decrement_indent(options, factor)
- end
- end
-
- def decrement_indent(options, factor=1)
- increment_indent(options, -factor)
- end
-
- def subject_for_project(project, options)
- subject(project.name, options, project)
- end
-
- def line_for_project(project, options)
- # Skip projects that don't have a start_date or due date
- if project.is_a?(Project) && project.start_date && project.due_date
- label = project.name
- line(project.start_date, project.due_date, nil, true, label, options, project)
- end
- end
-
- def subject_for_version(version, options)
- subject(version.to_s_with_project, options, version)
- 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
- label = "#{h(version)} #{h(version.visible_fixed_issues.completed_percent.to_f.round)}%"
- label = h("#{version.project} -") + label unless @project && @project == version.project
- line(version.start_date, version.due_date, version.visible_fixed_issues.completed_percent, true, label, options, version)
- end
- end
-
- def subject_for_issue(issue, options)
- subject(issue.subject, options, issue)
- 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
- label = issue.status.name.dup
- unless issue.disabled_core_fields.include?('done_ratio')
- label << " #{issue.done_ratio}%"
- end
- markers = !issue.leaf?
- line(issue.start_date, issue.due_before, issue.done_ratio, markers, label, options, issue)
- end
- end
-
- def column_content_for_issue(issue, options)
- if options[:format] == :html
- data_options = {}
- data_options[:collapse_expand] = "issue-#{issue.id}"
- style = "position: absolute;top: #{options[:top]}px; font-size: 0.8em;"
- content = view.content_tag(:div, view.column_content(options[:column], issue), :style => style, :class => "issue_#{options[:column].name}", :id => "#{options[:column].name}_issue_#{issue.id}", :data => data_options)
- @columns[options[:column].name] << content if @columns.has_key?(options[:column].name)
- content
- end
- end
-
- def subject(label, options, object=nil)
- send "#{options[:format]}_subject", options, label, object
- end
-
- def line(start_date, end_date, done_ratio, markers, label, options, object=nil)
- options[:zoom] ||= 1
- options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
- coords = coordinates(start_date, end_date, done_ratio, options[:zoom])
- send "#{options[:format]}_task", options, coords, markers, label, object
- end
-
- # Generates a gantt image
- # Only defined if MiniMagick 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
- # TODO: Remove rmagick_font_path in a later version
- Rails.logger.warn('rmagick_font_path option is deprecated. Use minimagick_font_path instead.') \
- unless Redmine::Configuration['rmagick_font_path'].nil?
- font_path = Redmine::Configuration['minimagick_font_path'].presence || Redmine::Configuration['rmagick_font_path'].presence
- img = MiniMagick::Image.create(".#{format}", false)
- MiniMagick::Tool::Convert.new do |gc|
- gc.size('%dx%d' % [subject_width + g_width + 1, height])
- gc.xc('white')
- gc.font(font_path) if font_path.present?
- # 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.strokewidth(1)
- gc.draw('rectangle %d,%d %d,%d' % [
- left, 0, left + width, height
- ])
- gc.fill('black')
- gc.stroke('transparent')
- gc.strokewidth(1)
- gc.draw('text %d,%d %s' % [
- left.round + 8, 14, Redmine::Utils::Shell.shell_quote("#{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.strokewidth(1)
- gc.draw('rectangle %d,%d %d,%d' % [
- 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.strokewidth(1)
- gc.draw('rectangle %d,%d %d,%d' % [
- left.round, header_height, left.round + width, 2 * header_height + g_height - 1
- ])
- gc.fill('black')
- gc.stroke('transparent')
- gc.strokewidth(1)
- gc.draw('text %d,%d %s' % [
- left.round + 2, header_height + 14, Redmine::Utils::Shell.shell_quote(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.strokewidth(1)
- gc.draw('rectangle %d,%d %d,%d' % [
- 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.strokewidth(1)
- gc.draw('rectangle %d,%d %d,%d' % [
- 0, 0, subject_width + g_width, headers_height
- ])
- gc.stroke('black')
- gc.draw('rectangle %d,%d %d,%d' % [
- 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 User.current.today >= @date_from and User.current.today <= date_to
- gc.stroke('red')
- x = (User.current.today - @date_from + 1) * zoom + subject_width
- gc.draw('line %g,%g %g,%g' % [
- x, headers_height, x, headers_height + g_height - 1
- ])
- end
- gc << img.path
- end
- img.to_blob
- ensure
- img.destroy! if img
- end if Object.const_defined?(:MiniMagick)
-
- 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(User.current.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
- if self.months < 2
- show_day_num = true
- headers_height = 4 * header_height
- end
- 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
- # Day numbers headers
- if show_day_num
- left = subject_width
- height = header_height
- day_num = self.date_from
- 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 + header_height * 2)
- pdf.SetX(left)
- pdf.SetTextColor(non_working_week_days.include?(wday) ? 150 : 0)
- pdf.RDMCell(width, height, day_num.day.to_s, "LTR", 0, "C")
- left = left + width
- day_num = day_num + 1
- wday = wday + 1
- wday = 1 if wday > 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 + header_height * (show_day_num ? 3 : 2))
- pdf.SetX(left)
- pdf.SetTextColor(non_working_week_days.include?(wday) ? 150 : 0)
- 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.SetTextColor(0)
- 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 + 1
- 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 <= User.current.today
- late_date = [User.current.today, end_date].min + 1
- 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
- else
- coords[:bar_late_end] = self.date_to - self.date_from + 1
- end
- end
- end
- end
- end
- # Transforms dates into pixels witdh
- coords.each_key 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
-
- # Singleton class method is public
- class << self
- def sort_issues!(issues)
- issues.sort_by! {|issue| sort_issue_logic(issue)}
- end
-
- def sort_issue_logic(issue)
- julian_date = Date.new
- ancesters_start_date = []
- current_issue = issue
- begin
- ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
- current_issue = current_issue.parent
- end while (current_issue)
- ancesters_start_date
- end
-
- def sort_versions!(versions)
- versions.sort!
- 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_content(object)
- case object
- when Issue
- issue = object
- 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
- s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-gravatar')
- s << view.link_to_issue(issue).html_safe
- s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => issue.id, :style => 'display:none;', :class => 'toggle-selection')
- view.content_tag(:span, s, :class => css_classes).html_safe
- when Version
- version = object
- 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.visible_fixed_issues.completed_percent
- progress_date = calc_progress_date(version.start_date,
- version.due_date, version.visible_fixed_issues.completed_percent)
- 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
- view.content_tag(:span, s, :class => html_class).html_safe
- when Project
- project = object
- html_class = +""
- html_class << 'icon icon-projects '
- html_class << (project.overdue? ? 'project-overdue' : '')
- s = view.link_to_project(project).html_safe
- view.content_tag(:span, s, :class => html_class).html_safe
- end
- end
-
- def html_subject(params, subject, object)
- content = html_subject_content(object) || subject
- tag_options = {}
- case object
- when Issue
- tag_options[:id] = "issue-#{object.id}"
- tag_options[:class] = "issue-subject hascontextmenu"
- tag_options[:title] = object.subject
- children = object.children & project_issues(object.project)
- has_children = children.present? && (children.collect(&:fixed_version).uniq & [object.fixed_version]).present?
- when Version
- tag_options[:id] = "version-#{object.id}"
- tag_options[:class] = "version-name"
- has_children = object.fixed_issues.exists?
- when Project
- tag_options[:class] = "project-name"
- has_children = object.issues.exists? || object.versions.exists?
- end
- if object
- tag_options[:data] = {
- :collapse_expand => {
- :top_increment => params[:top_increment],
- :obj_id => "#{object.class}-#{object.id}".downcase,
- },
- }
- end
- if has_children
- content = view.content_tag(:span, nil, :class => 'icon icon-expended expander') + content
- tag_options[:class] += ' open'
- else
- if params[:indent]
- params = params.dup
- params[:indent] += 12
- end
- end
- style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
- style += "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
- tag_options[:style] = style
- output = view.content_tag(:div, content, tag_options)
- @subjects << output
- output
- end
-
- def pdf_subject(params, subject, options={})
- pdf_new_page?(params)
- 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].strokewidth(1)
- params[:image].draw('text %d,%d %s' % [
- params[:indent], params[:top] + 2, Redmine::Utils::Shell.shell_quote(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, markers, label, object)
- output = +''
- data_options = {}
- data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase if object
- css = "task " +
- case object
- when Project
- "project"
- when Version
- "version"
- when Issue
- object.leaf? ? 'leaf' : 'parent'
- else
- ""
- end
- # 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-#{object.id}" if object.is_a?(Issue)
- html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
- content_opt = {:style => style,
- :class => "#{css} task_todo",
- :id => html_id,
- :data => {}}
- if object.is_a?(Issue)
- rels = issue_relations(object)
- if rels.present?
- content_opt[:data] = {"rels" => rels.to_json}
- end
- end
- content_opt[:data].merge!(data_options)
- 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 => "#{css} task_late",
- :data => data_options)
- 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-#{object.id}" if object.is_a?(Issue)
- html_id = "task-done-version-#{object.id}" if object.is_a?(Version)
- output << view.content_tag(:div, ' '.html_safe,
- :style => style,
- :class => "#{css} task_done",
- :id => html_id,
- :data => data_options)
- end
- end
- # Renders the markers
- if 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 => "#{css} marker starting",
- :data => data_options)
- end
- if coords[:end]
- style = +""
- style << "top:#{params[:top]}px;"
- style << "left:#{coords[:end]}px;"
- style << "width:15px;"
- output << view.content_tag(:div, ' '.html_safe,
- :style => style,
- :class => "#{css} marker ending",
- :data => data_options)
- end
- end
- # Renders the label on the right
- if label
- style = +""
- style << "top:#{params[:top]}px;"
- style << "left:#{(coords[:bar_end] || 0) + 8}px;"
- style << "width:15px;"
- output << view.content_tag(:div, label,
- :style => style,
- :class => "#{css} label",
- :data => data_options)
- end
- # Renders the tooltip
- if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end]
- s = view.content_tag(:span,
- view.render_issue_tooltip(object).html_safe,
- :class => "tip")
- s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection')
- 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 hascontextmenu",
- :data => data_options)
- end
- @lines << output
- output
- end
-
- def pdf_task(params, coords, markers, label, object)
- cell_height_ratio = params[:pdf].get_cell_height_ratio
- params[:pdf].set_cell_height_ratio(0.1)
-
- height = 2
- height /= 2 if markers
- # Renders the task bar, with progress and late
- if coords[:bar_start] && coords[:bar_end]
- width = [1, coords[:bar_end] - coords[:bar_start]].max
- 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(width, height, "", 0, 0, "", 1)
- if coords[:bar_late_end]
- width = [1, coords[:bar_late_end] - coords[:bar_start]].max
- 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(width, height, "", 0, 0, "", 1)
- end
- if coords[:bar_progress_end]
- width = [1, coords[:bar_progress_end] - coords[:bar_start]].max
- 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(width, height, "", 0, 0, "", 1)
- end
- end
- # Renders the markers
- if 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 label
- params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
- params[:pdf].RDMCell(30, 2, label)
- end
-
- params[:pdf].set_cell_height_ratio(cell_height_ratio)
- end
-
- def image_task(params, coords, markers, label, object)
- height = 6
- height /= 2 if markers
- # Renders the task bar, with progress and late
- if coords[:bar_start] && coords[:bar_end]
- params[:image].fill('#aaa')
- params[:image].draw('rectangle %d,%d %d,%d' % [
- 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].draw('rectangle %d,%d %d,%d' % [
- 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].draw('rectangle %d,%d %d,%d' % [
- params[:subject_width] + coords[:bar_start],
- params[:top],
- params[:subject_width] + coords[:bar_progress_end],
- params[:top] - height
- ])
- end
- end
- # Renders the markers
- if markers
- if coords[:start]
- x = params[:subject_width] + coords[:start]
- y = params[:top] - height / 2
- params[:image].fill('blue')
- params[:image].draw('polygon %d,%d %d,%d %d,%d %d,%d' % [
- x - 4, y,
- x, y - 4,
- x + 4, y,
- x, y + 4
- ])
- end
- if coords[:end]
- x = params[:subject_width] + coords[:end]
- y = params[:top] - height / 2
- params[:image].fill('blue')
- params[:image].draw('polygon %d,%d %d,%d %d,%d %d,%d' % [
- x - 4, y,
- x, y - 4,
- x + 4, y,
- x, y + 4
- ])
- end
- end
- # Renders the label on the right
- if label
- params[:image].fill('black')
- params[:image].draw('text %d,%d %s' % [
- params[:subject_width] + (coords[:bar_end] || 0) + 5, params[:top] + 1, Redmine::Utils::Shell.shell_quote(label)
- ])
- end
- end
- end
- end
- end
|