Added checks to ensure that only one active sprint is allowed for a
product. If an active sprint already exists then other sprints are
disallowed from going into the active state.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/models/sprint.rb | 32 ++++++++++++++++------------
app/views/sprints/show.html.erb | 4 +-
public/stylesheets/tables.css | 2 +-
test/fixtures/sprints.yml | 18 ++++++++++++++++
test/unit/sprint_test.rb | 43 ++++++++++++++++++++++++++++++++++++++-
5 files changed, 81 insertions(+), 18 deletions(-)
diff --git a/app/models/sprint.rb b/app/models/sprint.rb
index f6d808f..a185f93 100644
--- a/app/models/sprint.rb
+++ b/app/models/sprint.rb
@@ -37,16 +37,16 @@
# sprint is considered healthy.
#
class Sprint < ActiveRecord::Base
- STATUS_PLANNED = 0
- STATUS_ACTIVE = 1
- STATUS_CLOSED = 2
- STATUS_CANCELED = 3
+ STATUS_PLANNED = 0
+ STATUS_ACTIVE = 1
+ STATUS_CLOSED = 2
+ STATUS_CANCELLED = 3
STATUS_TEXT =
{
- 'Planned' => STATUS_PLANNED,
- 'Active' => STATUS_ACTIVE,
- 'Closed' => STATUS_CLOSED,
- 'Canceled' => STATUS_CANCELED
+ 'Planned' => STATUS_PLANNED,
+ 'Active' => STATUS_ACTIVE,
+ 'Closed' => STATUS_CLOSED,
+ 'Cancelled' => STATUS_CANCELLED
}.sort_by { |k,v| v }
validates_presence_of :product_id,
@@ -90,6 +90,7 @@ class Sprint < ActiveRecord::Base
{ :conditions => product_id ? ["product_id = ?", product_id]: []}
}
named_scope :planned, {:conditions => ['status = ?', STATUS_PLANNED]}
+ named_scope :active, {:conditions => ['status = ?', STATUS_ACTIVE]}
# Returns the text for the status.
def status_text
@@ -148,14 +149,12 @@ class Sprint < ActiveRecord::Base
# Returns whether the sprint can be moved to the given status.
def allowed_status?(status)
case self.status
- when STATUS_PLANNED:
- return true if status == STATUS_ACTIVE
+ when STATUS_PLANNED: return status == STATUS_ACTIVE &&
Sprint.for_product(product).active.empty?
+ when STATUS_CLOSED: return status == STATUS_ACTIVE &&
Sprint.for_product(product).active.empty?
+ when STATUS_CANCELLED: return status == STATUS_ACTIVE &&
Sprint.for_product(product).active.empty?
when STATUS_ACTIVE:
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
+ return true if [STATUS_CANCELLED, STATUS_CLOSED].include?(status)
end
return false
@@ -176,6 +175,11 @@ class Sprint < ActiveRecord::Base
status == STATUS_CLOSED
end
+ # Returns whether the sprint is cancelled.
+ def cancelled?
+ status == STATUS_CANCELLED
+ end
+
# Returns whether a burndown chart can be viewed for this sprint.
def can_view_burndown?
status != STATUS_PLANNED
diff --git a/app/views/sprints/show.html.erb b/app/views/sprints/show.html.erb
index ce58061..51e3380 100644
--- a/app/views/sprints/show.html.erb
+++ b/app/views/sprints/show.html.erb
@@ -62,9 +62,9 @@
:class => "command" %>
<% end %>
- <% if @sprint.allowed_status?(Sprint::STATUS_CANCELED) %>
+ <% if @sprint.allowed_status?(Sprint::STATUS_CANCELLED) %>
<%= link_to "Cancel this sprint...",
- status_sprint_path(@sprint, :status => Sprint::STATUS_CANCELED),
+ status_sprint_path(@sprint, :status => Sprint::STATUS_CANCELLED),
:method => :put, :confirm => "Cancel this sprint? Are you sure?",
:class => "command" %>
<% end %>
diff --git a/public/stylesheets/tables.css b/public/stylesheets/tables.css
index b5e88bd..24ba278 100644
--- a/public/stylesheets/tables.css
+++ b/public/stylesheets/tables.css
@@ -92,7 +92,7 @@ table.main-list tr.status-2 { /* closed */
background-color: #0ff;
}
-table.main-list tr.status-3 { /* canceled */
+table.main-list tr.status-3 { /* cancelled */
background-color: #f00;
}
diff --git a/test/fixtures/sprints.yml b/test/fixtures/sprints.yml
index 049d5f6..33d834b 100644
--- a/test/fixtures/sprints.yml
+++ b/test/fixtures/sprints.yml
@@ -16,6 +16,15 @@ closed_sprint:
status: <%= Sprint::STATUS_CLOSED %>
team_lead_id: <%= Fixtures.identify(:team_lead) %>
+cancelled_sprint:
+ product_id: <%= Fixtures.identify(:projxp_web) %>
+ title: This sprint is cancelled.
+ start: <%= DateTime.now.to_s(:db) %>
+ duration: 28
+ goals: Get more stuff done.
+ status: <%= Sprint::STATUS_CANCELLED %>
+ team_lead_id: <%= Fixtures.identify(:team_lead) %>
+
inactive_sprint:
product_id: <%= Fixtures.identify(:projxp_web_services) %>
title: This is the planned sprint.
@@ -42,3 +51,12 @@ teatime_iphone_active_sprint:
goals: Get some iphone stuff done.
status: <%= Sprint::STATUS_ACTIVE %>
team_lead_id: <%= Fixtures.identify(:mcpierce) %>
+
+teatime_midp_planned_sprint:
+ product_id: <%= Fixtures.identify(:teatime_midp) %>
+ title: This is the planned sprint.
+ start: <%= DateTime.now.to_s(:db) %>
+ duration: 14
+ goals: Things and things and things.
+ status: <%= Sprint::STATUS_PLANNED %>
+ team_lead_id: <%= Fixtures.identify(:mcpierce) %>
diff --git a/test/unit/sprint_test.rb b/test/unit/sprint_test.rb
index c01f1a0..986963a 100644
--- a/test/unit/sprint_test.rb
+++ b/test/unit/sprint_test.rb
@@ -46,6 +46,10 @@ class SprintTest < ActiveSupport::TestCase
backlog_item.tasks << Task.new(:hours => 25)
@unhealthy_backlog = [ backlog_item ]
+ @planned_sprint = sprints(:teatime_midp_planned_sprint)
+ raise "Sprint must be in planned state!" unless @planned_sprint.pending?
+ raise "Product cannot have active sprint!" unless
Sprint.for_product((a)planned_sprint.product).active.empty?
+
@existing_sprint = sprints(:active_sprint)
@team_lead = @existing_sprint.team_lead
@owner = @product.owner
@@ -53,10 +57,23 @@ class SprintTest < ActiveSupport::TestCase
@closed_sprint = sprints(:closed_sprint)
raise "Sprint must be closed!" unless @closed_sprint.closed?
+ raise "Sprint must be for the same product!" unless
@closed_sprint.product_id == @sprint.product_id
+
+ @cancelled_sprint = sprints(:cancelled_sprint)
+ raise "Sprint must be cancelled!" unless @cancelled_sprint.cancelled?
+ raise "Sprint must be for same product!" unless
@cancelled_sprint.product_id == @closed_sprint.product_id
@active_sprint_only = sprints(:teatime_iphone_active_sprint)
raise "Sprint must be in the active state!" unless
@active_sprint_only.active?
raise "Product cannot have a planned sprint!" unless
Sprint.for_product((a)active_sprint_only).planned.empty?
+
+ product_with_active_sprint = @active_sprint_only.product
+ @new_planned_sprint = Sprint.new(:product => product_with_active_sprint,
+ :title => "Active sprint",
+ :start => Date.today,
+ :duration => 14,
+ :goals => "Get things done",
+ :team_lead => product_with_active_sprint.owner)
end
# Ensures that a sprint has to have a product.
@@ -164,7 +181,7 @@ class SprintTest < ActiveSupport::TestCase
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
+ def test_allowed_state_planned_with_planned_sprint
flunk "Product cannot have two planned sprints." if
@active_sprint.allowed_status?(Sprint::STATUS_PLANNED)
end
@@ -172,4 +189,28 @@ class SprintTest < ActiveSupport::TestCase
def test_allowed_state_planned
flunk "A sprint can move back to the planned state." unless
@active_sprint_only.allowed_status?(Sprint::STATUS_PLANNED)
end
+
+ # Ensures that a planned sprint cannot be made active if an active sprint exists.
+ def test_allowed_state_fails_on_planned_with_active_sprint
+ if @new_planned_sprint.save
+ flunk "Only one active sprint is allowed." if
@new_planned_sprint.allowed_status?(Sprint::STATUS_ACTIVE)
+ else
+ raise "Sprint should have saved!"
+ end
+ end
+
+ # Ensures that a closed sprint cannot be made active if an active sprint exists.
+ def test_allowed_state_fails_on_closed_with_active_sprint
+ flunk "Only one active sprint is allowed!" if
@closed_sprint.allowed_status?(Sprint::STATUS_ACTIVE)
+ end
+
+ # Ensures that a cancelled sprint cannot be made active if an active sprint exists.
+ def test_allowed_state_fails_on_cancelled_with_active
+ flunk "Only one active sprint is allowed!" if
@cancelled_sprint.allowed_status?(Sprint::STATUS_ACTIVE)
+ end
+
+ # Ensures that a planned sprint can be moved to the active state.
+ def test_allowed_state_planned_to_active
+ flunk "Planned sprints should be able to become active." unless
@planned_sprint.allowed_status?(Sprint::STATUS_ACTIVE)
+ end
end
--
1.6.0.6