[Projxp-devel] [2400] Sprints have an explicit state.

Darryl Pierce mcpierce at gmail.com
Sat Oct 11 18:12:18 EDT 2008


This patch contains the code for adding a column, named status, to the
sprints table. Additionally, there is code in the controller for
allowing the product owner to change the status of a sprint, and also
code in the sprint view to display the status.

Index: test/functional/sprint_controller_test.rb
===================================================================
--- test/functional/sprint_controller_test.rb	(revision 30)
+++ test/functional/sprint_controller_test.rb	(working copy)
@@ -30,8 +30,11 @@
       :start    => DateTime.now.to_s(:db),
       :duration => 14
     }
-    @sprint = sprints(:initial_development_sprint)
+    @sprint   = sprints(:initial_development_sprint)
+    @product  = @sprint.product
+    @owner    = @product.owner
     @nonowner = users(:jdonuts)
+    flunk "Owner and nonowner cannot be the same!" if @owner.id == @nonowner.id
     @user_story = user_stories(:create_update_profile)
     @user_story_ids = [@user_story.id]
   end
@@ -47,11 +50,11 @@
   # Ensures that sprints can be viewed.
   #
   def test_view
-    get :view, {:id => sprints(:initial_development_sprint).id}
+    get :view, {:id => @sprint.id}

     assert_response :success
     assert assigns['sprint'], 'Did not load a sprint.'
-    assert_equal sprints(:initial_development_sprint).id, assigns['sprint'].id,
+    assert_equal @sprint.id, assigns['sprint'].id,
       'Did not load the correct sprint.'
   end

@@ -66,7 +69,7 @@
   # Ensures that the product is valid before creating a sprint.
   #
   def test_create_with_invalid_product
-    get :create, {:product => 9999}, {:user_id => users(:mcpierce).id}
+    get :create, {:product => 9999}, {:user_id => @owner.id}

     assert_redirected_to :controller => :home, :action => :error
   end
@@ -74,15 +77,17 @@
   # Ensures that only the product owner can create a sprint.
   #
   def test_create_requires_product_owner
-    get :create, {:product => products(:cairo_web).id}, {:user_id =>
users(:jdonuts).id}
+    get :create,
+      {:product => @product.id},
+      {:user_id => @nonowner.id}

-    assert_redirected_to :controller => :product, :action => :view,
:id => products(:cairo_web).id
+    assert_redirected_to :controller => :product, :action => :view,
:id => @product.id
   end

   # Ensures that a well-formed create request works as expected.
   #
   def test_create
-    get :create, {:product => products(:cairo_web)}, {:user_id =>
products(:cairo_web).owner_id}
+    get :create, {:product => @product}, {:user_id => @owner.id}

     assert_response :success
   end
@@ -93,8 +98,8 @@
     @details[:duration] = -2

     post :create,
-      {:product => products(:cairo_web).id, :sprint => @details},
-      {:user_id => products(:cairo_web).owner_id}
+      {:product => @product.id, :sprint => @details},
+      {:user_id => @owner.id}

     assert_response :success
     assert_template 'sprint/create'
@@ -104,8 +109,8 @@
   #
   def test_create_as_save
     post :create,
-      {:product => products(:cairo_web).id, :sprint => @details},
-      {:user_id => products(:cairo_web).owner_id}
+      {:product => @product.id, :sprint => @details},
+      {:user_id => @owner.id}

     sprint = Sprint.find_by_title(@details[:title])
     assert sprint, 'Failed to save the sprint.'
@@ -123,7 +128,7 @@
   # Ensures that an invalid sprint id fails.
   #
   def test_modify_with_invalid_sprint_id
-    get :modify, {:id => 9999}, {:user_id => users(:mcpierce).id}
+    get :modify, {:id => 9999}, {:user_id => @owner.id}

     assert_redirected_to :controller => :home, :action => :error
   end
@@ -132,19 +137,18 @@
   #
   def test_modify_requires_product_owner
     get :modify,
-      {:id => sprints(:initial_development_sprint).id},
-      {:user_id => users(:jdonuts).id}
+      {:id => @sprint.id},
+      {:user_id => @nonowner.id}

-    assert_redirected_to :controller => :product, :action => :view,
-      :id => sprints(:initial_development_sprint).product_id
+    assert_redirected_to :action => :view, :id => @sprint.id
   end

   # Ensures that a proper request to modify a sprint works.
   #
   def test_modify
     get :modify,
-      {:id => sprints(:initial_development_sprint).id},
-      {:user_id => products(:cairo_web).owner_id}
+      {:id => @sprint.id},
+      {:user_id => @owner.id}

     assert_response :success
   end
