Patch #1671 » version_times_251.diff
| app/helpers/versions_helper.rb (kopia robocza) | ||
|---|---|---|
| 32 | 32 |
def render_issue_status_by(version, criteria) |
| 33 | 33 |
criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria) |
| 34 | 34 | |
| 35 |
h = Hash.new {|k,v| k[v] = [0, 0]}
|
|
| 36 |
begin
|
|
| 37 |
# Total issue count
|
|
| 38 |
Issue.where(:fixed_version_id => version.id).group(criteria).count.each {|c,s| h[c][0] = s}
|
|
| 39 |
# Open issues count
|
|
| 40 |
Issue.open.where(:fixed_version_id => version.id).group(criteria).count.each {|c,s| h[c][1] = s}
|
|
| 41 |
rescue ActiveRecord::RecordNotFound
|
|
| 42 |
# When grouping by an association, Rails throws this exception if there's no result (bug)
|
|
| 35 |
#sort them alphabetically by category name
|
|
| 36 |
metrics = version.get_grouped_metrics(criteria).to_a.sort {|x, y| x[0].to_s <=> y[0].to_s}
|
|
| 37 |
max = {}
|
|
| 38 |
|
|
| 39 |
[{:count => :total}, {:time => :total}].each do |metric_info|
|
|
| 40 |
metrics_group, total_metric = metric_info.to_a.flatten
|
|
| 41 |
max[metrics_group] = metrics.map{|item| item[1]}.map {|item| item[metrics_group]}.map {|item| item[total_metric]}.max
|
|
| 42 |
max[metrics_group] = 1 if max[metrics_group] == 0
|
|
| 43 | 43 |
end |
| 44 | 44 |
# Sort with nil keys in last position |
| 45 |
counts = h.keys.sort {|a,b| a.nil? ? 1 : (b.nil? ? -1 : a <=> b)}.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
|
|
| 46 |
max = counts.collect {|c| c[:total]}.max
|
|
| 47 | ||
| 48 |
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
|
|
| 45 |
render :partial => 'issue_counts', :locals => {:version => version,
|
|
| 46 |
:criteria => criteria, :grouped_metrics => metrics, :max => max,
|
|
| 47 |
:spent_time_allowed => User.current.allowed_to?(:view_time_entries, @project), |
|
| 48 |
}
|
|
| 49 | 49 |
end |
| 50 |
|
|
| 51 |
def time_progress(time_info) |
|
| 52 |
logger.debug "time_info[:spent] = #{time_info[:spent].inspect}"
|
|
| 53 |
logger.debug "time_info[:total] = #{time_info[:total].inspect}"
|
|
| 54 |
if (time_info[:total] != 0) |
|
| 55 |
time_progress = time_info[:spent].to_f / time_info[:total] |
|
| 56 |
else |
|
| 57 |
time_progress = 0 #no total also means there's no spent time |
|
| 58 |
end |
|
| 59 |
time_progress |
|
| 60 |
end |
|
| 50 | 61 | |
| 51 | 62 |
def status_by_options_for_select(value) |
| 52 | 63 |
options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
|
| app/models/version.rb (kopia robocza) | ||
|---|---|---|
| 82 | 82 |
def spent_hours |
| 83 | 83 |
@spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
|
| 84 | 84 |
end |
| 85 | ||
| 85 |
|
|
| 86 |
def calc_remaining_and_total_time |
|
| 87 |
@remaining_hours = 0 |
|
| 88 |
@total_hours = 0 |
|
| 89 |
get_grouped_metrics(:category).to_a.map{|item| item[1]}.map {|item| item[:time]}.each do |times|
|
|
| 90 |
@remaining_hours += times[:remaining] |
|
| 91 |
@total_hours += times[:total] |
|
| 92 |
end |
|
| 93 |
end |
|
| 94 |
|
|
| 95 |
def remaining_hours |
|
| 96 |
return @remaining_hours if @remaining_hours |
|
| 97 |
calc_remaining_and_total_time |
|
| 98 |
@remaining_hours |
|
| 99 |
end |
|
| 100 |
|
|
| 101 |
def total_hours |
|
| 102 |
return @total_hours if @total_hours |
|
| 103 |
calc_remaining_and_total_time |
|
| 104 |
@total_hours |
|
| 105 |
end |
|
| 106 |
|
|
| 86 | 107 |
def closed? |
| 87 | 108 |
status == 'closed' |
| 88 | 109 |
end |
| ... | ... | |
| 163 | 184 |
@closed_issues_count |
| 164 | 185 |
end |
| 165 | 186 | |
| 187 |
def not_estimated_undone_count |
|
| 188 |
@not_estimated_undone_count ||= Issue.count(:all, :conditions => |
|
| 189 |
["fixed_version_id = ? AND estimated_hours IS NULL AND " + |
|
| 190 |
"(closed_on is null)", self.id |
|
| 191 |
], :include => :status) |
|
| 192 |
end |
|
| 193 | ||
| 194 | ||
| 166 | 195 |
def wiki_page |
| 167 | 196 |
if project.wiki && !wiki_page_title.blank? |
| 168 | 197 |
@wiki_page ||= project.wiki.find_page(wiki_page_title) |
| ... | ... | |
| 226 | 255 |
end |
| 227 | 256 |
end |
| 228 | 257 | |
| 258 |
def get_grouped_metrics(criteria) |
|
| 259 |
condition = issues_version_condition |
|
| 260 |
|
|
| 261 |
issues = Issue.find(:all, :include => [:status, criteria], |
|
| 262 |
:conditions => condition) |
|
| 263 |
|
|
| 264 |
spent_times = {}
|
|
| 265 |
TimeEntry.sum(:hours, :group => :issue_id, :include => :issue, |
|
| 266 |
:conditions => condition).each do |issue_id, hours| |
|
| 267 |
|
|
| 268 |
spent_times[issue_id] = hours |
|
| 269 |
end |
|
| 270 | ||
| 271 |
categories_metrics = {}
|
|
| 272 |
issues.each do |issue| |
|
| 273 |
category = issue.send(criteria) |
|
| 274 |
categories_metrics[category] ||= {}
|
|
| 275 |
categories_metrics[category][:time] ||= {:estimated => 0,
|
|
| 276 |
:spent => 0, :remaining => 0, :total => 0} |
|
| 277 |
metrics = categories_metrics[category][:time] |
|
| 278 |
|
|
| 279 |
estimated = issue.estimated_hours || 0 |
|
| 280 |
metrics[:estimated] += estimated |
|
| 281 |
spent = spent_times[issue.id] || 0 |
|
| 282 |
metrics[:spent] += spent |
|
| 283 |
remaining = issue.closed? ? 0 : estimated - spent |
|
| 284 |
remaining = 0 if remaining < 0 |
|
| 285 |
metrics[:remaining] += remaining |
|
| 286 |
metrics[:total] += (remaining + spent) |
|
| 287 |
|
|
| 288 |
categories_metrics[category][:count] ||= {:open => 0, :closed => 0, :total => 0}
|
|
| 289 |
metrics = categories_metrics[category][:count] |
|
| 290 |
metrics[:total] += 1 |
|
| 291 |
if issue.closed? |
|
| 292 |
metrics[:closed] += 1 |
|
| 293 |
else |
|
| 294 |
metrics[:open] += 1 |
|
| 295 |
end |
|
| 296 |
end |
|
| 297 |
categories_metrics |
|
| 298 |
end |
|
| 299 |
|
|
| 300 |
def issues_version_condition |
|
| 301 |
["#{Issue.table_name}.fixed_version_id = ?", id]
|
|
| 302 |
end |
|
| 303 | ||
| 229 | 304 |
private |
| 230 | ||
| 231 | 305 |
def load_issue_counts |
| 232 | 306 |
unless @issue_count |
| 233 | 307 |
@open_issues_count = 0 |
| app/views/versions/_issue_counts.html.erb (kopia robocza) | ||
|---|---|---|
| 7 | 7 |
:id => 'status_by_select', |
| 8 | 8 |
:data => {:remote => true, :method => 'post', :url => status_by_version_path(version)})).html_safe %>
|
| 9 | 9 |
</legend> |
| 10 |
<% if counts.empty? %>
|
|
| 10 |
<% if grouped_metrics.empty? %>
|
|
| 11 | 11 |
<p><em><%= l(:label_no_data) %></em></p> |
| 12 | 12 |
<% else %> |
| 13 |
<table> |
|
| 14 |
<% counts.each do |count| %> |
|
| 15 |
<tr> |
|
| 16 |
<td style="width:130px; text-align:right;"> |
|
| 17 |
<% if count[:group] -%> |
|
| 18 |
<%= link_to(h(count[:group]), project_issues_path(version.project, :set_filter => 1, :status_id => '*', :fixed_version_id => version, "#{criteria}_id" => count[:group])) %>
|
|
| 19 |
<% else -%> |
|
| 20 |
<%= link_to(l(:label_none), project_issues_path(version.project, :set_filter => 1, :status_id => '*', :fixed_version_id => version, "#{criteria}_id" => "!*")) %>
|
|
| 21 |
<% end %> |
|
| 22 |
</td> |
|
| 23 |
<td style="width:240px;"> |
|
| 24 |
<%= progress_bar((count[:closed].to_f / count[:total])*100, |
|
| 25 |
:legend => "#{count[:closed]}/#{count[:total]}",
|
|
| 26 |
:width => "#{(count[:total].to_f / max * 200).floor}px;") %>
|
|
| 27 |
</td> |
|
| 28 |
</tr> |
|
| 13 |
<table class="category_metrics"> |
|
| 14 |
<% grouped_metrics.each do |metrics_group| |
|
| 15 |
category, metrics = *metrics_group |
|
| 16 |
%> |
|
| 17 |
<% color_class = cycle('odd', 'even')%>
|
|
| 18 |
<tr class="header <%= color_class %>"> |
|
| 19 |
<td colspan="5"> |
|
| 20 |
<%= criteria_operator = category ? "=" : "!*" |
|
| 21 |
link_to category || "[#{l(:text_not_assigned)}]",
|
|
| 22 |
{:controller => 'issues',
|
|
| 23 |
:action => 'index', |
|
| 24 |
:project_id => version.project, |
|
| 25 |
:set_filter => 1, |
|
| 26 |
:fields => ["#{criteria}_id", "fixed_version_id", "status_id"],
|
|
| 27 |
:values => {"#{criteria}_id" => [category], "fixed_version_id" => [version], "status_id" => [1]},
|
|
| 28 |
:operators => {"#{criteria}_id" => criteria_operator, "fixed_version_id" => "=", "status_id" => "*"}
|
|
| 29 |
} |
|
| 30 |
%> |
|
| 31 |
</td> |
|
| 32 |
</tr> |
|
| 33 |
<tr class="<%= color_class %>"> |
|
| 34 |
<td><%= l(:label_issues_count) %> </td> |
|
| 35 |
<% if spent_time_allowed %> |
|
| 36 |
<td><%= l(:label_time) %></td> |
|
| 37 |
<% end %> |
|
| 38 |
<td class="metric_comment"> |
|
| 39 |
<span title="<%= l(:field_estimated_hours) %>"> |
|
| 40 |
<%= l(:label_estimated_time_short) %> |
|
| 41 |
</span> |
|
| 42 |
</td> |
|
| 43 |
<% if spent_time_allowed %> |
|
| 44 |
<td class="metric_comment"> |
|
| 45 |
<span title="<%= l(:label_spent_time) %>"> |
|
| 46 |
<%= l(:label_spent_time_short) %> |
|
| 47 |
</span> |
|
| 48 |
</td> |
|
| 49 |
<td class="metric_comment"> |
|
| 50 |
<span title="<%= l(:label_remaining_time) %>"> |
|
| 51 |
<%= l(:label_remaining_time_short) %> |
|
| 52 |
</span> |
|
| 53 |
</td> |
|
| 54 |
<% max_progress_width = 95 %> |
|
| 55 |
<% else |
|
| 56 |
max_progress_width = 150 |
|
| 57 |
end %> |
|
| 58 |
</tr> |
|
| 59 |
<tr class="<%= color_class %>"> |
|
| 60 |
<td class="progress count"> |
|
| 61 |
<%= count = metrics[:count]; progress_bar((count[:closed].to_f / count[:total])*100, |
|
| 62 |
:legend => |
|
| 63 |
"#{count[:closed]}/#{count[:total]}</span>".html_safe,
|
|
| 64 |
:width => "#{(count[:total].to_f / max[:count] * max_progress_width).floor}px;") %>
|
|
| 65 |
</td> |
|
| 66 |
|
|
| 67 |
<% |
|
| 68 |
time = metrics[:time] |
|
| 69 |
if spent_time_allowed %> |
|
| 70 |
<td class="progress time"> |
|
| 71 |
<%= progress_bar(time_progress(time)*100, |
|
| 72 |
:legend => |
|
| 73 |
"#{time[:spent].ceil}/#{time[:total].ceil}",
|
|
| 74 |
:width => "#{(time[:total] / max[:time] * max_progress_width).floor}px;") %>
|
|
| 75 |
</td> |
|
| 76 |
<% end %> |
|
| 77 |
<% hours = l(:text_hours_short) %> |
|
| 78 |
<td class="metric_comment"> |
|
| 79 |
<span title="<%= l(:field_estimated_hours) %>"> |
|
| 80 |
<%= "#{time[:estimated].ceil}#{hours}" %>
|
|
| 81 |
</span> |
|
| 82 |
</td> |
|
| 83 |
<% if spent_time_allowed %> |
|
| 84 |
<td class="metric_comment"> |
|
| 85 |
<span title="<%= l(:label_spent_time) %>"> |
|
| 86 |
<%= "#{time[:spent].ceil}#{hours}" %>
|
|
| 87 |
</span> |
|
| 88 |
</td> |
|
| 89 |
<td class="metric_comment"> |
|
| 90 |
<span title="<%= l(:label_remaining_time) %>"> |
|
| 91 |
<%= "#{time[:remaining].ceil}#{hours}" %>
|
|
| 92 |
</span> |
|
| 93 |
</td> |
|
| 94 |
<% end %> |
|
| 95 |
</tr> |
|
| 29 | 96 |
<% end %> |
| 30 | 97 |
</table> |
| 31 | 98 |
<% end %> |
| app/views/versions/show.html.erb (kopia robocza) | ||
|---|---|---|
| 19 | 19 |
<th><%= l(:field_estimated_hours) %></th> |
| 20 | 20 |
<td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td> |
| 21 | 21 |
</tr> |
| 22 |
<% if @version.not_estimated_undone_count > 0 %> |
|
| 23 |
<tr> |
|
| 24 |
<th><%= l(:label_not_estimated_and_undone) %></th> |
|
| 25 |
<td class="not_estimated total-hours"><%= link_to("%s" %
|
|
| 26 |
[@version.not_estimated_undone_count.to_s+" "+l(:label_issue_plural).downcase, @version.not_estimated_undone_count], |
|
| 27 |
{:controller => 'issues',
|
|
| 28 |
:action => 'index', |
|
| 29 |
:project_id => @version.project, |
|
| 30 |
:set_filter => 1, |
|
| 31 |
:fields => ["estimated_hours", "fixed_version_id", "status_id"], |
|
| 32 |
:values => {"estimated_hours" => [1], "fixed_version_id" => [@version], "status_id" => [1]},
|
|
| 33 |
:operators => {"estimated_hours" => "!*", "fixed_version_id" => "=", "status_id" => "o"}
|
|
| 34 |
} |
|
| 35 |
)%></td> |
|
| 36 |
</tr> |
|
| 37 |
<% end %> |
|
| 38 | ||
| 22 | 39 |
<% if User.current.allowed_to?(:view_time_entries, @project) %> |
| 23 | 40 |
<tr> |
| 24 | 41 |
<th><%= l(:label_spent_time) %></th> |
| 25 | 42 |
<td class="total-hours"><%= html_hours(l_hours(@version.spent_hours)) %></td> |
| 26 | 43 |
</tr> |
| 44 |
<tr> |
|
| 45 |
<th><%= l(:label_remaining_time) %></th> |
|
| 46 |
<td class="total-hours"><%= html_hours(l_hours(@version.remaining_hours)) %></td> |
|
| 47 |
</tr> |
|
| 48 |
<tr> |
|
| 49 |
<% title = "#{l(:label_spent_time)} + #{l(:label_remaining_time)}" %>
|
|
| 50 |
<th title="<%= title %>"><%= l(:label_current_total_time) %></th> |
|
| 51 |
<td class="total-hours" "><%= html_hours(l_hours(@version.total_hours)) %></td> |
|
| 52 |
</tr> |
|
| 27 | 53 |
<% end %> |
| 28 | 54 |
</table> |
| 29 | 55 |
</fieldset> |
| config/locales/en.yml (kopia robocza) | ||
|---|---|---|
| 375 | 375 |
setting_enabled_scm: Enabled SCM |
| 376 | 376 |
setting_mail_handler_body_delimiters: "Truncate emails after one of these lines" |
| 377 | 377 |
setting_mail_handler_api_enabled: Enable WS for incoming emails |
| 378 |
label_remaining_time: Remaining time |
|
| 379 |
label_current_total_time: Total |
|
| 380 |
label_estimated_time_short: Est. |
|
| 381 |
label_spent_time_short: Spent |
|
| 382 |
label_remaining_time_short: Rem. |
|
| 383 |
label_issues_count: Count |
|
| 384 |
label_not_estimated_and_undone: Not estimated |
|
| 385 |
label_time: Time |
|
| 378 | 386 |
setting_mail_handler_api_key: API key |
| 379 | 387 |
setting_sequential_project_identifiers: Generate sequential project identifiers |
| 380 | 388 |
setting_gravatar_enabled: Use Gravatar user icons |
| ... | ... | |
| 610 | 618 |
label_open_issues_plural: open |
| 611 | 619 |
label_closed_issues: closed |
| 612 | 620 |
label_closed_issues_plural: closed |
| 621 |
label_done_issues: done |
|
| 622 |
label_done_issues_plural: done |
|
| 623 |
label_undone_issues: undone |
|
| 624 |
label_undone_issues_plural: undone |
|
| 613 | 625 |
label_x_open_issues_abbr_on_total: |
| 614 | 626 |
zero: 0 open / %{total}
|
| 615 | 627 |
one: 1 open / %{total}
|
| config/locales/pl.yml (kopia robocza) | ||
|---|---|---|
| 361 | 361 |
label_closed_issues_plural234: zamknięte |
| 362 | 362 |
label_closed_issues_plural5: zamknięte |
| 363 | 363 |
label_closed_issues_plural: zamknięte |
| 364 |
label_done_issues: done |
|
| 365 |
label_done_issues_plural: done |
|
| 366 |
label_undone_issues: undone |
|
| 367 |
label_undone_issues_plural: undone |
|
| 364 | 368 |
label_x_open_issues_abbr_on_total: |
| 365 | 369 |
zero: 0 otwartych / %{total}
|
| 366 | 370 |
one: 1 otwarty / %{total}
|
| ... | ... | |
| 749 | 753 |
setting_login_required: Wymagane zalogowanie |
| 750 | 754 |
setting_mail_from: Adres e-mail wysyłki |
| 751 | 755 |
setting_mail_handler_api_enabled: Uaktywnij usługi sieciowe (WebServices) dla poczty przychodzącej |
| 756 |
label_remaining_time: Pozostały czas |
|
| 757 |
label_current_total_time: Suma |
|
| 758 |
label_estimated_time_short: Szacz. |
|
| 759 |
label_spent_time_short: Przepr. |
|
| 760 |
label_remaining_time_short: Pozost. |
|
| 761 |
label_issues_count: Count |
|
| 762 |
label_time: Czas |
|
| 763 |
label_not_estimated_and_undone: Nie oszacowane |
|
| 764 |
text_hours_short: h |
|
| 765 |
text_not_assigned: Nie przydzielone |
|
| 752 | 766 |
setting_mail_handler_api_key: Klucz API |
| 753 | 767 |
setting_per_page_options: Opcje ilości obiektów na stronie |
| 754 | 768 |
setting_plain_text_mail: tylko tekst (bez HTML) |
| public/stylesheets/application.css (kopia robocza) | ||
|---|---|---|
| 413 | 413 |
div#version-summary fieldset { margin-bottom: 1em; }
|
| 414 | 414 |
div#version-summary fieldset.time-tracking table { width:100%; }
|
| 415 | 415 |
div#version-summary th, div#version-summary td.total-hours { text-align: right; }
|
| 416 |
.not_estimated a { color: red; }
|
|
| 416 | 417 | |
| 417 | 418 |
table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
|
| 418 | 419 |
table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
|