Adds a Setting to control how an Issue's done_ratio is calculated:

* Issue field (default) - the done_ratio field for the Issue
* Issue status - uses the Issue Status's value

  #4274

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@3151 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Eric Davis 2009-12-11 18:48:34 +00:00
parent a83501364d
commit 4fe14e71c2
18 changed files with 212 additions and 4 deletions

View File

@ -18,7 +18,7 @@
class IssueStatusesController < ApplicationController class IssueStatusesController < ApplicationController
before_filter :require_admin before_filter :require_admin
verify :method => :post, :only => [ :destroy, :create, :update, :move ], verify :method => :post, :only => [ :destroy, :create, :update, :move, :update_issue_done_ratio ],
:redirect_to => { :action => :list } :redirect_to => { :action => :list }
def index def index
@ -66,4 +66,13 @@ class IssueStatusesController < ApplicationController
flash[:error] = "Unable to delete issue status" flash[:error] = "Unable to delete issue status"
redirect_to :action => 'list' redirect_to :action => 'list'
end end
def update_issue_done_ratio
if IssueStatus.update_issue_done_ratios
flash[:notice] = l(:notice_issue_done_ratios_updated)
else
flash[:error] = l(:error_issue_done_ratios_not_updated)
end
redirect_to :action => 'list'
end
end end

View File

@ -45,6 +45,8 @@ class Issue < ActiveRecord::Base
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id :author_key => :author_id
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
validates_presence_of :subject, :priority, :project, :tracker, :author, :status validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255 validates_length_of :subject, :maximum => 255
@ -55,7 +57,8 @@ class Issue < ActiveRecord::Base
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } } :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
before_save :update_done_ratio_from_issue_status
after_save :create_journal after_save :create_journal
# Returns true if usr or current user is allowed to view the issue # Returns true if usr or current user is allowed to view the issue
@ -162,6 +165,22 @@ class Issue < ActiveRecord::Base
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end end
def done_ratio
if Issue.use_status_for_done_ratio? && !self.status.default_done_ratio.blank?
self.status.default_done_ratio
else
read_attribute(:done_ratio)
end
end
def self.use_status_for_done_ratio?
Setting.issue_done_ratio == 'issue_status'
end
def self.use_field_for_done_ratio?
Setting.issue_done_ratio == 'issue_field'
end
def validate def validate
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
errors.add :due_date, :not_a_date errors.add :due_date, :not_a_date
@ -198,6 +217,14 @@ class Issue < ActiveRecord::Base
end end
end end
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios
# even if the user turns off the setting later
def update_done_ratio_from_issue_status
if Issue.use_status_for_done_ratio? && !self.status.default_done_ratio.blank?
self.done_ratio = self.status.default_done_ratio
end
end
def after_save def after_save
# Reload is needed in order to get the right status # Reload is needed in order to get the right status
reload reload

View File

@ -33,6 +33,18 @@ class IssueStatus < ActiveRecord::Base
def self.default def self.default
find(:first, :conditions =>["is_default=?", true]) find(:first, :conditions =>["is_default=?", true])
end end
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
def self.update_issue_done_ratios
if Issue.use_status_for_done_ratio?
IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
Issue.update_all(["done_ratio = ?", status.default_done_ratio],
["status_id = ?", status.id])
end
end
return Issue.use_status_for_done_ratio?
end
# Returns an array of all statuses the given role can switch to # Returns an array of all statuses the given role can switch to
# Uses association cache when called more than one time # Uses association cache when called more than one time

View File

@ -5,6 +5,11 @@
<p><label for="issue_status_name"><%=l(:field_name)%><span class="required"> *</span></label> <p><label for="issue_status_name"><%=l(:field_name)%><span class="required"> *</span></label>
<%= text_field 'issue_status', 'name' %></p> <%= text_field 'issue_status', 'name' %></p>
<% if Issue.use_status_for_done_ratio? %>
<p><label for="issue_done_ratio"><%=l(:field_done_ratio)%></label>
<%= select 'issue_status', :default_done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
<% end %>
<p><label for="issue_status_is_closed"><%=l(:field_is_closed)%></label> <p><label for="issue_status_is_closed"><%=l(:field_is_closed)%></label>
<%= check_box 'issue_status', 'is_closed' %></p> <%= check_box 'issue_status', 'is_closed' %></p>

