Commit bdb3937e authored by Eric Davis's avatar Eric Davis

Rewrite the Gantt chart. #6276

This version of the Gantt chart supports nested charts. So Projects,
Versions, and Issues will be nested underneath their parents correctly.

Additional features:

* Move all Gantt code to Redmine::Helpers::Gantt class instead of having it in
  the Gantt class, controller, and view
* Recursive and nest sub-projects
* Recursive and nest versions
* Recursive and nest issues
* Draw a line showing when a Project is active and it's progress
* Draw a line showing when a Version is active and it's progress
* Show a version's % complete
* Change the color of Projects, Versions, and Issues if they are late or
  behind schedule
* Added Project#start_date and #due_date
* Added Project#completed_percent
* Use a mini-gravatar on the Gantt chart
* Added tests for the Gantt rendering

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@4072 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent 8d52608d
...@@ -4,6 +4,7 @@ class GanttsController < ApplicationController ...@@ -4,6 +4,7 @@ class GanttsController < ApplicationController
rescue_from Query::StatementInvalid, :with => :query_statement_invalid rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :gantt
helper :issues helper :issues
helper :projects helper :projects
helper :queries helper :queries
...@@ -14,32 +15,17 @@ class GanttsController < ApplicationController ...@@ -14,32 +15,17 @@ class GanttsController < ApplicationController
def show def show
@gantt = Redmine::Helpers::Gantt.new(params) @gantt = Redmine::Helpers::Gantt.new(params)
@gantt.project = @project
retrieve_query retrieve_query
@query.group_by = nil @query.group_by = nil
if @query.valid? @gantt.query = @query if @query.valid?
events = []
# Issues that have start and due dates
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
:order => "start_date, due_date",
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Issues that don't have a due date but that are assigned to a version with a date
events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
:order => "start_date, effective_date",
:conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Versions
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
@gantt.events = events
end
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt' basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format| respond_to do |format|
format.html { render :action => "show", :layout => !request.xhr? } format.html { render :action => "show", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image(@project), :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image') format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") } format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
end end
end end
......
...@@ -47,6 +47,7 @@ class IssuesController < ApplicationController ...@@ -47,6 +47,7 @@ class IssuesController < ApplicationController
include SortHelper include SortHelper
include IssuesHelper include IssuesHelper
helper :timelog helper :timelog
helper :gantt
include Redmine::Export::PDF include Redmine::Export::PDF
verify :method => [:post, :delete], verify :method => [:post, :delete],
......
...@@ -121,6 +121,11 @@ module ApplicationHelper ...@@ -121,6 +121,11 @@ module ApplicationHelper
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision)) link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
end end
def link_to_project(project, options={})
options[:class] ||= 'project'
link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => options[:class])
end
# Generates a link to a project if active # Generates a link to a project if active
# Examples: # Examples:
...@@ -832,6 +837,8 @@ module ApplicationHelper ...@@ -832,6 +837,8 @@ module ApplicationHelper
email = $1 email = $1
end end
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
else
''
end end
end end
......
# redMine - project management software
# Copyright (C) 2006 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 GanttHelper
def number_of_issues_on_versions(gantt)
versions = gantt.events.collect {|event| (event.is_a? Version) ? event : nil}.compact
versions.sum {|v| v.fixed_issues.for_gantt.with_query(@query).count}
end
end
...@@ -35,8 +35,10 @@ module IssuesHelper ...@@ -35,8 +35,10 @@ module IssuesHelper
@cached_label_due_date ||= l(:field_due_date) @cached_label_due_date ||= l(:field_due_date)
@cached_label_assigned_to ||= l(:field_assigned_to) @cached_label_assigned_to ||= l(:field_assigned_to)
@cached_label_priority ||= l(:field_priority) @cached_label_priority ||= l(:field_priority)
@cached_label_project ||= l(:field_project)
link_to_issue(issue) + "<br /><br />" + link_to_issue(issue) + "<br /><br />" +
"<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />" +
"<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" + "<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" +
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" + "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" + "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
......
...@@ -62,10 +62,28 @@ class Issue < ActiveRecord::Base ...@@ -62,10 +62,28 @@ class Issue < ActiveRecord::Base
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC" named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
named_scope :with_limit, lambda { |limit| { :limit => limit} } named_scope :with_limit, lambda { |limit| { :limit => limit} }
named_scope :on_active_project, :include => [:status, :project, :tracker], named_scope :on_active_project, :include => [:status, :project, :tracker],
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
named_scope :for_gantt, lambda {
{
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
:order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
}
}
named_scope :without_version, lambda {
{
:conditions => { :fixed_version_id => nil}
}
}
named_scope :with_query, lambda {|query|
{
:conditions => Query.merge_conditions(query.statement)
}
}
before_create :default_assign before_create :default_assign
before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
...@@ -357,6 +375,13 @@ class Issue < ActiveRecord::Base ...@@ -357,6 +375,13 @@ class Issue < ActiveRecord::Base
def overdue? def overdue?
!due_date.nil? && (due_date < Date.today) && !status.is_closed? !due_date.nil? && (due_date < Date.today) && !status.is_closed?
end end
# Is the amount of work done less than it should for the due date
def behind_schedule?
return false if start_date.nil? || due_date.nil?
done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
return done_date <= Date.today
end
# Users the issue can be assigned to # Users the issue can be assigned to
def assignable_users def assignable_users
......
...@@ -412,6 +412,50 @@ class Project < ActiveRecord::Base ...@@ -412,6 +412,50 @@ class Project < ActiveRecord::Base
def short_description(length = 255) def short_description(length = 255)
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
end end
# The earliest start date of a project, based on it's issues and versions
def start_date
if module_enabled?(:issue_tracking)
[
issues.minimum('start_date'),
shared_versions.collect(&:effective_date),
shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
].flatten.compact.min
end
end
# The latest due date of an issue or version
def due_date
if module_enabled?(:issue_tracking)
[
issues.maximum('due_date'),
shared_versions.collect(&:effective_date),
shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
].flatten.compact.max
end
end
def overdue?
active? && !due_date.nil? && (due_date < Date.today)
end
# Returns the percent completed for this project, based on the
# progress on it's versions.
def completed_percent(options={:include_subprojects => false})
if options.delete(:include_subprojects)
total = self_and_descendants.collect(&:completed_percent).sum
total / self_and_descendants.count
else
if versions.count > 0
total = versions.collect(&:completed_pourcent).sum
total / versions.count
else
100
end
end
end
# Return true if this project is allowed to do the specified action. # Return true if this project is allowed to do the specified action.
# action can be: # action can be:
......
...@@ -73,6 +73,18 @@ class Version < ActiveRecord::Base ...@@ -73,6 +73,18 @@ class Version < ActiveRecord::Base
def completed? def completed?
effective_date && (effective_date <= Date.today) && (open_issues_count == 0) effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
end end
def behind_schedule?
if completed_pourcent == 100
return false
elsif due_date && fixed_issues.present? && fixed_issues.minimum('start_date') # TODO: should use #start_date but that method is wrong...
start_date = fixed_issues.minimum('start_date')
done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
return done_date <= Date.today
else
false # No issues so it's not late
end
end
# Returns the completion percentage of this version based on the amount of open/closed issues # Returns the completion percentage of this version based on the amount of open/closed issues
# and the time spent on the open issues. # and the time spent on the open issues.
......
<% @gantt.view = self %>
<h2><%= l(:label_gantt) %></h2> <h2><%= l(:label_gantt) %></h2>
<% form_tag(gantt_path(:month => params[:month], :year => params[:year], :months => params[:months]), :method => :put, :id => 'query_form') do %> <% form_tag(gantt_path(:month => params[:month], :year => params[:year], :months => params[:months]), :method => :put, :id => 'query_form') do %>
...@@ -55,11 +56,12 @@ if @gantt.zoom >1 ...@@ -55,11 +56,12 @@ if @gantt.zoom >1
end end
end end
# Width of the entire chart
g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom
g_height = [(20 * @gantt.events.length + 6)+150, 206].max # Collect the number of issues on Versions
g_height = [(20 * (@gantt.number_of_rows + 6))+150, 206].max
t_height = g_height + headers_height t_height = g_height + headers_height
%> %>
<table width="100%" style="border:0; border-collapse: collapse;"> <table width="100%" style="border:0; border-collapse: collapse;">
<tr> <tr>
<td style="width:<%= subject_width %>px; padding:0px;"> <td style="width:<%= subject_width %>px; padding:0px;">
...@@ -67,26 +69,10 @@ t_height = g_height + headers_height ...@@ -67,26 +69,10 @@ t_height = g_height + headers_height
<div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;"> <div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
<div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div> <div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
<div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div> <div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
<% <% top = headers_height + 8 %>
#
# Tasks subjects <%= @gantt.subjects(:headers_height => headers_height, :top => top, :g_width => g_width) %>
#
top = headers_height + 8
@gantt.events.each do |i|
left = 4 + (i.is_a?(Issue) ? i.level * 16 : 0)
%>
<div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:<%= left %>px;overflow:hidden;"><small>
<% if i.is_a? Issue %>
<%= h("#{i.project} -") unless @project && @project == i.project %>
<%= link_to_issue i %>
<% else %>
<span class="icon icon-package">
<%= link_to_version i %>
</span>
<% end %>
</small></div>
<% top = top + 20
end %>
</div> </div>
</td> </td>
<td> <td>
...@@ -164,53 +150,9 @@ if show_days ...@@ -164,53 +150,9 @@ if show_days
end end
end %> end %>
<% <% top = headers_height + 10 %>
#
# Tasks <%= @gantt.lines(:top => top, :zoom => zoom, :g_width => g_width ) %>
#
top = headers_height + 10
@gantt.events.each do |i|
if i.is_a? Issue
i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from )
i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to )
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date )
i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - @gantt.date_from)*zoom).floor
i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
css = "task " + (i.leaf? ? 'leaf' : 'parent')
%>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="<%= css %> task_todo"><div class="left"></div>&nbsp;<div class="right"></div></div>
<% if l_width > 0 %>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="<%= css %> task_late">&nbsp;</div>
<% end %>
<% if d_width > 0 %>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="<%= css %> task_done">&nbsp;</div>
<% end %>
<div style="top:<%= top %>px;left:<%= i_left + i_width + 8 %>px;background:#fff;" class="<%= css %>">
<%= i.status.name %>
<%= (i.done_ratio).to_i %>%
</div>
<div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;">
<span class="tip">
<%= render_issue_tooltip i %>
</span></div>
<% else
i_left = ((i.start_date - @gantt.date_from)*zoom).floor
%>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone">&nbsp;</div>
<div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task">
<strong><%= format_version_name i %></strong>
</div>
<% end %>
<% top = top + 20
end %>
<% <%
# #
......
...@@ -184,7 +184,7 @@ module Redmine ...@@ -184,7 +184,7 @@ module Redmine
end end
pdf.Output pdf.Output
end end
# Returns a PDF string of a single issue # Returns a PDF string of a single issue
def issue_to_pdf(issue) def issue_to_pdf(issue)
pdf = IFPDF.new(current_language) pdf = IFPDF.new(current_language)
...@@ -208,7 +208,7 @@ module Redmine ...@@ -208,7 +208,7 @@ module Redmine
pdf.SetFontStyle('',9) pdf.SetFontStyle('',9)
pdf.Cell(60,5, issue.priority.to_s,"RT") pdf.Cell(60,5, issue.priority.to_s,"RT")
pdf.Ln pdf.Ln
pdf.SetFontStyle('B',9) pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_author) + ":","L") pdf.Cell(35,5, l(:field_author) + ":","L")
pdf.SetFontStyle('',9) pdf.SetFontStyle('',9)
...@@ -238,14 +238,14 @@ module Redmine ...@@ -238,14 +238,14 @@ module Redmine
pdf.SetFontStyle('',9) pdf.SetFontStyle('',9)
pdf.Cell(60,5, format_date(issue.due_date),"RB") pdf.Cell(60,5, format_date(issue.due_date),"RB")
pdf.Ln pdf.Ln
for custom_value in issue.custom_field_values for custom_value in issue.custom_field_values
pdf.SetFontStyle('B',9) pdf.SetFontStyle('B',9)
pdf.Cell(35,5, custom_value.custom_field.name + ":","L") pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
pdf.SetFontStyle('',9) pdf.SetFontStyle('',9)
pdf.MultiCell(155,5, (show_value custom_value),"R") pdf.MultiCell(155,5, (show_value custom_value),"R")
end end
pdf.SetFontStyle('B',9) pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_subject) + ":","LTB") pdf.Cell(35,5, l(:field_subject) + ":","LTB")
pdf.SetFontStyle('',9) pdf.SetFontStyle('',9)
...@@ -311,187 +311,7 @@ module Redmine ...@@ -311,187 +311,7 @@ module Redmine
end end
pdf.Output pdf.Output
end end
# Returns a PDF string of a gantt chart
def gantt_to_pdf(gantt, project)
pdf = IFPDF.new(current_language)
pdf.SetTitle("#{l(:label_gantt)} #{project}")
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage("L")
pdf.SetFontStyle('B',12)
pdf.SetX(15)
pdf.Cell(70, 20, project.to_s)
pdf.Ln
pdf.SetFontStyle('B',9)
subject_width = 100
header_heigth = 5
headers_heigth = header_heigth
show_weeks = false
show_days = false
if gantt.months < 7
show_weeks = true
headers_heigth = 2*header_heigth
if gantt.months < 3
show_days = true
headers_heigth = 3*header_heigth
end
end
g_width = 280 - subject_width
zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
g_height = 120
t_height = g_height + headers_heigth
y_start = pdf.GetY
# Months headers
month_f = gantt.date_from
left = subject_width
height = header_heigth
gantt.months.times do
width = ((month_f >> 1) - month_f) * zoom
pdf.SetY(y_start)
pdf.SetX(left)
pdf.Cell(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_heigth
if gantt.date_from.cwday == 1
# gantt.date_from is monday
week_f = gantt.date_from
else
# find next monday after gantt.date_from
week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
width = (7 - gantt.date_from.cwday + 1) * zoom-1
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width + 1, height, "", "LTR")
left = left + width+1
end
while week_f <= gantt.date_to
width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
left = left + width
week_f = week_f+7
end
end
# Days headers
if show_days
left = subject_width
height = header_heigth
wday = gantt.date_from.cwday
pdf.SetFontStyle('B',7)
(gantt.date_to - gantt.date_from + 1).to_i.times do
width = zoom
pdf.SetY(y_start + 2 * header_heigth)
pdf.SetX(left)
pdf.Cell(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.Cell(subject_width+g_width-15, headers_heigth, "", 1)
# Tasks
top = headers_heigth + y_start
pdf.SetFontStyle('B',7)
gantt.events.each do |i|
pdf.SetY(top)
pdf.SetX(15)
text = ""
if i.is_a? Issue
text = "#{i.tracker} #{i.id}: #{i.subject}"
else
text = i.name
end
text = "#{i.project} - #{text}" unless project && project == i.project
pdf.Cell(subject_width-15, 5, text, "LR")
pdf.SetY(top + 0.2)
pdf.SetX(subject_width)
pdf.SetFillColor(255, 255, 255)
pdf.Cell(g_width, 4.6, "", "LR", 0, "", 1)
pdf.SetY(top+1.5)
if i.is_a? Issue
i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - gantt.date_from)*zoom)
i_width = ((i_end_date - i_start_date + 1)*zoom)
d_width = ((i_done_date - i_start_date)*zoom)
l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
l_width ||= 0
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(200,200,200)
pdf.Cell(i_width, 2, "", 0, 0, "", 1)
if l_width > 0
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(255,100,100)
pdf.Cell(l_width, 2, "", 0, 0, "", 1)
end
if d_width > 0
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(100,100,255)
pdf.Cell(d_width, 2, "", 0, 0, "", 1)
end
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left + i_width)
pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
else
i_left = ((i.start_date - gantt.date_from)*zoom)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(50,200,50)
pdf.Cell(2, 2, "", 0, 0, "", 1)
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left + 3)
pdf.Cell(30, 2, "#{i.name}")
end
top = top + 5
pdf.SetDrawColor(200, 200, 200)
pdf.Line(15, top, subject_width+g_width, top)
if pdf.GetY() > 180
pdf.AddPage("L")
top = 20
pdf.Line(15, top, subject_width+g_width, top)
end
pdf.SetDrawColor(0, 0, 0)
end
pdf.Line(15, top, subject_width+g_width, top)
pdf.Output
end
end end
end end
end end
...@@ -19,11 +19,28 @@ module Redmine ...@@ -19,11 +19,28 @@ module Redmine
module Helpers module Helpers
# Simple class to handle gantt chart data # Simple class to handle gantt chart data
class Gantt class Gantt
attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :events include ERB::Util
include Redmine::I18n
# :nodoc:
# Some utility methods for the PDF export
class PDF
MaxCharactorsForSubject = 45
TotalWidth = 280
LeftPaneWidth = 100
def self.right_pane_width
TotalWidth - LeftPaneWidth
end
end
attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months
attr_accessor :query
attr_accessor :project
attr_accessor :view
def initialize(options={}) def initialize(options={})
options = options.dup options = options.dup
@events = []
if options[:year] && options[:year].to_i >0 if options[:year] && options[:year].to_i >0
@year_from = options[:year].to_i @year_from = options[:year].to_i
...@@ -52,31 +69,6 @@ module Redmine ...@@ -52,31 +69,6 @@ module Redmine
@date_to = (@date_from >> @months) - 1 @date_to = (@date_from >> @months) - 1
end end
def events=(e)
@events = e
# Adds all ancestors
root_ids = e.select {|i| i.is_a?(Issue) && i.parent_id? }.collect(&:root_id).uniq
if root_ids.any?
# Retrieves all nodes
parents = Issue.find_all_by_root_id(root_ids, :conditions => ["rgt - lft > 1"])
# Only add ancestors
@events += parents.select {|p| @events.detect {|i| i.is_a?(Issue) && p.is_ancestor_of?(i)}}
end
@events.uniq!
# Sort issues by hierarchy and start dates
@events.sort! {|x,y|
if x.is_a?(Issue) && y.is_a?(Issue)
gantt_issue_compare(x, y, @events)
else
gantt_start_compare(x, y)
end
}
# Removes issues that have no start or end date
@events.reject! {|i| i.is_a?(Issue) && (i.start_date.nil? || i.due_before.nil?) }
@events
end
def params def params
{ :zoom => zoom, :year => year_from, :month => month_from, :months => months } { :zoom => zoom, :year => year_from, :month => month_from, :months => months }
end end
...@@ -88,10 +80,652 @@ module Redmine ...@@ -88,10 +80,652 @@ module Redmine
def params_next def params_next
{ :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months } { :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months }
end end
### Extracted from the HTML view/helpers
# Returns the number of rows that will be rendered on the Gantt chart
def number_of_rows
if @project
return number_of_rows_on_project(@project)
else
Project.roots.inject(0) do |total, project|
total += number_of_rows_on_project(project)
end
end
end
# Returns the number of rows that will be used to list a project on
# the Gantt chart. This will recurse for each subproject.
def number_of_rows_on_project(project)
# Remove the project requirement for Versions because it will
# restrict issues to only be on the current project. This
# ends up missing issues which are assigned to shared versions.
@query.project = nil if @query.project
# One Root project
count = 1
# Issues without a Version
count += project.issues.for_gantt.without_version.with_query(@query).count
# Versions
count += project.versions.count
# Issues on the Versions
project.versions.each do |version|
count += version.fixed_issues.for_gantt.with_query(@query).count
end
# Subprojects
project.children.each do |subproject|
count += number_of_rows_on_project(subproject)
end
count
end
# Renders the subjects of the Gantt chart, the left side.
def subjects(options={})
options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
output = ''
if @project
output << render_project(@project, options)
else
Project.roots.each do |project|
output << render_project(project, options)
end
end
output
end
# Renders the lines of the Gantt chart, the right side
def lines(options={})
options = {:indent => 4, :render => :line, :format => :html}.merge(options)
output = ''
if @project
output << render_project(@project, options)
else
Project.roots.each do |project|
output << render_project(project, options)
end
end
output
end
def render_project(project, options={})
options[:top] = 0 unless options.key? :top
options[:indent_increment] = 20 unless options.key? :indent_increment
options[:top_increment] = 20 unless options.key? :top_increment
output = ''
# Project Header
project_header = if options[:render] == :subject
subject_for_project(project, options)
else
# :line
line_for_project(project, options)
end
output << project_header if options[:format] == :html
options[:top] += options[:top_increment]
options[:indent] += options[:indent_increment]
# Second, Issues without a version
issues = project.issues.for_gantt.without_version.with_query(@query)
if issues
issue_rendering = render_issues(issues, options)
output << issue_rendering if options[:format] == :html
end
# Third, Versions
project.versions.sort.each do |version|
version_rendering = render_version(version, options)
output << version_rendering if options[:format] == :html
end
# Fourth, subprojects
project.children.each do |project|
subproject_rendering = render_project(project, options)
output << subproject_rendering if options[:format] == :html
end
# Remove indent to hit the next sibling
options[:indent] -= options[:indent_increment]
output
end
def render_issues(issues, options={})
output = ''
issues.each do |i|
issue_rendering = if options[:render] == :subject
subject_for_issue(i, options)
else
# :line
line_for_issue(i, options)
end
output << issue_rendering if options[:format] == :html
options[:top] += options[:top_increment]
end
output
end
def render_version(version, options={})
output = ''
# Version header
version_rendering = if options[:render] == :subject
subject_for_version(version, options)
else
# :line
line_for_version(version, options)
end
output << version_rendering if options[:format] == :html
options[:top] += options[:top_increment]
# Remove the project requirement for Versions because it will
# restrict issues to only be on the current project. This
# ends up missing issues which are assigned to shared versions.
@query.project = nil if @query.project
issues = version.fixed_issues.for_gantt.with_query(@query)
if issues
# Indent issues
options[:indent] += options[:indent_increment]
output << render_issues(issues, options)
options[:indent] -= options[:indent_increment]
end
output
end
def subject_for_project(project, options)
case options[:format]
when :html
output = ''
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> "
if project.is_a? Project
output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
output << view.link_to_project(project)
output << '</span>'
else
ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project"
''
end
output << "</small></div>"
output
when :image
options[:image].fill('black')
options[:image].stroke('transparent')
options[:image].stroke_width(1)
options[:image].text(options[:indent], options[:top] + 2, project.name)
when :pdf
options[:pdf].SetY(options[:top])
options[:pdf].SetX(15)
char_limit = PDF::MaxCharactorsForSubject - options[:indent]
options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
options[:pdf].SetY(options[:top])
options[:pdf].SetX(options[:subject_width])
options[:pdf].Cell(options[:g_width], 5, "", "LR")
end
end
def line_for_project(project, options)
# Skip versions that don't have a start_date
if project.is_a?(Project) && project.start_date
options[:zoom] ||= 1
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
case options[:format]
when :html
output = ''
i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
start_date = project.start_date
start_date ||= self.date_from
start_left = ((start_date - self.date_from)*options[:zoom]).floor
i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if start_date < Date.today
i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
# Bar graphic
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_end > 0 && i_left <= options[:g_width]
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
end
if l_width > 0 && i_left <= options[:g_width]
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
end
if d_width > 0 && i_left <= options[:g_width]
output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
end
# Starting diamond
if start_left <= options[:g_width] && start_left > 0
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
output << "</div>"
end
# Ending diamond
# Don't show items too far ahead
if i_end <= options[:g_width] && i_end > 0
output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
end
# DIsplay the Project name and %
if i_end <= options[:g_width]
# Display the status even if it's floated off to the left
status_px = i_end + 12 # 12px for the diamond
status_px = 0 if status_px <= 0
output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
output << "</div>"
end
output
when :image
options[:image].stroke('transparent')
i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
# Make sure negative i_left doesn't overflow the subject
if i_left > options[:subject_width]
options[:image].fill('blue')
options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
options[:image].fill('black')
options[:image].text(i_left + 11, options[:top] + 1, project.name)
end
when :pdf
options[:pdf].SetY(options[:top]+1.5)
i_left = ((project.due_date - @date_from)*options[:zoom])
# Make sure negative i_left doesn't overflow the subject
if i_left > 0
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(50,50,200)
options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
options[:pdf].SetY(options[:top]+1.5)
options[:pdf].SetX(options[:subject_width] + i_left + 3)
options[:pdf].Cell(30, 2, "#{project.name}")
end
end
else
ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
''
end
end
def subject_for_version(version, options)
case options[:format]
when :html
output = ''
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> "
if version.is_a? Version
output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
output << view.link_to_version(version)
output << '</span>'
else
ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
''
end
output << "</small></div>"
output
when :image
options[:image].fill('black')
options[:image].stroke('transparent')
options[:image].stroke_width(1)
options[:image].text(options[:indent], options[:top] + 2, version.name)
when :pdf
options[:pdf].SetY(options[:top])
options[:pdf].SetX(15)
char_limit = PDF::MaxCharactorsForSubject - options[:indent]
options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
options[:pdf].SetY(options[:top])
options[:pdf].SetX(options[:subject_width])
options[:pdf].Cell(options[:g_width], 5, "", "LR")
end
end
def line_for_version(version, options)
# Skip versions that don't have a start_date
if version.is_a?(Version) && version.start_date
options[:zoom] ||= 1
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
case options[:format]
when :html
output = ''
i_left = ((version.start_date - self.date_from)*options[:zoom]).floor
# TODO: or version.fixed_issues.collect(&:start_date).min
start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present?
start_date ||= self.date_from
start_left = ((start_date - self.date_from)*options[:zoom]).floor
i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to )
i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor
i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if start_date < Date.today
i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel
# Bar graphic
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_width > 0 && i_left <= options[:g_width]
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'>&nbsp;</div>"
end
if l_width > 0 && i_left <= options[:g_width]
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'>&nbsp;</div>"
end
if d_width > 0 && i_left <= options[:g_width]
output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'>&nbsp;</div>"
end
# Starting diamond
if start_left <= options[:g_width] && start_left > 0
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'>&nbsp;</div>"
output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>"
output << "</div>"
end
# Ending diamond
# Don't show items too far ahead
if i_left <= options[:g_width] && i_end > 0
output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'>&nbsp;</div>"
end
# Display the Version name and %
if i_end <= options[:g_width]
# Display the status even if it's floated off to the left
status_px = i_end + 12 # 12px for the diamond
status_px = 0 if status_px <= 0
output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>"
output << h("#{version.project} -") unless @project && @project == version.project
output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>"
output << "</div>"
end
output
when :image
options[:image].stroke('transparent')
i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
# Make sure negative i_left doesn't overflow the subject
if i_left > options[:subject_width]
options[:image].fill('green')
options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
options[:image].fill('black')
options[:image].text(i_left + 11, options[:top] + 1, version.name)
end
when :pdf
options[:pdf].SetY(options[:top]+1.5)
i_left = ((version.start_date - @date_from)*options[:zoom])
# Make sure negative i_left doesn't overflow the subject
if i_left > 0
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(50,200,50)
options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
options[:pdf].SetY(options[:top]+1.5)
options[:pdf].SetX(options[:subject_width] + i_left + 3)
options[:pdf].Cell(30, 2, "#{version.name}")
end
end
else
ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
''
end
end
def subject_for_issue(issue, options)
case options[:format]
when :html
output = ''
output << "<div class='tooltip'>"
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> "
if issue.is_a? Issue
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
if issue.assigned_to.present?
assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
end
output << "<span class='#{css_classes.join(' ')}'>"
output << view.link_to_issue(issue)
output << ":"
output << h(issue.subject)
output << '</span>'
else
ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue"
''
end
output << "</small></div>"
# Tooltip
if issue.is_a? Issue
output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>"
output << view.render_issue_tooltip(issue)
output << "</span>"
end
output << "</div>"
output
when :image
options[:image].fill('black')
options[:image].stroke('transparent')
options[:image].stroke_width(1)
options[:image].text(options[:indent], options[:top] + 2, issue.subject)
when :pdf
options[:pdf].SetY(options[:top])
options[:pdf].SetX(15)
char_limit = PDF::MaxCharactorsForSubject - options[:indent]
options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
options[:pdf].SetY(options[:top])
options[:pdf].SetX(options[:subject_width])
options[:pdf].Cell(options[:g_width], 5, "", "LR")
end
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
case options[:format]
when :html
output = ''
# Handle nil start_dates, rare but can happen.
i_start_date = if issue.start_date && issue.start_date >= self.date_from
issue.start_date
else
self.date_from
end
i_end_date = ((issue.due_before && issue.due_before <= self.date_to) ? issue.due_before : self.date_to )
i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - self.date_from)*options[:zoom]).floor
i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - i_start_date)*options[:zoom]).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
css = "task " + (issue.leaf? ? 'leaf' : 'parent')
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_width > 0
output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;' class='#{css} task_todo'>&nbsp;</div>"
end
if l_width > 0
output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ l_width }px;' class='#{css} task_late'>&nbsp;</div>"
end
if d_width > 0
output<< "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ d_width }px;' class='#{css} task_done'>&nbsp;</div>"
end
# Display the status even if it's floated off to the left
status_px = i_left + i_width + 5
status_px = 5 if status_px <= 0
output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='#{css} label issue-name'>"
output << issue.status.name
output << ' '
output << (issue.done_ratio).to_i.to_s
output << "%"
output << "</div>"
output << "<div class='tooltip' style='position: absolute;top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;height:12px;'>"
output << '<span class="tip">'
output << view.render_issue_tooltip(issue)
output << "</span></div>"
output
when :image
# Handle nil start_dates, rare but can happen.
i_start_date = if issue.start_date && issue.start_date >= @date_from
issue.start_date
else
@date_from
end
i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to )
i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor
i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue
d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_width > 0
options[:image].fill('grey')
options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6)
options[:image].fill('red')
options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0
options[:image].fill('blue')
options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0
end
# Show the status and % done next to the subject if it overflows
options[:image].fill('black')
if i_width > 0
options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
else
options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
end
when :pdf
options[:pdf].SetY(options[:top]+1.5)
# Handle nil start_dates, rare but can happen.
i_start_date = if issue.start_date && issue.start_date >= @date_from
issue.start_date
else
@date_from
end
i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to )
i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - @date_from)*options[:zoom])
i_width = ((i_end_date - i_start_date + 1)*options[:zoom])
d_width = ((i_done_date - i_start_date)*options[:zoom])
l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date
l_width ||= 0
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_width > 0
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(200,200,200)
options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1)
end
if l_width > 0
options[:pdf].SetY(options[:top]+1.5)
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(255,100,100)
options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1)
end
if d_width > 0
options[:pdf].SetY(options[:top]+1.5)
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(100,100,255)
options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1)
end
options[:pdf].SetY(options[:top]+1.5)
# Make sure that negative i_left and i_width don't
# overflow the subject
if (i_left + i_width) >= 0
options[:pdf].SetX(options[:subject_width] + i_left + i_width)
else
options[:pdf].SetX(options[:subject_width])
end
options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%")
end
else
ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
''
end
end
# Generates a gantt image # Generates a gantt image
# Only defined if RMagick is avalaible # Only defined if RMagick is avalaible
def to_image(project, format='PNG') def to_image(format='PNG')
date_to = (@date_from >> @months)-1 date_to = (@date_from >> @months)-1
show_weeks = @zoom > 1 show_weeks = @zoom > 1
show_days = @zoom > 2 show_days = @zoom > 2
...@@ -101,7 +735,7 @@ module Redmine ...@@ -101,7 +735,7 @@ module Redmine
# width of one day in pixels # width of one day in pixels
zoom = @zoom*2 zoom = @zoom*2
g_width = (@date_to - @date_from + 1)*zoom g_width = (@date_to - @date_from + 1)*zoom
g_height = 20 * events.length + 20 g_height = 20 * number_of_rows + 30
headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
height = g_height + headers_heigth height = g_height + headers_heigth
...@@ -110,21 +744,7 @@ module Redmine ...@@ -110,21 +744,7 @@ module Redmine
gc = Magick::Draw.new gc = Magick::Draw.new
# Subjects # Subjects
top = headers_heigth + 20 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
events.each do |i|
text = ""
if i.is_a? Issue
text = "#{i.tracker} #{i.id}: #{i.subject}"
else
text = i.name
end
text = "#{i.project} - #{text}" unless project && project == i.project
gc.text(4, top + 2, text)
top = top + 20
end
# Months headers # Months headers
month_f = @date_from month_f = @date_from
...@@ -202,38 +822,8 @@ module Redmine ...@@ -202,38 +822,8 @@ module Redmine
# content # content
top = headers_heigth + 20 top = headers_heigth + 20
gc.stroke('transparent')
events.each do |i| lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
if i.is_a?(Issue)
i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from )
i_end_date = (i.due_before <= date_to ? i.due_before : date_to )
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = subject_width + ((i_start_date - @date_from)*zoom).floor
i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
d_width = ((i_done_date - i_start_date)*zoom).floor # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
gc.fill('grey')
gc.rectangle(i_left, top, i_left + i_width, top - 6)
gc.fill('red')
gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
gc.fill('blue')
gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
gc.fill('black')
gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
else
i_left = subject_width + ((i.start_date - @date_from)*zoom).floor
gc.fill('green')
gc.rectangle(i_left, top, i_left + 6, top - 6)
gc.fill('black')
gc.text(i_left + 11, top + 1, i.name)
end
top = top + 20
end
# today red line # today red line
if Date.today >= @date_from and Date.today <= date_to if Date.today >= @date_from and Date.today <= date_to
...@@ -246,36 +836,137 @@ module Redmine ...@@ -246,36 +836,137 @@ module Redmine
imgl.format = format imgl.format = format
imgl.to_blob imgl.to_blob
end if Object.const_defined?(:Magick) end if Object.const_defined?(:Magick)
private def to_pdf
pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
def gantt_issue_compare(x, y, issues) pdf.SetTitle("#{l(:label_gantt)} #{project}")
if x.parent_id == y.parent_id pdf.AliasNbPages
gantt_start_compare(x, y) pdf.footer_date = format_date(Date.today)
elsif x.is_ancestor_of?(y) pdf.AddPage("L")
-1 pdf.SetFontStyle('B',12)
elsif y.is_ancestor_of?(x) pdf.SetX(15)
1 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
else pdf.Ln
ax = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(x) && !i.is_ancestor_of?(y) }.sort_by(&:lft).first pdf.SetFontStyle('B',9)
ay = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(y) && !i.is_ancestor_of?(x) }.sort_by(&:lft).first
if ax.nil? && ay.nil? subject_width = PDF::LeftPaneWidth
gantt_start_compare(x, y) header_heigth = 5
headers_heigth = header_heigth
show_weeks = false
show_days = false
if self.months < 7
show_weeks = true
headers_heigth = 2*header_heigth
if self.months < 3
show_days = true
headers_heigth = 3*header_heigth
end
end
g_width = PDF.right_pane_width
zoom = (g_width) / (self.date_to - self.date_from + 1)
g_height = 120
t_height = g_height + headers_heigth
y_start = pdf.GetY
# Months headers
month_f = self.date_from
left = subject_width
height = header_heigth
self.months.times do
width = ((month_f >> 1) - month_f) * zoom
pdf.SetY(y_start)
pdf.SetX(left)
pdf.Cell(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_heigth
if self.date_from.cwday == 1
# self.date_from is monday
week_f = self.date_from
else else
gantt_issue_compare(ax || x, ay || y, issues) # 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_heigth)
pdf.SetX(left)
pdf.Cell(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_heigth)
pdf.SetX(left)
pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
left = left + width
week_f = week_f+7
end
end
# Days headers
if show_days
left = subject_width
height = header_heigth
wday = self.date_from.cwday
pdf.SetFontStyle('B',7)
(self.date_to - self.date_from + 1).to_i.times do
width = zoom
pdf.SetY(y_start + 2 * header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
left = left + width
wday = wday + 1
wday = 1 if wday > 7
end end
end end
pdf.SetY(y_start)
pdf.SetX(15)
pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
# Tasks
top = headers_heigth + y_start
pdf_subjects_and_lines(pdf, {
:top => top,
:zoom => zoom,
:subject_width => subject_width,
:g_width => g_width
})
pdf.Line(15, top, subject_width+g_width, top)
pdf.Output
end end
def gantt_start_compare(x, y) private
if x.start_date.nil?
-1 # Renders both the subjects and lines of the Gantt chart for the
elsif y.start_date.nil? # PDF format
1 def pdf_subjects_and_lines(pdf, options = {})
subject_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :subject, :format => :pdf, :pdf => pdf}.merge(options)
line_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :line, :format => :pdf, :pdf => pdf}.merge(options)
if @project
render_project(@project, subject_options)
render_project(@project, line_options)
else else
x.start_date <=> y.start_date Project.roots.each do |project|
render_project(project, subject_options)
render_project(project, line_options)
end
end end
end end
end end
end end
end end
public/images/task_done.png

