Feature #2024 » 0001-Moving-gantt-bar.patch
| app/controllers/gantts_controller.rb | ||
|---|---|---|
| 19 | 19 | |
| 20 | 20 |
class GanttsController < ApplicationController |
| 21 | 21 |
menu_item :gantt |
| 22 |
before_action :find_optional_project |
|
| 22 |
before_action :find_optional_project, :only => [:show]
|
|
| 23 | 23 | |
| 24 | 24 |
rescue_from Query::StatementInvalid, :with => :query_statement_invalid |
| 25 | 25 | |
| ... | ... | |
| 55 | 55 |
end |
| 56 | 56 |
end |
| 57 | 57 |
end |
| 58 | ||
| 59 |
def change_duration |
|
| 60 |
return render_error(:status => :unprocessable_entity) unless request.xhr? |
|
| 61 | ||
| 62 |
@obj = Issue.find(params[:id]) |
|
| 63 |
raise Unauthorized unless @obj.visible? |
|
| 64 | ||
| 65 |
ActiveRecord::Base.transaction do |
|
| 66 |
@obj.init_journal(User.current) |
|
| 67 |
@obj.safe_attributes = params[:change_duration] |
|
| 68 |
if !@obj.save |
|
| 69 |
render_403(:message => @obj.errors.full_messages.join) |
|
| 70 |
raise ActiveRecord::Rollback |
|
| 71 |
end |
|
| 72 |
end |
|
| 73 |
retrieve_query |
|
| 74 |
rescue ActiveRecord::StaleObjectError |
|
| 75 |
render_403(:message => :notice_issue_update_conflict) |
|
| 76 |
rescue ActiveRecord::RecordNotFound |
|
| 77 |
render_404 |
|
| 78 |
end |
|
| 58 | 79 |
end |
| app/views/gantts/change_duration.js.erb | ||
|---|---|---|
| 1 |
<% |
|
| 2 |
@draw_objs = [] |
|
| 3 | ||
| 4 |
def select_precedes(issue) |
|
| 5 |
issue.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to).each do |follows| |
|
| 6 |
next if @draw_objs.include?(follows) |
|
| 7 | ||
| 8 |
while follows do |
|
| 9 |
@draw_objs.concat [follows, follows.fixed_version, follows.project] |
|
| 10 |
select_precedes(follows) |
|
| 11 |
follows.children.each do |child| |
|
| 12 |
@draw_objs.concat [child, child.fixed_version, child.project] |
|
| 13 |
select_precedes(child) |
|
| 14 |
end |
|
| 15 |
follows = follows.parent |
|
| 16 |
end |
|
| 17 |
end |
|
| 18 |
end |
|
| 19 | ||
| 20 |
issue = @obj |
|
| 21 |
while issue do |
|
| 22 |
@draw_objs.concat [issue, issue.fixed_version, issue.project] |
|
| 23 |
select_precedes(issue) |
|
| 24 |
issue = issue.parent |
|
| 25 |
end |
|
| 26 |
@draw_objs = @draw_objs.compact.uniq |
|
| 27 |
@draw_objs.reject!{|obj| ![Project, Version, Issue].include?(obj.class)}
|
|
| 28 |
-%> |
|
| 29 |
var elm; |
|
| 30 |
<% |
|
| 31 |
gantt = Redmine::Helpers::Gantt.new(params) |
|
| 32 |
gantt.view = self |
|
| 33 |
gantt.query = @query |
|
| 34 | ||
| 35 |
@draw_objs.each do |obj| |
|
| 36 |
gantt.instance_variable_set(:@number_of_rows, 0) |
|
| 37 |
gantt.instance_variable_set(:@lines, '') |
|
| 38 |
gantt.render_object_row( |
|
| 39 |
obj, |
|
| 40 |
{format: :html, only: :lines, zoom: 2 ** gantt.zoom, top: 0, top_increment: 20}
|
|
| 41 |
) |
|
| 42 |
todo_content = Nokogiri::HTML.parse(gantt.instance_variable_get(:@lines)) |
|
| 43 |
todo_content = todo_content.xpath( |
|
| 44 |
"//div[contains(@class,'task') and contains(@class,'line')]/*" |
|
| 45 |
).to_s.tr("\n",'').gsub(/'/, "\\\\'")
|
|
| 46 | ||
| 47 |
klass_name = obj.class.name.underscore |
|
| 48 |
elm_todo = "[id=task-todo-#{klass_name}-#{obj.id}]"
|
|
| 49 |
css_subject = 'span:not(.expander)' |
|
| 50 |
elm_subject = raw("[id=#{klass_name}-#{obj.id}] > #{css_subject}")
|
|
| 51 | ||
| 52 |
subject_content = Nokogiri::HTML.parse(gantt.__send__(:html_subject_content, obj)) |
|
| 53 |
subject_content = subject_content.css(css_subject).to_s.tr("\n",'').gsub(/'/, "\\\\'")
|
|
| 54 |
-%> |
|
| 55 |
if($('<%= elm_subject %>').length){
|
|
| 56 |
$('<%= elm_todo %>').parent().html('<%= raw(todo_content) %>');
|
|
| 57 |
$('<%= elm_subject %>').replaceWith('<%= raw(subject_content) %>');
|
|
| 58 |
<% |
|
| 59 |
case obj |
|
| 60 |
when Issue |
|
| 61 |
@query.columns.each do |column| |
|
| 62 |
-%> |
|
| 63 |
elm = $('div.gantt_selected_column_content #<%= column.name %>_issue_<%= obj.id %>');
|
|
| 64 |
if(elm.length){
|
|
| 65 |
elm.html('<%= escape_javascript(column_content(column, obj)) %>');
|
|
| 66 |
} |
|
| 67 |
<% |
|
| 68 |
end |
|
| 69 |
end |
|
| 70 |
-%> |
|
| 71 |
} |
|
| 72 |
<% |
|
| 73 |
end |
|
| 74 |
-%> |
|
| config/routes.rb | ||
|---|---|---|
| 60 | 60 | |
| 61 | 61 |
get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt' |
| 62 | 62 |
get '/issues/gantt', :to => 'gantts#show' |
| 63 |
put '/gantt/:id/change_duration', :to => 'gantts#change_duration', :as => 'gantt_change_duration' |
|
| 63 | 64 | |
| 64 | 65 |
get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar' |
| 65 | 66 |
get '/issues/calendar', :to => 'calendars#show' |
| lib/redmine/helpers/gantt.rb | ||
|---|---|---|
| 771 | 771 |
tag_options[:class] = "version-name" |
| 772 | 772 |
has_children = object.fixed_issues.exists? |
| 773 | 773 |
when Project |
| 774 |
tag_options[:id] = "project-#{object.id}"
|
|
| 774 | 775 |
tag_options[:class] = "project-name" |
| 775 | 776 |
has_children = object.issues.exists? || object.versions.exists? |
| 776 | 777 |
end |
| ... | ... | |
| 849 | 850 |
end |
| 850 | 851 |
# Renders the task bar, with progress and late |
| 851 | 852 |
if coords[:bar_start] && coords[:bar_end] |
| 852 |
width = coords[:bar_end] - coords[:bar_start] - 2
|
|
| 853 |
width = coords[:bar_end] - coords[:bar_start] |
|
| 853 | 854 |
style = +"" |
| 854 |
style << "top:#{params[:top]}px;"
|
|
| 855 | 855 |
style << "left:#{coords[:bar_start]}px;"
|
| 856 | 856 |
style << "width:#{width}px;"
|
| 857 |
html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue)
|
|
| 858 |
html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
|
|
| 857 |
html_id = |
|
| 858 |
case object |
|
| 859 |
when Project |
|
| 860 |
"task-todo-project-#{object.id}"
|
|
| 861 |
when Version |
|
| 862 |
"task-todo-version-#{object.id}"
|
|
| 863 |
when Issue |
|
| 864 |
"task-todo-issue-#{object.id}"
|
|
| 865 |
end |
|
| 859 | 866 |
content_opt = {:style => style,
|
| 860 |
:class => "#{css} task_todo",
|
|
| 867 |
:class => "task_todo", |
|
| 861 | 868 |
:id => html_id, |
| 862 | 869 |
:data => {}}
|
| 863 | 870 |
if object.is_a?(Issue) |
| ... | ... | |
| 865 | 872 |
if rels.present? |
| 866 | 873 |
content_opt[:data] = {"rels" => rels.to_json}
|
| 867 | 874 |
end |
| 875 |
content_opt[:data].merge!({
|
|
| 876 |
:url_change_duration => Rails.application.routes.url_helpers.gantt_change_duration_path( |
|
| 877 |
object |
|
| 878 |
), |
|
| 879 |
:object => {
|
|
| 880 |
:start_date => object.start_date, |
|
| 881 |
:due_date => object.due_date, |
|
| 882 |
:lock_version => object.lock_version, |
|
| 883 |
}.to_json, |
|
| 884 |
}) |
|
| 868 | 885 |
end |
| 869 | 886 |
content_opt[:data].merge!(data_options) |
| 870 |
output << view.content_tag(:div, ' '.html_safe, content_opt)
|
|
| 887 |
bar_contents = []
|
|
| 871 | 888 |
if coords[:bar_late_end] |
| 872 |
width = coords[:bar_late_end] - coords[:bar_start] - 2
|
|
| 889 |
width = coords[:bar_late_end] - coords[:bar_start] |
|
| 873 | 890 |
style = +"" |
| 874 |
style << "top:#{params[:top]}px;"
|
|
| 875 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 876 | 891 |
style << "width:#{width}px;"
|
| 877 |
output << view.content_tag(:div, ' '.html_safe,
|
|
| 878 |
:style => style, |
|
| 879 |
:class => "#{css} task_late",
|
|
| 880 |
:data => data_options) |
|
| 892 |
bar_contents << view.content_tag(:div, ' '.html_safe,
|
|
| 893 |
:style => style,
|
|
| 894 |
:class => "task_late",
|
|
| 895 |
:data => data_options)
|
|
| 881 | 896 |
end |
| 882 | 897 |
if coords[:bar_progress_end] |
| 883 |
width = coords[:bar_progress_end] - coords[:bar_start] - 2
|
|
| 898 |
width = coords[:bar_progress_end] - coords[:bar_start] |
|
| 884 | 899 |
style = +"" |
| 885 |
style << "top:#{params[:top]}px;"
|
|
| 886 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 887 | 900 |
style << "width:#{width}px;"
|
| 888 | 901 |
html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue)
|
| 889 | 902 |
html_id = "task-done-version-#{object.id}" if object.is_a?(Version)
|
| 890 |
output << view.content_tag(:div, ' '.html_safe, |
|
| 903 |
bar_contents << view.content_tag(:div, ' '.html_safe, |
|
| 904 |
:style => style, |
|
| 905 |
:class => "task_done", |
|
| 906 |
:id => html_id, |
|
| 907 |
:data => data_options) |
|
| 908 |
end |
|
| 909 | ||
| 910 |
# Renders the tooltip |
|
| 911 |
if object.is_a?(Issue) |
|
| 912 |
s = view.content_tag(:span, |
|
| 913 |
view.render_issue_tooltip(object).html_safe, |
|
| 914 |
:class => "tip") |
|
| 915 |
s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection') |
|
| 916 |
style = +"" |
|
| 917 |
style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
|
|
| 918 |
style << "height:12px;" |
|
| 919 |
bar_contents << view.content_tag(:div, s.html_safe, |
|
| 920 |
:style => style, |
|
| 921 |
:class => "tooltip hascontextmenu", |
|
| 922 |
:data => data_options) |
|
| 923 |
end |
|
| 924 | ||
| 925 |
# Renders the label on the right |
|
| 926 |
if label |
|
| 927 |
style = +"" |
|
| 928 |
style << "top:0px;" |
|
| 929 |
style << "left:#{coords[:bar_end] - coords[:bar_start] + 8}px;"
|
|
| 930 |
bar_contents << view.content_tag(:div, label, |
|
| 931 |
:style => style, |
|
| 932 |
:class => "label", |
|
| 933 |
:data => data_options) |
|
| 934 |
end |
|
| 935 | ||
| 936 |
bar_contents = bar_contents.join.presence |
|
| 937 |
output << view.content_tag(:div, (bar_contents || ' ').html_safe, content_opt) |
|
| 938 |
else |
|
| 939 |
# Renders the label on the right |
|
| 940 |
if label |
|
| 941 |
style = +"" |
|
| 942 |
style << "top:1px;" |
|
| 943 |
style << "left:#{(coords[:bar_end] || 0) + 8}px;"
|
|
| 944 |
output << view.content_tag(:div, label, |
|
| 891 | 945 |
:style => style, |
| 892 |
:class => "#{css} task_done",
|
|
| 893 |
:id => html_id, |
|
| 946 |
:class => "label", |
|
| 894 | 947 |
:data => data_options) |
| 895 | 948 |
end |
| 896 | 949 |
end |
| ... | ... | |
| 898 | 951 |
if markers |
| 899 | 952 |
if coords[:start] |
| 900 | 953 |
style = +"" |
| 901 |
style << "top:#{params[:top]}px;"
|
|
| 902 | 954 |
style << "left:#{coords[:start]}px;"
|
| 903 |
style << "width:15px;" |
|
| 904 | 955 |
output << view.content_tag(:div, ' '.html_safe, |
| 905 | 956 |
:style => style, |
| 906 |
:class => "#{css} marker starting",
|
|
| 957 |
:class => "marker starting", |
|
| 907 | 958 |
:data => data_options) |
| 908 | 959 |
end |
| 909 | 960 |
if coords[:end] |
| 910 | 961 |
style = +"" |
| 911 |
style << "top:#{params[:top]}px;"
|
|
| 912 | 962 |
style << "left:#{coords[:end]}px;"
|
| 913 |
style << "width:15px;" |
|
| 914 | 963 |
output << view.content_tag(:div, ' '.html_safe, |
| 915 | 964 |
:style => style, |
| 916 |
:class => "#{css} marker ending",
|
|
| 965 |
:class => "marker ending", |
|
| 917 | 966 |
:data => data_options) |
| 918 | 967 |
end |
| 919 | 968 |
end |
| 920 |
# Renders the label on the right |
|
| 921 |
if label |
|
| 922 |
style = +"" |
|
| 923 |
style << "top:#{params[:top]}px;"
|
|
| 924 |
style << "left:#{(coords[:bar_end] || 0) + 8}px;"
|
|
| 925 |
style << "width:15px;" |
|
| 926 |
output << view.content_tag(:div, label, |
|
| 927 |
:style => style, |
|
| 928 |
:class => "#{css} label",
|
|
| 929 |
:data => data_options) |
|
| 930 |
end |
|
| 931 |
# Renders the tooltip |
|
| 932 |
if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end] |
|
| 933 |
s = view.content_tag(:span, |
|
| 934 |
view.render_issue_tooltip(object).html_safe, |
|
| 935 |
:class => "tip") |
|
| 936 |
s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', |
|
| 937 |
:value => object.id, :style => 'display:none;', |
|
| 938 |
:class => 'toggle-selection') |
|
| 939 |
style = +"" |
|
| 940 |
style << "position: absolute;" |
|
| 941 |
style << "top:#{params[:top]}px;"
|
|
| 942 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 943 |
style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
|
|
| 944 |
style << "height:12px;" |
|
| 945 |
output << view.content_tag(:div, s.html_safe, |
|
| 946 |
:style => style, |
|
| 947 |
:class => "tooltip hascontextmenu", |
|
| 948 |
:data => data_options) |
|
| 949 |
end |
|
| 969 |
output = view.content_tag(:div, output.html_safe, |
|
| 970 |
:class => "#{css} line",
|
|
| 971 |
:style => "top:#{params[:top]}px;width:#{params[:g_width] - 1}px;",
|
|
| 972 |
:data => data_options |
|
| 973 |
) |
|
| 950 | 974 |
@lines << output |
| 951 | 975 |
output |
| 952 | 976 |
end |
| public/javascripts/gantt.js | ||
|---|---|---|
| 4 | 4 |
var draw_gantt = null; |
| 5 | 5 |
var draw_top; |
| 6 | 6 |
var draw_right; |
| 7 |
var draw_left; |
|
| 8 | 7 | |
| 9 | 8 |
var rels_stroke_width = 2; |
| 10 | 9 | |
| 11 | 10 |
function setDrawArea() {
|
| 12 |
draw_top = $("#gantt_draw_area").position().top;
|
|
| 11 |
draw_top = $("#gantt_draw_area").offset().top;
|
|
| 13 | 12 |
draw_right = $("#gantt_draw_area").width();
|
| 14 |
draw_left = $("#gantt_area").scrollLeft();
|
|
| 15 | 13 |
} |
| 16 | 14 | |
| 17 | 15 |
function getRelationsArray() {
|
| ... | ... | |
| 42 | 40 |
return; |
| 43 | 41 |
} |
| 44 | 42 |
var issue_height = issue_from.height(); |
| 45 |
var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top;
|
|
| 43 |
var issue_from_top = issue_from.offset().top + (issue_height / 2) - draw_top;
|
|
| 46 | 44 |
var issue_from_right = issue_from.position().left + issue_from.width(); |
| 47 |
var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top;
|
|
| 45 |
var issue_to_top = issue_to.offset().top + (issue_height / 2) - draw_top;
|
|
| 48 | 46 |
var issue_to_left = issue_to.position().left; |
| 49 | 47 |
var color = issue_relation_type[element_issue["rel_type"]]["color"]; |
| 50 | 48 |
var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"]; |
| 51 | 49 |
var issue_from_right_rel = issue_from_right + landscape_margin; |
| 52 | 50 |
var issue_to_left_rel = issue_to_left - landscape_margin; |
| 53 |
draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top,
|
|
| 54 |
"L", issue_from_right_rel + draw_left, issue_from_top])
|
|
| 51 |
draw_gantt.path(["M", issue_from_right, issue_from_top, |
|
| 52 |
"L", issue_from_right_rel, issue_from_top]) |
|
| 55 | 53 |
.attr({stroke: color,
|
| 56 | 54 |
"stroke-width": rels_stroke_width |
| 57 | 55 |
}); |
| 58 | 56 |
if (issue_from_right_rel < issue_to_left_rel) {
|
| 59 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
|
|
| 60 |
"L", issue_from_right_rel + draw_left, issue_to_top])
|
|
| 57 |
draw_gantt.path(["M", issue_from_right_rel, issue_from_top, |
|
| 58 |
"L", issue_from_right_rel, issue_to_top]) |
|
| 61 | 59 |
.attr({stroke: color,
|
| 62 | 60 |
"stroke-width": rels_stroke_width |
| 63 | 61 |
}); |
| 64 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top,
|
|
| 65 |
"L", issue_to_left + draw_left, issue_to_top])
|
|
| 62 |
draw_gantt.path(["M", issue_from_right_rel, issue_to_top, |
|
| 63 |
"L", issue_to_left, issue_to_top]) |
|
| 66 | 64 |
.attr({stroke: color,
|
| 67 | 65 |
"stroke-width": rels_stroke_width |
| 68 | 66 |
}); |
| ... | ... | |
| 70 | 68 |
var issue_middle_top = issue_to_top + |
| 71 | 69 |
(issue_height * |
| 72 | 70 |
((issue_from_top > issue_to_top) ? 1 : -1)); |
| 73 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
|
|
| 74 |
"L", issue_from_right_rel + draw_left, issue_middle_top])
|
|
| 71 |
draw_gantt.path(["M", issue_from_right_rel, issue_from_top, |
|
| 72 |
"L", issue_from_right_rel, issue_middle_top]) |
|
| 75 | 73 |
.attr({stroke: color,
|
| 76 | 74 |
"stroke-width": rels_stroke_width |
| 77 | 75 |
}); |
| 78 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top,
|
|
| 79 |
"L", issue_to_left_rel + draw_left, issue_middle_top])
|
|
| 76 |
draw_gantt.path(["M", issue_from_right_rel, issue_middle_top, |
|
| 77 |
"L", issue_to_left_rel, issue_middle_top]) |
|
| 80 | 78 |
.attr({stroke: color,
|
| 81 | 79 |
"stroke-width": rels_stroke_width |
| 82 | 80 |
}); |
| 83 |
draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top,
|
|
| 84 |
"L", issue_to_left_rel + draw_left, issue_to_top])
|
|
| 81 |
draw_gantt.path(["M", issue_to_left_rel, issue_middle_top, |
|
| 82 |
"L", issue_to_left_rel, issue_to_top]) |
|
| 85 | 83 |
.attr({stroke: color,
|
| 86 | 84 |
"stroke-width": rels_stroke_width |
| 87 | 85 |
}); |
| 88 |
draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top,
|
|
| 89 |
"L", issue_to_left + draw_left, issue_to_top])
|
|
| 86 |
draw_gantt.path(["M", issue_to_left_rel, issue_to_top, |
|
| 87 |
"L", issue_to_left, issue_to_top]) |
|
| 90 | 88 |
.attr({stroke: color,
|
| 91 | 89 |
"stroke-width": rels_stroke_width |
| 92 | 90 |
}); |
| 93 | 91 |
} |
| 94 |
draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top,
|
|
| 92 |
draw_gantt.path(["M", issue_to_left, issue_to_top, |
|
| 95 | 93 |
"l", -4 * rels_stroke_width, -2 * rels_stroke_width, |
| 96 | 94 |
"l", 0, 4 * rels_stroke_width, "z"]) |
| 97 | 95 |
.attr({stroke: "none",
|
| ... | ... | |
| 104 | 102 | |
| 105 | 103 |
function getProgressLinesArray() {
|
| 106 | 104 |
var arr = new Array(); |
| 107 |
var today_left = $('#today_line').position().left;
|
|
| 105 |
var today_left = $('#today_line').position().left + $("#gantt_area").scrollLeft();
|
|
| 108 | 106 |
arr.push({left: today_left, top: 0});
|
| 109 | 107 |
$.each($('div.issue-subject, div.version-name'), function(index, element) {
|
| 110 | 108 |
if(!$(element).is(':visible')) return true;
|
| 111 |
var t = $(element).position().top - draw_top ;
|
|
| 109 |
var t = $(element).offset().top - draw_top ;
|
|
| 112 | 110 |
var h = ($(element).height() / 9); |
| 113 | 111 |
var element_top_upper = t - h; |
| 114 | 112 |
var element_top_center = t + (h * 3); |
| ... | ... | |
| 125 | 123 |
arr.push({left: draw_right, top: element_top_upper, is_right_edge: true});
|
| 126 | 124 |
arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true});
|
| 127 | 125 |
} else if (issue_done.length > 0) {
|
| 128 |
var done_left = issue_done.first().position().left + |
|
| 129 |
issue_done.first().width(); |
|
| 126 |
var done_left = today_left; |
|
| 127 |
var issue_todo = $("#task-todo-" + $(element).attr("id"));
|
|
| 128 |
if (issue_todo.length > 0){
|
|
| 129 |
done_left = issue_todo.first().position().left; |
|
| 130 |
} |
|
| 130 | 131 |
arr.push({left: done_left, top: element_top_center});
|
| 131 | 132 |
} else if (is_behind_start) {
|
| 132 | 133 |
arr.push({left: 0 , top: element_top_upper, is_left_edge: true});
|
| ... | ... | |
| 145 | 146 |
} |
| 146 | 147 | |
| 147 | 148 |
function drawGanttProgressLines() {
|
| 149 |
if(!$("#today_line").length) return;
|
|
| 148 | 150 |
var arr = getProgressLinesArray(); |
| 149 | 151 |
var color = $("#today_line")
|
| 150 | 152 |
.css("border-left-color");
|
| ... | ... | |
| 154 | 156 |
(!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) &&
|
| 155 | 157 |
!("is_left_edge" in arr[i - 1] && "is_left_edge" in arr[i]))
|
| 156 | 158 |
) {
|
| 157 |
var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left;
|
|
| 158 |
var x2 = (arr[i].left == 0) ? 0 : arr[i].left + draw_left;
|
|
| 159 |
var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left; |
|
| 160 |
var x2 = (arr[i].left == 0) ? 0 : arr[i].left; |
|
| 159 | 161 |
draw_gantt.path(["M", x1, arr[i - 1].top, |
| 160 | 162 |
"L", x2, arr[i].top]) |
| 161 | 163 |
.attr({stroke: color, "stroke-width": 2});
|
| ... | ... | |
| 301 | 303 |
$('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true);
|
| 302 | 304 |
}); |
| 303 | 305 |
} |
| 306 | ||
| 307 |
initGanttDnD = function(){
|
|
| 308 |
var grid_x = 0; |
|
| 309 |
if($('#zoom').length){
|
|
| 310 |
switch(parseInt($('#zoom').val())){
|
|
| 311 |
case 4: |
|
| 312 |
grid_x = 16; |
|
| 313 |
break; |
|
| 314 |
case 3: |
|
| 315 |
grid_x = 8; |
|
| 316 |
break; |
|
| 317 |
} |
|
| 318 |
} |
|
| 319 |
if(grid_x > 0){
|
|
| 320 |
$('.leaf .task_todo').draggable({
|
|
| 321 |
containment: 'parent', |
|
| 322 |
axis: 'x', |
|
| 323 |
grid: [grid_x, 0], |
|
| 324 |
opacity: 0.5, |
|
| 325 |
cursor: 'move', |
|
| 326 |
revertDuration: 100, |
|
| 327 |
start: function (event, ui) {
|
|
| 328 |
var helper = ui.helper[0]; |
|
| 329 |
helper.startLeft = ui.position.left; |
|
| 330 |
}, |
|
| 331 |
}); |
|
| 332 | ||
| 333 |
$('.task.line').droppable({
|
|
| 334 |
accept: '.leaf .task_todo', |
|
| 335 |
drop: function (event, ui) {
|
|
| 336 |
var target = $(ui.draggable); |
|
| 337 |
var url = target.attr('data-url-change-duration');
|
|
| 338 |
var object = JSON.parse(target.attr('data-object'));
|
|
| 339 |
var startLeft = target[0].startLeft; |
|
| 340 |
var relative_days = Math.floor((ui.position.left - startLeft) / grid_x); |
|
| 341 |
if(relative_days == 0){
|
|
| 342 |
return; |
|
| 343 |
} |
|
| 344 |
var start_date = new Date(object.start_date); |
|
| 345 |
start_date.setDate(start_date.getDate() + relative_days); |
|
| 346 |
start_date = |
|
| 347 |
[ |
|
| 348 |
start_date.getFullYear(), |
|
| 349 |
('0' + (start_date.getMonth() + 1)).slice(-2),
|
|
| 350 |
('0' + start_date.getDate()).slice(-2),
|
|
| 351 |
].join('-');
|
|
| 352 |
var due_date = null; |
|
| 353 |
if(object.due_date != null){
|
|
| 354 |
due_date = new Date(object.due_date); |
|
| 355 |
due_date.setDate(due_date.getDate() + relative_days); |
|
| 356 |
due_date = |
|
| 357 |
[ |
|
| 358 |
due_date.getFullYear(), |
|
| 359 |
('0' + (due_date.getMonth() + 1)).slice(-2),
|
|
| 360 |
('0' + due_date.getDate()).slice(-2),
|
|
| 361 |
].join('-');
|
|
| 362 |
} |
|
| 363 | ||
| 364 |
$('#selected_c option:not(:disabled)').prop('selected', true);
|
|
| 365 |
var form = $('#query_form').serializeArray();
|
|
| 366 |
var json_param = {};
|
|
| 367 |
form.forEach(function(data){
|
|
| 368 |
var key = data.name; |
|
| 369 |
var value = data.value; |
|
| 370 |
if(/\[\]$/.test(key)){
|
|
| 371 |
if(!json_param.hasOwnProperty(key)){
|
|
| 372 |
json_param[key] = []; |
|
| 373 |
} |
|
| 374 |
json_param[key].push(value); |
|
| 375 |
} |
|
| 376 |
else{
|
|
| 377 |
json_param[key] = value; |
|
| 378 |
} |
|
| 379 |
}); |
|
| 380 |
$('#selected_c option:not(:disabled)').prop('selected', false);
|
|
| 381 |
Object.assign(json_param, {
|
|
| 382 |
change_duration: {
|
|
| 383 |
start_date: start_date, |
|
| 384 |
due_date: due_date, |
|
| 385 |
lock_version: object.lock_version, |
|
| 386 |
}, |
|
| 387 |
}); |
|
| 388 | ||
| 389 |
$.ajax({
|
|
| 390 |
type: 'PUT', |
|
| 391 |
url: url, |
|
| 392 |
data: json_param, |
|
| 393 |
}).done(function(data){
|
|
| 394 |
drawGanttHandler(); |
|
| 395 |
initGanttDnD(); |
|
| 396 |
}).fail(function(jqXHR){
|
|
| 397 |
var contents = $('<div>' + jqXHR.responseText + '</div>');
|
|
| 398 |
var error_message = contents.find('p#errorExplanation');
|
|
| 399 |
if(error_message.length){
|
|
| 400 |
$('div#content h2:first-of-type').after(error_message);
|
|
| 401 |
$('p#errorExplanation').hide('fade', {}, 3000, function(){
|
|
| 402 |
$(this).remove(); |
|
| 403 |
}); |
|
| 404 |
} |
|
| 405 |
ui.draggable.animate({'left': ui.draggable[0].startLeft}, 'fast');
|
|
| 406 |
}); |
|
| 407 |
} |
|
| 408 |
}); |
|
| 409 |
} |
|
| 410 |
}; |
|
| 411 | ||
| 412 |
$(document).ready(initGanttDnD); |
|
| public/stylesheets/application.css | ||
|---|---|---|
| 1410 | 1410 | |
| 1411 | 1411 |
.task {
|
| 1412 | 1412 |
position: absolute; |
| 1413 |
height:8px;
|
|
| 1413 |
height:10px;
|
|
| 1414 | 1414 |
font-size:0.8em; |
| 1415 | 1415 |
color:#888; |
| 1416 | 1416 |
padding:0; |
| ... | ... | |
| 1419 | 1419 |
white-space:nowrap; |
| 1420 | 1420 |
} |
| 1421 | 1421 | |
| 1422 |
.task.label {width:100%;}
|
|
| 1423 |
.task.label.project, .task.label.version { font-weight: bold; }
|
|
| 1422 |
.task.line { left: 0; }
|
|
| 1423 |
.task div.tooltip:hover span.tip { font-size: inherit; }
|
|
| 1424 |
.task .task_todo .label { font-size: inherit; }
|
|
| 1425 |
.task.project .task_todo .label { margin-top: -4px; }
|
|
| 1426 |
.task.version .task_todo .label { margin-top: -3px; }
|
|
| 1424 | 1427 | |
| 1425 |
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
|
|
| 1426 |
.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
|
|
| 1427 |
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
|
|
| 1428 |
.task .label { position: absolute; width: auto; }
|
|
| 1429 |
.task.project .label, .task.version .label { font-weight: bold; }
|
|
| 1428 | 1430 | |
| 1429 |
.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
|
|
| 1430 |
.task_late.parent, .task_done.parent { height: 3px;}
|
|
| 1431 |
.task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
|
|
| 1432 |
.task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
|
|
| 1431 |
.task_late { position: absolute; height: inherit; background:#f66; }
|
|
| 1432 |
.task_done { position: absolute; height: inherit; background:#00c600; }
|
|
| 1433 |
.task_todo { position: absolute; height: inherit; background:#aaa; }
|
|
| 1433 | 1434 | |
| 1434 |
.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
|
|
| 1435 |
.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
|
|
| 1436 |
.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
|
|
| 1437 |
.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
|
|
| 1435 |
.parent .task_todo { background: #888; height: 5px; }
|
|
| 1436 |
.parent .task_late, .parent .task_done { height: 5px; }
|
|
| 1437 |
.parent .marker {
|
|
| 1438 |
background: #888; |
|
| 1439 |
display: inline-block; |
|
| 1440 |
position: absolute; |
|
| 1441 |
width: 8px; |
|
| 1442 |
height: 6px; |
|
| 1443 |
margin-left: -5px; |
|
| 1444 |
margin-bottom: -4px; |
|
| 1445 |
} |
|
| 1446 |
.parent .marker:after {
|
|
| 1447 |
border-top: 3px solid #888; |
|
| 1448 |
border-left: 4px solid transparent; |
|
| 1449 |
border-right: 4px solid transparent; |
|
| 1450 |
content: ''; |
|
| 1451 |
height: 0; |
|
| 1452 |
left: 0; |
|
| 1453 |
position: absolute; |
|
| 1454 |
bottom: -3px; |
|
| 1455 |
width: 0; |
|
| 1456 |
} |
|
| 1457 | ||
| 1458 |
.version .task_late { background:#f66; height: 4px; }
|
|
| 1459 |
.version .task_done { background:#00c600; height: 4px; }
|
|
| 1460 |
.version .task_todo { background:#aaa; height: 4px; margin-top: 3px; }
|
|
| 1461 |
.version .marker {
|
|
| 1462 |
width: 0; |
|
| 1463 |
height: 0; |
|
| 1464 |
border: 5px solid transparent; |
|
| 1465 |
border-bottom-color: black; |
|
| 1466 |
position: absolute; |
|
| 1467 |
margin-top: -5px; |
|
| 1468 |
margin-left: -6px; |
|
| 1469 |
} |
|
| 1470 |
.version .marker:after {
|
|
| 1471 |
content: ''; |
|
| 1472 |
position: absolute; |
|
| 1473 |
left: -5px; |
|
| 1474 |
top: 5px; |
|
| 1475 |
width: 0; |
|
| 1476 |
height: 0; |
|
| 1477 |
border: 5px solid transparent; |
|
| 1478 |
border-top-color: black; |
|
| 1479 |
} |
|
| 1438 | 1480 | |
| 1439 |
.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
|
|
| 1440 |
.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
|
|
| 1441 |
.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
|
|
| 1442 |
.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
|
|
| 1481 |
.project .task_late { background:#f66; height: 2px; }
|
|
| 1482 |
.project .task_done { background:#00c600; height: 2px; }
|
|
| 1483 |
.project .task_todo { background:#aaa; height: 2px; margin-top: 4px; }
|
|
| 1484 |
.project .marker {
|
|
| 1485 |
width: 0; |
|
| 1486 |
height: 0; |
|
| 1487 |
border: 5px solid transparent; |
|
| 1488 |
border-bottom-color: blue; |
|
| 1489 |
position: absolute; |
|
| 1490 |
margin-top: -5px; |
|
| 1491 |
margin-left: -6px; |
|
| 1492 |
} |
|
| 1493 |
.project .marker:after {
|
|
| 1494 |
content: ''; |
|
| 1495 |
position: absolute; |
|
| 1496 |
left: -5px; |
|
| 1497 |
top: 5px; |
|
| 1498 |
width: 0; |
|
| 1499 |
height: 0; |
|
| 1500 |
border: 5px solid transparent; |
|
| 1501 |
border-top-color: blue; |
|
| 1502 |
} |
|
| 1443 | 1503 | |
| 1444 | 1504 |
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
|
| 1445 | 1505 |
.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
|
| test/integration/routing/gantts_test.rb | ||
|---|---|---|
| 26 | 26 | |
| 27 | 27 |
should_route 'GET /projects/foo/issues/gantt' => 'gantts#show', :project_id => 'foo' |
| 28 | 28 |
should_route 'GET /projects/foo/issues/gantt.pdf' => 'gantts#show', :project_id => 'foo', :format => 'pdf' |
| 29 | ||
| 30 |
should_route 'PUT /gantt/123/change_duration' => 'gantts#change_duration', :id => '123' |
|
| 29 | 31 |
end |
| 30 | 32 |
end |
| test/unit/lib/redmine/helpers/gantt_test.rb | ||
|---|---|---|
| 235 | 235 |
@project.issues << @issue |
| 236 | 236 |
@output_buffer = @gantt.lines |
| 237 | 237 | |
| 238 |
assert_select "div.project.task_todo" |
|
| 239 |
assert_select "div.project.starting" |
|
| 240 |
assert_select "div.project.ending" |
|
| 241 |
assert_select "div.label.project", /#{@project.name}/
|
|
| 238 |
assert_select "div.task.project" do |
|
| 239 |
assert_select "> div.task_todo" do |
|
| 240 |
assert_select "> div.label", /#{@project.name}/
|
|
| 241 |
end |
|
| 242 |
assert_select "> div.starting" |
|
| 243 |
assert_select "> div.ending" |
|
| 244 |
end |
|
| 242 | 245 | |
| 243 |
assert_select "div.version.task_todo" |
|
| 244 |
assert_select "div.version.starting" |
|
| 245 |
assert_select "div.version.ending" |
|
| 246 |
assert_select "div.label.version", /#{@version.name}/
|
|
| 246 |
assert_select "div.task.version" do |
|
| 247 |
assert_select "> div.task_todo" do |
|
| 248 |
assert_select "div.label", /#{@version.name}/
|
|
| 249 |
end |
|
| 250 |
assert_select "> div.starting" |
|
| 251 |
assert_select "> div.ending" |
|
| 252 |
end |
|
| 247 | 253 | |
| 248 |
assert_select "div.task_todo" |
|
| 249 |
assert_select "div.task.label", /#{@issue.done_ratio}/
|
|
| 250 |
assert_select "div.tooltip", /#{@issue.subject}/
|
|
| 254 |
assert_select "div.task" do |
|
| 255 |
assert_select "> div.task_todo" do |
|
| 256 |
assert_select "> div.label", /#{@issue.done_ratio}/
|
|
| 257 |
assert_select "> div.tooltip", /#{@issue.subject}/
|
|
| 258 |
end |
|
| 259 |
end |
|
| 251 | 260 |
end |
| 252 | 261 | |
| 253 | 262 |
test "#selected_column_content" do |
| ... | ... | |
| 331 | 340 |
@project.stubs(:start_date).returns(today - 7) |
| 332 | 341 |
@project.stubs(:due_date).returns(today + 7) |
| 333 | 342 |
@output_buffer = @gantt.line_for_project(@project, :format => :html) |
| 334 |
assert_select "div.project.label", :text => @project.name |
|
| 343 |
assert_select "div.task.project > div.task_todo" do |
|
| 344 |
assert_select "> div.label", :text => @project.name |
|
| 345 |
end |
|
| 335 | 346 |
end |
| 336 | 347 | |
| 337 | 348 |
test "#line_for_version" do |
| ... | ... | |
| 341 | 352 |
version.stubs(:due_date).returns(today + 7) |
| 342 | 353 |
version.stubs(:visible_fixed_issues => stub(:completed_percent => 30)) |
| 343 | 354 |
@output_buffer = @gantt.line_for_version(version, :format => :html) |
| 344 |
assert_select "div.version.label", :text => /Foo/ |
|
| 345 |
assert_select "div.version.label", :text => /30%/ |
|
| 355 |
assert_select "div.task.version > div.task_todo" do |
|
| 356 |
assert_select "> div.label", :text => 'Foo 30%' |
|
| 357 |
end |
|
| 346 | 358 |
end |
| 347 | 359 | |
| 348 | 360 |
test "#line_for_issue" do |
| 349 | 361 |
create_gantt |
| 350 | 362 |
issue = Issue.generate!(:project => @project, :start_date => today - 7, :due_date => today + 7, :done_ratio => 30) |
| 351 | 363 |
@output_buffer = @gantt.line_for_issue(issue, :format => :html) |
| 352 |
assert_select "div.task.label", :text => /#{issue.status.name}/
|
|
| 353 |
assert_select "div.task.label", :text => /30%/ |
|
| 354 |
assert_select "div.tooltip", /#{issue.subject}/
|
|
| 364 |
assert_select "div.task_todo" do |
|
| 365 |
assert_select "> div.label", :text => "#{issue.status.name} 30%"
|
|
| 366 |
assert_select "> div.tooltip", /#{issue.subject}/
|
|
| 367 |
end |
|
| 355 | 368 |
end |
| 356 | 369 | |
| 357 | 370 |
test "#line todo line should start from the starting point on the left" do |
| ... | ... | |
| 365 | 378 |
[gantt_start - 1, gantt_start].each do |start_date| |
| 366 | 379 |
@output_buffer = @gantt.line(start_date, gantt_start, 30, false, 'line', :format => :html, :zoom => 4) |
| 367 | 380 |
# the leftmost date (Date.today - 14 days) |
| 368 |
assert_select 'div.task_todo[style*="left:0px"]', 1, @output_buffer |
|
| 369 |
assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer |
|
| 381 |
assert_select 'div.task_todo[style*="left:0px"][style*="width:4px"]', 1, @output_buffer |
|
| 370 | 382 |
end |
| 371 | 383 |
end |
| 372 | 384 | |
| ... | ... | |
| 375 | 387 |
[gantt_end, gantt_end + 1].each do |end_date| |
| 376 | 388 |
@output_buffer = @gantt.line(gantt_end, end_date, 30, false, 'line', :format => :html, :zoom => 4) |
| 377 | 389 |
# the rightmost date (Date.today + 14 days) |
| 378 |
assert_select 'div.task_todo[style*="left:112px"]', 1, @output_buffer |
|
| 379 |
assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer |
|
| 390 |
assert_select 'div.task_todo[style*="left:112px"][style*="width:4px"]', 1, @output_buffer |
|
| 380 | 391 |
end |
| 381 | 392 |
end |
| 382 | 393 | |
| 383 | 394 |
test "#line todo line should be the total width" do |
| 384 | 395 |
create_gantt |
| 385 | 396 |
@output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) |
| 386 |
assert_select 'div.task_todo[style*="width:58px"]', 1
|
|
| 397 |
assert_select 'div.task_todo[style*="width:60px"]', 1
|
|
| 387 | 398 |
end |
| 388 | 399 | |
| 389 | 400 |
test "#line late line should start from the starting point on the left" do |
| 390 | 401 |
create_gantt |
| 391 | 402 |
@output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) |
| 392 |
assert_select 'div.task_late[style*="left:28px"]', 1 |
|
| 403 |
assert_select 'div.task_todo[style*="left:28px"]' do |
|
| 404 |
assert_select '> div.task_late', 1 |
|
| 405 |
end |
|
| 393 | 406 |
end |
| 394 | 407 | |
| 395 | 408 |
test "#line late line should be the total delayed width" do |
| 396 | 409 |
create_gantt |
| 397 | 410 |
@output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) |
| 398 |
assert_select 'div.task_late[style*="width:30px"]', 1
|
|
| 411 |
assert_select 'div.task_late[style*="width:32px"]', 1
|
|
| 399 | 412 |
end |
| 400 | 413 | |
| 401 | 414 |
test "#line late line should be the same width as task_todo if start date and end date are the same day" do |
| 402 | 415 |
create_gantt |
| 403 | 416 |
@output_buffer = @gantt.line(today - 7, today - 7, 0, false, 'line', :format => :html, :zoom => 4) |
| 404 |
assert_select 'div.task_late[style*="width:2px"]', 1 |
|
| 405 |
assert_select 'div.task_todo[style*="width:2px"]', 1 |
|
| 417 |
assert_select 'div.task_todo[style*="width:4px"]' do |
|
| 418 |
assert_select '> div.task_late[style*="width:4px"]', 1 |
|
| 419 |
end |
|
| 406 | 420 |
end |
| 407 | 421 | |
| 408 | 422 |
test "#line late line should be the same width as task_todo if start date and today are the same day" do |
| 409 | 423 |
create_gantt |
| 410 | 424 |
@output_buffer = @gantt.line(today, today, 0, false, 'line', :format => :html, :zoom => 4) |
| 411 |
assert_select 'div.task_late[style*="width:2px"]', 1 |
|
| 412 |
assert_select 'div.task_todo[style*="width:2px"]', 1 |
|
| 425 |
assert_select 'div.task_todo[style*="width:4px"]' do |
|
| 426 |
assert_select '> div.task_late[style*="width:4px"]', 1 |
|
| 427 |
end |
|
| 413 | 428 |
end |
| 414 | 429 | |
| 415 | 430 |
test "#line done line should start from the starting point on the left" do |
| 416 | 431 |
create_gantt |
| 417 | 432 |
@output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) |
| 418 |
assert_select 'div.task_done[style*="left:28px"]', 1 |
|
| 433 |
assert_select 'div.task_todo[style*="left:28px"]' do |
|
| 434 |
assert_select '> div.task_done', 1 |
|
| 435 |
end |
|
| 419 | 436 |
end |
| 420 | 437 | |
| 421 | 438 |
test "#line done line should be the width for the done ratio" do |
| 422 | 439 |
create_gantt |
| 423 | 440 |
@output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) |
| 424 |
# 15 days * 4 px * 30% - 2 px for borders = 16 px
|
|
| 425 |
assert_select 'div.task_done[style*="width:16px"]', 1
|
|
| 441 |
# 15 days * 4 px * 30% = 18 px
|
|
| 442 |
assert_select 'div.task_done[style*="width:18px"]', 1
|
|
| 426 | 443 |
end |
| 427 | 444 | |
| 428 | 445 |
test "#line done line should be the total width for 100% done ratio" do |
| 429 | 446 |
create_gantt |
| 430 | 447 |
@output_buffer = @gantt.line(today - 7, today + 7, 100, false, 'line', :format => :html, :zoom => 4) |
| 431 |
# 15 days * 4 px - 2 px for borders = 58 px
|
|
| 432 |
assert_select 'div.task_done[style*="width:58px"]', 1
|
|
| 448 |
# 15 days * 4 px = 60 px
|
|
| 449 |
assert_select 'div.task_done[style*="width:60px"]', 1
|
|
| 433 | 450 |
end |
| 434 | 451 | |
| 435 | 452 |
test "#line done line should be the total width for 100% done ratio with same start and end dates" do |
| 436 | 453 |
create_gantt |
| 437 | 454 |
@output_buffer = @gantt.line(today + 7, today + 7, 100, false, 'line', :format => :html, :zoom => 4) |
| 438 |
assert_select 'div.task_done[style*="width:2px"]', 1
|
|
| 455 |
assert_select 'div.task_done[style*="width:4px"]', 1
|
|
| 439 | 456 |
end |
| 440 | 457 | |
| 441 | 458 |
test "#line done line should not be the total done width if the gantt starts after start date" do |
| 442 | 459 |
create_gantt |
| 443 | 460 |
@output_buffer = @gantt.line(today - 16, today - 2, 30, false, 'line', :format => :html, :zoom => 4) |
| 444 |
assert_select 'div.task_done[style*="left:0px"]', 1 |
|
| 445 |
assert_select 'div.task_done[style*="width:8px"]', 1 |
|
| 461 |
assert_select 'div.task_todo[style*="left:0px"]' do |
|
| 462 |
assert_select '> div.task_done[style*="width:10px"]', 1 |
|
| 463 |
end |
|
| 446 | 464 |
end |
| 447 | 465 | |
| 448 | 466 |
test "#line starting marker should appear at the start date" do |