View File

@ -1,5 +1,6 @@
<div class="contextual"> <div class="contextual">
<%= link_to l(:label_issue_status_new), {:action => 'new'}, :class => 'icon icon-add' %> <%= link_to l(:label_issue_status_new), {:action => 'new'}, :class => 'icon icon-add' %>
<%= link_to(l(:label_update_issue_done_ratios), {:action => 'update_issue_done_ratio'}, :class => 'icon icon-multiple', :method => 'post', :confirm => l(:text_are_you_sure)) if Issue.use_status_for_done_ratio? %>
</div> </div>
<h2><%=l(:label_issue_status_plural)%></h2> <h2><%=l(:label_issue_status_plural)%></h2>
@ -7,6 +8,9 @@
<table class="list"> <table class="list">
<thead><tr> <thead><tr>
<th><%=l(:field_status)%></th> <th><%=l(:field_status)%></th>
<% if Issue.use_status_for_done_ratio? %>
<th><%=l(:field_done_ratio)%></th>
<% end %>
<th><%=l(:field_is_default)%></th> <th><%=l(:field_is_default)%></th>
<th><%=l(:field_is_closed)%></th> <th><%=l(:field_is_closed)%></th>
<th><%=l(:button_sort)%></th> <th><%=l(:button_sort)%></th>
@ -16,6 +20,9 @@
<% for status in @issue_statuses %> <% for status in @issue_statuses %>
<tr class="<%= cycle("odd", "even") %>"> <tr class="<%= cycle("odd", "even") %>">
<td><%= link_to status.name, :action => 'edit', :id => status %></td> <td><%= link_to status.name, :action => 'edit', :id => status %></td>
<% if Issue.use_status_for_done_ratio? %>
<td align="center"><%= h status.default_done_ratio %></td>
<% end %>
<td align="center"><%= image_tag 'true.png' if status.is_default? %></td> <td align="center"><%= image_tag 'true.png' if status.is_default? %></td>
<td align="center"><%= image_tag 'true.png' if status.is_closed? %></td> <td align="center"><%= image_tag 'true.png' if status.is_closed? %></td>
<td align="center" style="width:15%;"><%= reorder_links('issue_status', {:action => 'update', :id => status}) %></td> <td align="center" style="width:15%;"><%= reorder_links('issue_status', {:action => 'update', :id => status}) %></td>

View File

@ -34,7 +34,9 @@
<p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p> <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
<p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p> <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
<p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p> <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
<% if Issue.use_field_for_done_ratio? %>
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p> <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
<% end %>
</div> </div>
<div style="clear:both;"> </div> <div style="clear:both;"> </div>

View File

@ -4,7 +4,9 @@
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p> <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
</div> </div>
<div class="splitcontentright"> <div class="splitcontentright">
<% if Issue.use_field_for_done_ratio? %>
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p> <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
<% end %>
<% unless @issue.assignable_versions.empty? %> <% unless @issue.assignable_versions.empty? %>
<p><%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %></p> <p><%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %></p>
<% end %> <% end %>

View File

@ -39,8 +39,10 @@
<%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label> <%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
<label><%= l(:field_due_date) %>: <label><%= l(:field_due_date) %>:
<%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label> <%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
<% if Issue.use_field_for_done_ratio? %>
<label><%= l(:field_done_ratio) %>: <label><%= l(:field_done_ratio) %>:
<%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label> <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
<% end %>
</p> </p>
<% @custom_fields.each do |custom_field| %> <% @custom_fields.each do |custom_field| %>

View File

@ -77,6 +77,7 @@
</ul> </ul>
</li> </li>
<% end -%> <% end -%>
<% if Issue.use_field_for_done_ratio? %>
<li class="folder"> <li class="folder">
<a href="#" class="submenu"><%= l(:field_done_ratio) %></a> <a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
<ul> <ul>
@ -86,7 +87,7 @@
<% end -%> <% end -%>
</ul> </ul>
</li> </li>
<% end %>
<% if !@issue.nil? %> <% if !@issue.nil? %>
<% if @can[:log_time] -%> <% if @can[:log_time] -%>
<li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue},

View File

@ -11,6 +11,9 @@
<%= check_box_tag 'settings[display_subprojects_issues]', 1, Setting.display_subprojects_issues? %> <%= check_box_tag 'settings[display_subprojects_issues]', 1, Setting.display_subprojects_issues? %>
</p> </p>
<p><label><%= l(:setting_issue_done_ratio) %></label>
<%= select_tag 'settings[issue_done_ratio]', options_for_select(Issue::DONE_RATIO_OPTIONS.collect {|i| [l(i.to_sym), i]}, Setting.issue_done_ratio) %></p>
<p><label><%= l(:setting_issues_export_limit) %></label> <p><label><%= l(:setting_issues_export_limit) %></label>
<%= text_field_tag 'settings[issues_export_limit]', Setting.issues_export_limit, :size => 6 %></p> <%= text_field_tag 'settings[issues_export_limit]', Setting.issues_export_limit, :size => 6 %></p>
</div> </div>

View File

@ -147,6 +147,7 @@ en:
notice_account_pending: "Your account was created and is now pending administrator approval." notice_account_pending: "Your account was created and is now pending administrator approval."
notice_default_data_loaded: Default configuration successfully loaded. notice_default_data_loaded: Default configuration successfully loaded.
notice_unable_delete_version: Unable to delete version. notice_unable_delete_version: Unable to delete version.
notice_issue_done_ratios_updated: Issue done ratios updated.
error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}" error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
error_scm_not_found: "The entry or revision was not found in the repository." error_scm_not_found: "The entry or revision was not found in the repository."
@ -157,7 +158,8 @@ en:
error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened' error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
error_can_not_archive_project: This project can not be archived error_can_not_archive_project: This project can not be archived
error_issue_done_ratios_not_updated: "Issue done ratios not updated."
warning_attachments_not_saved: "{{count}} file(s) could not be saved." warning_attachments_not_saved: "{{count}} file(s) could not be saved."
mail_subject_lost_password: "Your {{value}} password" mail_subject_lost_password: "Your {{value}} password"
@ -309,6 +311,7 @@ en:
setting_sequential_project_identifiers: Generate sequential project identifiers setting_sequential_project_identifiers: Generate sequential project identifiers
setting_gravatar_enabled: Use Gravatar user icons setting_gravatar_enabled: Use Gravatar user icons
setting_gravatar_default: Default Gravatar image setting_gravatar_default: Default Gravatar image
setting_issue_done_ratio: Calculate the issue done ratio with
setting_diff_max_lines_displayed: Max number of diff lines displayed setting_diff_max_lines_displayed: Max number of diff lines displayed
setting_file_max_size_displayed: Max size of text files displayed inline setting_file_max_size_displayed: Max size of text files displayed inline
setting_repository_log_display_limit: Maximum number of revisions displayed on file log setting_repository_log_display_limit: Maximum number of revisions displayed on file log
@ -716,6 +719,7 @@ en:
label_version_sharing_hierarchy: With project hierarchy label_version_sharing_hierarchy: With project hierarchy
label_version_sharing_tree: With project tree label_version_sharing_tree: With project tree
label_version_sharing_system: With all projects label_version_sharing_system: With all projects
label_update_issue_done_ratios: Update issue done ratios
button_login: Login button_login: Login
button_submit: Submit button_submit: Submit
@ -850,3 +854,6 @@ en:
enumeration_doc_categories: Document categories enumeration_doc_categories: Document categories
enumeration_activities: Activities (time tracking) enumeration_activities: Activities (time tracking)
enumeration_system_activity: System Activity enumeration_system_activity: System Activity
issue_field: Use the issue field
issue_status: Use the issue status

View File

@ -129,6 +129,8 @@ issue_list_default_columns:
- updated_on - updated_on
display_subprojects_issues: display_subprojects_issues:
default: 1 default: 1
issue_done_ratio:
default: 'issue_field'
default_projects_public: default_projects_public:
default: 1 default: 1
default_projects_modules: default_projects_modules:

View File

@ -0,0 +1,9 @@
class AddDefaultDoneRatioToIssueStatus < ActiveRecord::Migration
def self.up
add_column :issue_statuses, :default_done_ratio, :integer
end
def self.down
remove_column :issue_statuses, :default_done_ratio
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

View File

