Feature #2024 » 0001-refs-2024-integrate-gantt-change_duration.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 = duration_params |
|
| 68 |
unless @obj.save |
|
| 69 |
render_403(:message => @obj.errors.full_messages.join) |
|
| 70 |
raise ActiveRecord::Rollback |
|
| 71 |
end |
|
| 72 |
retrieve_query |
|
| 73 |
rescue ActiveRecord::StaleObjectError |
|
| 74 |
render_403(:message => :notice_issue_update_conflict) |
|
| 75 |
rescue ActiveRecord::RecordNotFound |
|
| 76 |
render_404 |
|
| 77 |
end |
|
| 78 |
end |
|
| 79 | ||
| 80 |
private |
|
| 81 | ||
| 82 |
def duration_params |
|
| 83 |
params.require(:change_duration).permit(:start_date, :due_date, :lock_version) |
|
| 84 |
end |
|
| 58 | 85 |
end |
| app/views/gantts/change_duration.js.erb | ||
|---|---|---|
| 1 |
var elm; |
|
| 2 |
<% |
|
| 3 |
gantt = Redmine::Helpers::Gantt.new(params) |
|
| 4 |
durations = gantt.duration(@obj, self, @query) |
|
| 5 |
durations.each do |duration| |
|
| 6 |
elm_subject = duration[:elm_subject] |
|
| 7 |
elm_todo = duration[:elm_todo] |
|
| 8 |
todo_context = duration[:todo_context] |
|
| 9 |
subject_content = duration[:subject_content] |
|
| 10 |
columns = duration[:columns] |
|
| 11 |
obj = duration[:obj] |
|
| 12 |
%> |
|
| 13 |
if ($('<%= raw(elm_subject) %>').length) {
|
|
| 14 |
$('<%= elm_todo %>').each(function (_, task) {
|
|
| 15 |
var el_parent = $(task).parent(); |
|
| 16 |
el_parent.html('<%= raw(todo_context) %>');
|
|
| 17 |
var number_of_rows = el_parent.attr('data-number-of-rows');
|
|
| 18 |
if (number_of_rows) {
|
|
| 19 |
el_parent.find('div[data-number-of-rows]').attr('data-number-of-rows', number_of_rows);
|
|
| 20 |
} |
|
| 21 |
}); |
|
| 22 |
$('<%= raw(elm_subject) %>').replaceWith('<%= raw(subject_content) %>');
|
|
| 23 |
<% columns.each do |column| -%> |
|
| 24 |
elm = $('div.gantt_selected_column_content #<%= column.name %>_issue_<%= obj.id %>');
|
|
| 25 |
if(elm.length) {
|
|
| 26 |
elm.html('<%= escape_javascript(column_content(column, obj)) %>');
|
|
| 27 |
} |
|
| 28 |
<% end -%> |
|
| 29 |
} |
|
| 30 |
<% end -%> |
|
| config/routes.rb | ||
|---|---|---|
| 63 | 63 | |
| 64 | 64 |
get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt' |
| 65 | 65 |
get '/issues/gantt', :to => 'gantts#show' |
| 66 |
put '/issues/gantt/:id/change_duration', :to => 'gantts#change_duration', :as => 'gantt_change_duration' |
|
| 66 | 67 | |
| 67 | 68 |
get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar' |
| 68 | 69 |
get '/issues/calendar', :to => 'calendars#show' |
| lib/redmine/helpers/gantt.rb | ||
|---|---|---|
| 629 | 629 |
pdf.Output |
| 630 | 630 |
end |
| 631 | 631 | |
| 632 |
def duration(issue, view, query) |
|
| 633 |
@view = view |
|
| 634 |
@query = query |
|
| 635 |
draw_obj = [] |
|
| 636 | ||
| 637 |
select_precedes = ->(issue) do |
|
| 638 |
issue.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to).each do |follows| |
|
| 639 |
next if draw_obj.include?(follows) |
|
| 640 | ||
| 641 |
while follows |
|
| 642 |
draw_obj.push(follows, follows.fixed_version, follows.project) |
|
| 643 |
select_precedes.call(follows) |
|
| 644 |
follows.children.each do |child| |
|
| 645 |
draw_obj.push(child, child.fixed_version, child.project) |
|
| 646 |
select_precedes.call(child) |
|
| 647 |
end |
|
| 648 |
follows = follows.parent |
|
| 649 |
end |
|
| 650 |
end |
|
| 651 |
end |
|
| 652 | ||
| 653 |
while issue |
|
| 654 |
draw_obj.push(issue, issue.fixed_version, issue.project) |
|
| 655 |
select_precedes.call(issue) |
|
| 656 |
issue = issue.parent |
|
| 657 |
end |
|
| 658 |
draw_objs = draw_obj.compact.uniq |
|
| 659 |
draw_objs.reject!{|obj| ![Project, Version, Issue].include?(obj.class)}
|
|
| 660 | ||
| 661 |
return_values = [] |
|
| 662 |
draw_objs.each do |obj| |
|
| 663 |
@number_of_rows = 0 |
|
| 664 |
@lines = +'' |
|
| 665 |
render_object_row(obj, {format: :html, only: :lines, zoom: 2 ** @zoom, top: 0, top_increment: 20})
|
|
| 666 |
todo_content = Nokogiri::HTML.parse(@lines) |
|
| 667 |
todo_context = todo_content.xpath( |
|
| 668 |
"//div[contains(@class, 'task') and contains(@class, 'line')]/*" |
|
| 669 |
).to_s.tr("\n", '').gsub("'", "\\\\'")
|
|
| 670 | ||
| 671 |
klass_name = obj.class.name.underscore |
|
| 672 |
elm_todo = "[id=task-todo-#{klass_name}-#{obj.id}]"
|
|
| 673 |
css_subject = 'span:not(.expander)' |
|
| 674 |
elm_subject = "[id=#{klass_name}-#{obj.id}] > #{css_subject}"
|
|
| 675 | ||
| 676 |
subject_content = Nokogiri::HTML.parse(html_subject_content(obj)) |
|
| 677 |
subject_content = subject_content.css(css_subject).to_s.tr("\n", '').gsub("'", "\\\\'")
|
|
| 678 | ||
| 679 |
columns = [] |
|
| 680 |
case obj |
|
| 681 |
when Issue |
|
| 682 |
columns = query.columns |
|
| 683 |
end |
|
| 684 |
return_values << {:elm_subject => elm_subject,
|
|
| 685 |
:elm_todo => elm_todo, |
|
| 686 |
:todo_context => todo_context, |
|
| 687 |
:subject_content => subject_content, |
|
| 688 |
:columns => columns, |
|
| 689 |
:obj => obj} |
|
| 690 |
end |
|
| 691 |
return_values |
|
| 692 |
end |
|
| 693 | ||
| 632 | 694 |
private |
| 633 | 695 | |
| 634 | 696 |
def coordinates(start_date, end_date, progress, zoom=nil) |
| ... | ... | |
| 774 | 836 |
tag_options[:class] = "version-name" |
| 775 | 837 |
has_children = object.fixed_issues.exists? |
| 776 | 838 |
when Project |
| 839 |
tag_options[:id] = "project-#{object.id}"
|
|
| 777 | 840 |
tag_options[:class] = "project-name" |
| 778 | 841 |
has_children = object.issues.exists? || object.versions.exists? |
| 779 | 842 |
end |
| ... | ... | |
| 856 | 919 |
end |
| 857 | 920 |
# Renders the task bar, with progress and late |
| 858 | 921 |
if coords[:bar_start] && coords[:bar_end] |
| 859 |
width = coords[:bar_end] - coords[:bar_start] - 2
|
|
| 922 |
width = coords[:bar_end] - coords[:bar_start] |
|
| 860 | 923 |
style = +"" |
| 861 |
style << "top:#{params[:top]}px;"
|
|
| 862 | 924 |
style << "left:#{coords[:bar_start]}px;"
|
| 863 | 925 |
style << "width:#{width}px;"
|
| 864 |
html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue)
|
|
| 865 |
html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
|
|
| 926 |
html_id = |
|
| 927 |
case object |
|
| 928 |
when Project |
|
| 929 |
"task-todo-project-#{object.id}"
|
|
| 930 |
when Version |
|
| 931 |
"task-todo-version-#{object.id}"
|
|
| 932 |
when Issue |
|
| 933 |
"task-todo-issue-#{object.id}"
|
|
| 934 |
end |
|
| 866 | 935 |
content_opt = {:style => style,
|
| 867 |
:class => "#{css} task_todo",
|
|
| 936 |
:class => "task_todo", |
|
| 868 | 937 |
:id => html_id, |
| 869 | 938 |
:data => {}}
|
| 870 | 939 |
if object.is_a?(Issue) |
| ... | ... | |
| 872 | 941 |
if rels.present? |
| 873 | 942 |
content_opt[:data] = {"rels" => rels.to_json}
|
| 874 | 943 |
end |
| 944 |
content_opt[:data].merge!({
|
|
| 945 |
:url_change_duration => Rails.application.routes.url_helpers.gantt_change_duration_path( |
|
| 946 |
object |
|
| 947 |
), |
|
| 948 |
:object => {
|
|
| 949 |
:start_date => object.start_date, |
|
| 950 |
:due_date => object.due_date, |
|
| 951 |
:lock_version => object.lock_version, |
|
| 952 |
}.to_json, |
|
| 953 |
}) |
|
| 875 | 954 |
end |
| 876 | 955 |
content_opt[:data].merge!(data_options) |
| 877 |
output << view.content_tag(:div, ' '.html_safe, content_opt)
|
|
| 956 |
bar_contents = []
|
|
| 878 | 957 |
if coords[:bar_late_end] |
| 879 |
width = coords[:bar_late_end] - coords[:bar_start] - 2
|
|
| 958 |
width = coords[:bar_late_end] - coords[:bar_start] |
|
| 880 | 959 |
style = +"" |
| 881 |
style << "top:#{params[:top]}px;"
|
|
| 882 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 883 | 960 |
style << "width:#{width}px;"
|
| 884 |
output << view.content_tag(:div, ' '.html_safe,
|
|
| 885 |
:style => style, |
|
| 886 |
:class => "#{css} task_late",
|
|
| 887 |
:data => data_options) |
|
| 961 |
bar_contents << view.content_tag(:div, ' '.html_safe,
|
|
| 962 |
:style => style,
|
|
| 963 |
:class => "#{css} task_late",
|
|
| 964 |
:data => data_options)
|
|
| 888 | 965 |
end |
| 889 | 966 |
if coords[:bar_progress_end] |
| 890 |
width = coords[:bar_progress_end] - coords[:bar_start] - 2
|
|
| 967 |
width = coords[:bar_progress_end] - coords[:bar_start] |
|
| 891 | 968 |
style = +"" |
| 892 |
style << "top:#{params[:top]}px;"
|
|
| 893 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 894 | 969 |
style << "width:#{width}px;"
|
| 895 | 970 |
html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue)
|
| 896 | 971 |
html_id = "task-done-version-#{object.id}" if object.is_a?(Version)
|
| 897 |
output << view.content_tag(:div, ' '.html_safe, |
|
| 972 |
bar_contents << view.content_tag(:div, ' '.html_safe, |
|
| 973 |
:style => style, |
|
| 974 |
:class => "task_done", |
|
| 975 |
:id => html_id, |
|
| 976 |
:data => data_options) |
|
| 977 |
end |
|
| 978 | ||
| 979 |
# Renders the tooltip |
|
| 980 |
if object.is_a?(Issue) |
|
| 981 |
s = view.content_tag(:span, |
|
| 982 |
view.render_issue_tooltip(object).html_safe, |
|
| 983 |
:class => "tip") |
|
| 984 |
s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection') |
|
| 985 |
style = +"" |
|
| 986 |
style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
|
|
| 987 |
style << "height:12px;" |
|
| 988 |
bar_contents << view.content_tag(:div, s.html_safe, |
|
| 989 |
:style => style, |
|
| 990 |
:class => "tooltip hascontextmenu", |
|
| 991 |
:data => data_options) |
|
| 992 |
end |
|
| 993 | ||
| 994 |
# Renders the label on the right |
|
| 995 |
if label |
|
| 996 |
style = +"" |
|
| 997 |
style << "top:0px;" |
|
| 998 |
style << "left:#{coords[:bar_end] - coords[:bar_start] + 8}px;"
|
|
| 999 |
style << "height:12px;" |
|
| 1000 |
bar_contents << view.content_tag(:div, label, |
|
| 1001 |
:style => style, |
|
| 1002 |
:class => "label", |
|
| 1003 |
:data => data_options) |
|
| 1004 |
end |
|
| 1005 | ||
| 1006 |
bar_contents = bar_contents.join.presence |
|
| 1007 |
output << view.content_tag(:div, (bar_contents || ' ').html_safe, content_opt) |
|
| 1008 |
else |
|
| 1009 |
# Renders the label on the right |
|
| 1010 |
if label |
|
| 1011 |
style = +"" |
|
| 1012 |
style << "top:1px;" |
|
| 1013 |
style << "left:#{(coords[:bar_end] || 0) + 8}px;"
|
|
| 1014 |
output << view.content_tag(:div, label, |
|
| 898 | 1015 |
:style => style, |
| 899 |
:class => "#{css} task_done",
|
|
| 900 | 1016 |
:id => html_id, |
| 1017 |
:class => "label", |
|
| 901 | 1018 |
:data => data_options) |
| 902 | 1019 |
end |
| 903 | 1020 |
end |
| ... | ... | |
| 905 | 1022 |
if markers |
| 906 | 1023 |
if coords[:start] |
| 907 | 1024 |
style = +"" |
| 908 |
style << "top:#{params[:top]}px;"
|
|
| 909 | 1025 |
style << "left:#{coords[:start]}px;"
|
| 910 |
style << "width:15px;" |
|
| 911 | 1026 |
output << view.content_tag(:div, ' '.html_safe, |
| 912 | 1027 |
:style => style, |
| 913 |
:class => "#{css} marker starting",
|
|
| 1028 |
:class => "marker starting", |
|
| 914 | 1029 |
:data => data_options) |
| 915 | 1030 |
end |
| 916 | 1031 |
if coords[:end] |
| 917 | 1032 |
style = +"" |
| 918 |
style << "top:#{params[:top]}px;"
|
|
| 919 | 1033 |
style << "left:#{coords[:end]}px;"
|
| 920 |
style << "width:15px;" |
|
| 921 | 1034 |
output << view.content_tag(:div, ' '.html_safe, |
| 922 | 1035 |
:style => style, |
| 923 |
:class => "#{css} marker ending",
|
|
| 1036 |
:class => "marker ending", |
|
| 924 | 1037 |
:data => data_options) |
| 925 | 1038 |
end |
| 926 | 1039 |
end |
| 927 |
# Renders the label on the right |
|
| 928 |
if label |
|
| 929 |
style = +"" |
|
| 930 |
style << "top:#{params[:top]}px;"
|
|
| 931 |
style << "left:#{(coords[:bar_end] || 0) + 8}px;"
|
|
| 932 |
style << "width:15px;" |
|
| 933 |
output << view.content_tag(:div, label, |
|
| 934 |
:style => style, |
|
| 935 |
:class => "#{css} label",
|
|
| 936 |
:data => data_options) |
|
| 937 |
end |
|
| 938 |
# Renders the tooltip |
|
| 939 |
if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end] |
|
| 940 |
s = view.content_tag(:span, |
|
| 941 |
view.render_issue_tooltip(object).html_safe, |
|
| 942 |
:class => "tip") |
|
| 943 |
s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', |
|
| 944 |
:value => object.id, :style => 'display:none;', |
|
| 945 |
:class => 'toggle-selection') |
|
| 946 |
style = +"" |
|
| 947 |
style << "position: absolute;" |
|
| 948 |
style << "top:#{params[:top]}px;"
|
|
| 949 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 950 |
style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
|
|
| 951 |
style << "height:12px;" |
|
| 952 |
output << view.content_tag(:div, s.html_safe, |
|
| 953 |
:style => style, |
|
| 954 |
:class => "tooltip hascontextmenu", |
|
| 955 |
:data => data_options) |
|
| 956 |
end |
|
| 1040 |
output = view.content_tag(:div, output.html_safe, |
|
| 1041 |
:class => "#{css} line",
|
|
| 1042 |
:style => "top:#{params[:top]}px;width:#{params[:g_width] - 1}px;",
|
|
| 1043 |
:data => data_options |
|
| 1044 |
) |
|
| 957 | 1045 |
@lines << output |
| 958 | 1046 |
output |
| 959 | 1047 |
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_done.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});
|
| ... | ... | |
| 303 | 305 |
$('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true);
|
| 304 | 306 |
}); |
| 305 | 307 |
} |
| 308 | ||
| 309 |
initGanttDnD = function() {
|
|
| 310 |
var grid_x = 0; |
|
| 311 |
if ($('#zoom').length) {
|
|
| 312 |
switch(parseInt($('#zoom').val())) {
|
|
| 313 |
case 4: |
|
| 314 |
grid_x = 16; |
|
| 315 |
break; |
|
| 316 |
case 3: |
|
| 317 |
grid_x = 8; |
|
| 318 |
break; |
|
| 319 |
} |
|
| 320 |
} |
|
| 321 |
if (grid_x > 0) {
|
|
| 322 |
$('.leaf .task_todo').draggable({
|
|
| 323 |
containment: 'parent', |
|
| 324 |
axis: 'x', |
|
| 325 |
grid: [grid_x, 0], |
|
| 326 |
opacity: 0.5, |
|
| 327 |
cursor: 'move', |
|
| 328 |
revertDuration: 100, |
|
| 329 |
start: function (_, ui) {
|
|
| 330 |
var helper = ui.helper[0]; |
|
| 331 |
helper.startLeft = ui.position.left; |
|
| 332 |
}, |
|
| 333 |
}); |
|
| 334 | ||
| 335 |
$('.task.line').droppable({
|
|
| 336 |
accept: '.leaf .task_todo', |
|
| 337 |
drop: function (event, ui) {
|
|
| 338 |
var target = $(ui.draggable); |
|
| 339 |
var url = target.attr('data-url-change-duration');
|
|
| 340 |
var object = JSON.parse(target.attr('data-object'));
|
|
| 341 |
var startLeft = target[0].startLeft; |
|
| 342 |
var relative_days = Math.floor((ui.position.left - startLeft) / grid_x); |
|
| 343 |
if (relative_days == 0) {
|
|
| 344 |
return; |
|
| 345 |
} |
|
| 346 |
var start_date = new Date(object.start_date); |
|
| 347 |
start_date.setDate(start_date.getDate() + relative_days); |
|
| 348 |
start_date = |
|
| 349 |
[ |
|
| 350 |
start_date.getFullYear(), |
|
| 351 |
('0' + (start_date.getMonth() + 1)).slice(-2),
|
|
| 352 |
('0' + start_date.getDate()).slice(-2)
|
|
| 353 |
].join('-');
|
|
| 354 |
var due_date = null; |
|
| 355 |
if (object.due_date !== null) {
|
|
| 356 |
due_date = new Date(object.due_date); |
|
| 357 |
due_date.setDate(due_date.getDate() + relative_days); |
|
| 358 |
due_date = |
|
| 359 |
[ |
|
| 360 |
due_date.getFullYear(), |
|
| 361 |
('0' + (due_date.getMonth() + 1)).slice(-2),
|
|
| 362 |
('0' + due_date.getDate()).slice(-2)
|
|
| 363 |
].join('-');
|
|
| 364 |
} |
|
| 365 | ||
| 366 |
$('#selected_c option:not(:disabled)').prop('selected', true);
|
|
| 367 |
var form = $('#query_from').serializeArray();
|
|
| 368 |
var json_param = {};
|
|
| 369 |
form.forEach(function (data) {
|
|
| 370 |
var key = data.name; |
|
| 371 |
var value = data.value; |
|
| 372 |
if (/\[\]$/.test(key)) {
|
|
| 373 |
if (!json_param.hasOwnProperty(key)) {
|
|
| 374 |
json_param[key] = []; |
|
| 375 |
} |
|
| 376 |
json_param[key].push(value); |
|
| 377 |
} else {
|
|
| 378 |
json_param[key] = value; |
|
| 379 |
} |
|
| 380 |
}); |
|
| 381 |
$('#selected_c option:not(:disabled)').prop('selected', false);
|
|
| 382 |
Object.assign(json_param, {
|
|
| 383 |
change_duration: {
|
|
| 384 |
start_date: start_date, |
|
| 385 |
due_date: due_date, |
|
| 386 |
lock_version: object.lock_version, |
|
| 387 |
}, |
|
| 388 |
}); |
|
| 389 | ||
| 390 |
$.ajax({
|
|
| 391 |
type: 'PUT', |
|
| 392 |
url: url, |
|
| 393 |
data: json_param, |
|
| 394 |
dataType: 'script', |
|
| 395 |
}).done(function (_) {
|
|
| 396 |
drawGanttHandler(); |
|
| 397 |
initGanttDnD(); |
|
| 398 |
}).fail(function (jqXHR) {
|
|
| 399 |
var contents = $('<div>' + jqXHR.responseText + '</div>');
|
|
| 400 |
var error_message = contents.find('p#errorExplanation');
|
|
| 401 |
if (error_message.length) {
|
|
| 402 |
$('div#content h2:first-of-type').after(error_message);
|
|
| 403 |
$('p#errorExplanation').hide('fade', {}, 3000, function() {
|
|
| 404 |
$(this).remove(); |
|
| 405 |
}); |
|
| 406 |
} |
|
| 407 |
ui.draggable.animate({left: ui.draggable[0].startLeft}, 'fast');
|
|
| 408 |
}); |
|
| 409 |
} |
|
| 410 |
}); |
|
| 411 |
} |
|
| 412 |
}; |
|
| 413 | ||
| 414 |
$(document).ready(initGanttDnD); |
|
| public/stylesheets/application.css | ||
|---|---|---|
| 1496 | 1496 | |
| 1497 | 1497 |
.task {
|
| 1498 | 1498 |
position: absolute; |
| 1499 |
height:8px;
|
|
| 1499 |
height:10px;
|
|
| 1500 | 1500 |
font-size:0.8em; |
| 1501 | 1501 |
color:#888; |
| 1502 | 1502 |
padding:0; |
| ... | ... | |
| 1505 | 1505 |
white-space:nowrap; |
| 1506 | 1506 |
} |
| 1507 | 1507 | |
| 1508 |
.task.label {width:100%;}
|
|
| 1509 |
.task.label.project, .task.label.version { font-weight: bold; }
|
|
| 1508 |
.task.line { left: 0; }
|
|
| 1509 |
.task div.tooltip:hover span.tip { font-size: inherit; }
|
|
| 1510 |
.task .task_todo .label { font-size: inherit; }
|
|
| 1511 |
.task.project .task_todo .label { margin-top: -4px; }
|
|
| 1512 |
.task.version .task_todo .label { margin-top: -3px; }
|
|
| 1510 | 1513 | |
| 1511 |
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
|
|
| 1512 |
.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
|
|
| 1513 |
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
|
|
| 1514 |
.task .label { position: absolute; width: auto; }
|
|
| 1515 |
.task.project .label, .task.version .label { font-weight: bold; }
|
|
| 1514 | 1516 | |
| 1515 |
.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
|
|
| 1516 |
.task_late.parent, .task_done.parent { height: 3px;}
|
|
| 1517 |
.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;}
|
|
| 1518 |
.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;}
|
|
| 1517 |
.task_late { position: absolute; height: inherit; background:#f66; }
|
|
| 1518 |
.task_done { position: absolute; height: inherit; background:#00c600; }
|
|
| 1519 |
.task_todo { position: absolute; height: inherit; background:#aaa; }
|
|
| 1519 | 1520 | |
| 1520 |
.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
|
|
| 1521 |
.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
|
|
| 1522 |
.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
|
|
| 1523 |
.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
|
|
| 1521 |
.parent .task_todo { background: #888; height: 5px; }
|
|
| 1522 |
.parent .task_late, .parent .task_done { height: 5px; }
|
|
| 1523 |
.parent .marker {
|
|
| 1524 |
background: #888; |
|
| 1525 |
display: inline-block; |
|
| 1526 |
position: absolute; |
|
| 1527 |
width: 8px; |
|
| 1528 |
height: 6px; |
|
| 1529 |
margin-left: -5px; |
|
| 1530 |
margin-bottom: -4px; |
|
| 1531 |
} |
|
| 1532 | ||
| 1533 |
.version .task_late { background:#f66; height: 4px; }
|
|
| 1534 |
.version .task_done { background:#00c600; height: 4px; }
|
|
| 1535 |
.version .task_todo { background:#aaa; height: 4px; margin-top: 3px; }
|
|
| 1536 |
.verison .marker {
|
|
| 1537 |
width: 0; |
|
| 1538 |
height: 0; |
|
| 1539 |
border: 5px solid trasnparent; |
|
| 1540 |
border-bottom-color: black; |
|
| 1541 |
position: absolute; |
|
| 1542 |
margin-top: -5px; |
|
| 1543 |
margin-left: -6px; |
|
| 1544 |
} |
|
| 1545 |
.version .marker:after {
|
|
| 1546 |
content: ''; |
|
| 1547 |
position: absolute; |
|
| 1548 |
left: -5px; |
|
| 1549 |
top: 5px; |
|
| 1550 |
width: 0; |
|
| 1551 |
height: 0; |
|
| 1552 |
border: 5px solid transparent; |
|
| 1553 |
border-top-color: black; |
|
| 1554 |
} |
|
| 1524 | 1555 | |
| 1525 |
.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
|
|
| 1526 |
.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
|
|
| 1527 |
.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
|
|
| 1528 |
.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
|
|
| 1556 |
.project .task_late { background:#f66; height: 2px; }
|
|
| 1557 |
.project .task_done { background:#00c600; height: 2px; }
|
|
| 1558 |
.project .task_todo { background:#aaa; height: 2px; margin-top: 4px; }
|
|
| 1559 |
.project .marker {
|
|
| 1560 |
width: 0; |
|
| 1561 |
height: 0; |
|
| 1562 |
border: 5px solid transparent; |
|
| 1563 |
border-bottom-color: blue; |
|
| 1564 |
position: absolute; |
|
| 1565 |
margin-top: -5px; |
|
| 1566 |
margin-left: -6px; |
|
| 1567 |
} |
|
| 1568 |
.project .marker:after {
|
|
| 1569 |
content: ''; |
|
| 1570 |
position: absolute; |
|
| 1571 |
left: -5px; |
|
| 1572 |
top: 5px; |
|
| 1573 |
width: 0; |
|
| 1574 |
height: 0; |
|
| 1575 |
border: 5px solid transparent; |
|
| 1576 |
border-top-color: blue; |
|
| 1577 |
} |
|
| 1529 | 1578 | |
| 1530 | 1579 |
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
|
| 1531 | 1580 |
.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 /issues/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", :text => /#{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 |
- « Previous
- 1
- …
- 33
- 34
- 35
- Next »