Mercurial > hg > soundsoftware-site
comparison lib/redmine/helpers/gantt.rb @ 22:40f7cfd4df19
* Update to SVN trunk rev 4173
| author | Chris Cannam <chris.cannam@soundsoftware.ac.uk> |
|---|---|
| date | Fri, 24 Sep 2010 14:06:04 +0100 |
| parents | cca12e1c1fd4 |
| children | 94944d00e43c |
comparison
equal
deleted
inserted
replaced
| 14:1d32c0a0efbf | 22:40f7cfd4df19 |
|---|---|
| 17 | 17 |
| 18 module Redmine | 18 module Redmine |
| 19 module Helpers | 19 module Helpers |
| 20 # Simple class to handle gantt chart data | 20 # Simple class to handle gantt chart data |
| 21 class Gantt | 21 class Gantt |
| 22 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :events | 22 include ERB::Util |
| 23 | 23 include Redmine::I18n |
| 24 | |
| 25 # :nodoc: | |
| 26 # Some utility methods for the PDF export | |
| 27 class PDF | |
| 28 MaxCharactorsForSubject = 45 | |
| 29 TotalWidth = 280 | |
| 30 LeftPaneWidth = 100 | |
| 31 | |
| 32 def self.right_pane_width | |
| 33 TotalWidth - LeftPaneWidth | |
| 34 end | |
| 35 end | |
| 36 | |
| 37 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months | |
| 38 attr_accessor :query | |
| 39 attr_accessor :project | |
| 40 attr_accessor :view | |
| 41 | |
| 24 def initialize(options={}) | 42 def initialize(options={}) |
| 25 options = options.dup | 43 options = options.dup |
| 26 @events = [] | |
| 27 | 44 |
| 28 if options[:year] && options[:year].to_i >0 | 45 if options[:year] && options[:year].to_i >0 |
| 29 @year_from = options[:year].to_i | 46 @year_from = options[:year].to_i |
| 30 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12 | 47 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12 |
| 31 @month_from = options[:month].to_i | 48 @month_from = options[:month].to_i |
| 49 end | 66 end |
| 50 | 67 |
| 51 @date_from = Date.civil(@year_from, @month_from, 1) | 68 @date_from = Date.civil(@year_from, @month_from, 1) |
| 52 @date_to = (@date_from >> @months) - 1 | 69 @date_to = (@date_from >> @months) - 1 |
| 53 end | 70 end |
| 54 | 71 |
| 55 | 72 def common_params |
| 56 def events=(e) | 73 { :controller => 'gantts', :action => 'show', :project_id => @project } |
| 57 @events = e | |
| 58 # Adds all ancestors | |
| 59 root_ids = e.select {|i| i.is_a?(Issue) && i.parent_id? }.collect(&:root_id).uniq | |
| 60 if root_ids.any? | |
| 61 # Retrieves all nodes | |
| 62 parents = Issue.find_all_by_root_id(root_ids, :conditions => ["rgt - lft > 1"]) | |
| 63 # Only add ancestors | |
| 64 @events += parents.select {|p| @events.detect {|i| i.is_a?(Issue) && p.is_ancestor_of?(i)}} | |
| 65 end | |
| 66 @events.uniq! | |
| 67 # Sort issues by hierarchy and start dates | |
| 68 @events.sort! {|x,y| | |
| 69 if x.is_a?(Issue) && y.is_a?(Issue) | |
| 70 gantt_issue_compare(x, y, @events) | |
| 71 else | |
| 72 gantt_start_compare(x, y) | |
| 73 end | |
| 74 } | |
| 75 # Removes issues that have no start or end date | |
| 76 @events.reject! {|i| i.is_a?(Issue) && (i.start_date.nil? || i.due_before.nil?) } | |
| 77 @events | |
| 78 end | 74 end |
| 79 | 75 |
| 80 def params | 76 def params |
| 81 { :zoom => zoom, :year => year_from, :month => month_from, :months => months } | 77 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months }) |
| 82 end | 78 end |
| 83 | 79 |
| 84 def params_previous | 80 def params_previous |
| 85 { :year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months } | 81 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months }) |
| 86 end | 82 end |
| 87 | 83 |
| 88 def params_next | 84 def params_next |
| 89 { :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months } | 85 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months }) |
| 90 end | 86 end |
| 91 | 87 |
| 88 ### Extracted from the HTML view/helpers | |
| 89 # Returns the number of rows that will be rendered on the Gantt chart | |
| 90 def number_of_rows | |
| 91 if @project | |
| 92 return number_of_rows_on_project(@project) | |
| 93 else | |
| 94 Project.roots.inject(0) do |total, project| | |
| 95 total += number_of_rows_on_project(project) | |
| 96 end | |
| 97 end | |
| 98 end | |
| 99 | |
| 100 # Returns the number of rows that will be used to list a project on | |
| 101 # the Gantt chart. This will recurse for each subproject. | |
| 102 def number_of_rows_on_project(project) | |
| 103 # Remove the project requirement for Versions because it will | |
| 104 # restrict issues to only be on the current project. This | |
| 105 # ends up missing issues which are assigned to shared versions. | |
| 106 @query.project = nil if @query.project | |
| 107 | |
| 108 # One Root project | |
| 109 count = 1 | |
| 110 # Issues without a Version | |
| 111 count += project.issues.for_gantt.without_version.with_query(@query).count | |
| 112 | |
| 113 # Versions | |
| 114 count += project.versions.count | |
| 115 | |
| 116 # Issues on the Versions | |
| 117 project.versions.each do |version| | |
| 118 count += version.fixed_issues.for_gantt.with_query(@query).count | |
| 119 end | |
| 120 | |
| 121 # Subprojects | |
| 122 project.children.each do |subproject| | |
| 123 count += number_of_rows_on_project(subproject) | |
| 124 end | |
| 125 | |
| 126 count | |
| 127 end | |
| 128 | |
| 129 # Renders the subjects of the Gantt chart, the left side. | |
| 130 def subjects(options={}) | |
| 131 options = {:indent => 4, :render => :subject, :format => :html}.merge(options) | |
| 132 | |
| 133 output = '' | |
| 134 if @project | |
| 135 output << render_project(@project, options) | |
| 136 else | |
| 137 Project.roots.each do |project| | |
| 138 output << render_project(project, options) | |
| 139 end | |
| 140 end | |
| 141 | |
| 142 output | |
| 143 end | |
| 144 | |
| 145 # Renders the lines of the Gantt chart, the right side | |
| 146 def lines(options={}) | |
| 147 options = {:indent => 4, :render => :line, :format => :html}.merge(options) | |
| 148 output = '' | |
| 149 | |
| 150 if @project | |
| 151 output << render_project(@project, options) | |
| 152 else | |
| 153 Project.roots.each do |project| | |
| 154 output << render_project(project, options) | |
| 155 end | |
| 156 end | |
| 157 | |
| 158 output | |
| 159 end | |
| 160 | |
| 161 def render_project(project, options={}) | |
| 162 options[:top] = 0 unless options.key? :top | |
| 163 options[:indent_increment] = 20 unless options.key? :indent_increment | |
| 164 options[:top_increment] = 20 unless options.key? :top_increment | |
| 165 | |
| 166 output = '' | |
| 167 # Project Header | |
| 168 project_header = if options[:render] == :subject | |
| 169 subject_for_project(project, options) | |
| 170 else | |
| 171 # :line | |
| 172 line_for_project(project, options) | |
| 173 end | |
| 174 output << project_header if options[:format] == :html | |
| 175 | |
| 176 options[:top] += options[:top_increment] | |
| 177 options[:indent] += options[:indent_increment] | |
| 178 | |
| 179 # Second, Issues without a version | |
| 180 issues = project.issues.for_gantt.without_version.with_query(@query) | |
| 181 if issues | |
| 182 issue_rendering = render_issues(issues, options) | |
| 183 output << issue_rendering if options[:format] == :html | |
| 184 end | |
| 185 | |
| 186 # Third, Versions | |
| 187 project.versions.sort.each do |version| | |
| 188 version_rendering = render_version(version, options) | |
| 189 output << version_rendering if options[:format] == :html | |
| 190 end | |
| 191 | |
| 192 # Fourth, subprojects | |
| 193 project.children.each do |project| | |
| 194 subproject_rendering = render_project(project, options) | |
| 195 output << subproject_rendering if options[:format] == :html | |
| 196 end | |
| 197 | |
| 198 # Remove indent to hit the next sibling | |
| 199 options[:indent] -= options[:indent_increment] | |
| 200 | |
| 201 output | |
| 202 end | |
| 203 | |
| 204 def render_issues(issues, options={}) | |
| 205 output = '' | |
| 206 issues.each do |i| | |
| 207 issue_rendering = if options[:render] == :subject | |
| 208 subject_for_issue(i, options) | |
| 209 else | |
| 210 # :line | |
| 211 line_for_issue(i, options) | |
| 212 end | |
| 213 output << issue_rendering if options[:format] == :html | |
| 214 options[:top] += options[:top_increment] | |
| 215 end | |
| 216 output | |
| 217 end | |
| 218 | |
| 219 def render_version(version, options={}) | |
| 220 output = '' | |
| 221 # Version header | |
| 222 version_rendering = if options[:render] == :subject | |
| 223 subject_for_version(version, options) | |
| 224 else | |
| 225 # :line | |
| 226 line_for_version(version, options) | |
| 227 end | |
| 228 | |
| 229 output << version_rendering if options[:format] == :html | |
| 230 | |
| 231 options[:top] += options[:top_increment] | |
| 232 | |
| 233 # Remove the project requirement for Versions because it will | |
| 234 # restrict issues to only be on the current project. This | |
| 235 # ends up missing issues which are assigned to shared versions. | |
| 236 @query.project = nil if @query.project | |
| 237 | |
| 238 issues = version.fixed_issues.for_gantt.with_query(@query) | |
| 239 if issues | |
| 240 # Indent issues | |
| 241 options[:indent] += options[:indent_increment] | |
| 242 output << render_issues(issues, options) | |
| 243 options[:indent] -= options[:indent_increment] | |
| 244 end | |
| 245 | |
| 246 output | |
| 247 end | |
| 248 | |
| 249 def subject_for_project(project, options) | |
| 250 case options[:format] | |
| 251 when :html | |
| 252 output = '' | |
| 253 | |
| 254 output << "<div class='project-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> " | |
| 255 if project.is_a? Project | |
| 256 output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>" | |
| 257 output << view.link_to_project(project) | |
| 258 output << '</span>' | |
| 259 else | |
| 260 ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project" | |
| 261 '' | |
| 262 end | |
| 263 output << "</small></div>" | |
| 264 | |
| 265 output | |
| 266 when :image | |
| 267 | |
| 268 options[:image].fill('black') | |
| 269 options[:image].stroke('transparent') | |
| 270 options[:image].stroke_width(1) | |
| 271 options[:image].text(options[:indent], options[:top] + 2, project.name) | |
| 272 when :pdf | |
| 273 options[:pdf].SetY(options[:top]) | |
| 274 options[:pdf].SetX(15) | |
| 275 | |
| 276 char_limit = PDF::MaxCharactorsForSubject - options[:indent] | |
| 277 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR") | |
| 278 | |
| 279 options[:pdf].SetY(options[:top]) | |
| 280 options[:pdf].SetX(options[:subject_width]) | |
| 281 options[:pdf].Cell(options[:g_width], 5, "", "LR") | |
| 282 end | |
| 283 end | |
| 284 | |
| 285 def line_for_project(project, options) | |
| 286 # Skip versions that don't have a start_date | |
| 287 if project.is_a?(Project) && project.start_date | |
| 288 options[:zoom] ||= 1 | |
| 289 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] | |
| 290 | |
| 291 | |
| 292 case options[:format] | |
| 293 when :html | |
| 294 output = '' | |
| 295 i_left = ((project.start_date - self.date_from)*options[:zoom]).floor | |
| 296 | |
| 297 start_date = project.start_date | |
| 298 start_date ||= self.date_from | |
| 299 start_left = ((start_date - self.date_from)*options[:zoom]).floor | |
| 300 | |
| 301 i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to ) | |
| 302 i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor | |
| 303 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date ) | |
| 304 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date ) | |
| 305 | |
| 306 i_late_date = [i_end_date, Date.today].min if start_date < Date.today | |
| 307 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor | |
| 308 | |
| 309 i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders) | |
| 310 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width | |
| 311 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width | |
| 312 | |
| 313 # Bar graphic | |
| 314 | |
| 315 # Make sure that negative i_left and i_width don't | |
| 316 # overflow the subject | |
| 317 if i_end > 0 && i_left <= options[:g_width] | |
| 318 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'> </div>" | |
| 319 end | |
| 320 | |
| 321 if l_width > 0 && i_left <= options[:g_width] | |
| 322 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'> </div>" | |
| 323 end | |
| 324 if d_width > 0 && i_left <= options[:g_width] | |
| 325 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'> </div>" | |
| 326 end | |
| 327 | |
| 328 | |
| 329 # Starting diamond | |
| 330 if start_left <= options[:g_width] && start_left > 0 | |
| 331 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'> </div>" | |
| 332 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>" | |
| 333 output << "</div>" | |
| 334 end | |
| 335 | |
| 336 # Ending diamond | |
| 337 # Don't show items too far ahead | |
| 338 if i_end <= options[:g_width] && i_end > 0 | |
| 339 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'> </div>" | |
| 340 end | |
| 341 | |
| 342 # DIsplay the Project name and % | |
| 343 if i_end <= options[:g_width] | |
| 344 # Display the status even if it's floated off to the left | |
| 345 status_px = i_end + 12 # 12px for the diamond | |
| 346 status_px = 0 if status_px <= 0 | |
| 347 | |
| 348 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>" | |
| 349 output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>" | |
| 350 output << "</div>" | |
| 351 end | |
| 352 | |
| 353 output | |
| 354 when :image | |
| 355 options[:image].stroke('transparent') | |
| 356 i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor | |
| 357 | |
| 358 # Make sure negative i_left doesn't overflow the subject | |
| 359 if i_left > options[:subject_width] | |
| 360 options[:image].fill('blue') | |
| 361 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6) | |
| 362 options[:image].fill('black') | |
| 363 options[:image].text(i_left + 11, options[:top] + 1, project.name) | |
| 364 end | |
| 365 when :pdf | |
| 366 options[:pdf].SetY(options[:top]+1.5) | |
| 367 i_left = ((project.due_date - @date_from)*options[:zoom]) | |
| 368 | |
| 369 # Make sure negative i_left doesn't overflow the subject | |
| 370 if i_left > 0 | |
| 371 options[:pdf].SetX(options[:subject_width] + i_left) | |
| 372 options[:pdf].SetFillColor(50,50,200) | |
| 373 options[:pdf].Cell(2, 2, "", 0, 0, "", 1) | |
| 374 | |
| 375 options[:pdf].SetY(options[:top]+1.5) | |
| 376 options[:pdf].SetX(options[:subject_width] + i_left + 3) | |
| 377 options[:pdf].Cell(30, 2, "#{project.name}") | |
| 378 end | |
| 379 end | |
| 380 else | |
| 381 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date" | |
| 382 '' | |
| 383 end | |
| 384 end | |
| 385 | |
| 386 def subject_for_version(version, options) | |
| 387 case options[:format] | |
| 388 when :html | |
| 389 output = '' | |
| 390 output << "<div class='version-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> " | |
| 391 if version.is_a? Version | |
| 392 output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>" | |
| 393 output << view.link_to_version(version) | |
| 394 output << '</span>' | |
| 395 else | |
| 396 ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version" | |
| 397 '' | |
| 398 end | |
| 399 output << "</small></div>" | |
| 400 | |
| 401 output | |
| 402 when :image | |
| 403 options[:image].fill('black') | |
| 404 options[:image].stroke('transparent') | |
| 405 options[:image].stroke_width(1) | |
| 406 options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project) | |
| 407 when :pdf | |
| 408 options[:pdf].SetY(options[:top]) | |
| 409 options[:pdf].SetX(15) | |
| 410 | |
| 411 char_limit = PDF::MaxCharactorsForSubject - options[:indent] | |
| 412 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR") | |
| 413 | |
| 414 options[:pdf].SetY(options[:top]) | |
| 415 options[:pdf].SetX(options[:subject_width]) | |
| 416 options[:pdf].Cell(options[:g_width], 5, "", "LR") | |
| 417 end | |
| 418 end | |
| 419 | |
| 420 def line_for_version(version, options) | |
| 421 # Skip versions that don't have a start_date | |
| 422 if version.is_a?(Version) && version.start_date | |
| 423 options[:zoom] ||= 1 | |
| 424 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] | |
| 425 | |
| 426 case options[:format] | |
| 427 when :html | |
| 428 output = '' | |
| 429 i_left = ((version.start_date - self.date_from)*options[:zoom]).floor | |
| 430 # TODO: or version.fixed_issues.collect(&:start_date).min | |
| 431 start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present? | |
| 432 start_date ||= self.date_from | |
| 433 start_left = ((start_date - self.date_from)*options[:zoom]).floor | |
| 434 | |
| 435 i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to ) | |
| 436 i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor | |
| 437 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date ) | |
| 438 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date ) | |
| 439 | |
| 440 i_late_date = [i_end_date, Date.today].min if start_date < Date.today | |
| 441 | |
| 442 i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders) | |
| 443 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width | |
| 444 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width | |
| 445 | |
| 446 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel | |
| 447 | |
| 448 # Bar graphic | |
| 449 | |
| 450 # Make sure that negative i_left and i_width don't | |
| 451 # overflow the subject | |
| 452 if i_width > 0 && i_left <= options[:g_width] | |
| 453 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'> </div>" | |
| 454 end | |
| 455 if l_width > 0 && i_left <= options[:g_width] | |
| 456 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'> </div>" | |
| 457 end | |
| 458 if d_width > 0 && i_left <= options[:g_width] | |
| 459 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'> </div>" | |
| 460 end | |
| 461 | |
| 462 | |
| 463 # Starting diamond | |
| 464 if start_left <= options[:g_width] && start_left > 0 | |
| 465 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'> </div>" | |
| 466 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>" | |
| 467 output << "</div>" | |
| 468 end | |
| 469 | |
| 470 # Ending diamond | |
| 471 # Don't show items too far ahead | |
| 472 if i_left <= options[:g_width] && i_end > 0 | |
| 473 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'> </div>" | |
| 474 end | |
| 475 | |
| 476 # Display the Version name and % | |
| 477 if i_end <= options[:g_width] | |
| 478 # Display the status even if it's floated off to the left | |
| 479 status_px = i_end + 12 # 12px for the diamond | |
| 480 status_px = 0 if status_px <= 0 | |
| 481 | |
| 482 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>" | |
| 483 output << h("#{version.project} -") unless @project && @project == version.project | |
| 484 output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>" | |
| 485 output << "</div>" | |
| 486 end | |
| 487 | |
| 488 output | |
| 489 when :image | |
| 490 options[:image].stroke('transparent') | |
| 491 i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor | |
| 492 | |
| 493 # Make sure negative i_left doesn't overflow the subject | |
| 494 if i_left > options[:subject_width] | |
| 495 options[:image].fill('green') | |
| 496 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6) | |
| 497 options[:image].fill('black') | |
| 498 options[:image].text(i_left + 11, options[:top] + 1, version.name) | |
| 499 end | |
| 500 when :pdf | |
| 501 options[:pdf].SetY(options[:top]+1.5) | |
| 502 i_left = ((version.start_date - @date_from)*options[:zoom]) | |
| 503 | |
| 504 # Make sure negative i_left doesn't overflow the subject | |
| 505 if i_left > 0 | |
| 506 options[:pdf].SetX(options[:subject_width] + i_left) | |
| 507 options[:pdf].SetFillColor(50,200,50) | |
| 508 options[:pdf].Cell(2, 2, "", 0, 0, "", 1) | |
| 509 | |
| 510 options[:pdf].SetY(options[:top]+1.5) | |
| 511 options[:pdf].SetX(options[:subject_width] + i_left + 3) | |
| 512 options[:pdf].Cell(30, 2, "#{version.name}") | |
| 513 end | |
| 514 end | |
| 515 else | |
| 516 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date" | |
| 517 '' | |
| 518 end | |
| 519 end | |
| 520 | |
| 521 def subject_for_issue(issue, options) | |
| 522 case options[:format] | |
| 523 when :html | |
| 524 output = '' | |
| 525 output << "<div class='tooltip'>" | |
| 526 output << "<div class='issue-subject' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> " | |
| 527 if issue.is_a? Issue | |
| 528 css_classes = [] | |
| 529 css_classes << 'issue-overdue' if issue.overdue? | |
| 530 css_classes << 'issue-behind-schedule' if issue.behind_schedule? | |
| 531 css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to | |
| 532 | |
| 533 if issue.assigned_to.present? | |
| 534 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name | |
| 535 output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string) | |
| 536 end | |
| 537 output << "<span class='#{css_classes.join(' ')}'>" | |
| 538 output << view.link_to_issue(issue) | |
| 539 output << ":" | |
| 540 output << h(issue.subject) | |
| 541 output << '</span>' | |
| 542 else | |
| 543 ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue" | |
| 544 '' | |
| 545 end | |
| 546 output << "</small></div>" | |
| 547 | |
| 548 # Tooltip | |
| 549 if issue.is_a? Issue | |
| 550 output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>" | |
| 551 output << view.render_issue_tooltip(issue) | |
| 552 output << "</span>" | |
| 553 end | |
| 554 | |
| 555 output << "</div>" | |
| 556 output | |
| 557 when :image | |
| 558 options[:image].fill('black') | |
| 559 options[:image].stroke('transparent') | |
| 560 options[:image].stroke_width(1) | |
| 561 options[:image].text(options[:indent], options[:top] + 2, issue.subject) | |
| 562 when :pdf | |
| 563 options[:pdf].SetY(options[:top]) | |
| 564 options[:pdf].SetX(15) | |
| 565 | |
| 566 char_limit = PDF::MaxCharactorsForSubject - options[:indent] | |
| 567 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR") | |
| 568 | |
| 569 options[:pdf].SetY(options[:top]) | |
| 570 options[:pdf].SetX(options[:subject_width]) | |
| 571 options[:pdf].Cell(options[:g_width], 5, "", "LR") | |
| 572 end | |
| 573 end | |
| 574 | |
| 575 def line_for_issue(issue, options) | |
| 576 # Skip issues that don't have a due_before (due_date or version's due_date) | |
| 577 if issue.is_a?(Issue) && issue.due_before | |
| 578 case options[:format] | |
| 579 when :html | |
| 580 output = '' | |
| 581 # Handle nil start_dates, rare but can happen. | |
| 582 i_start_date = if issue.start_date && issue.start_date >= self.date_from | |
| 583 issue.start_date | |
| 584 else | |
| 585 self.date_from | |
| 586 end | |
| 587 | |
| 588 i_end_date = ((issue.due_before && issue.due_before <= self.date_to) ? issue.due_before : self.date_to ) | |
| 589 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor | |
| 590 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date ) | |
| 591 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date ) | |
| 592 | |
| 593 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today | |
| 594 | |
| 595 i_left = ((i_start_date - self.date_from)*options[:zoom]).floor | |
| 596 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor - 2 # total width of the issue (- 2 for left and right borders) | |
| 597 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor - 2 # done width | |
| 598 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor - 2 : 0 # delay width | |
| 599 css = "task " + (issue.leaf? ? 'leaf' : 'parent') | |
| 600 | |
| 601 # Make sure that negative i_left and i_width don't | |
| 602 # overflow the subject | |
| 603 if i_width > 0 | |
| 604 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;' class='#{css} task_todo'> </div>" | |
| 605 end | |
| 606 if l_width > 0 | |
| 607 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ l_width }px;' class='#{css} task_late'> </div>" | |
| 608 end | |
| 609 if d_width > 0 | |
| 610 output<< "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ d_width }px;' class='#{css} task_done'> </div>" | |
| 611 end | |
| 612 | |
| 613 # Display the status even if it's floated off to the left | |
| 614 status_px = i_left + i_width + 5 | |
| 615 status_px = 5 if status_px <= 0 | |
| 616 | |
| 617 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='#{css} label issue-name'>" | |
| 618 output << issue.status.name | |
| 619 output << ' ' | |
| 620 output << (issue.done_ratio).to_i.to_s | |
| 621 output << "%" | |
| 622 output << "</div>" | |
| 623 | |
| 624 output << "<div class='tooltip' style='position: absolute;top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;height:12px;'>" | |
| 625 output << '<span class="tip">' | |
| 626 output << view.render_issue_tooltip(issue) | |
| 627 output << "</span></div>" | |
| 628 output | |
| 629 | |
| 630 when :image | |
| 631 # Handle nil start_dates, rare but can happen. | |
| 632 i_start_date = if issue.start_date && issue.start_date >= @date_from | |
| 633 issue.start_date | |
| 634 else | |
| 635 @date_from | |
| 636 end | |
| 637 | |
| 638 i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to ) | |
| 639 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor | |
| 640 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) | |
| 641 i_done_date = (i_done_date >= date_to ? date_to : i_done_date ) | |
| 642 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today | |
| 643 | |
| 644 i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor | |
| 645 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue | |
| 646 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width | |
| 647 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width | |
| 648 | |
| 649 | |
| 650 # Make sure that negative i_left and i_width don't | |
| 651 # overflow the subject | |
| 652 if i_width > 0 | |
| 653 options[:image].fill('grey') | |
| 654 options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6) | |
| 655 options[:image].fill('red') | |
| 656 options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0 | |
| 657 options[:image].fill('blue') | |
| 658 options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0 | |
| 659 end | |
| 660 | |
| 661 # Show the status and % done next to the subject if it overflows | |
| 662 options[:image].fill('black') | |
| 663 if i_width > 0 | |
| 664 options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%") | |
| 665 else | |
| 666 options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%") | |
| 667 end | |
| 668 | |
| 669 when :pdf | |
| 670 options[:pdf].SetY(options[:top]+1.5) | |
| 671 # Handle nil start_dates, rare but can happen. | |
| 672 i_start_date = if issue.start_date && issue.start_date >= @date_from | |
| 673 issue.start_date | |
| 674 else | |
| 675 @date_from | |
| 676 end | |
| 677 | |
| 678 i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to ) | |
| 679 | |
| 680 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor | |
| 681 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) | |
| 682 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date ) | |
| 683 | |
| 684 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today | |
| 685 | |
| 686 i_left = ((i_start_date - @date_from)*options[:zoom]) | |
| 687 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]) | |
| 688 d_width = ((i_done_date - i_start_date)*options[:zoom]) | |
| 689 l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date | |
| 690 l_width ||= 0 | |
| 691 | |
| 692 # Make sure that negative i_left and i_width don't | |
| 693 # overflow the subject | |
| 694 if i_width > 0 | |
| 695 options[:pdf].SetX(options[:subject_width] + i_left) | |
| 696 options[:pdf].SetFillColor(200,200,200) | |
| 697 options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1) | |
| 698 end | |
| 699 | |
| 700 if l_width > 0 | |
| 701 options[:pdf].SetY(options[:top]+1.5) | |
| 702 options[:pdf].SetX(options[:subject_width] + i_left) | |
| 703 options[:pdf].SetFillColor(255,100,100) | |
| 704 options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1) | |
| 705 end | |
| 706 if d_width > 0 | |
| 707 options[:pdf].SetY(options[:top]+1.5) | |
| 708 options[:pdf].SetX(options[:subject_width] + i_left) | |
| 709 options[:pdf].SetFillColor(100,100,255) | |
| 710 options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1) | |
| 711 end | |
| 712 | |
| 713 options[:pdf].SetY(options[:top]+1.5) | |
| 714 | |
| 715 # Make sure that negative i_left and i_width don't | |
| 716 # overflow the subject | |
| 717 if (i_left + i_width) >= 0 | |
| 718 options[:pdf].SetX(options[:subject_width] + i_left + i_width) | |
| 719 else | |
| 720 options[:pdf].SetX(options[:subject_width]) | |
| 721 end | |
| 722 options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%") | |
| 723 end | |
| 724 else | |
| 725 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before" | |
| 726 '' | |
| 727 end | |
| 728 end | |
| 729 | |
| 92 # Generates a gantt image | 730 # Generates a gantt image |
| 93 # Only defined if RMagick is avalaible | 731 # Only defined if RMagick is avalaible |
| 94 def to_image(project, format='PNG') | 732 def to_image(format='PNG') |
| 95 date_to = (@date_from >> @months)-1 | 733 date_to = (@date_from >> @months)-1 |
| 96 show_weeks = @zoom > 1 | 734 show_weeks = @zoom > 1 |
| 97 show_days = @zoom > 2 | 735 show_days = @zoom > 2 |
| 98 | 736 |
| 99 subject_width = 400 | 737 subject_width = 400 |
| 100 header_heigth = 18 | 738 header_heigth = 18 |
| 101 # width of one day in pixels | 739 # width of one day in pixels |
| 102 zoom = @zoom*2 | 740 zoom = @zoom*2 |
| 103 g_width = (@date_to - @date_from + 1)*zoom | 741 g_width = (@date_to - @date_from + 1)*zoom |
| 104 g_height = 20 * events.length + 20 | 742 g_height = 20 * number_of_rows + 30 |
| 105 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) | 743 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) |
| 106 height = g_height + headers_heigth | 744 height = g_height + headers_heigth |
| 107 | 745 |
| 108 imgl = Magick::ImageList.new | 746 imgl = Magick::ImageList.new |
| 109 imgl.new_image(subject_width+g_width+1, height) | 747 imgl.new_image(subject_width+g_width+1, height) |
| 110 gc = Magick::Draw.new | 748 gc = Magick::Draw.new |
| 111 | 749 |
| 112 # Subjects | 750 # Subjects |
| 113 top = headers_heigth + 20 | 751 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image) |
| 114 gc.fill('black') | |
| 115 gc.stroke('transparent') | |
| 116 gc.stroke_width(1) | |
| 117 events.each do |i| | |
| 118 text = "" | |
| 119 if i.is_a? Issue | |
| 120 text = "#{i.tracker} #{i.id}: #{i.subject}" | |
| 121 else | |
| 122 text = i.name | |
| 123 end | |
| 124 text = "#{i.project} - #{text}" unless project && project == i.project | |
| 125 gc.text(4, top + 2, text) | |
| 126 top = top + 20 | |
| 127 end | |
| 128 | 752 |
| 129 # Months headers | 753 # Months headers |
| 130 month_f = @date_from | 754 month_f = @date_from |
| 131 left = subject_width | 755 left = subject_width |
| 132 @months.times do | 756 @months.times do |
| 200 gc.stroke('black') | 824 gc.stroke('black') |
| 201 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1) | 825 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1) |
| 202 | 826 |
| 203 # content | 827 # content |
| 204 top = headers_heigth + 20 | 828 top = headers_heigth + 20 |
| 205 gc.stroke('transparent') | 829 |
| 206 events.each do |i| | 830 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image) |
| 207 if i.is_a?(Issue) | |
| 208 i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from ) | |
| 209 i_end_date = (i.due_before <= date_to ? i.due_before : date_to ) | |
| 210 i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor | |
| 211 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) | |
| 212 i_done_date = (i_done_date >= date_to ? date_to : i_done_date ) | |
| 213 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today | |
| 214 | |
| 215 i_left = subject_width + ((i_start_date - @date_from)*zoom).floor | |
| 216 i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue | |
| 217 d_width = ((i_done_date - i_start_date)*zoom).floor # done width | |
| 218 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width | |
| 219 | |
| 220 gc.fill('grey') | |
| 221 gc.rectangle(i_left, top, i_left + i_width, top - 6) | |
| 222 gc.fill('red') | |
| 223 gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0 | |
| 224 gc.fill('blue') | |
| 225 gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0 | |
| 226 gc.fill('black') | |
| 227 gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%") | |
| 228 else | |
| 229 i_left = subject_width + ((i.start_date - @date_from)*zoom).floor | |
| 230 gc.fill('green') | |
| 231 gc.rectangle(i_left, top, i_left + 6, top - 6) | |
| 232 gc.fill('black') | |
| 233 gc.text(i_left + 11, top + 1, i.name) | |
| 234 end | |
| 235 top = top + 20 | |
| 236 end | |
| 237 | 831 |
| 238 # today red line | 832 # today red line |
| 239 if Date.today >= @date_from and Date.today <= date_to | 833 if Date.today >= @date_from and Date.today <= date_to |
| 240 gc.stroke('red') | 834 gc.stroke('red') |
| 241 x = (Date.today-@date_from+1)*zoom + subject_width | 835 x = (Date.today-@date_from+1)*zoom + subject_width |
| 244 | 838 |
| 245 gc.draw(imgl) | 839 gc.draw(imgl) |
| 246 imgl.format = format | 840 imgl.format = format |
| 247 imgl.to_blob | 841 imgl.to_blob |
| 248 end if Object.const_defined?(:Magick) | 842 end if Object.const_defined?(:Magick) |
| 843 | |
| 844 def to_pdf | |
| 845 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language) | |
| 846 pdf.SetTitle("#{l(:label_gantt)} #{project}") | |
| 847 pdf.AliasNbPages | |
| 848 pdf.footer_date = format_date(Date.today) | |
| 849 pdf.AddPage("L") | |
| 850 pdf.SetFontStyle('B',12) | |
| 851 pdf.SetX(15) | |
| 852 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s) | |
| 853 pdf.Ln | |
| 854 pdf.SetFontStyle('B',9) | |
| 855 | |
| 856 subject_width = PDF::LeftPaneWidth | |
| 857 header_heigth = 5 | |
| 858 | |
| 859 headers_heigth = header_heigth | |
| 860 show_weeks = false | |
| 861 show_days = false | |
| 862 | |
| 863 if self.months < 7 | |
| 864 show_weeks = true | |
| 865 headers_heigth = 2*header_heigth | |
| 866 if self.months < 3 | |
| 867 show_days = true | |
| 868 headers_heigth = 3*header_heigth | |
| 869 end | |
| 870 end | |
| 871 | |
| 872 g_width = PDF.right_pane_width | |
| 873 zoom = (g_width) / (self.date_to - self.date_from + 1) | |
| 874 g_height = 120 | |
| 875 t_height = g_height + headers_heigth | |
| 876 | |
| 877 y_start = pdf.GetY | |
| 878 | |
| 879 # Months headers | |
| 880 month_f = self.date_from | |
| 881 left = subject_width | |
| 882 height = header_heigth | |
| 883 self.months.times do | |
| 884 width = ((month_f >> 1) - month_f) * zoom | |
| 885 pdf.SetY(y_start) | |
| 886 pdf.SetX(left) | |
| 887 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C") | |
| 888 left = left + width | |
| 889 month_f = month_f >> 1 | |
| 890 end | |
| 891 | |
| 892 # Weeks headers | |
| 893 if show_weeks | |
| 894 left = subject_width | |
| 895 height = header_heigth | |
| 896 if self.date_from.cwday == 1 | |
| 897 # self.date_from is monday | |
| 898 week_f = self.date_from | |
| 899 else | |
| 900 # find next monday after self.date_from | |
| 901 week_f = self.date_from + (7 - self.date_from.cwday + 1) | |
| 902 width = (7 - self.date_from.cwday + 1) * zoom-1 | |
| 903 pdf.SetY(y_start + header_heigth) | |
| 904 pdf.SetX(left) | |
| 905 pdf.Cell(width + 1, height, "", "LTR") | |
| 906 left = left + width+1 | |
| 907 end | |
| 908 while week_f <= self.date_to | |
| 909 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom | |
| 910 pdf.SetY(y_start + header_heigth) | |
| 911 pdf.SetX(left) | |
| 912 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") | |
| 913 left = left + width | |
| 914 week_f = week_f+7 | |
| 915 end | |
| 916 end | |
| 917 | |
| 918 # Days headers | |
| 919 if show_days | |
| 920 left = subject_width | |
| 921 height = header_heigth | |
| 922 wday = self.date_from.cwday | |
| 923 pdf.SetFontStyle('B',7) | |
| 924 (self.date_to - self.date_from + 1).to_i.times do | |
| 925 width = zoom | |
| 926 pdf.SetY(y_start + 2 * header_heigth) | |
| 927 pdf.SetX(left) | |
| 928 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C") | |
| 929 left = left + width | |
| 930 wday = wday + 1 | |
| 931 wday = 1 if wday > 7 | |
| 932 end | |
| 933 end | |
| 934 | |
| 935 pdf.SetY(y_start) | |
| 936 pdf.SetX(15) | |
| 937 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1) | |
| 938 | |
| 939 # Tasks | |
| 940 top = headers_heigth + y_start | |
| 941 pdf_subjects_and_lines(pdf, { | |
| 942 :top => top, | |
| 943 :zoom => zoom, | |
| 944 :subject_width => subject_width, | |
| 945 :g_width => g_width | |
| 946 }) | |
| 947 | |
| 948 | |
| 949 pdf.Line(15, top, subject_width+g_width, top) | |
| 950 pdf.Output | |
| 951 | |
| 952 | |
| 953 end | |
| 249 | 954 |
| 250 private | 955 private |
| 251 | 956 |
| 252 def gantt_issue_compare(x, y, issues) | 957 # Renders both the subjects and lines of the Gantt chart for the |
| 253 if x.parent_id == y.parent_id | 958 # PDF format |
| 254 gantt_start_compare(x, y) | 959 def pdf_subjects_and_lines(pdf, options = {}) |
| 255 elsif x.is_ancestor_of?(y) | 960 subject_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :subject, :format => :pdf, :pdf => pdf}.merge(options) |
| 256 -1 | 961 line_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :line, :format => :pdf, :pdf => pdf}.merge(options) |
| 257 elsif y.is_ancestor_of?(x) | 962 |
| 258 1 | 963 if @project |
| 964 render_project(@project, subject_options) | |
| 965 render_project(@project, line_options) | |
| 259 else | 966 else |
| 260 ax = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(x) && !i.is_ancestor_of?(y) }.sort_by(&:lft).first | 967 Project.roots.each do |project| |
| 261 ay = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(y) && !i.is_ancestor_of?(x) }.sort_by(&:lft).first | 968 render_project(project, subject_options) |
| 262 if ax.nil? && ay.nil? | 969 render_project(project, line_options) |
| 263 gantt_start_compare(x, y) | 970 end |
| 264 else | 971 end |
| 265 gantt_issue_compare(ax || x, ay || y, issues) | 972 end |
| 266 end | 973 |
| 267 end | |
| 268 end | |
| 269 | |
| 270 def gantt_start_compare(x, y) | |
| 271 if x.start_date.nil? | |
| 272 -1 | |
| 273 elsif y.start_date.nil? | |
| 274 1 | |
| 275 else | |
| 276 x.start_date <=> y.start_date | |
| 277 end | |
| 278 end | |
| 279 end | 974 end |
| 280 end | 975 end |
| 281 end | 976 end |
