From 89507fc46df01e02e4deab09070f8bc561496226 Mon Sep 17 00:00:00 2001 From: Mischa The Evil Date: Mon, 9 Aug 2021 13:26:46 +0200 Subject: [PATCH] Add a basic and incomplete POC-implementation of (total) spent_time_ratio columns. This implementation for #35657 does not yet provide: * Column sorting functionality; * Filters for the added columns; * Rendering of (total) spent_time_ratio values in CSV, PDF & API output-formats; * I18n; * Test coverage. --- app/helpers/issues_helper.rb | 32 +++++++++++++++++++++++++++ app/helpers/queries_helper.rb | 2 ++ app/models/issue.rb | 35 ++++++++++++++++++++++++++++++ app/models/issue_query.rb | 18 +++++++++++++++ app/models/version.rb | 11 ++++++++++ app/views/issues/show.html.erb | 3 +++ app/views/versions/show.html.erb | 7 ++++++ public/stylesheets/application.css | 16 ++++++++++---- 8 files changed, 120 insertions(+), 4 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index b8e1dd414..2e2beb194 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -284,6 +284,38 @@ module IssuesHelper s end + def spent_time_ratio_details(issue) + if issue.total_estimated_hours.present? && issue.total_estimated_hours > 0 + if issue.total_spent_time_ratio == issue.spent_time_ratio + content_tag( + :span, + "#{issue.spent_time_ratio.to_s(:percentage, precision: 2)}", + class: "spent-time-ratio-value" + ) + else + s = !issue.spent_time_ratio.nil? && (issue.spent_time_ratio > 0) ? + content_tag( + :span, + "#{issue.spent_time_ratio.to_s(:percentage, precision: 2)}", + class: "spent-time-ratio-value" + ) : + "" + label = l(:label_total) + value = content_tag( + :span, + "#{issue.total_spent_time_ratio.to_s(:percentage, precision: 2)}", + class: "total-spent-time-ratio-value" + ) + s += " (" + s += label + s += ": " + s += value + s += ")" + s.html_safe + end + end + end + # Returns a link for adding a new subtask to the given issue def link_to_new_subtask(issue) link_to(l(:button_add), url_for_new_subtask(issue)) diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index a76ae9573..08aa56acc 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -273,6 +273,8 @@ module QueriesHelper link_to_if(value > 0, format_hours(value), project_time_entries_path(item.project, :issue_id => "~#{item.id}")) when :attachments value.to_a.map {|a| format_object(a)}.join(" ").html_safe + when :spent_time_ratio, :total_spent_time_ratio + !value.nil? ? value.to_s(:percentage, precision: 2) : '' else format_object(value) end diff --git a/app/models/issue.rb b/app/models/issue.rb index 09f8400cc..42189903b 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -952,6 +952,14 @@ class Issue < ActiveRecord::Base due_date.present? && (due_date < User.current.today) && !closed? end + def overspent? + spent_time_ratio.present? && spent_time_ratio > 100 + end + + def total_overspent? + total_spent_time_ratio.present? && total_spent_time_ratio > 100 + end + # Is the amount of work done less than it should for the due date def behind_schedule? return false if start_date.nil? || due_date.nil? @@ -1146,6 +1154,29 @@ class Issue < ActiveRecord::Base end end + def spent_time_ratio + if (estimated_hours.present? && estimated_hours > 0) + if (spent_hours.present? && spent_hours == 0) + 0.to_f.round(2) + elsif (spent_hours.present? && spent_hours > 0) + spent_time_ratio = spent_hours.to_f / estimated_hours.to_f * 100.0 + spent_time_ratio.round(2) + end + end + end + + def total_spent_time_ratio + if (total_estimated_hours.present? && total_estimated_hours > 0) + if (total_spent_hours.present? && total_spent_hours == 0) + 0.to_f.round(2) + elsif (total_spent_hours.present? && total_spent_hours > 0) + total_spent_time_ratio = + total_spent_hours.to_f / total_estimated_hours.to_f * 100.0 + total_spent_time_ratio.round(2) + end + end + end + def relations @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort) end @@ -1439,6 +1470,10 @@ class Issue < ActiveRecord::Base s << ' parent' unless leaf? s << ' private' if is_private? s << ' behind-schedule' if behind_schedule? + if user.allowed_to?(:view_time_entries, project, :global => true) + s << ' overspent' if overspent? + s << ' total-overspent' if total_overspent? + end if user.logged? s << ' created-by-me' if author_id == user.id s << ' assigned-to-me' if assigned_to_id == user.id diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index efd0ad0f1..b769d60b9 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -305,6 +305,18 @@ class IssueQuery < Query :default_order => 'desc', :caption => :label_total_spent_time) ) + + @available_columns.insert( + index + 2, + QueryColumn.new(:spent_time_ratio, + :caption => "Spent time ratio") + ) + + @available_columns.insert( + index + 3, + QueryColumn.new(:total_spent_time_ratio, + :caption => "Total spent time ratio") + ) end if User.current.allowed_to?(:set_issues_private, nil, :global => true) || @@ -399,6 +411,12 @@ class IssueQuery < Query if has_column?(:total_spent_hours) Issue.load_visible_total_spent_hours(issues) end + if has_column?(:spent_time_ratio) + Issue.load_visible_spent_hours(issues) + end + if has_column?(:total_spent_time_ratio) + Issue.load_visible_total_spent_hours(issues) + end if has_column?(:last_updated_by) Issue.load_visible_last_updated_by(issues) end diff --git a/app/models/version.rb b/app/models/version.rb index 2e5430746..de256414d 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -241,6 +241,17 @@ class Version < ActiveRecord::Base @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f end + def spent_time_ratio + if (estimated_hours.present? && estimated_hours > 0) + if (spent_hours.present? && spent_hours == 0) + 0.to_f.round(2) + elsif (spent_hours.present? && spent_hours > 0) + spent_time_ratio = spent_hours.to_f / estimated_hours.to_f * 100.0 + spent_time_ratio.round(2) + end + end + end + def closed? status == 'closed' end diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index 6b84bdfb3..72d60a1c7 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -73,6 +73,9 @@ end if User.current.allowed_to?(:view_time_entries, @project) && @issue.total_spent_hours > 0 rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time' + if spent_time_ratio_details(@issue).present? + rows.right 'Spent time ratio', spent_time_ratio_details(@issue), :class => 'spent-time-ratio' + end end end %> <%= render_half_width_custom_fields_rows(@issue) %> diff --git a/app/views/versions/show.html.erb b/app/views/versions/show.html.erb index 0527eae9c..b2fb70f27 100644 --- a/app/views/versions/show.html.erb +++ b/app/views/versions/show.html.erb @@ -28,6 +28,13 @@ <%= link_to html_hours(l_hours(@version.spent_hours)), project_time_entries_path(@version.project, :set_filter => 1, :"issue.fixed_version_id" => @version.id) %> +<% if !@version.spent_time_ratio.nil? %> + + <%= 'Spent time ratio' %> + <% overspent_class = @version.spent_time_ratio > 100 ? 'overspent' : '' %> + <%= @version.spent_time_ratio.to_s(:percentage, precision: 2) %> + +<% end %> <% end %> diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index e117bc1d0..17f4f0fe6 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -263,7 +263,10 @@ table.list td.buttons img, div.buttons img {vertical-align:middle;} table.list td.reorder {width:15%; white-space:nowrap; text-align:center; } table.list table.progress td {padding-right:0px;} table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; } -table.list tr.overdue td.due_date { color: #c22; } +table.list tr.overdue td.due_date, +table.list tr.overspent td.spent_time_ratio, +table.list tr.total-overspent td.total_spent_time_ratio, +div#version-summary td.spent-time-ratio.overspent { color: #c22; } #role-permissions-trackers table.list th {white-space:normal;} .table-list-cell {display: table-cell; vertical-align: top; padding:2px; } @@ -540,7 +543,9 @@ div.issue .attributes {margin-top: 2em;} div.issue .attributes .attribute {padding-left:180px; clear:left; min-height: 1.8em;} div.issue .attributes .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left; overflow:hidden; text-overflow: ellipsis;} div.issue .attribute .value {overflow:auto; text-overflow: ellipsis;} -div.issue.overdue .due-date .value { color: #c22; } +div.issue.overdue .due-date .value, +div.issue.overspent .spent-time-ratio.attribute .value span.spent-time-ratio-value, +div.issue.total-overpent .spent-time-ratio.attribute .value, span.total-spent-time-ratio-value { color: #c22; } body.controller-issues h2.inline-flex {padding-right: 0} #issue_tree table.issues, #relations table.issues { border: 0; } @@ -643,7 +648,9 @@ body.controller-versions.action-show div#roadmap .related-issues {width:70%;} div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; } div#version-summary fieldset { margin-bottom: 1em; } div#version-summary fieldset.time-tracking table { width:100%; } -div#version-summary th, div#version-summary td.total-hours { text-align: right; } +div#version-summary th, +div#version-summary td.total-hours, +div#version-summary td.spent-time-ratio { text-align: right; } table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; } table#time-report tbody tr.subtotal { font-style: italic; color:#777;} @@ -725,7 +732,8 @@ ul.properties {padding:0; font-size: 0.9em; color: #777;} ul.properties li {list-style-type:none;} ul.properties li span {font-style:italic;} -.total-hours { font-size: 110%; font-weight: bold; } +.total-hours, +div#version-summary .spent-time-ratio { font-size: 110%; font-weight: bold; } .total-hours span.hours-int { font-size: 120%; } .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em; position: relative;} -- 2.26.0.windows.1