Added a new business rule for enforcing deferred rules. Only items on
active sprints can be deferred.
Added an action to ItemsController, with tests, to allow users to defer
backlog items.
Fixed the styling for deferred backlog items displayed within the item
list.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/items_controller.rb | 21 ++++++++++
app/models/backlog_item.rb | 27 +++++++++----
app/views/items/show.html.erb | 5 ++
config/routes.rb | 3 +-
doc/ChangeLog | 1 +
public/stylesheets/tables.css | 7 +++-
test/functional/items_controller_test.rb | 30 +++++++++++++++
test/unit/backlog_item_test.rb | 60 ++++++++++++++++++++++++-----
8 files changed, 133 insertions(+), 21 deletions(-)
diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb
index 0b35ebe..f0037ea 100644
--- a/app/controllers/items_controller.rb
+++ b/app/controllers/items_controller.rb
@@ -22,6 +22,7 @@ class ItemsController < ApplicationController
before_filter :load_sprint, :only => [:new, :create]
before_filter :load_backlog_item, :except => [:index, :new, :create]
before_filter :verify_can_delete, :only => [:destroy]
+ before_filter :verify_can_defer, :only => [:defer]
before_filter :verify_can_create, :only => [:new, :create]
before_filter :verify_sprint_is_active, :only => [:accept, :estimate, :drop,
:complete, :reopen, :blocked, :update_blocked]
before_filter :verify_is_member, :only => [:accept, :drop, :complete,
:reopen]
@@ -162,6 +163,22 @@ class ItemsController < ApplicationController
end
end
+ # PUT /items/1/defer
+ def defer
+ BacklogItem.transaction do
+ @backlog_item.defer
+
+ if @backlog_item.save
+ flash[:message] = "Item has been deferred."
+ respond_to do |format|
+ format.html {redirect_to items_path(:sprint => @sprint)}
+ end
+ else
+ report_error "Unable to defer item at this time."
+ end
+ end
+ end
+
# POST /items/1/estimate
def estimate
if @backlog_item.can_estimate?(@user)
@@ -258,6 +275,10 @@ class ItemsController < ApplicationController
report_error "You are not allowed to delete this item." unless
@backlog_item.can_delete?(@user)
end
+ def verify_can_defer
+ report_error "You may not defer this item." unless
@backlog_item.can_defer?(@user)
+ end
+
def verify_can_create
report_error "You are not allowed to create or edit backlog items." unless
@sprint.can_add_backlog_items?(@user)
end
diff --git a/app/models/backlog_item.rb b/app/models/backlog_item.rb
index 9f14862..407efb1 100644
--- a/app/models/backlog_item.rb
+++ b/app/models/backlog_item.rb
@@ -42,7 +42,7 @@ class BacklogItem < ActiveRecord::Base
STATE_ASSIGNED = 1
STATE_COMPLETED = 2
STATE_DROPPED = 3
- STATE_CANCELED = 4
+ STATE_DEFERRED = 4
STATE_TEXT =
{
@@ -50,7 +50,7 @@ class BacklogItem < ActiveRecord::Base
STATE_ASSIGNED => 'Assigned',
STATE_COMPLETED => 'Completed',
STATE_DROPPED => 'Dropped',
- STATE_CANCELED => 'Canceled'
+ STATE_DEFERRED => 'Deferred'
}
named_scope :default, { :order => 'priority ASC' }
@@ -71,7 +71,7 @@ class BacklogItem < ActiveRecord::Base
end
def remaining_hours
- return 0.0 if [STATE_COMPLETED, STATE_CANCELED].include?(state)
+ return 0.0 if [STATE_COMPLETED, STATE_DEFERRED].include?(state)
remaining_hours_estimates.empty? ?
estimated_hours :
@@ -109,6 +109,11 @@ class BacklogItem < ActiveRecord::Base
state == STATE_COMPLETED
end
+ # Returns whether the item is deferred.
+ def deferred?
+ state == STATE_DEFERRED
+ end
+
# Sets the state to accepted.
def accept(user)
if user
@@ -133,6 +138,12 @@ class BacklogItem < ActiveRecord::Base
self.state = STATE_COMPLETED
end
+ # Marks the backlog item as deferred.
+ def defer
+ self.state = STATE_DEFERRED
+ self.owner = nil
+ end
+
# Marks the backlog item as reopened.
def reopen
self.state = STATE_PENDING
@@ -151,7 +162,7 @@ class BacklogItem < ActiveRecord::Base
# Returns whether the user can assign this item to another.
def can_assign?(user)
- product_owner?(user) && ![STATE_COMPLETED, STATE_CANCELED].include?(state)
+ product_owner?(user) && ![STATE_COMPLETED, STATE_DEFERRED].include?(state)
end
# Returns whether the user can accept this item.
@@ -197,16 +208,16 @@ class BacklogItem < ActiveRecord::Base
product_owner?(user) && tasks.empty? && !sprint.active?
end
- # Returns whether the user can cancel this backlog item.
- def can_cancel?(user)
- product_owner?(user) && sprint.active? &&
+ # Returns whether the user can defer this backlog item.
+ def can_defer?(user)
+ team_lead?(user) && sprint.active? &&
[STATE_PENDING, STATE_ASSIGNED, STATE_DROPPED].include?(state)
end
# Returns whether the user can reopen this backlog item.
def can_reopen?(user)
(owner?(user) || product_owner?(user) || team_lead?(user)) &&
- [STATE_COMPLETED, STATE_CANCELED].include?(state) &&
+ [STATE_COMPLETED, STATE_DEFERRED].include?(state) &&
sprint.active?
end
diff --git a/app/views/items/show.html.erb b/app/views/items/show.html.erb
index cd172f9..e6e352f 100644
--- a/app/views/items/show.html.erb
+++ b/app/views/items/show.html.erb
@@ -82,6 +82,11 @@
:confirm => "Drop this item? Are you sure?", :class =>
"command" %>
<% end %>
+ <% if @backlog_item.can_defer?(@user) %>
+ <%= link_to "Defer this item...", defer_item_path(@backlog_item),
+ :method => :put, :confirm => "Defer this item? Are you sure?",
:class => "command" %>
+ <% end %>
+
<% if @backlog_item.can_complete?(@user) %>
<%= link_to"Mark this item completed...",
complete_item_path(@backlog_item, :url => request.request_uri),
:confirm => "Mark as completed? Are you sure?", :class =>
"command" %>
diff --git a/config/routes.rb b/config/routes.rb
index e52c84e..9738de7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -62,7 +62,8 @@ ActionController::Routing::Routes.draw do |map|
:drop => :get,
:complete => :get,
:reopen => :get,
- :estimate => :post
+ :estimate => :post,
+ :defer => :put
}
map.resources :users, :member =>
diff --git a/doc/ChangeLog b/doc/ChangeLog
index 819772a..7d42ef9 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -2,6 +2,7 @@ Change Log (0.3.0):
* #156 - Backlog items can be marked as blocked.
* #157 - Items can be marked completed when a task is added.
* #167 - Blocker messages are included in the daily updates email.
+ * #173 - Backlog items can be dropped from an active sprint.
* #175 - When viewing an unapproved project's product list, the sidebar is
misplaced. (BUG)
* #176 - Added a breadcrumb trail to the navigation bar.
* #177 - Epics cannot be created for unapproved projects.
diff --git a/public/stylesheets/tables.css b/public/stylesheets/tables.css
index bc60ca6..b5e88bd 100644
--- a/public/stylesheets/tables.css
+++ b/public/stylesheets/tables.css
@@ -126,8 +126,13 @@ table.main-list tr.state-3 { /* dropped */
background-color: #cff;
}
-table.main-list tr.state-4 { /* canceled */
+table.main-list tr.state-4 { /* deferred */
background-color: #202020;
+ color: #afafaf;
+}
+
+table.main-list tr.state-4 a {
+ color: #a0a0a0;
}
/* edit table styles */
diff --git a/test/functional/items_controller_test.rb
b/test/functional/items_controller_test.rb
index 1960059..3cb35ed 100644
--- a/test/functional/items_controller_test.rb
+++ b/test/functional/items_controller_test.rb
@@ -362,4 +362,34 @@ class ItemsControllerTest < ActionController::TestCase
assert result.blocked, "Item should have been marked as blocked."
assert_equal "Message", result.blocker_message.body, "The message was
not properly saved."
end
+
+ # Ensures that anonymous users cannot defer items.
+ def test_defer_as_anonymous
+ put :defer
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid item id is required.
+ def test_defer_with_invalid_item_id
+ put :defer, {}, {:user_id => @team_lead.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that deferring rules are applied.
+ def test_defer_as_non_team_lead
+ put :defer, {:id => @item.id}, {:user_id => @non_team_lead.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensure that deferring works as expected.
+ def test_defer
+ put :defer, {:id => @item.id}, {:user_id => @team_lead.id}
+
+ assert_redirected_to items_path(:sprint => @item.sprint)
+ result = BacklogItem.find_by_id((a)item.id)
+ assert result.deferred?, "Item should have been deferred."
+ end
end
diff --git a/test/unit/backlog_item_test.rb b/test/unit/backlog_item_test.rb
index 0eb43e6..d6521da 100644
--- a/test/unit/backlog_item_test.rb
+++ b/test/unit/backlog_item_test.rb
@@ -25,9 +25,13 @@ class BacklogItemTest < ActiveSupport::TestCase
def setup
@sprint = sprints(:active_sprint)
+ @team_lead = @sprint.team_lead
@product = @sprint.product
- @item = BacklogItem.new(
+ @non_team_lead = users(:mcpierce)
+ raise "Non-team lead cannot be the team lead!" if @non_team_lead.id ==
@team_lead.id
+
+ @new_item = BacklogItem.new(
:sprint_id => @sprint.id,
:user_story_id => user_stories(:create_login).id,
:estimated_hours => 5.0)
@@ -57,45 +61,47 @@ class BacklogItemTest < ActiveSupport::TestCase
@item_on_closed_sprint = backlog_items(:closed_sprint_backlog_item)
raise "Item must have an owner!" unless @item_on_closed_sprint.owner
raise "That sprint must be closed!" unless
@item_on_closed_sprint.sprint.closed?
+
+ @deferrable_item = backlog_items(:owned_backlog_item)
end
# Ensures that a sprint is required.
def test_valid_fails_without_sprint
- @item.sprint_id = nil
+ @new_item.sprint_id = nil
- flunk "A sprint is required." if @item.valid?
+ flunk "A sprint is required." if @new_item.valid?
end
# Ensures that a user story is required.
def test_valid_fails_without_user_story
- @item.user_story_id = nil
+ @new_item.user_story_id = nil
- flunk "A user story is required." if @item.valid?
+ flunk "A user story is required." if @new_item.valid?
end
# Ensures a well-formed story passes validation.
def test_valid
- flunk "Something's fundamentally wrong." unless @item.valid?
+ flunk "Something's fundamentally wrong." unless @new_item.valid?
end
# Ensures that the default value for remaining hours is the original estimated
# hours.
def test_remaining_when_undefined
- @item.estimated_hours = 2.5
+ @new_item.estimated_hours = 2.5
assert_equal 2.5,
- @item.remaining_hours,
+ @new_item.remaining_hours,
'Remaining hours should equal estimated hours.'
end
# Ensures that adding a new remaining hours estimation affects the hours
# reported.
def test_remaining_hours_when_updated
- @item.estimated_hours = 5.0
- @item.remaining_hours_estimates << RemainingHoursEstimate.new(:hours => 2.5,
:estimated_on => Date.today.next)
+ @new_item.estimated_hours = 5.0
+ @new_item.remaining_hours_estimates << RemainingHoursEstimate.new(:hours =>
2.5, :estimated_on => Date.today.next)
assert_equal 2.5,
- @item.remaining_hours,
+ @new_item.remaining_hours,
'Remaining hours is incorrectly reported.'
end
@@ -189,4 +195,36 @@ class BacklogItemTest < ActiveSupport::TestCase
def test_mark_blocked
fail "An item must allow the owner to mark it as blocked." unless
@owned_item.can_mark_blocked?(@owner)
end
+
+ # Ensures that only the team lead can defer an item.
+ def test_defer_as_non_team_lead
+ fail "Only the team lead can defer an item." if
@deferrable_item.can_defer?(@non_team_lead)
+ end
+
+ # Ensures that an item not on an active sprint cannot be deferred.
+ def test_defer_for_inactive_sprint_item
+ fail "Defer is only for active sprints." if
@item_on_inactive_sprint.can_defer?((a)item_on_inactive_sprint.sprint.team_lead)
+ end
+
+ # Ensures that an owned item can be deferred
+ def test_defer_for_owned_items
+ fail "Items should be deferrable by the team lead." unless
@owned_item.can_defer?((a)owned_item.sprint.team_lead)
+
+ @owned_item.defer
+ assert @owned_item.deferred?, "Item should have been marked as deferred."
+ assert !(a)owned_item.owner, "Deferred items cannot have owners."
+ end
+
+ # Ensures that completed items cannot be deferred.
+ def test_defer_for_completed_items
+ fail "Closed items cannot be deferred." if
@closed_item.can_defer?((a)closed_item.sprint.team_lead)
+ end
+
+ # Ensures that items can be deferred.
+ def test_defer
+ fail "Items should be deferrable by the team lead." unless
@unowned_item.can_defer?((a)unowned_item.sprint.team_lead)
+
+ @unowned_item.defer
+ assert @unowned_item.deferred?, "Item should have been deferred."
+ end
end
--
1.6.0.6