@@ -152,7 +156,7 @@
   # Ensures that a modify as save with an invalid sprint fails.
   #
   def test_modify_as_save_with_invalid_sprint
-    sprint = sprints(:initial_development_sprint)
+    sprint = @sprint
     sprint.duration = -2

     post :modify,
@@ -167,7 +171,7 @@
   # Ensures that a modify as save works as expected.
   #
   def test_modify_as_save
-    sprint = sprints(:initial_development_sprint)
+    sprint = @sprint
     sprint.duration *= 2

     post :modify,
@@ -199,7 +203,7 @@
   def test_populate_not_as_product_owner
     get :populate, {:id => @sprint.id}, {:user_id => @nonowner.id}

-    assert_redirected_to :controller => :product, :action => :view,
:id => @sprint.product
+    assert_redirected_to :action => :view, :id => @sprint.id
   end

   # Ensures that entering the sprint populate view works.
@@ -261,4 +265,58 @@

     assert_redirected_to :action => :view, :id => @sprint.id
   end
+
+  # Ensures that anonymous users cannot change a sprint's status.
+  #
+  def test_status_as_anonymous
+    get :status
+
+    assert_redirected_to :controller => :home, :action => :login
+  end
+
+  # Ensures that you cannot change status on an invalid sprint.
+  #
+  def test_status_with_invalid_id
+    get :status, {:id => 9999}, {:user_id => @owner.id}
+
+    assert_redirected_to :controller => :home, :action => :error
+  end
+
+  # Ensures that someone who's not the product owner can't change the status.
+  #
+  def test_status_as_nonowner
+    status = @sprint.status
+
+    get :status, {:id => @sprint.id}, {:user_id => @nonowner.id}
+
+    assert_redirected_to :action => :view, :id => @sprint.id
+    result = Sprint.find_by_id(@sprint.id)
+    assert_equal status, result.status, 'Status should not have changed.'
+  end
+
+  # Ensures that a status must be included.
+  #
+  def test_status_without_value
+    status = @sprint.status
+
+    post :status, {:id => @sprint.id}, {:user_id => @owner.id}
+
+    assert_redirected_to :action => :view, :id => @sprint.id
+    result = Sprint.find_by_id(@sprint.id)
+    assert_equal status, result.status, 'status should not have changed.'
+  end
+
+  # Ensures that a status must be valid.
+  #
+  def test_status_with_invalid_value
+    status = @sprint.status
+
+    post :status,
+      {:id => @sprint.id, :status => 9999},
+      {:user_id => @owner.id}
+
+    assert_redirected_to :action => :view, :id => @sprint.id
+    result = Sprint.find_by_id(@sprint.id)
+    assert_equal status, result.status, 'status should not have changed.'
+  end
 end
Index: test/fixtures/sprints.yml
===================================================================
--- test/fixtures/sprints.yml	(revision 30)
+++ test/fixtures/sprints.yml	(working copy)
@@ -4,3 +4,4 @@
     start: <%= DateTime.now.to_s(:db) %>
     duration: 28
     goals: Get things done.
+    status: <%= Sprint::STATUS_PLANNED %>
\ No newline at end of file
Index: app/helpers/sprint_helper.rb
===================================================================
--- app/helpers/sprint_helper.rb	(revision 30)
+++ app/helpers/sprint_helper.rb	(working copy)
@@ -21,4 +21,8 @@

     return false
   end
+
+  def user_can_change_sprint_status(user, sprint)
+    user && (user.id == sprint.product.owner.id)
+  end
 end
Index: app/models/sprint.rb
===================================================================
--- app/models/sprint.rb	(revision 30)
+++ app/models/sprint.rb	(working copy)
@@ -61,6 +61,18 @@
   has_many :backlog_items
   has_many :user_stories, :through => :backlog_items, :order => :priority

+  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 }
+
   def end_date
     self.start + (self.duration - 1)
   end
@@ -96,9 +108,9 @@
     return result
   end

-  # Returns the overage hours on the project. Overage hours are based on the
+  # Returns the overage hours on the project. Overage hours are based on the
   # number of hours over estimation a backlog item took before it was
completed.
-  #
+  #
   def overage_hours
     result = 0.0

Index: app/controllers/sprint_controller.rb
===================================================================
--- app/controllers/sprint_controller.rb	(revision 30)
+++ app/controllers/sprint_controller.rb	(working copy)
@@ -18,9 +18,9 @@
 # +SprintController+ handles the business rules for sprints.
 #
 class SprintController < ApplicationController