855 Bytes | W: | H:

public/images/task_done.png

137 Bytes | W: | H:

public/images/task_done.png
public/images/task_done.png
public/images/task_done.png
public/images/task_done.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -787,8 +787,10 @@ background-image:url('../images/close_hl.png'); ...@@ -787,8 +787,10 @@ background-image:url('../images/close_hl.png');
white-space:nowrap; white-space:nowrap;
} }
.task.label {width:100%;}
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
.task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; } .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
.task_todo.parent { background: #888; border: 1px solid #888; height: 6px;} .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;}
...@@ -796,7 +798,17 @@ background-image:url('../images/close_hl.png'); ...@@ -796,7 +798,17 @@ background-image:url('../images/close_hl.png');
.task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;} .task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;}
.task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;} .task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;}
.milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; } .milestone { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; }
.milestone_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
.milestone_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
.milestone_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
.project-line { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; }
.project_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
.project_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
.project_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
/***** Icons *****/ /***** Icons *****/
.icon { .icon {
...@@ -840,6 +852,7 @@ padding-bottom: 3px; ...@@ -840,6 +852,7 @@ padding-bottom: 3px;
.icon-comment { background-image: url(../images/comment.png); } .icon-comment { background-image: url(../images/comment.png); }
.icon-summary { background-image: url(../images/lightning.png); } .icon-summary { background-image: url(../images/lightning.png); }
.icon-server-authentication { background-image: url(../images/server_key.png); } .icon-server-authentication { background-image: url(../images/server_key.png); }
.icon-issue { background-image: url(../images/ticket.png); }
.icon-file { background-image: url(../images/files/default.png); } .icon-file { background-image: url(../images/files/default.png); }
.icon-file.text-plain { background-image: url(../images/files/text.png); } .icon-file.text-plain { background-image: url(../images/files/text.png); }
...@@ -898,6 +911,12 @@ td.username img.gravatar { ...@@ -898,6 +911,12 @@ td.username img.gravatar {
margin: 0 1em 1em 0; margin: 0 1em 1em 0;
} }
/* Used on 12px Gravatar img tags without the icon background */
.icon-gravatar {
float: left;
margin-right: 4px;
}
#activity dt, #activity dt,
.journal { .journal {
clear: left; clear: left;
......
...@@ -5,20 +5,20 @@ class GanttsControllerTest < ActionController::TestCase ...@@ -5,20 +5,20 @@ class GanttsControllerTest < ActionController::TestCase
context "#gantt" do context "#gantt" do
should "work" do should "work" do
i2 = Issue.find(2)
i2.update_attribute(:due_date, 1.month.from_now)
get :show, :project_id => 1 get :show, :project_id => 1
assert_response :success assert_response :success
assert_template 'show.html.erb' assert_template 'show.html.erb'
assert_not_nil assigns(:gantt) assert_not_nil assigns(:gantt)
events = assigns(:gantt).events
assert_not_nil events
# Issue with start and due dates # Issue with start and due dates
i = Issue.find(1) i = Issue.find(1)
assert_not_nil i.due_date assert_not_nil i.due_date
assert events.include?(Issue.find(1)) assert_select "div a.issue", /##{i.id}/
# Issue with without due date but targeted to a version with date # Issue with on a targeted version should not be in the events but loaded in the html
i = Issue.find(2) i = Issue.find(2)
assert_nil i.due_date assert_select "div a.issue", /##{i.id}/
assert events.include?(i)
end end
should "work cross project" do should "work cross project" do
...@@ -26,8 +26,8 @@ class GanttsControllerTest < ActionController::TestCase ...@@ -26,8 +26,8 @@ class GanttsControllerTest < ActionController::TestCase
assert_response :success assert_response :success
assert_template 'show.html.erb' assert_template 'show.html.erb'
assert_not_nil assigns(:gantt) assert_not_nil assigns(:gantt)
events = assigns(:gantt).events assert_not_nil assigns(:gantt).query
assert_not_nil events assert_nil assigns(:gantt).project
end end
should "export to pdf" do should "export to pdf" do
......
...@@ -25,8 +25,9 @@ module ObjectDaddyHelpers ...@@ -25,8 +25,9 @@ module ObjectDaddyHelpers
def Issue.generate_for_project!(project, attributes={}) def Issue.generate_for_project!(project, attributes={})
issue = Issue.spawn(attributes) do |issue| issue = Issue.spawn(attributes) do |issue|
issue.project = project issue.project = project
issue.tracker = project.trackers.first unless project.trackers.empty?
yield issue if block_given?
end end
issue.tracker = project.trackers.first unless project.trackers.empty?
issue.save! issue.save!
issue issue
end end
......
...@@ -601,7 +601,7 @@ EXPECTED ...@@ -601,7 +601,7 @@ EXPECTED
# turn off avatars # turn off avatars
Setting.gravatar_enabled = '0' Setting.gravatar_enabled = '0'
assert_nil avatar(User.find_by_mail('jsmith@somenet.foo')) assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
end end
def test_link_to_user def test_link_to_user
......
...@@ -510,6 +510,28 @@ class IssueTest < ActiveSupport::TestCase ...@@ -510,6 +510,28 @@ class IssueTest < ActiveSupport::TestCase
assert !Issue.new(:due_date => nil).overdue? assert !Issue.new(:due_date => nil).overdue?
assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue? assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
end end
context "#behind_schedule?" do
should "be false if the issue has no start_date" do
assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
end
should "be false if the issue has no end_date" do
assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
end
should "be false if the issue has more done than it's calendar time" do
assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
end
should "be true if the issue hasn't been started at all" do
assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
end
should "be true if the issue has used more calendar time than it's done ratio" do
assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
end
end
def test_assignable_users def test_assignable_users
assert_kind_of User, Issue.find(1).assignable_users.first assert_kind_of User, Issue.find(1).assignable_users.first
......
# redMine - project management software
# Copyright (C) 2006-2008 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.
require File.dirname(__FILE__) + '/../../../../test_helper'
class Redmine::Helpers::GanttTest < ActiveSupport::TestCase
# Utility methods and classes so assert_select can be used.
class GanttViewTest < ActionView::Base
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TextHelper
include ActionController::UrlWriter
include ApplicationHelper
include ProjectsHelper
include IssuesHelper
def self.default_url_options
{:only_path => true }
end
end
include ActionController::Assertions::SelectorAssertions
def setup
@response = ActionController::TestResponse.new
# Fixtures
ProjectCustomField.delete_all
Project.destroy_all
User.current = User.find(1)
end
def build_view
@view = GanttViewTest.new
end
def html_document
HTML::Document.new(@response.body)
end
# Creates a Gantt chart for a 4 week span
def create_gantt(project=Project.generate!)
@project = project
@gantt = Redmine::Helpers::Gantt.new
@gantt.project = @project
@gantt.query = Query.generate_default!(:project => @project)
@gantt.view = build_view
@gantt.instance_variable_set('@date_from', 2.weeks.ago.to_date)
@gantt.instance_variable_set('@date_to', 2.weeks.from_now.to_date)
end
context "#number_of_rows" do
context "with one project" do
should "return the number of rows just for that project"
end
context "with no project" do
should "return the total number of rows for all the projects, resursively"
end
end
context "#number_of_rows_on_project" do
setup do
create_gantt
end
should "clear the @query.project so cross-project issues and versions can be counted" do
assert @gantt.query.project
@gantt.number_of_rows_on_project(@project)
assert_nil @gantt.query.project
end
should "count 1 for the project itself" do
assert_equal 1, @gantt.number_of_rows_on_project(@project)
end
should "count the number of issues without a version" do
@project.issues << Issue.generate_for_project!(@project, :fixed_version => nil)
assert_equal 2, @gantt.number_of_rows_on_project(@project)
end
should "count the number of versions" do
@project.versions << Version.generate!
@project.versions << Version.generate!
assert_equal 3, @gantt.number_of_rows_on_project(@project)
end
should "count the number of issues on versions, including cross-project" do
version = Version.generate!
@project.versions << version
@project.issues << Issue.generate_for_project!(@project, :fixed_version => version)
assert_equal 3, @gantt.number_of_rows_on_project(@project)
end
should "recursive and count the number of rows on each subproject" do
@project.versions << Version.generate! # +1
@subproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1
@subproject.set_parent!(@project)
@subproject.issues << Issue.generate_for_project!(@subproject) # +1
@subproject.issues << Issue.generate_for_project!(@subproject) # +1
@subsubproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1
@subsubproject.set_parent!(@subproject)
@subsubproject.issues << Issue.generate_for_project!(@subsubproject) # +1
assert_equal 7, @gantt.number_of_rows_on_project(@project) # +1 for self
end
end
# TODO: more of an integration test
context "#subjects" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => 1.week.from_now.to_date, :sharing => 'none')
@project.versions << @version
@issue = Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
@project.issues << @issue
@response.body = @gantt.subjects
end
context "project" do
should "be rendered" do
assert_select "div.project-name a", /#{@project.name}/
end
should "have an indent of 4" do
assert_select "div.project-name[style*=left:4px]"
end
end
context "version" do
should "be rendered" do
assert_select "div.version-name a", /#{@version.name}/
end
should "be indented 24 (one level)" do
assert_select "div.version-name[style*=left:24px]"
end
end
context "issue" do
should "be rendered" do
assert_select "div.issue-subject", /#{@issue.subject}/
end
should "be indented 44 (two levels)" do
assert_select "div.issue-subject[style*=left:44px]"
end
end
end
context "#lines" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => 1.week.from_now.to_date)
@project.versions << @version
@issue = Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
@project.issues << @issue
@response.body = @gantt.lines
end
context "project" do
should "be rendered" do
assert_select "div.project_todo"
assert_select "div.project-line.starting"
assert_select "div.project-line.ending"
assert_select "div.label.project-name", /#{@project.name}/
end
end
context "version" do
should "be rendered" do
assert_select "div.milestone_todo"
assert_select "div.milestone.starting"
assert_select "div.milestone.ending"
assert_select "div.label.version-name", /#{@version.name}/
end
end
context "issue" do
should "be rendered" do
assert_select "div.task_todo"
assert_select "div.label.issue-name", /#{@issue.done_ratio}/
assert_select "div.tooltip", /#{@issue.subject}/
end
end
end
context "#render_project" do
should "be tested"
end
context "#render_issues" do
should "be tested"
end
context "#render_version" do
should "be tested"
end
context "#subject_for_project" do
setup do
create_gantt
end
context ":html format" do
should "add an absolute positioned div" do
@response.body = @gantt.subject_for_project(@project, {:format => :html})
assert_select "div[style*=absolute]"
end
should "use the indent option to move the div to the right" do
@response.body = @gantt.subject_for_project(@project, {:format => :html, :indent => 40})
assert_select "div[style*=left:40]"
end
should "include the project name" do
@response.body = @gantt.subject_for_project(@project, {:format => :html})
assert_select 'div', :text => /#{@project.name}/
end
should "include a link to the project" do
@response.body = @gantt.subject_for_project(@project, {:format => :html})
assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/
end
should "style overdue projects" do
@project.enabled_module_names = [:issue_tracking]
@project.versions << Version.generate!(:effective_date => Date.yesterday)
assert @project.overdue?, "Need an overdue project for this test"
@response.body = @gantt.subject_for_project(@project, {:format => :html})
assert_select 'div span.project-overdue'
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#line_for_project" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => Date.yesterday)
@project.versions << @version
@project.issues << Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
end
context ":html format" do
context "todo line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_todo[style*=left:52px]"
end
should "be the total width of the project" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_todo[style*=width:31px]"
end
end
context "late line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_late[style*=left:52px]"
end
should "be the total delayed width of the project" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_late[style*=width:6px]"
end
end
context "done line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_done[style*=left:52px]"
end
should "Be the total done width of the project" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_done[style*=left:52px]"
end
end
context "starting marker" do
should "not appear if the starting point is off the gantt chart" do
# Shift the date range of the chart
@gantt.instance_variable_set('@date_from', Date.today)
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-line.starting", false
end
should "appear at the starting point" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-line.starting[style*=left:52px]"
end
end
context "ending marker" do
should "not appear if the starting point is off the gantt chart" do
# Shift the date range of the chart
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-line.ending", false
end
should "appear at the end of the date range" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-line.ending[style*=left:84px]"
end
end
context "status content" do
should "appear at the far left, even if it's far in the past" do
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-name", /#{@project.name}/
end
should "show the project name" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-name", /#{@project.name}/
end
should "show the percent complete" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-name", /0%/
end
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#subject_for_version" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => Date.yesterday)
@project.versions << @version
@project.issues << Issue.generate!(:fixed_version => @version,
:subject => "gantt#subject_for_version",
:tracker => @tracker,
:project => @project,
:start_date => Date.today)
end
context ":html format" do
should "add an absolute positioned div" do
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select "div[style*=absolute]"
end
should "use the indent option to move the div to the right" do
@response.body = @gantt.subject_for_version(@version, {:format => :html, :indent => 40})
assert_select "div[style*=left:40]"
end
should "include the version name" do
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select 'div', :text => /#{@version.name}/
end
should "include a link to the version" do
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select 'a[href=?]', Regexp.escape("/versions/show/#{@version.to_param}"), :text => /#{@version.name}/
end
should "style late versions" do
assert @version.overdue?, "Need an overdue version for this test"
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select 'div span.version-behind-schedule'
end
should "style behind schedule versions" do
assert @version.behind_schedule?, "Need a behind schedule version for this test"
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select 'div span.version-behind-schedule'
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#line_for_version" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => 1.week.from_now.to_date)
@project.versions << @version
@project.issues << Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
end
context ":html format" do
context "todo line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_todo[style*=left:52px]"
end
should "be the total width of the version" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_todo[style*=width:31px]"
end
end
context "late line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_late[style*=left:52px]"
end
should "be the total delayed width of the version" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_late[style*=width:6px]"
end
end
context "done line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_done[style*=left:52px]"
end
should "Be the total done width of the version" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_done[style*=left:52px]"
end
end
context "starting marker" do
should "not appear if the starting point is off the gantt chart" do
# Shift the date range of the chart
@gantt.instance_variable_set('@date_from', Date.today)
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone.starting", false
end
should "appear at the starting point" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone.starting[style*=left:52px]"
end
end
context "ending marker" do
should "not appear if the starting point is off the gantt chart" do
# Shift the date range of the chart
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone.ending", false
end
should "appear at the end of the date range" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone.ending[style*=left:84px]"
end
end
context "status content" do
should "appear at the far left, even if it's far in the past" do
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.version-name", /#{@version.name}/
end
should "show the version name" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.version-name", /#{@version.name}/
end
should "show the percent complete" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.version-name", /30%/
end
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#subject_for_issue" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@issue = Issue.generate!(:subject => "gantt#subject_for_issue",
:tracker => @tracker,
:project => @project,
:start_date => 3.days.ago.to_date,
:due_date => Date.yesterday)
@project.issues << @issue
end
context ":html format" do
should "add an absolute positioned div" do
@response.body = @gantt.subject_for_issue(@issue, {:format => :html})
assert_select "div[style*=absolute]"
end
should "use the indent option to move the div to the right" do
@response.body = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40})
assert_select "div[style*=left:40]"
end
should "include the issue subject" do
@response.body = @gantt.subject_for_issue(@issue, {:format => :html})
assert_select 'div', :text => /#{@issue.subject}/
end
should "include a link to the issue" do
@response.body = @gantt.subject_for_issue(@issue, {:format => :html})
assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/
end
should "style overdue issues" do
assert @issue.overdue?, "Need an overdue issue for this test"
@response.body = @gantt.subject_for_issue(@issue, {:format => :html})
assert_select 'div span.issue-overdue'
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#line_for_issue" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => 1.week.from_now.to_date)
@project.versions << @version
@issue = Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
@project.issues << @issue
end
context ":html format" do
context "todo line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_todo[style*=left:52px]"
end
should "be the total width of the issue" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_todo[style*=width:34px]"
end
end
context "late line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_late[style*=left:52px]"
end
should "be the total delayed width of the issue" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_late[style*=width:6px]"
end
end
context "done line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_done[style*=left:52px]"
end
should "Be the total done width of the issue" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_done[style*=left:52px]"
end
end
context "status content" do
should "appear at the far left, even if it's far in the past" do
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.issue-name"
end
should "show the issue status" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.issue-name", /#{@issue.status.name}/
end
should "show the percent complete" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.issue-name", /30%/
end
end
end
should "have an issue tooltip" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.tooltip", /#{@issue.subject}/
end
should "test the PNG format"
should "test the PDF format"
end
context "#to_image" do
should "be tested"
end
context "#to_pdf" do
should "be tested"
end
end
...@@ -842,4 +842,122 @@ class ProjectTest < ActiveSupport::TestCase ...@@ -842,4 +842,122 @@ class ProjectTest < ActiveSupport::TestCase
end end
context "#start_date" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
@project = Project.generate!(:identifier => 'test0')
@project.trackers << Tracker.generate!
end
should "be nil if there are no issues on the project" do
assert_nil @project.start_date
end
should "be nil if issue tracking is disabled" do
Issue.generate_for_project!(@project, :start_date => Date.today)
@project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
@project.reload
assert_nil @project.start_date
end
should "be the earliest start date of it's issues" do
early = 7.days.ago.to_date
Issue.generate_for_project!(@project, :start_date => Date.today)
Issue.generate_for_project!(@project, :start_date => early)
assert_equal early, @project.start_date
end
end
context "#due_date" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
@project = Project.generate!(:identifier => 'test0')
@project.trackers << Tracker.generate!
end
should "be nil if there are no issues on the project" do
assert_nil @project.due_date
end
should "be nil if issue tracking is disabled" do
Issue.generate_for_project!(@project, :due_date => Date.today)
@project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
@project.reload
assert_nil @project.due_date
end
should "be the latest due date of it's issues" do
future = 7.days.from_now.to_date
Issue.generate_for_project!(@project, :due_date => future)
Issue.generate_for_project!(@project, :due_date => Date.today)
assert_equal future, @project.due_date
end
should "be the latest due date of it's versions" do
future = 7.days.from_now.to_date
@project.versions << Version.generate!(:effective_date => future)
@project.versions << Version.generate!(:effective_date => Date.today)
assert_equal future, @project.due_date
end
should "pick the latest date from it's issues and versions" do
future = 7.days.from_now.to_date
far_future = 14.days.from_now.to_date
Issue.generate_for_project!(@project, :due_date => far_future)
@project.versions << Version.generate!(:effective_date => future)
assert_equal far_future, @project.due_date
end
end
context "Project#completed_percent" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
@project = Project.generate!(:identifier => 'test0')
@project.trackers << Tracker.generate!
end
context "no versions" do
should "be 100" do
assert_equal 100, @project.completed_percent
end
end
context "with versions" do
should "return 0 if the versions have no issues" do
Version.generate!(:project => @project)
Version.generate!(:project => @project)
assert_equal 0, @project.completed_percent
end
should "return 100 if the version has only closed issues" do
v1 = Version.generate!(:project => @project)
Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
v2 = Version.generate!(:project => @project)
Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
assert_equal 100, @project.completed_percent
end
should "return the averaged completed percent of the versions (not weighted)" do
v1 = Version.generate!(:project => @project)
Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
v2 = Version.generate!(:project => @project)
Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
assert_equal 50, @project.completed_percent
end
end
end
end end
...@@ -104,7 +104,57 @@ class VersionTest < ActiveSupport::TestCase ...@@ -104,7 +104,57 @@ class VersionTest < ActiveSupport::TestCase
assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent
assert_progress_equal 25.0/100.0*100, v.closed_pourcent assert_progress_equal 25.0/100.0*100, v.closed_pourcent
end end
context "#behind_schedule?" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
@project = Project.generate!(:identifier => 'test0')
@project.trackers << Tracker.generate!
@version = Version.generate!(:project => @project, :effective_date => nil)
end
should "be false if there are no issues assigned" do
@version.update_attribute(:effective_date, Date.yesterday)
assert_equal false, @version.behind_schedule?
end
should "be false if there is no effective_date" do
assert_equal false, @version.behind_schedule?
end
should "be false if all of the issues are ahead of schedule" do
@version.update_attribute(:effective_date, 7.days.from_now.to_date)
@version.fixed_issues = [
Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60), # 14 day span, 60% done, 50% time left
Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
]
assert_equal 60, @version.completed_pourcent
assert_equal false, @version.behind_schedule?
end
should "be true if any of the issues are behind schedule" do
@version.update_attribute(:effective_date, 7.days.from_now.to_date)
@version.fixed_issues = [
Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60), # 14 day span, 60% done, 50% time left
Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left
]
assert_equal 40, @version.completed_pourcent
assert_equal true, @version.behind_schedule?
end
should "be false if all of the issues are complete" do
@version.update_attribute(:effective_date, 7.days.from_now.to_date)
@version.fixed_issues = [
Issue.generate_for_project!(@project, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)), # 7 day span
Issue.generate_for_project!(@project, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
]
assert_equal 100, @version.completed_pourcent
assert_equal false, @version.behind_schedule?
end
end
context "#estimated_hours" do context "#estimated_hours" do
setup do setup do
@version = Version.create!(:project_id => 1, :name => '#estimated_hours') @version = Version.create!(:project_id => 1, :name => '#estimated_hours')
......
...@@ -6,7 +6,7 @@ task :default => :spec ...@@ -6,7 +6,7 @@ task :default => :spec
desc 'Run all application-specific specs' desc 'Run all application-specific specs'
Spec::Rake::SpecTask.new(:spec) do |t| Spec::Rake::SpecTask.new(:spec) do |t|
t.rcov = true # t.rcov = true
end end
desc "Report code statistics (KLOCs, etc) from the application" desc "Report code statistics (KLOCs, etc) from the application"
......
...@@ -26,6 +26,9 @@ module GravatarHelper ...@@ -26,6 +26,9 @@ module GravatarHelper
# decorational picture, the alt text should be empty according to the # decorational picture, the alt text should be empty according to the
# XHTML specs. # XHTML specs.
:alt => '', :alt => '',
# The title text to use for the img tag for the gravatar.
:title => '',
# The class to assign to the img tag for the gravatar. # The class to assign to the img tag for the gravatar.
:class => 'gravatar', :class => 'gravatar',
...@@ -48,8 +51,8 @@ module GravatarHelper ...@@ -48,8 +51,8 @@ module GravatarHelper
def gravatar(email, options={}) def gravatar(email, options={})
src = h(gravatar_url(email, options)) src = h(gravatar_url(email, options))
options = DEFAULT_OPTIONS.merge(options) options = DEFAULT_OPTIONS.merge(options)
[:class, :alt, :size].each { |opt| options[opt] = h(options[opt]) } [:class, :alt, :size, :title].each { |opt| options[opt] = h(options[opt]) }
"<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />" "<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" title=\"#{options[:title]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
end end
# Returns the base Gravatar URL for the given email hash. If ssl evaluates to true, # Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,
...@@ -82,4 +85,4 @@ module GravatarHelper ...@@ -82,4 +85,4 @@ module GravatarHelper
end end
end end
\ No newline at end of file
...@@ -4,34 +4,40 @@ require 'active_support' # to get "returning" ...@@ -4,34 +4,40 @@ require 'active_support' # to get "returning"
require File.dirname(__FILE__) + '/../lib/gravatar' require File.dirname(__FILE__) + '/../lib/gravatar'
include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util
context "gravatar_url with a custom default URL" do describe "gravatar_url with a custom default URL" do
setup do before(:each) do
@original_options = DEFAULT_OPTIONS.dup @original_options = DEFAULT_OPTIONS.dup
DEFAULT_OPTIONS[:default] = "no_avatar.png" DEFAULT_OPTIONS[:default] = "no_avatar.png"
@url = gravatar_url("somewhere") @url = gravatar_url("somewhere")
end end
specify "should include the \"default\" argument in the result" do it "should include the \"default\" argument in the result" do
@url.should match(/&default=no_avatar.png/) @url.should match(/&default=no_avatar.png/)
end end
teardown do after(:each) do
DEFAULT_OPTIONS.merge!(@original_options) DEFAULT_OPTIONS.merge!(@original_options)
end end
end end
context "gravatar_url with default settings" do describe "gravatar_url with default settings" do
setup do before(:each) do
@url = gravatar_url("somewhere") @url = gravatar_url("somewhere")
end end
specify "should have a nil default URL" do it "should have a nil default URL" do
DEFAULT_OPTIONS[:default].should be_nil DEFAULT_OPTIONS[:default].should be_nil
end end
specify "should not include the \"default\" argument in the result" do it "should not include the \"default\" argument in the result" do
@url.should_not match(/&default=/) @url.should_not match(/&default=/)
end end
end end
\ No newline at end of file
describe "gravatar with a custom title option" do
it "should include the title in the result" do
gravatar('example@example.com', :title => "This is a title attribute").should match(/This is a title attribute/)
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment