Added a new named scope to Sprint, called planned, that only returns
sprints that are in the planned state.
Product.can_create_sprints? now checks whether there are any sprints
that are currently in the planned state. If there are, then it returns
that a sprint cannot be created.
Fixed the use cases and added additional fixtures to create products
with no sprints.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/models/product.rb | 2 +-
app/models/sprint.rb | 32 ++++++++++++++--------------
test/fixtures/product_roles.yml | 7 ++++++
test/fixtures/products.yml | 6 +++++
test/fixtures/sprints.yml | 27 +++++++++++++++++++++++
test/fixtures/user_stories.yml | 7 ++++++
test/functional/sprints_controller_test.rb | 23 ++++++++++++-------
test/unit/product_test.rb | 13 +++++++++-
test/unit/sprint_test.rb | 16 ++++++++++++++
9 files changed, 105 insertions(+), 28 deletions(-)
diff --git a/app/models/product.rb b/app/models/product.rb
index 5b12778..3611c97 100644
--- a/app/models/product.rb
+++ b/app/models/product.rb
@@ -94,7 +94,7 @@ class Product < ActiveRecord::Base
# Returns whether the user can create sprints for this product.
def can_create_sprints?(user)
- user && (user.id == owner.id) && !user_stories.empty?
+ user && (user.id == owner.id) && !user_stories.empty? &&
Sprint.for_product(self).planned.empty?
end
# Returns whether the user specified is the product owner.
diff --git a/app/models/sprint.rb b/app/models/sprint.rb
index 7b855a1..f6d808f 100644
--- a/app/models/sprint.rb
+++ b/app/models/sprint.rb
@@ -37,6 +37,18 @@
# sprint is considered healthy.
#
class Sprint < ActiveRecord::Base
+ STATUS_PLANNED = 0
+ STATUS_ACTIVE = 1
+ STATUS_CLOSED = 2
+ STATUS_CANCELED = 3
+ STATUS_TEXT =
+ {
+ 'Planned' => STATUS_PLANNED,
+ 'Active' => STATUS_ACTIVE,
+ 'Closed' => STATUS_CLOSED,
+ 'Canceled' => STATUS_CANCELED
+ }.sort_by { |k,v| v }
+
validates_presence_of :product_id,
:message => 'Sprints must be associated with a product.'
@@ -77,18 +89,7 @@ class Sprint < ActiveRecord::Base
named_scope :for_product, lambda { |product_id|
{ :conditions => product_id ? ["product_id = ?", product_id]: []}
}
-
- STATUS_PLANNED = 0
- STATUS_ACTIVE = 1
- STATUS_CLOSED = 2
- STATUS_CANCELED = 3
- STATUS_TEXT =
- {
- 'Planned' => STATUS_PLANNED,
- 'Active' => STATUS_ACTIVE,
- 'Closed' => STATUS_CLOSED,
- 'Canceled' => STATUS_CANCELED
- }.sort_by { |k,v| v }
+ named_scope :planned, {:conditions => ['status = ?', STATUS_PLANNED]}
# Returns the text for the status.
def status_text
@@ -150,10 +151,9 @@ class Sprint < ActiveRecord::Base
when STATUS_PLANNED:
return true if status == STATUS_ACTIVE
when STATUS_ACTIVE:
- if(status == STATUS_PLANNED && actual_hours == 0) ||
- ([STATUS_CANCELED, STATUS_CLOSED].include?(status))
- return true
- end
+ return true if (status == STATUS_PLANNED && actual_hours == 0 &&
Sprint.for_product(product).planned.empty?)
+ return true if [STATUS_CANCELED, STATUS_CLOSED].include?(status)
+
when STATUS_CLOSED: return true if status == STATUS_ACTIVE
when STATUS_CANCELED: return true if status == STATUS_ACTIVE
end
diff --git a/test/fixtures/product_roles.yml b/test/fixtures/product_roles.yml
index 27ec3ce..28fdfd8 100644
--- a/test/fixtures/product_roles.yml
+++ b/test/fixtures/product_roles.yml
@@ -32,3 +32,10 @@ mcpierce_projxp_web_services:
role_id: <%= Fixtures.identify(:developer) %>
pending: false
is_approved: true
+
+mcpierce_teatime_iphone:
+ product_id: <%= Fixtures.identify(:teatime_iphone) %>
+ user_id: <%= Fixtures.identify(:mcpierce) %>
+ role_id: <%= Fixtures.identify(:developer) %>
+ pending: false
+ is_approved: true
diff --git a/test/fixtures/products.yml b/test/fixtures/products.yml
index 7c7ba03..cfe4a5a 100644
--- a/test/fixtures/products.yml
+++ b/test/fixtures/products.yml
@@ -16,3 +16,9 @@ teatime_midp:
owner_id: <%= Fixtures.identify(:teatime_owner) %>
name: Teatime for JavaME devices.
description: JavaME implementation.
+
+teatime_iphone:
+ project_id: <%= Fixtures.identify(:teatime) %>
+ owner_id: <%= Fixtures.identify(:teatime_owner) %>
+ name: Teatime for the iPhone.
+ description: Teatime for the iPhone.
diff --git a/test/fixtures/sprints.yml b/test/fixtures/sprints.yml
index e6263c3..f1034d5 100644
--- a/test/fixtures/sprints.yml
+++ b/test/fixtures/sprints.yml
@@ -24,3 +24,30 @@ closed_sprint:
goals: Get more stuff done.
status: <%= Sprint::STATUS_CLOSED %>
team_lead_id: <%= Fixtures.identify(:team_lead) %>
+
+projxp_web_services_planned_sprint:
+ product_id: <%= Fixtures.identify(:projxp_web_services) %>
+ title: This is the planned sprint.
+ start: <%= (DateTime.now + 28).to_s(:db) %>
+ duration: 28
+ goals: Get some web services written.
+ status: <%= Sprint::STATUS_PLANNED %>
+ team_lead_id: <%= Fixtures.identify(:mcpierce) %>
+
+projxp_web_services_active_sprint:
+ product_id: <%= Fixtures.identify(:projxp_web_services) %>
+ title: This is the active sprint.
+ start: <%= DateTime.now.to_s(:db) %>
+ duration: 28
+ goals: Get some web services written.
+ status: <%= Sprint::STATUS_ACTIVE %>
+ team_lead_id: <%= Fixtures.identify(:mcpierce) %>
+
+teatime_iphone_active_sprint:
+ product_id: <%= Fixtures.identify(:teatime_iphone) %>
+ title: This is an active sprint.
+ start: <%= DateTime.now.to_s(:db) %>
+ duration: 14
+ goals: Get some iphone stuff done.
+ status: <%= Sprint::STATUS_ACTIVE %>
+ team_lead_id: <%= Fixtures.identify(:mcpierce) %>
diff --git a/test/fixtures/user_stories.yml b/test/fixtures/user_stories.yml
index 216be55..0c7035d 100644
--- a/test/fixtures/user_stories.yml
+++ b/test/fixtures/user_stories.yml
@@ -56,3 +56,10 @@ freerange_user_story:
title: Just something to fix the unit tests.
description: More words than you'll care to read.
closed: false
+
+login_teatime_iphone:
+ product_id: <%= Fixtures.identify(:teatime_iphone) %>
+ priority: 1
+ title: Users on the iPhone can log into the server.
+ description: Read the title!
+ closed: false
diff --git a/test/functional/sprints_controller_test.rb
b/test/functional/sprints_controller_test.rb
index 6bc0781..b1db71d 100644
--- a/test/functional/sprints_controller_test.rb
+++ b/test/functional/sprints_controller_test.rb
@@ -49,6 +49,10 @@ class SprintsControllerTest < ActionController::TestCase
@product_with_no_stories = products(:teatime_midp)
raise "Product cannot have user stories!" unless
@product_with_no_stories.user_stories.empty?
+ @product_with_no_sprints = products(:teatime_iphone)
+ raise "Product cannot have anys prints!" unless
@product_with_no_sprints.sprints.empty?
+ raise "Member must be on product team!" unless
@product_with_no_sprints.is_member?(@member)
+
@new_sprint = {
:title => "New Sprint",
:start => Date.today,
@@ -105,19 +109,19 @@ class SprintsControllerTest < ActionController::TestCase
# Ensures that only the owner can create a new sprint.
def test_new_as_nonowner
- get :new, {:product => @product.id}, {:user_id => @nonowner.id}
+ get :new, {:product => @product_with_no_sprints.id}, {:user_id =>
@nonowner.id}
assert_redirected_to error_path
end
# Ensures that creating a new sprint works as expected.
def test_new
- get :new, {:product => @product.id}, {:user_id => @owner.id}
+ get :new, {:product => @product_with_no_sprints.id}, {:user_id =>
@product_with_no_sprints.owner_id}
assert_response :success
assert assigns['sprint'], "Failed to create a new sprint."
assert assigns['selected'], "Failed to create the default selection
list."
- assert_equal @product.id, assigns['sprint'].product_id,
+ assert_equal @product_with_no_sprints.id, assigns['sprint'].product_id,
"Did not set the product correctly."
end
@@ -160,16 +164,17 @@ class SprintsControllerTest < ActionController::TestCase
# Ensures that an invalid sprint redirects to the edit page.
def test_create_with_invalid_sprint
- post :create, {:product => @product.id, :sprint => {:title => "Invalid
Sprint"}}, {:user_id => @owner.id}
+ post :create, {:product => @product_with_no_sprints.id, :sprint => {:title
=> "Invalid Sprint"}},
+ {:user_id => @product_with_no_sprints.owner_id}
assert_response :success
- assert !Sprint.find_by_title("Invalid Sprint"),
- "Should not have saved the sprint."
+ assert !Sprint.find_by_title("Invalid Sprint"), "Should not have saved
the sprint."
end
# Ensures that, if no members were selected, the sprint is created with an empty
membership.
def test_create_with_no_members
- post :create, {:product => @product.id, :sprint => @new_sprint}, {:user_id
=> @owner.id}
+ post :create, {:product => @product_with_no_sprints.id, :sprint =>
@new_sprint},
+ {:user_id => @product_with_no_sprints.owner_id}
assert_redirected_to plan_sprint_path(assigns['sprint'])
assert assigns['sprint'].members.empty?, "No members should be
defined"
@@ -194,8 +199,8 @@ class SprintsControllerTest < ActionController::TestCase
# Ensures that creating a sprint works as expected.
def test_create
- post :create,{:product => @product.id, :sprint => @new_sprint, :selected =>
[@member.id]},
- {:user_id => @owner.id}
+ post :create,{:product => @product_with_no_sprints.id, :sprint => @new_sprint,
:selected => [@member.id]},
+ {:user_id => @product_with_no_sprints.owner_id}
assert_redirected_to plan_sprint_path(assigns['sprint'])
result = Sprint.find_by_title(@new_sprint[:title])
diff --git a/test/unit/product_test.rb b/test/unit/product_test.rb
index e6cf5c3..5aac4ce 100644
--- a/test/unit/product_test.rb
+++ b/test/unit/product_test.rb
@@ -43,6 +43,9 @@ class ProductTest < ActiveSupport::TestCase
@product_with_no_stories = products(:teatime_midp)
raise "Product must not have stories!" unless
@product_with_no_stories.user_stories.empty?
+
+ @product_with_planned_sprint = products(:projxp_web)
+ raise "Product must have at least one planned sprint!" if
Sprint.for_product((a)product_with_planned_sprint).planned.empty?
end
# Ensures that a project is required.
@@ -102,7 +105,13 @@ class ProductTest < ActiveSupport::TestCase
# Ensures that a product with no user stories won't allow sprints.
def test_can_create_with_no_user_stories
- assert
!@product_with_no_stories.can_create_sprints?((a)product_with_no_stories.owner),
- "The owner should not be allowed to create sprints when there are no
stories."
+ flunk "The owner should not be allowed to create sprints when there are no
stories." if
+ @product_with_no_stories.can_create_sprints?((a)product_with_no_stories.owner)
+ end
+
+ # Ensures that a new sprint cannot be created if an existing one exists in the planned
state.
+ def test_can_create_sprint_with_existing_planned_sprint
+ flunk "You cannot have two planned sprints." if
+
@product_with_planned_sprint.can_create_sprints?((a)product_with_planned_sprint.owner)
end
end
diff --git a/test/unit/sprint_test.rb b/test/unit/sprint_test.rb
index c57cde6..d05f935 100644
--- a/test/unit/sprint_test.rb
+++ b/test/unit/sprint_test.rb
@@ -33,6 +33,10 @@ class SprintTest < ActiveSupport::TestCase
@developer = users(:mcpierce)
raise "Developer is not a member of the product team." unless
@product.is_member?(@developer)
+ @sprint = sprints(:active_sprint)
+ @active_sprint = sprints(:projxp_web_services_active_sprint)
+ raise "There needs to be a planned sprint!" if
Sprint.for_product((a)active_sprint.product).planned.empty?
+
backlog_item = BacklogItem.new(:estimated_hours => 100)
backlog_item.tasks << Task.new(:hours => 50)
backlog_item.update_remaining_hours(50.0,@developer)
@@ -154,4 +158,16 @@ class SprintTest < ActiveSupport::TestCase
def test_can_add_backlog_items_for_inactive_sprint
flunk "Inactive sprints cannot add backlog items." if
@closed_sprint.can_add_backlog_items?((a)closed_sprint.team_lead)
end
+
+ # Ensures that a sprint cannot be moved back to planned state if a planned sprint
exists.
+ def test_allowed_state_planned_with_planned_sprint_planned
+ flunk "Product cannot have two planned sprints." if
@active_sprint.allowed_status?(Sprint::STATUS_PLANNED)
+ end
+
+ # Ensures that a sprint can be moved back to the planned state.
+ def test_allowed_state_planned
+ sprint = sprints(:teatime_iphone_active_sprint)
+
+ flunk "A sprint can move back to the planned state." unless
sprint.allowed_status?(Sprint::STATUS_PLANNED)
+ end
end
--
1.6.0.6