-  before_filter :authenticated,      :only => [:create, :modify,
:populate, :cancel]
+  before_filter :authenticated,      :only => [:create, :modify,
:populate, :cancel, :status]
   before_filter :load_product,       :only => [:create]
-  before_filter :load_sprint,        :only => [:view, :modify, :populate]
+  before_filter :load_sprint,        :only => [:view, :modify,
:populate, :status]
   before_filter :validate_estimates, :only => [:populate]
   before_filter :only_product_owner, :except => [:view]

@@ -119,6 +119,28 @@

   def cancel
   end
+
+  def status
+    if request.post?
+      if params[:status] &&
(0..Sprint::STATUS_TEXT.size).include?(params[:status])
+        begin
+          Sprint.transaction do
+            @sprint.status = params[:status]
+
+            @sprint.save
+          end
+
+        rescue Exception => error
+          flash[:message] = error.message
+        end
+
+        redirect_to :action => :view, :id => @sprint.id
+      else
+        flash[:message] = 'No status was specified or was invalid.'
+        redirect_to :action => :view, :id => @sprint.id
+      end
+    end
+  end

   private

@@ -162,7 +184,11 @@
     unless @product && @product.owner && (@product.owner.id == @user.id)
       flash[:message] = 'Only the product owner can perform that action.'

-      redirect_to :controller => :product, :action => :view, :id => @product
+      if @sprint
+        redirect_to :action => :view, :id => @sprint.id
+      else
+        redirect_to :controller => :product, :action => :view, :id =>
@product.id
+      end
     end
   end

Index: app/views/sprint/view.html.erb
===================================================================
--- app/views/sprint/view.html.erb	(revision 30)
+++ app/views/sprint/view.html.erb	(working copy)
@@ -1,8 +1,10 @@
 <div style="width: 100%; overflow: auto">
   <div style="float: left; width: 33%;">
     <div class="toolbar">
+
       <%= link_to "Edit", :action => :modify, :id => @sprint %>
       <%= link_to "Populate", :action => :populate, :id => @sprint %>
+
     </div>
     <table class="details">
       <thead>
@@ -13,6 +15,21 @@

       <tbody>
         <tr>
+          <td class="label">Status:</td>
+          <td class="value">
+            <% if user_can_change_sprint_status(@user, @sprint) %>
+              <% form_tag(:action => :status, :id => @sprint.id) do %>
+                <%= select_tag "status",
+                  options_for_select(Sprint::STATUS_TEXT, @sprint.status) %>
+                <%= submit_tag "Apply" %>
+              <% end %>
+            <% else %>
+              <%= Sprint::STATUS_TEXT[@sprint.status] %>
+            <% end %>
+          </td>
+        </tr>
+
+        <tr>
           <td class="label">Starts:</td>
           <td class="value"><%= @sprint.start.to_s(:date) %></td>
         </tr>
@@ -28,11 +45,6 @@
         </tr>

         <tr>
-          <td class="label">Status:</td>
-          <td class="value"><%= show_sprint_health @sprint  %></td>
-        </tr>
-
-        <tr>
           <td class="text" colspan="2"><%= simple_format @sprint.goals %></td>
         </tr>
       </tbody>
Index: db/schema.rb
===================================================================
--- db/schema.rb	(revision 30)
+++ db/schema.rb	(working copy)
@@ -9,7 +9,7 @@
 #
 # It's strongly recommended to check this file into your version
control system.

-ActiveRecord::Schema.define(:version => 12) do
+ActiveRecord::Schema.define(:version => 13) do

   create_table "backlog_items", :force => true do |t|
     t.integer  "sprint_id",
             :null => false
@@ -93,13 +93,14 @@
   add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at"

   create_table "sprints", :force => true do |t|
-    t.integer  "product_id",                 :null => false
-    t.string   "title",      :limit => 100,  :null => false
-    t.date     "start",                      :null => false
-    t.integer  "duration",                   :null => false
-    t.string   "goals",      :limit => 1000, :null => false
+    t.integer  "product_id",                                :null => false
+    t.string   "title",      :limit => 100,                 :null => false
+    t.date     "start",                                     :null => false
+    t.integer  "duration",                                  :null => false
+    t.string   "goals",      :limit => 1000,                :null => false
     t.datetime "created_at"
     t.datetime "updated_at"
+    t.integer  "status",                     :default => 0, :null => false
   end

   add_index "sprints", ["product_id"], :name => "fk_sprint_product"


-- 
Darryl L. Pierce <mcpierce at gmail.com>
Visit the Infobahn Offramp: <http://mcpierce.multiply.com>
"Bury me next to my wife. Nothing too fancy..." - Ulysses S. Grant


More information about the Projxp-devel mailing list