@ -702,6 +702,7 @@ vertical-align: middle;
.icon-move { background-image: url(../images/move.png); } .icon-move { background-image: url(../images/move.png); }
.icon-save { background-image: url(../images/save.png); } .icon-save { background-image: url(../images/save.png); }
.icon-cancel { background-image: url(../images/cancel.png); } .icon-cancel { background-image: url(../images/cancel.png); }
.icon-multiple { background-image: url(../images/table_multiple.png); }
.icon-folder { background-image: url(../images/folder.png); } .icon-folder { background-image: url(../images/folder.png); }
.open .icon-folder { background-image: url(../images/folder_open.png); } .open .icon-folder { background-image: url(../images/folder_open.png); }
.icon-package { background-image: url(../images/package.png); } .icon-package { background-image: url(../images/package.png); }

View File

@ -70,4 +70,27 @@ class IssueStatusesControllerTest < ActionController::TestCase
assert_redirected_to 'issue_statuses/list' assert_redirected_to 'issue_statuses/list'
assert_not_nil IssueStatus.find_by_id(1) assert_not_nil IssueStatus.find_by_id(1)
end end
context "on POST to :update_issue_done_ratio" do
context "with Setting.issue_done_ratio using the issue_field" do
setup do
Setting.issue_done_ratio = 'issue_field'
post :update_issue_done_ratio
end
should_set_the_flash_to /not updated/
should_redirect_to('the list') { '/issue_statuses/list' }
end
context "with Setting.issue_done_ratio using the issue_status" do
setup do
Setting.issue_done_ratio = 'issue_status'
post :update_issue_done_ratio
end
should_set_the_flash_to /Issue done ratios updated/
should_redirect_to('the list') { '/issue_statuses/list' }
end
end
end end

View File

@ -66,4 +66,40 @@ class IssueStatusTest < ActiveSupport::TestCase
status.reload status.reload
assert status.is_default? assert status.is_default?
end end
context "#update_done_ratios" do
setup do
@issue = Issue.find(1)
@issue_status = IssueStatus.find(1)
@issue_status.update_attribute(:default_done_ratio, 50)
end
context "with Setting.issue_done_ratio using the issue_field" do
setup do
Setting.issue_done_ratio = 'issue_field'
end
should "change nothing" do
IssueStatus.update_issue_done_ratios
assert_equal 0, Issue.count(:conditions => {:done_ratio => 50})
end
end
context "with Setting.issue_done_ratio using the issue_status" do
setup do
Setting.issue_done_ratio = 'issue_status'
end
should "update all of the issue's done_ratios to match their Issue Status" do
IssueStatus.update_issue_done_ratios
issues = Issue.find([1,3,4,5,6,7,9,10])
issues.each do |issue|
assert_equal @issue_status, issue.status
assert_equal 50, issue.read_attribute(:done_ratio)
end
end
end
end
end end

View File

@ -529,4 +529,64 @@ class IssueTest < ActiveSupport::TestCase
end end
assert ActionMailer::Base.deliveries.empty? assert ActionMailer::Base.deliveries.empty?
end end
context "#done_ratio" do
setup do
@issue = Issue.find(1)
@issue_status = IssueStatus.find(1)
@issue_status.update_attribute(:default_done_ratio, 50)
end
context "with Setting.issue_done_ratio using the issue_field" do
setup do
Setting.issue_done_ratio = 'issue_field'
end
should "read the issue's field" do
assert_equal 0, @issue.done_ratio
end
end
context "with Setting.issue_done_ratio using the issue_status" do
setup do
Setting.issue_done_ratio = 'issue_status'
end
should "read the Issue Status's default done ratio" do
assert_equal 50, @issue.done_ratio
end
end
end
context "#update_done_ratio_from_issue_status" do
setup do
@issue = Issue.find(1)
@issue_status = IssueStatus.find(1)
@issue_status.update_attribute(:default_done_ratio, 50)
end
context "with Setting.issue_done_ratio using the issue_field" do
setup do
Setting.issue_done_ratio = 'issue_field'
end
should "not change the issue" do
@issue.update_done_ratio_from_issue_status
assert_equal 0, @issue.done_ratio
end
end
context "with Setting.issue_done_ratio using the issue_status" do
setup do
Setting.issue_done_ratio = 'issue_status'
end
should "not change the issue's done ratio" do
@issue.update_done_ratio_from_issue_status
assert_equal 50, @issue.done_ratio
end
end
end
end end