| 38 | 38 |       attr_accessor :query
 | 
  | 39 | 39 |       attr_accessor :project
 | 
  | 40 | 40 |       attr_accessor :view
 | 
  | 41 |  |       
 | 
  |  | 41 | 
 | 
  | 42 | 42 |       def initialize(options={})
 | 
  | 43 | 43 |         options = options.dup
 | 
  | 44 |  |         
 | 
  |  | 44 | 
 | 
  | 45 | 45 |         if options[:year] && options[:year].to_i >0
 | 
  | 46 | 46 |           @year_from = options[:year].to_i
 | 
  | 47 | 47 |           if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
 | 
  | ... | ... |  | 
  | 53 | 53 |           @month_from ||= Date.today.month
 | 
  | 54 | 54 |           @year_from ||= Date.today.year
 | 
  | 55 | 55 |         end
 | 
  | 56 |  |         
 | 
  |  | 56 | 
 | 
  | 57 | 57 |         zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
 | 
  | 58 | 58 |         @zoom = (zoom > 0 && zoom < 5) ? zoom : 2    
 | 
  | 59 | 59 |         months = (options[:months] || User.current.pref[:gantt_months]).to_i
 | 
  | 60 | 60 |         @months = (months > 0 && months < 25) ? months : 6
 | 
  | 61 |  |         
 | 
  |  | 61 | 
 | 
  | 62 | 62 |         # Save gantt parameters as user preference (zoom and months count)
 | 
  | 63 | 63 |         if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
 | 
  | 64 | 64 |           User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
 | 
  | 65 | 65 |           User.current.preference.save
 | 
  | 66 | 66 |         end
 | 
  | 67 |  |         
 | 
  |  | 67 | 
 | 
  | 68 | 68 |         @date_from = Date.civil(@year_from, @month_from, 1)
 | 
  | 69 | 69 |         @date_to = (@date_from >> @months) - 1
 | 
  | 70 | 70 |         
 | 
  | ... | ... |  | 
  | 98 | 98 |         common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
 | 
  | 99 | 99 |       end
 | 
  | 100 | 100 | 
 | 
  | 101 |  |             ### Extracted from the HTML view/helpers
 | 
  |  | 101 |       ### Extracted from the HTML view/helpers
 | 
  | 102 | 102 |       # Returns the number of rows that will be rendered on the Gantt chart
 | 
  | 103 | 103 |       def number_of_rows
 | 
  | 104 |  |         return @number_of_rows if @number_of_rows
 | 
  | 105 |  |         
 | 
  | 106 |  |         rows = if @project
 | 
  | 107 |  |           number_of_rows_on_project(@project)
 | 
  | 108 |  |         else
 | 
  | 109 |  |           Project.roots.visible.has_module('issue_tracking').inject(0) do |total, project|
 | 
  | 110 |  |             total += number_of_rows_on_project(project)
 | 
  | 111 |  |           end
 | 
  | 112 |  |         end
 | 
  | 113 |  |         
 | 
  | 114 |  |         rows > @max_rows ? @max_rows : rows
 | 
  |  | 104 |         @number_of_rows
 | 
  | 115 | 105 |       end
 | 
  | 116 | 106 | 
 | 
  | 117 |  |       # Returns the number of rows that will be used to list a project on
 | 
  | 118 |  |       # the Gantt chart.  This will recurse for each subproject.
 | 
  | 119 |  |       def number_of_rows_on_project(project)
 | 
  | 120 |  |         # Remove the project requirement for Versions because it will
 | 
  | 121 |  |         # restrict issues to only be on the current project.  This
 | 
  | 122 |  |         # ends up missing issues which are assigned to shared versions.
 | 
  | 123 |  |         @query.project = nil if @query.project
 | 
  | 124 |  | 
 | 
  | 125 |  |         # One Root project
 | 
  | 126 |  |         count = 1
 | 
  | 127 |  |         # Issues without a Version
 | 
  | 128 |  |         count += project.issues.for_gantt.without_version.with_query(@query).count
 | 
  | 129 |  | 
 | 
  | 130 |  |         # Versions
 | 
  | 131 |  |         count += project.versions.count
 | 
  | 132 |  | 
 | 
  | 133 |  |         # Issues on the Versions
 | 
  | 134 |  |         project.versions.each do |version|
 | 
  | 135 |  |           count += version.fixed_issues.for_gantt.with_query(@query).count
 | 
  | 136 |  |         end
 | 
  | 137 |  | 
 | 
  | 138 |  |         # Subprojects
 | 
  | 139 |  |         project.children.visible.has_module('issue_tracking').each do |subproject|
 | 
  | 140 |  |           count += number_of_rows_on_project(subproject)
 | 
  | 141 |  |         end
 | 
  | 142 |  | 
 | 
  | 143 |  |         count
 | 
  | 144 |  |       end
 | 
  | 145 |  | 
 | 
  | 146 | 107 |       # Renders the subjects of the Gantt chart, the left side.
 | 
  | 147 | 108 |       def subjects(options={})
 | 
  | 148 | 109 |         render(options.merge(:only => :subjects)) unless @subjects_rendered
 | 
  | ... | ... |  | 
  | 154 | 115 |         render(options.merge(:only => :lines)) unless @lines_rendered
 | 
  | 155 | 116 |         @lines
 | 
  | 156 | 117 |       end
 | 
  | 157 |  |       
 | 
  |  | 118 | 
 | 
  | 158 | 119 |       def render(options={})
 | 
  | 159 | 120 |         options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
 | 
  | 160 |  |         
 | 
  |  | 121 | 
 | 
  | 161 | 122 |         @subjects = '' unless options[:only] == :lines
 | 
  | 162 | 123 |         @lines = '' unless options[:only] == :subjects
 | 
  | 163 | 124 |         @number_of_rows = 0
 | 
  | 164 |  |         
 | 
  | 165 |  |         if @project
 | 
  | 166 |  |           render_project(@project, options)
 | 
  | 167 |  |         else
 | 
  | 168 |  |           Project.roots.visible.has_module('issue_tracking').each do |project|
 | 
  | 169 |  |             render_project(project, options)
 | 
  |  | 125 | 
 | 
  |  | 126 |         options[:render_issues_without_version_first] = true unless options.key? :render_issues_without_version_first
 | 
  |  | 127 | 
 | 
  |  | 128 |         options[:top] = 0 unless options.key? :top
 | 
  |  | 129 |         options[:indent_increment] = 20 unless options.key? :indent_increment
 | 
  |  | 130 |         options[:top_increment] = 20 unless options.key? :top_increment
 | 
  |  | 131 | 
 | 
  |  | 132 |         options[:gantt_indent] = options[:indent]
 | 
  |  | 133 | 
 | 
  |  | 134 |         # We sort by project.lft so that projects are listed before their children
 | 
  |  | 135 |         top_projects = Project.find( :all,
 | 
  |  | 136 |           :conditions => @query.project_statement(Project.table_name),
 | 
  |  | 137 |           :order => ["#{Project.table_name}.lft ASC"])
 | 
  |  | 138 | 
 | 
  |  | 139 |         # Purge descendants to be sure to have only top projects left
 | 
  |  | 140 |         prev_top = nil
 | 
  |  | 141 |         top_projects.delete_if do |top|
 | 
  |  | 142 |           remove_descendant = prev_top && top.is_descendant_of?(prev_top)
 | 
  |  | 143 |           prev_top = top
 | 
  |  | 144 |           remove_descendant
 | 
  |  | 145 |         end
 | 
  |  | 146 | 
 | 
  |  | 147 |         # Get issues with theirs versions and subprojects as filtered by query criterias
 | 
  |  | 148 |         issues = Issue.find(
 | 
  |  | 149 |           :all,
 | 
  |  | 150 |           :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
 | 
  |  | 151 |           :conditions => @query.statement,
 | 
  |  | 152 |           :joins => [
 | 
  |  | 153 |             "LEFT OUTER JOIN #{Project.table_name} AS top_project ON top_project.id IN (#{top_projects.collect(&:id).join(', ')}) AND top_project.lft <= #{Project.table_name}.lft AND top_project.rgt >= #{Project.table_name}.rgt " + # Top project
 | 
  |  | 154 |             "LEFT OUTER JOIN #{Project.table_name} AS version_project ON version_project.id = #{Version.table_name}.project_id " + # Project of the fixed version
 | 
  |  | 155 |             "LEFT OUTER JOIN #{Project.table_name} AS version_top_project ON " +
 | 
  |  | 156 |                     "(version_top_project.id = #{Project.table_name}.id AND #{Version.table_name}.sharing = 'none')" +
 | 
  |  | 157 |                 " OR (version_top_project.id = top_project.id AND #{Version.table_name}.sharing IN ('hierarchy', 'system', 'tree'))" +
 | 
  |  | 158 |                 " OR (version_top_project.id = CASE WHEN top_project.lft < version_project.lft THEN version_project.id ELSE top_project.id END AND #{Version.table_name}.sharing = 'descendants')"], 
 | 
  |  | 159 |           :order => [
 | 
  |  | 160 |             "top_project.lft ASC, top_project.name ASC, " + # Top project
 | 
  |  | 161 |             "COALESCE(version_top_project.lft, #{Project.table_name}.lft) ASC, " + # Version top project or project if none
 | 
  |  | 162 |             "CASE WHEN #{Issue.table_name}.fixed_version_id IS NULL THEN #{ options[:render_issues_without_version_first] ? '0 ELSE 1' : '1 ELSE 0' } END ASC, " + # Issues without version first (or last)
 | 
  |  | 163 |             "CASE WHEN #{Version.table_name}.effective_date IS NULL THEN 0 ELSE 1 END DESC, " + # Version with effective_date first
 | 
  |  | 164 |             "#{Version.table_name}.effective_date ASC, #{Version.table_name}.name ASC, " +
 | 
  |  | 165 |             "#{Project.table_name}.lft ASC"] )
 | 
  |  | 166 | 
 | 
  |  | 167 |         # TODO : at this point we could have been returned 3 extra Project objects useful for building project path : top_project, version_project and top_version_project
 | 
  |  | 168 |         # There is no way ActiveRecord can map sql query results to more than one model (hibernate does), so we have to compute them again in update_and_render_project_path(), adding a dummy time & cpu overhead to the whole rendering process
 | 
  |  | 169 |         # Idea : use an IssueForGantt class with association to issue, top_project, version_project and top_version_project ?
 | 
  |  | 170 | 
 | 
  |  | 171 |         prev_issue = nil
 | 
  |  | 172 |         path_issues = []
 | 
  |  | 173 | 
 | 
  |  | 174 |         # TODO : use a Hash rather than an Array when switching to Ruby 1.9, as Hash is ordered in 1.9
 | 
  |  | 175 |         issue_path = []
 | 
  |  | 176 | 
 | 
  |  | 177 |         # Render Gantt by looping on issues
 | 
  |  | 178 |         issues.each do |i|
 | 
  |  | 179 | 
 | 
  |  | 180 |           # Stack issues within the same project path to allow them to be sorted before rendering
 | 
  |  | 181 |           if prev_issue && (
 | 
  |  | 182 |                 prev_issue.project != i.project ||
 | 
  |  | 183 |                 prev_issue.fixed_version != i.fixed_version)
 | 
  |  | 184 | 
 | 
  |  | 185 |             render_path_issues(path_issues, options) unless path_issues.empty?
 | 
  | 170 | 186 |             break if abort?
 | 
  |  | 187 | 
 | 
  |  | 188 |             path_issues.clear
 | 
  | 171 | 189 |           end
 | 
  |  | 190 | 
 | 
  |  | 191 |           issue_path = update_and_render_project_path(top_projects, i, prev_issue, issue_path, options)
 | 
  |  | 192 |           break if abort?
 | 
  |  | 193 | 
 | 
  |  | 194 |           path_issues << i
 | 
  |  | 195 | 
 | 
  |  | 196 |           prev_issue = i
 | 
  | 172 | 197 |         end
 | 
  | 173 |  |         
 | 
  |  | 198 | 
 | 
  |  | 199 |         render_path_issues(path_issues, options) unless abort?
 | 
  |  | 200 | 
 | 
  | 174 | 201 |         @subjects_rendered = true unless options[:only] == :lines
 | 
  | 175 | 202 |         @lines_rendered = true unless options[:only] == :subjects
 | 
  | 176 |  |         
 | 
  |  | 203 | 
 | 
  | 177 | 204 |         render_end(options)
 | 
  | 178 | 205 |       end
 | 
  | 179 | 206 | 
 | 
  | 180 |  |       def render_project(project, options={})
 | 
  | 181 |  |         options[:top] = 0 unless options.key? :top
 | 
  | 182 |  |         options[:indent_increment] = 20 unless options.key? :indent_increment
 | 
  | 183 |  |         options[:top_increment] = 20 unless options.key? :top_increment
 | 
  |  | 207 |       # Renders issues in a version/project path
 | 
  |  | 208 |       def render_path_issues(issues, options)
 | 
  | 184 | 209 | 
 | 
  | 185 |  |         subject_for_project(project, options) unless options[:only] == :lines
 | 
  | 186 |  |         line_for_project(project, options) unless options[:only] == :subjects
 | 
  | 187 |  |         
 | 
  | 188 |  |         options[:top] += options[:top_increment]
 | 
  | 189 |  |         options[:indent] += options[:indent_increment]
 | 
  | 190 |  |         @number_of_rows += 1
 | 
  | 191 |  |         return if abort?
 | 
  | 192 |  |         
 | 
  | 193 |  |         # Second, Issues without a version
 | 
  | 194 |  |         issues = project.issues.for_gantt.without_version.with_query(@query).all(:limit => current_limit)
 | 
  | 195 |  |         sort_issues!(issues)
 | 
  | 196 |  |         if issues
 | 
  | 197 |  |           render_issues(issues, options)
 | 
  | 198 |  |           return if abort?
 | 
  | 199 |  |         end
 | 
  |  | 210 |         sort_issues! issues
 | 
  | 200 | 211 | 
 | 
  | 201 |  |         # Third, Versions
 | 
  | 202 |  |         project.versions.sort.each do |version|
 | 
  | 203 |  |           render_version(version, options)
 | 
  | 204 |  |           return if abort?
 | 
  | 205 |  |         end
 | 
  |  | 212 |         base_indent = options[:indent]
 | 
  | 206 | 213 | 
 | 
  | 207 |  |         # Fourth, subprojects
 | 
  | 208 |  |         project.children.visible.has_module('issue_tracking').each do |project|
 | 
  | 209 |  |           render_project(project, options)
 | 
  | 210 |  |           return if abort?
 | 
  | 211 |  |         end unless project.leaf?
 | 
  |  | 214 |         prev_issue = nil
 | 
  |  | 215 |         issue_hierarchy = []
 | 
  | 212 | 216 | 
 | 
  | 213 |  |         # Remove indent to hit the next sibling
 | 
  | 214 |  |         options[:indent] -= options[:indent_increment]
 | 
  | 215 |  |       end
 | 
  |  | 217 |         issues.each do |i|
 | 
  | 216 | 218 | 
 | 
  | 217 |  |       def render_issues(issues, options={})
 | 
  | 218 |  |         @issue_ancestors = []
 | 
  | 219 |  |         
 | 
  | 220 |  |         issues.each do |i|
 | 
  |  | 219 |           # Indent issue hierarchy (issue in the same hierarchy tree should follow thanks to the sorting order)
 | 
  |  | 220 |           ancestor_idx = -1
 | 
  |  | 221 |           (issue_hierarchy.length - 1).downto(0) do |h_i|
 | 
  |  | 222 |             if issue_hierarchy[h_i].is_ancestor_of?(i)
 | 
  |  | 223 |               ancestor_idx = h_i
 | 
  |  | 224 |               break
 | 
  |  | 225 |             end
 | 
  |  | 226 |           end
 | 
  |  | 227 |           issue_hierarchy.slice!((ancestor_idx + 1)..-1)
 | 
  |  | 228 | 
 | 
  |  | 229 |           issue_hierarchy << i
 | 
  |  | 230 | 
 | 
  |  | 231 |           options[:indent] = base_indent + (issue_hierarchy.length - 1) * options[:indent_increment]
 | 
  |  | 232 | 
 | 
  |  | 233 |           # Render issue node
 | 
  | 221 | 234 |           subject_for_issue(i, options) unless options[:only] == :lines
 | 
  | 222 | 235 |           line_for_issue(i, options) unless options[:only] == :subjects
 | 
  | 223 |  |           
 | 
  | 224 |  |           options[:top] += options[:top_increment]
 | 
  | 225 | 236 |           @number_of_rows += 1
 | 
  | 226 | 237 |           break if abort?
 | 
  |  | 238 |           options[:top] += options[:top_increment]
 | 
  |  | 239 | 
 | 
  |  | 240 |           prev_issue = i
 | 
  | 227 | 241 |         end
 | 
  | 228 |  |         
 | 
  | 229 |  |         options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
 | 
  |  | 242 | 
 | 
  | 230 | 243 |       end
 | 
  | 231 | 244 | 
 | 
  |  | 245 |       def update_and_render_project_path(top_projects, i, prev_issue, issue_path, options)
 | 
  |  | 246 | 
 | 
  |  | 247 |         prev_version = prev_issue.nil? ? nil : prev_issue.fixed_version
 | 
  |  | 248 |         project_change = prev_issue.nil? || i.project != prev_issue.project
 | 
  |  | 249 | 
 | 
  |  | 250 |         first_item_to_render_idx = nil
 | 
  |  | 251 | 
 | 
  |  | 252 |         # Render the project path to the project the issue belongs to
 | 
  |  | 253 |         if project_change
 | 
  |  | 254 | 
 | 
  |  | 255 |           top_project = issue_path.empty? ? nil : issue_path.first 
 | 
  |  | 256 | 
 | 
  |  | 257 |           # Remove invalid path queue
 | 
  |  | 258 |           if top_project.nil? || !i.project.is_or_is_descendant_of?(top_project)
 | 
  |  | 259 |             # New branch
 | 
  |  | 260 |             top_project = top_projects.detect { |p| p.is_or_is_ancestor_of?(i.project) }
 | 
  |  | 261 |             top_projects.delete(top_project) # Lighten the top_projects map as a top project should not be rendered twice 
 | 
  |  | 262 |             issue_path = [top_project]
 | 
  |  | 263 |             first_item_to_render_idx = 0
 | 
  |  | 264 |             prev_version = nil
 | 
  |  | 265 |           elsif !prev_issue.nil? && !i.project.is_descendant_of?(prev_issue.project)
 | 
  |  | 266 |             # Truncate actual path (in any way, we keep the top project node)
 | 
  |  | 267 |             # TODO : use index { } with Ruby 1.8.7+
 | 
  |  | 268 |             first_distinct_project_idx = nil
 | 
  |  | 269 |             1.upto(issue_path.length - 1) do |pi_idx|
 | 
  |  | 270 |               if issue_path[pi_idx].is_a?(Project) && !issue_path[pi_idx].is_ancestor_of?(i.project)
 | 
  |  | 271 |                 first_distinct_project_idx = pi_idx
 | 
  |  | 272 |                 break
 | 
  |  | 273 |               end
 | 
  |  | 274 |             end
 | 
  |  | 275 |             issue_path.slice!(first_distinct_project_idx..-1)
 | 
  |  | 276 |           end
 | 
  |  | 277 | 
 | 
  |  | 278 |           # TODO : use rindex { } with Ruby 1.9
 | 
  |  | 279 |           last_common_project_idx = nil
 | 
  |  | 280 |           (issue_path.length - 1).downto(0) do |pi_idx|
 | 
  |  | 281 |             if issue_path[pi_idx].is_a?(Project)
 | 
  |  | 282 |               last_common_project_idx = pi_idx
 | 
  |  | 283 |               break
 | 
  |  | 284 |             end
 | 
  |  | 285 |           end
 | 
  |  | 286 | 
 | 
  |  | 287 |           # Complete path
 | 
  |  | 288 |           rpath_queue = []
 | 
  |  | 289 |           subproject = i.project
 | 
  |  | 290 |           until subproject == issue_path.at(last_common_project_idx)
 | 
  |  | 291 |             rpath_queue << subproject
 | 
  |  | 292 |             subproject = subproject.parent
 | 
  |  | 293 |           end
 | 
  |  | 294 | 
 | 
  |  | 295 |           unless rpath_queue.empty?
 | 
  |  | 296 |             first_item_to_render_idx = issue_path.length if first_item_to_render_idx.nil?
 | 
  |  | 297 |             issue_path += rpath_queue.reverse 
 | 
  |  | 298 |           end
 | 
  |  | 299 |         end
 | 
  |  | 300 | 
 | 
  |  | 301 |         version_change = i.fixed_version != prev_version
 | 
  |  | 302 | 
 | 
  |  | 303 |         if version_change
 | 
  |  | 304 |           # Remove previous Version node from path
 | 
  |  | 305 |           unless prev_version.nil?
 | 
  |  | 306 |             prev_version_idx = issue_path.index(prev_version)
 | 
  |  | 307 |             unless prev_version_idx.nil? # May have been removed with the project path truncation
 | 
  |  | 308 |               issue_path.delete_at(prev_version_idx)
 | 
  |  | 309 |               first_item_to_render_idx = prev_version_idx if prev_version_idx < issue_path.length || (!first_item_to_render_idx.nil? && prev_version_idx < first_item_to_render_idx)
 | 
  |  | 310 |             end
 | 
  |  | 311 |           end
 | 
  |  | 312 | 
 | 
  |  | 313 |           # Place new Version node within the path
 | 
  |  | 314 |           unless i.fixed_version.nil?
 | 
  |  | 315 |             # TODO : use index { } with Ruby 1.8.7+
 | 
  |  | 316 |             new_version_idx = nil
 | 
  |  | 317 |             0.upto(issue_path.length - 1) do |pi_idx|
 | 
  |  | 318 |               # TODO : p.shared_versions() can slow down rendering, maybe add a has_shared_version?(v) in Project ?
 | 
  |  | 319 |               if issue_path[pi_idx].shared_versions.include?(i.fixed_version)
 | 
  |  | 320 |                 new_version_idx = pi_idx + 1
 | 
  |  | 321 |                 break
 | 
  |  | 322 |               end
 | 
  |  | 323 |             end
 | 
  |  | 324 |             issue_path.insert(new_version_idx, i.fixed_version)
 | 
  |  | 325 |             first_item_to_render_idx = new_version_idx if first_item_to_render_idx.nil? || new_version_idx < first_item_to_render_idx
 | 
  |  | 326 |           end
 | 
  |  | 327 |         end
 | 
  |  | 328 | 
 | 
  |  | 329 |         # Render new path part
 | 
  |  | 330 |         unless first_item_to_render_idx.nil?
 | 
  |  | 331 |           options[:indent] = options[:gantt_indent] + first_item_to_render_idx * options[:indent_increment]
 | 
  |  | 332 | 
 | 
  |  | 333 |           issue_path.slice(first_item_to_render_idx..-1).each do |pi|
 | 
  |  | 334 | 
 | 
  |  | 335 |             if pi.is_a?(Version)
 | 
  |  | 336 |               # Render version node
 | 
  |  | 337 |               render_version(pi, options)
 | 
  |  | 338 |             else
 | 
  |  | 339 |               # Render project node
 | 
  |  | 340 |               render_subproject(pi, options)
 | 
  |  | 341 |             end
 | 
  |  | 342 |             break if abort?
 | 
  |  | 343 |             options[:indent] += options[:indent_increment]
 | 
  |  | 344 |             options[:top] += options[:top_increment]
 | 
  |  | 345 |           end
 | 
  |  | 346 |         else
 | 
  |  | 347 |           options[:indent] = options[:gantt_indent] + issue_path.length * options[:indent_increment]
 | 
  |  | 348 |         end
 | 
  |  | 349 | 
 | 
  |  | 350 |         issue_path
 | 
  |  | 351 |       end
 | 
  |  | 352 | 
 | 
  | 232 | 353 |       def render_version(version, options={})
 | 
  | 233 | 354 |         # Version header
 | 
  | 234 | 355 |         subject_for_version(version, options) unless options[:only] == :lines
 | 
  | 235 | 356 |         line_for_version(version, options) unless options[:only] == :subjects
 | 
  | 236 |  |         
 | 
  | 237 |  |         options[:top] += options[:top_increment]
 | 
  | 238 | 357 |         @number_of_rows += 1
 | 
  | 239 |  |         return if abort?
 | 
  | 240 |  |         
 | 
  | 241 |  |         # Remove the project requirement for Versions because it will
 | 
  | 242 |  |         # restrict issues to only be on the current project.  This
 | 
  | 243 |  |         # ends up missing issues which are assigned to shared versions.
 | 
  | 244 |  |         @query.project = nil if @query.project
 | 
  | 245 |  |         
 | 
  | 246 |  |         issues = version.fixed_issues.for_gantt.with_query(@query).all(:limit => current_limit)
 | 
  | 247 |  |         if issues
 | 
  | 248 |  |           sort_issues!(issues)
 | 
  | 249 |  |           # Indent issues
 | 
  | 250 |  |           options[:indent] += options[:indent_increment]
 | 
  | 251 |  |           render_issues(issues, options)
 | 
  | 252 |  |           options[:indent] -= options[:indent_increment]
 | 
  | 253 |  |         end
 | 
  | 254 | 358 |       end
 | 
  | 255 |  |       
 | 
  |  | 359 | 
 | 
  |  | 360 |       def render_subproject(subproject, options={})
 | 
  |  | 361 |         # Subproject header
 | 
  |  | 362 |         subject_for_project(subproject, options) unless options[:only] == :lines
 | 
  |  | 363 |         line_for_project(subproject, options) unless options[:only] == :subjects
 | 
  |  | 364 |         @number_of_rows += 1
 | 
  |  | 365 |       end
 | 
  |  | 366 | 
 | 
  | 256 | 367 |       def render_end(options={})
 | 
  | 257 | 368 |         case options[:format]
 | 
  | 258 | 369 |         when :pdf        
 | 
  | ... | ... |  | 
  | 318 | 429 |         if version.is_a?(Version) && version.start_date && version.due_date
 | 
  | 319 | 430 |           options[:zoom] ||= 1
 | 
  | 320 | 431 |           options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
 | 
  | 321 |  |           
 | 
  |  | 432 | 
 | 
  | 322 | 433 |           coords = coordinates(version.start_date, version.due_date, version.completed_pourcent, options[:zoom])
 | 
  | 323 | 434 |           label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
 | 
  | 324 | 435 |           label = h("#{version.project} -") + label unless @project && @project == version.project
 | 
  | ... | ... |  | 
  | 338 | 449 |       end
 | 
  | 339 | 450 | 
 | 
  | 340 | 451 |       def subject_for_issue(issue, options)
 | 
  | 341 |  |         while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
 | 
  | 342 |  |           @issue_ancestors.pop
 | 
  | 343 |  |           options[:indent] -= options[:indent_increment]
 | 
  | 344 |  |         end
 | 
  | 345 |  |           
 | 
  | 346 | 452 |         output = case options[:format]
 | 
  | 347 | 453 |         when :html
 | 
  | 348 | 454 |           css_classes = ''
 | 
  | 349 | 455 |           css_classes << ' issue-overdue' if issue.overdue?
 | 
  | 350 | 456 |           css_classes << ' issue-behind-schedule' if issue.behind_schedule?
 | 
  | 351 | 457 |           css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
 | 
  | 352 |  |           
 | 
  |  | 458 | 
 | 
  | 353 | 459 |           subject = "<span class='#{css_classes}'>"
 | 
  | 354 | 460 |           if issue.assigned_to.present?
 | 
  | 355 | 461 |             assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
 | 
  | ... | ... |  | 
  | 365 | 471 |           pdf_subject(options, issue.subject)
 | 
  | 366 | 472 |         end
 | 
  | 367 | 473 | 
 | 
  | 368 |  |         unless issue.leaf?
 | 
  | 369 |  |           @issue_ancestors << issue
 | 
  | 370 |  |           options[:indent] += options[:indent_increment]
 | 
  | 371 |  |         end
 | 
  | 372 |  |         
 | 
  | 373 | 474 |         output
 | 
  | 374 | 475 |       end
 | 
  | 375 | 476 | 
 | 
  | ... | ... |  | 
  | 405 | 506 |         # width of one day in pixels
 | 
  | 406 | 507 |         zoom = @zoom*2
 | 
  | 407 | 508 |         g_width = (@date_to - @date_from + 1)*zoom
 | 
  | 408 |  |         g_height = 20 * number_of_rows + 30
 | 
  |  | 509 |         g_height = 20 * 5 + 30
 | 
  | 409 | 510 |         headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
 | 
  | 410 | 511 |         height = g_height + headers_heigth
 | 
  | 411 |  |             
 | 
  |  | 512 |         width = subject_width+g_width+1
 | 
  |  | 513 | 
 | 
  | 412 | 514 |         imgl = Magick::ImageList.new
 | 
  | 413 |  |         imgl.new_image(subject_width+g_width+1, height)
 | 
  |  | 515 |         imgl.new_image(width, height)
 | 
  | 414 | 516 |         gc = Magick::Draw.new
 | 
  | 415 |  |         
 | 
  |  | 517 | 
 | 
  | 416 | 518 |         # Subjects
 | 
  | 417 | 519 |         gc.stroke('transparent')
 | 
  | 418 | 520 |         subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
 | 
  | 419 |  |     
 | 
  |  | 521 |         g_height = 20 * number_of_rows + 30
 | 
  |  | 522 |         height = g_height + headers_heigth
 | 
  |  | 523 |         imgl.last.resize!(width, height)
 | 
  |  | 524 | 
 | 
  | 420 | 525 |         # Months headers
 | 
  | 421 | 526 |         month_f = @date_from
 | 
  | 422 | 527 |         left = subject_width
 | 
  | ... | ... |  | 
  | 433 | 538 |           left = left + width
 | 
  | 434 | 539 |           month_f = month_f >> 1
 | 
  | 435 | 540 |         end
 | 
  | 436 |  |         
 | 
  |  | 541 | 
 | 
  | 437 | 542 |         # Weeks headers
 | 
  | 438 | 543 |         if show_weeks
 | 
  | 439 | 544 |         	left = subject_width
 | 
  | ... | ... |  | 
  | 465 | 570 |         		week_f = week_f+7
 | 
  | 466 | 571 |         	end
 | 
  | 467 | 572 |         end
 | 
  | 468 |  |         
 | 
  |  | 573 | 
 | 
  | 469 | 574 |         # Days details (week-end in grey)
 | 
  | 470 | 575 |         if show_days
 | 
  | 471 | 576 |         	left = subject_width
 | 
  | ... | ... |  | 
  | 482 | 587 |               wday = 1 if wday > 7
 | 
  | 483 | 588 |         	end
 | 
  | 484 | 589 |         end
 | 
  | 485 |  |     
 | 
  |  | 590 | 
 | 
  | 486 | 591 |         # border
 | 
  | 487 | 592 |         gc.fill('transparent')
 | 
  | 488 | 593 |         gc.stroke('grey')
 | 
  | ... | ... |  | 
  | 490 | 595 |         gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
 | 
  | 491 | 596 |         gc.stroke('black')
 | 
  | 492 | 597 |         gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
 | 
  | 493 |  |             
 | 
  |  | 598 | 
 | 
  | 494 | 599 |         # content
 | 
  | 495 | 600 |         top = headers_heigth + 20
 | 
  | 496 | 601 | 
 | 
  | ... | ... |  | 
  | 502 | 607 |           gc.stroke('red')
 | 
  | 503 | 608 |           x = (Date.today-@date_from+1)*zoom + subject_width
 | 
  | 504 | 609 |           gc.line(x, headers_heigth, x, headers_heigth + g_height-1)      
 | 
  | 505 |  |         end    
 | 
  | 506 |  |         
 | 
  |  | 610 |         end
 | 
  |  | 611 | 
 | 
  | 507 | 612 |         gc.draw(imgl)
 | 
  | 508 | 613 |         imgl.format = format
 | 
  | 509 | 614 |         imgl.to_blob
 | 
  | ... | ... |  | 
  | 520 | 625 |         pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
 | 
  | 521 | 626 |         pdf.Ln
 | 
  | 522 | 627 |         pdf.SetFontStyle('B',9)
 | 
  | 523 |  |         
 | 
  |  | 628 | 
 | 
  | 524 | 629 |         subject_width = PDF::LeftPaneWidth
 | 
  | 525 | 630 |         header_heigth = 5
 | 
  | 526 |  |         
 | 
  |  | 631 | 
 | 
  | 527 | 632 |         headers_heigth = header_heigth
 | 
  | 528 | 633 |         show_weeks = false
 | 
  | 529 | 634 |         show_days = false
 | 
  | 530 |  |         
 | 
  |  | 635 | 
 | 
  | 531 | 636 |         if self.months < 7
 | 
  | 532 | 637 |           show_weeks = true
 | 
  | 533 | 638 |           headers_heigth = 2*header_heigth
 | 
  | ... | ... |  | 
  | 536 | 641 |             headers_heigth = 3*header_heigth
 | 
  | 537 | 642 |           end
 | 
  | 538 | 643 |         end
 | 
  | 539 |  |         
 | 
  |  | 644 | 
 | 
  | 540 | 645 |         g_width = PDF.right_pane_width
 | 
  | 541 | 646 |         zoom = (g_width) / (self.date_to - self.date_from + 1)
 | 
  | 542 | 647 |         g_height = 120
 | 
  | 543 | 648 |         t_height = g_height + headers_heigth
 | 
  | 544 |  |         
 | 
  |  | 649 | 
 | 
  | 545 | 650 |         y_start = pdf.GetY
 | 
  | 546 |  |         
 | 
  |  | 651 | 
 | 
  | 547 | 652 |         # Months headers
 | 
  | 548 | 653 |         month_f = self.date_from
 | 
  | 549 | 654 |         left = subject_width
 | 
  | ... | ... |  | 
  | 556 | 661 |           left = left + width
 | 
  | 557 | 662 |           month_f = month_f >> 1
 | 
  | 558 | 663 |         end  
 | 
  | 559 |  |         
 | 
  |  | 664 | 
 | 
  | 560 | 665 |         # Weeks headers
 | 
  | 561 | 666 |         if show_weeks
 | 
  | 562 | 667 |           left = subject_width
 | 
  | ... | ... |  | 
  | 582 | 687 |             week_f = week_f+7
 | 
  | 583 | 688 |           end
 | 
  | 584 | 689 |         end
 | 
  | 585 |  |         
 | 
  |  | 690 | 
 | 
  | 586 | 691 |         # Days headers
 | 
  | 587 | 692 |         if show_days
 | 
  | 588 | 693 |           left = subject_width
 | 
  | ... | ... |  | 
  | 599 | 704 |             wday = 1 if wday > 7
 | 
  | 600 | 705 |           end
 | 
  | 601 | 706 |         end
 | 
  | 602 |  |         
 | 
  |  | 707 | 
 | 
  | 603 | 708 |         pdf.SetY(y_start)
 | 
  | 604 | 709 |         pdf.SetX(15)
 | 
  | 605 | 710 |         pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
 | 
  | 606 |  |         
 | 
  |  | 711 | 
 | 
  | 607 | 712 |         # Tasks
 | 
  | 608 | 713 |         top = headers_heigth + y_start
 | 
  | 609 | 714 |         options = {
 | 
  | ... | ... |  | 
  | 620 | 725 |         render(options)
 | 
  | 621 | 726 |         pdf.Output
 | 
  | 622 | 727 |       end
 | 
  | 623 |  |       
 | 
  |  | 728 | 
 | 
  | 624 | 729 |       private
 | 
  | 625 |  |       
 | 
  |  | 730 | 
 | 
  | 626 | 731 |       def coordinates(start_date, end_date, progress, zoom=nil)
 | 
  | 627 | 732 |         zoom ||= @zoom
 | 
  | 628 |  |         
 | 
  |  | 733 | 
 | 
  | 629 | 734 |         coords = {}
 | 
  | 630 | 735 |         if start_date && end_date && start_date < self.date_to && end_date > self.date_from
 | 
  | 631 | 736 |           if start_date > self.date_from
 | 
  | ... | ... |  | 
  | 640 | 745 |           else
 | 
  | 641 | 746 |             coords[:bar_end] = self.date_to - self.date_from + 1
 | 
  | 642 | 747 |           end
 | 
  | 643 |  |         
 | 
  |  | 748 | 
 | 
  | 644 | 749 |           if progress
 | 
  | 645 | 750 |             progress_date = start_date + (end_date - start_date) * (progress / 100.0)
 | 
  | 646 | 751 |             if progress_date > self.date_from && progress_date > start_date
 | 
  | ... | ... |  | 
  | 650 | 755 |                 coords[:bar_progress_end] = self.date_to - self.date_from + 1
 | 
  | 651 | 756 |               end
 | 
  | 652 | 757 |             end
 | 
  | 653 |  |             
 | 
  |  | 758 | 
 | 
  | 654 | 759 |             if progress_date < Date.today
 | 
  | 655 | 760 |               late_date = [Date.today, end_date].min
 | 
  | 656 | 761 |               if late_date > self.date_from && late_date > start_date
 | 
  | ... | ... |  | 
  | 663 | 768 |             end
 | 
  | 664 | 769 |           end
 | 
  | 665 | 770 |         end
 | 
  | 666 |  |         
 | 
  |  | 771 | 
 | 
  | 667 | 772 |         # Transforms dates into pixels witdh
 | 
  | 668 | 773 |         coords.keys.each do |key|
 | 
  | 669 | 774 |           coords[key] = (coords[key] * zoom).floor
 | 
  | ... | ... |  | 
  | 671 | 776 |         coords
 | 
  | 672 | 777 |       end
 | 
  | 673 | 778 | 
 | 
  | 674 |  |       # Sorts a collection of issues by start_date, due_date, id for gantt rendering
 | 
  |  | 779 |       # Sorts a collection of issues for gantt rendering
 | 
  | 675 | 780 |       def sort_issues!(issues)
 | 
  | 676 |  |         issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
 | 
  |  | 781 |         issues.sort! { |a, b| gantt_issue_compare(a, b) }
 | 
  | 677 | 782 |       end
 | 
  | 678 |  |   
 | 
  | 679 |  |       # TODO: top level issues should be sorted by start date
 | 
  | 680 |  |       def gantt_issue_compare(x, y, issues)
 | 
  | 681 |  |         if x.root_id == y.root_id
 | 
  |  | 783 | 
 | 
  |  | 784 |       # Compare issues for rendering order 
 | 
  |  | 785 |       def gantt_issue_compare(x, y)
 | 
  |  | 786 | 
 | 
  |  | 787 |         if x.parent_id == y.parent_id
 | 
  |  | 788 |           # Same level in issue hierarchy
 | 
  |  | 789 |           basic_gantt_issue_compare(x, y)
 | 
  |  | 790 |         elsif x.root_id == y.root_id || x.root_id == y.id || y.root_id == x.id
 | 
  |  | 791 |           # Same issue hierarchy
 | 
  | 682 | 792 |           x.lft <=> y.lft
 | 
  | 683 | 793 |         else
 | 
  |  | 794 |           # Distinct hierarchies
 | 
  | 684 | 795 |           x.root_id <=> y.root_id
 | 
  |  | 796 |           # TODO : advanced sort between issues when ancestors have been filtered out by query
 | 
  | 685 | 797 |         end
 | 
  | 686 | 798 |       end
 | 
  | 687 |  |       
 | 
  |  | 799 | 
 | 
  |  | 800 |       def basic_gantt_issue_compare(x, y)
 | 
  |  | 801 |         if x.start_date == y.start_date
 | 
  |  | 802 |           x.id <=> y.id
 | 
  |  | 803 |         elsif x.start_date.nil?
 | 
  |  | 804 |           1 # null date appears at the end
 | 
  |  | 805 |         elsif y.start_date.nil?
 | 
  |  | 806 |           -1
 | 
  |  | 807 |         else
 | 
  |  | 808 |           x.start_date <=> y.start_date
 | 
  |  | 809 |         end
 | 
  |  | 810 |       end
 | 
  |  | 811 | 
 | 
  | 688 | 812 |       def current_limit
 | 
  | 689 | 813 |         if @max_rows
 | 
  | 690 | 814 |           @max_rows - @number_of_rows
 | 
  | ... | ... |  | 
  | 692 | 816 |           nil
 | 
  | 693 | 817 |         end
 | 
  | 694 | 818 |       end
 | 
  | 695 |  |       
 | 
  |  | 819 | 
 | 
  | 696 | 820 |       def abort?
 | 
  | 697 | 821 |         if @max_rows && @number_of_rows >= @max_rows
 | 
  | 698 | 822 |           @truncated = true
 | 
  | 699 | 823 |         end
 | 
  | 700 | 824 |       end
 | 
  | 701 |  |       
 | 
  |  | 825 | 
 | 
  | 702 | 826 |       def pdf_new_page?(options)
 | 
  | 703 | 827 |         if options[:top] > 180
 | 
  | 704 | 828 |           options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
 | 
  | ... | ... |  | 
  | 707 | 831 |           options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
 | 
  | 708 | 832 |         end
 | 
  | 709 | 833 |       end
 | 
  | 710 |  |       
 | 
  |  | 834 | 
 | 
  | 711 | 835 |       def html_subject(params, subject, options={})
 | 
  | 712 | 836 |         output = "<div class=' #{options[:css] }' style='position: absolute;line-height:1.2em;height:16px;top:#{params[:top]}px;left:#{params[:indent]}px;overflow:hidden;'>"
 | 
  | 713 | 837 |         output << subject
 | 
  | ... | ... |  | 
  | 715 | 839 |         @subjects << output
 | 
  | 716 | 840 |         output
 | 
  | 717 | 841 |       end
 | 
  | 718 |  |       
 | 
  |  | 842 | 
 | 
  | 719 | 843 |       def pdf_subject(params, subject, options={})
 | 
  | 720 | 844 |         params[:pdf].SetY(params[:top])
 | 
  | 721 | 845 |         params[:pdf].SetX(15)
 | 
  | 722 |  |         
 | 
  |  | 846 | 
 | 
  | 723 | 847 |         char_limit = PDF::MaxCharactorsForSubject - params[:indent]
 | 
  | 724 | 848 |         params[:pdf].Cell(params[:subject_width]-15, 5, (" " * params[:indent]) +  subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
 | 
  | 725 |  |       
 | 
  |  | 849 | 
 | 
  | 726 | 850 |         params[:pdf].SetY(params[:top])
 | 
  | 727 | 851 |         params[:pdf].SetX(params[:subject_width])
 | 
  | 728 | 852 |         params[:pdf].Cell(params[:g_width], 5, "", "LR")
 | 
  | 729 | 853 |       end
 | 
  | 730 |  |       
 | 
  |  | 854 | 
 | 
  | 731 | 855 |       def image_subject(params, subject, options={})
 | 
  | 732 | 856 |         params[:image].fill('black')
 | 
  | 733 | 857 |         params[:image].stroke('transparent')
 | 
  | 734 | 858 |         params[:image].stroke_width(1)
 | 
  | 735 | 859 |         params[:image].text(params[:indent], params[:top] + 2, subject)
 | 
  | 736 | 860 |       end
 | 
  | 737 |  |       
 | 
  |  | 861 | 
 | 
  | 738 | 862 |       def html_task(params, coords, options={})
 | 
  | 739 | 863 |         output = ''
 | 
  | 740 | 864 |         # Renders the task bar, with progress and late
 | 
  | ... | ... |  | 
  | 773 | 897 |         @lines << output
 | 
  | 774 | 898 |         output
 | 
  | 775 | 899 |       end
 | 
  | 776 |  |       
 | 
  |  | 900 | 
 | 
  | 777 | 901 |       def pdf_task(params, coords, options={})
 | 
  | 778 | 902 |         height = options[:height] || 2
 | 
  | 779 | 903 |         
 |