Revision 1298:4f746d8966dd lib/redmine/helpers
| lib/redmine/helpers/calendar.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| lib/redmine/helpers/diff.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| lib/redmine/helpers/gantt.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 23 | 23 |
include Redmine::I18n |
| 24 | 24 |
include Redmine::Utils::DateCalculation |
| 25 | 25 |
|
| 26 |
# Relation types that are rendered |
|
| 27 |
DRAW_TYPES = {
|
|
| 28 |
IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' },
|
|
| 29 |
IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
|
|
| 30 |
}.freeze |
|
| 31 |
|
|
| 26 | 32 |
# :nodoc: |
| 27 | 33 |
# Some utility methods for the PDF export |
| 28 | 34 |
class PDF |
| ... | ... | |
| 136 | 142 |
) |
| 137 | 143 |
end |
| 138 | 144 |
|
| 145 |
# Returns a hash of the relations between the issues that are present on the gantt |
|
| 146 |
# and that should be displayed, grouped by issue ids. |
|
| 147 |
def relations |
|
| 148 |
return @relations if @relations |
|
| 149 |
if issues.any? |
|
| 150 |
issue_ids = issues.map(&:id) |
|
| 151 |
@relations = IssueRelation. |
|
| 152 |
where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys). |
|
| 153 |
group_by(&:issue_from_id) |
|
| 154 |
else |
|
| 155 |
@relations = {}
|
|
| 156 |
end |
|
| 157 |
end |
|
| 158 |
|
|
| 139 | 159 |
# Return all the project nodes that will be displayed |
| 140 | 160 |
def projects |
| 141 | 161 |
return @projects if @projects |
| ... | ... | |
| 277 | 297 |
pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) |
| 278 | 298 |
end |
| 279 | 299 |
else |
| 280 |
ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date" |
|
| 281 | 300 |
'' |
| 282 | 301 |
end |
| 283 | 302 |
end |
| ... | ... | |
| 289 | 308 |
html_class << 'icon icon-package ' |
| 290 | 309 |
html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " |
| 291 | 310 |
html_class << (version.overdue? ? 'version-overdue' : '') |
| 311 |
html_class << ' version-closed' unless version.open? |
|
| 312 |
if version.start_date && version.due_date && version.completed_pourcent |
|
| 313 |
progress_date = calc_progress_date(version.start_date, |
|
| 314 |
version.due_date, version.completed_pourcent) |
|
| 315 |
html_class << ' behind-start-date' if progress_date < self.date_from |
|
| 316 |
html_class << ' over-end-date' if progress_date > self.date_to |
|
| 317 |
end |
|
| 292 | 318 |
s = view.link_to_version(version).html_safe |
| 293 | 319 |
subject = view.content_tag(:span, s, |
| 294 | 320 |
:class => html_class).html_safe |
| 295 |
html_subject(options, subject, :css => "version-name") |
|
| 321 |
html_subject(options, subject, :css => "version-name", |
|
| 322 |
:id => "version-#{version.id}")
|
|
| 296 | 323 |
when :image |
| 297 | 324 |
image_subject(options, version.to_s_with_project) |
| 298 | 325 |
when :pdf |
| ... | ... | |
| 303 | 330 |
|
| 304 | 331 |
def line_for_version(version, options) |
| 305 | 332 |
# Skip versions that don't have a start_date |
| 306 |
if version.is_a?(Version) && version.start_date && version.due_date
|
|
| 333 |
if version.is_a?(Version) && version.due_date && version.start_date
|
|
| 307 | 334 |
options[:zoom] ||= 1 |
| 308 | 335 |
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] |
| 309 | 336 |
coords = coordinates(version.start_date, |
| 310 |
version.due_date, version.completed_pourcent,
|
|
| 337 |
version.due_date, version.completed_percent,
|
|
| 311 | 338 |
options[:zoom]) |
| 312 |
label = "#{h version} #{h version.completed_pourcent.to_i.to_s}%"
|
|
| 339 |
label = "#{h version} #{h version.completed_percent.to_i.to_s}%"
|
|
| 313 | 340 |
label = h("#{version.project} -") + label unless @project && @project == version.project
|
| 314 | 341 |
case options[:format] |
| 315 | 342 |
when :html |
| 316 |
html_task(options, coords, :css => "version task", :label => label, :markers => true) |
|
| 343 |
html_task(options, coords, :css => "version task", |
|
| 344 |
:label => label, :markers => true, :version => version) |
|
| 317 | 345 |
when :image |
| 318 | 346 |
image_task(options, coords, :label => label, :markers => true, :height => 3) |
| 319 | 347 |
when :pdf |
| 320 | 348 |
pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) |
| 321 | 349 |
end |
| 322 | 350 |
else |
| 323 |
ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date" |
|
| 324 | 351 |
'' |
| 325 | 352 |
end |
| 326 | 353 |
end |
| ... | ... | |
| 336 | 363 |
css_classes << ' issue-overdue' if issue.overdue? |
| 337 | 364 |
css_classes << ' issue-behind-schedule' if issue.behind_schedule? |
| 338 | 365 |
css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to |
| 366 |
css_classes << ' issue-closed' if issue.closed? |
|
| 367 |
if issue.start_date && issue.due_before && issue.done_ratio |
|
| 368 |
progress_date = calc_progress_date(issue.start_date, |
|
| 369 |
issue.due_before, issue.done_ratio) |
|
| 370 |
css_classes << ' behind-start-date' if progress_date < self.date_from |
|
| 371 |
css_classes << ' over-end-date' if progress_date > self.date_to |
|
| 372 |
end |
|
| 339 | 373 |
s = "".html_safe |
| 340 | 374 |
if issue.assigned_to.present? |
| 341 | 375 |
assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name |
| ... | ... | |
| 347 | 381 |
s << view.link_to_issue(issue).html_safe |
| 348 | 382 |
subject = view.content_tag(:span, s, :class => css_classes).html_safe |
| 349 | 383 |
html_subject(options, subject, :css => "issue-subject", |
| 350 |
:title => issue.subject) + "\n" |
|
| 384 |
:title => issue.subject, :id => "issue-#{issue.id}") + "\n"
|
|
| 351 | 385 |
when :image |
| 352 | 386 |
image_subject(options, issue.subject) |
| 353 | 387 |
when :pdf |
| ... | ... | |
| 378 | 412 |
pdf_task(options, coords, :label => label) |
| 379 | 413 |
end |
| 380 | 414 |
else |
| 381 |
ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before" |
|
| 382 | 415 |
'' |
| 383 | 416 |
end |
| 384 | 417 |
end |
| ... | ... | |
| 611 | 644 |
coords[:bar_end] = self.date_to - self.date_from + 1 |
| 612 | 645 |
end |
| 613 | 646 |
if progress |
| 614 |
progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0)
|
|
| 647 |
progress_date = calc_progress_date(start_date, end_date, progress)
|
|
| 615 | 648 |
if progress_date > self.date_from && progress_date > start_date |
| 616 | 649 |
if progress_date < self.date_to |
| 617 | 650 |
coords[:bar_progress_end] = progress_date - self.date_from |
| ... | ... | |
| 638 | 671 |
coords |
| 639 | 672 |
end |
| 640 | 673 |
|
| 641 |
# Sorts a collection of issues by start_date, due_date, id for gantt rendering |
|
| 674 |
def calc_progress_date(start_date, end_date, progress) |
|
| 675 |
start_date + (end_date - start_date + 1) * (progress / 100.0) |
|
| 676 |
end |
|
| 677 |
|
|
| 678 |
# TODO: Sorts a collection of issues by start_date, due_date, id for gantt rendering |
|
| 642 | 679 |
def sort_issues!(issues) |
| 643 | 680 |
issues.sort! { |a, b| gantt_issue_compare(a, b) }
|
| 644 | 681 |
end |
| ... | ... | |
| 678 | 715 |
def html_subject(params, subject, options={})
|
| 679 | 716 |
style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
|
| 680 | 717 |
style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
|
| 681 |
output = view.content_tag('div', subject,
|
|
| 718 |
output = view.content_tag(:div, subject,
|
|
| 682 | 719 |
:class => options[:css], :style => style, |
| 683 |
:title => options[:title]) |
|
| 720 |
:title => options[:title], |
|
| 721 |
:id => options[:id]) |
|
| 684 | 722 |
@subjects << output |
| 685 | 723 |
output |
| 686 | 724 |
end |
| ... | ... | |
| 705 | 743 |
params[:image].text(params[:indent], params[:top] + 2, subject) |
| 706 | 744 |
end |
| 707 | 745 |
|
| 746 |
def issue_relations(issue) |
|
| 747 |
rels = {}
|
|
| 748 |
if relations[issue.id] |
|
| 749 |
relations[issue.id].each do |relation| |
|
| 750 |
(rels[relation.relation_type] ||= []) << relation.issue_to_id |
|
| 751 |
end |
|
| 752 |
end |
|
| 753 |
rels |
|
| 754 |
end |
|
| 755 |
|
|
| 708 | 756 |
def html_task(params, coords, options={})
|
| 709 | 757 |
output = '' |
| 710 | 758 |
# Renders the task bar, with progress and late |
| ... | ... | |
| 714 | 762 |
style << "top:#{params[:top]}px;"
|
| 715 | 763 |
style << "left:#{coords[:bar_start]}px;"
|
| 716 | 764 |
style << "width:#{width}px;"
|
| 717 |
output << view.content_tag(:div, ' '.html_safe, |
|
| 718 |
:style => style, |
|
| 719 |
:class => "#{options[:css]} task_todo")
|
|
| 765 |
html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
|
|
| 766 |
html_id = "task-todo-version-#{options[:version].id}" if options[:version]
|
|
| 767 |
content_opt = {:style => style,
|
|
| 768 |
:class => "#{options[:css]} task_todo",
|
|
| 769 |
:id => html_id} |
|
| 770 |
if options[:issue] |
|
| 771 |
rels = issue_relations(options[:issue]) |
|
| 772 |
if rels.present? |
|
| 773 |
content_opt[:data] = {"rels" => rels.to_json}
|
|
| 774 |
end |
|
| 775 |
end |
|
| 776 |
output << view.content_tag(:div, ' '.html_safe, content_opt) |
|
| 720 | 777 |
if coords[:bar_late_end] |
| 721 | 778 |
width = coords[:bar_late_end] - coords[:bar_start] - 2 |
| 722 | 779 |
style = "" |
| ... | ... | |
| 733 | 790 |
style << "top:#{params[:top]}px;"
|
| 734 | 791 |
style << "left:#{coords[:bar_start]}px;"
|
| 735 | 792 |
style << "width:#{width}px;"
|
| 793 |
html_id = "task-done-issue-#{options[:issue].id}" if options[:issue]
|
|
| 794 |
html_id = "task-done-version-#{options[:version].id}" if options[:version]
|
|
| 736 | 795 |
output << view.content_tag(:div, ' '.html_safe, |
| 737 | 796 |
:style => style, |
| 738 |
:class => "#{options[:css]} task_done")
|
|
| 797 |
:class => "#{options[:css]} task_done",
|
|
| 798 |
:id => html_id) |
|
| 739 | 799 |
end |
| 740 | 800 |
end |
| 741 | 801 |
# Renders the markers |
| lib/redmine/helpers/time_report.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 18 | 18 |
module Redmine |
| 19 | 19 |
module Helpers |
| 20 | 20 |
class TimeReport |
| 21 |
attr_reader :criteria, :columns, :from, :to, :hours, :total_hours, :periods
|
|
| 21 |
attr_reader :criteria, :columns, :hours, :total_hours, :periods |
|
| 22 | 22 |
|
| 23 |
def initialize(project, issue, criteria, columns, from, to)
|
|
| 23 |
def initialize(project, issue, criteria, columns, time_entry_scope)
|
|
| 24 | 24 |
@project = project |
| 25 | 25 |
@issue = issue |
| 26 | 26 |
|
| ... | ... | |
| 30 | 30 |
@criteria = @criteria[0,3] |
| 31 | 31 |
|
| 32 | 32 |
@columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month' |
| 33 |
@from = from |
|
| 34 |
@to = to |
|
| 33 |
@scope = time_entry_scope |
|
| 35 | 34 |
|
| 36 | 35 |
run |
| 37 | 36 |
end |
| ... | ... | |
| 44 | 43 |
|
| 45 | 44 |
def run |
| 46 | 45 |
unless @criteria.empty? |
| 47 |
scope = TimeEntry.visible.spent_between(@from, @to) |
|
| 48 |
if @issue |
|
| 49 |
scope = scope.on_issue(@issue) |
|
| 50 |
elsif @project |
|
| 51 |
scope = scope.on_project(@project, Setting.display_subprojects_issues?) |
|
| 52 |
end |
|
| 53 | 46 |
time_columns = %w(tyear tmonth tweek spent_on) |
| 54 | 47 |
@hours = [] |
| 55 |
scope.sum(:hours, :include => :issue, :group => @criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns).each do |hash, hours|
|
|
| 48 |
@scope.sum(:hours, |
|
| 49 |
:include => [:issue, :activity], |
|
| 50 |
:group => @criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns,
|
|
| 51 |
:joins => @criteria.collect{|criteria| @available_criteria[criteria][:joins]}.compact).each do |hash, hours|
|
|
| 56 | 52 |
h = {'hours' => hours}
|
| 57 | 53 |
(@criteria + time_columns).each_with_index do |name, i| |
| 58 | 54 |
h[name] = hash[i] |
| ... | ... | |
| 67 | 63 |
when 'month' |
| 68 | 64 |
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
|
| 69 | 65 |
when 'week' |
| 70 |
row['week'] = "#{row['tyear']}-#{row['tweek']}"
|
|
| 66 |
row['week'] = "#{row['spent_on'].cwyear}-#{row['tweek']}"
|
|
| 71 | 67 |
when 'day' |
| 72 | 68 |
row['day'] = "#{row['spent_on']}"
|
| 73 | 69 |
end |
| 74 | 70 |
end |
| 75 | 71 |
|
| 76 |
if @from.nil? |
|
| 77 |
min = @hours.collect {|row| row['spent_on']}.min
|
|
| 78 |
@from = min ? min.to_date : Date.today |
|
| 79 |
end |
|
| 72 |
min = @hours.collect {|row| row['spent_on']}.min
|
|
| 73 |
@from = min ? min.to_date : Date.today |
|
| 80 | 74 |
|
| 81 |
if @to.nil? |
|
| 82 |
max = @hours.collect {|row| row['spent_on']}.max
|
|
| 83 |
@to = max ? max.to_date : Date.today |
|
| 84 |
end |
|
| 75 |
max = @hours.collect {|row| row['spent_on']}.max
|
|
| 76 |
@to = max ? max.to_date : Date.today |
|
| 85 | 77 |
|
| 86 | 78 |
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
|
| 87 | 79 |
|
| ... | ... | |
| 98 | 90 |
@periods << "#{date_from.year}-#{date_from.month}"
|
| 99 | 91 |
date_from = (date_from + 1.month).at_beginning_of_month |
| 100 | 92 |
when 'week' |
| 101 |
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
|
|
| 93 |
@periods << "#{date_from.to_date.cwyear}-#{date_from.to_date.cweek}"
|
|
| 102 | 94 |
date_from = (date_from + 7.day).at_beginning_of_week |
| 103 | 95 |
when 'day' |
| 104 | 96 |
@periods << "#{date_from.to_date}"
|
| ... | ... | |
| 121 | 113 |
'category' => {:sql => "#{Issue.table_name}.category_id",
|
| 122 | 114 |
:klass => IssueCategory, |
| 123 | 115 |
:label => :field_category}, |
| 124 |
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
|
|
| 116 |
'user' => {:sql => "#{TimeEntry.table_name}.user_id",
|
|
| 125 | 117 |
:klass => User, |
| 126 |
:label => :label_member},
|
|
| 118 |
:label => :label_user},
|
|
| 127 | 119 |
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
|
| 128 | 120 |
:klass => Tracker, |
| 129 | 121 |
:label => :label_tracker}, |
| ... | ... | |
| 135 | 127 |
:label => :label_issue} |
| 136 | 128 |
} |
| 137 | 129 |
|
| 130 |
# Add time entry custom fields |
|
| 131 |
custom_fields = TimeEntryCustomField.all |
|
| 132 |
# Add project custom fields |
|
| 133 |
custom_fields += ProjectCustomField.all |
|
| 134 |
# Add issue custom fields |
|
| 135 |
custom_fields += (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields) |
|
| 136 |
# Add time entry activity custom fields |
|
| 137 |
custom_fields += TimeEntryActivityCustomField.all |
|
| 138 |
|
|
| 138 | 139 |
# Add list and boolean custom fields as available criteria |
| 139 |
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields) |
|
| 140 | 140 |
custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
|
| 141 |
@available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id ORDER BY c.value LIMIT 1)",
|
|
| 142 |
:format => cf.field_format, |
|
| 143 |
:label => cf.name} |
|
| 144 |
end if @project |
|
| 145 |
|
|
| 146 |
# Add list and boolean time entry custom fields |
|
| 147 |
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
|
|
| 148 |
@available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id ORDER BY c.value LIMIT 1)",
|
|
| 149 |
:format => cf.field_format, |
|
| 150 |
:label => cf.name} |
|
| 151 |
end |
|
| 152 |
|
|
| 153 |
# Add list and boolean time entry activity custom fields |
|
| 154 |
TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
|
|
| 155 |
@available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id ORDER BY c.value LIMIT 1)",
|
|
| 141 |
@available_criteria["cf_#{cf.id}"] = {:sql => "#{cf.join_alias}.value",
|
|
| 142 |
:joins => cf.join_for_order_statement, |
|
| 156 | 143 |
:format => cf.field_format, |
| 157 | 144 |
:label => cf.name} |
| 158 | 145 |
end |
Also available in: Unified diff