THIS PATCH WILL REQUIRE A MIGRATION.
The user_stories table has a column, epic_id, that relates the user
story back to an epic story.
When creating or editing a user story, the user can select from a
dropdown of the list of all epics for that project that are not marked
as closed.
On the user stories list, the epic is shown for each user story
displayed, with a link back to view the epic.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/stories_controller.rb | 11 ++++++++-
app/models/epic.rb | 5 ++++
app/models/user_story.rb | 1 +
app/views/stories/_edit.html.erb | 8 +++++++
app/views/stories/_list.html.erb | 11 ++++++---
db/migrate/031_add_epic_id_to_user_stories.rb | 29 +++++++++++++++++++++++++
test/fixtures/epics.yml | 7 +++++-
test/fixtures/user_stories.yml | 4 +++
test/functional/stories_controller_test.rb | 2 +
9 files changed, 72 insertions(+), 6 deletions(-)
create mode 100644 db/migrate/031_add_epic_id_to_user_stories.rb
diff --git a/app/controllers/stories_controller.rb
b/app/controllers/stories_controller.rb
index 7cfd233..f18d490 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -47,6 +47,7 @@ class StoriesController < ApplicationController
@title = "User Story (New)"
@user_story = UserStory.new(:product_id => @product.id)
@source = params[:source]
+ load_epics
else
flash[:error] = "You are not allowed to write user stories for
#{(a)product.name}."
redirect_to params[:source] ? params[:source] : product_path(@product)
@@ -57,6 +58,7 @@ class StoriesController < ApplicationController
def edit
if @user_story.can_edit?(@user)
@title = "User Story - #{(a)user_story.title} (Edit)"
+ load_epics
unless @product.id == @user_story.product_id
flash[:error] = "This user story does not belong to that product."
redirect_to params[:source] ? params[:source] : product_stories_path(@product)
@@ -81,7 +83,7 @@ class StoriesController < ApplicationController
if params[:add_another]
format.html {
- redirect_to params[:source] ?
+ redirect_to params[:source] ?
new_product_story_url(@product, :source => params[:source]) :
new_product_story_path(@product)
}
@@ -91,6 +93,7 @@ class StoriesController < ApplicationController
else
@title = "User Story (New)"
@user_story.valid?
+ load_epics
format.html { render :action => :edit }
end
end
@@ -114,6 +117,7 @@ class StoriesController < ApplicationController
else
@title = "User Story - #{(a)user_story.title} (Edit)"
@user_story.valid?
+ load_epics
format.html { render :action => :edit }
end
end
@@ -146,6 +150,7 @@ class StoriesController < ApplicationController
def load_product
@product = Product.find_by_id(params[:product_id])
+ @project = @product.project if @product
unless @product
flash[:error] = "Missing or invalid product."
@@ -174,4 +179,8 @@ class StoriesController < ApplicationController
end
end
end
+
+ def load_epics
+ @epics = Epic.find_all_by_project_id((a)project.id, :order => 'priority',
:conditions => "closed = false")
+ end
end
diff --git a/app/models/epic.rb b/app/models/epic.rb
index 6763cae..b86e7f7 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -31,4 +31,9 @@ class Epic < ActiveRecord::Base
:greater_than => 0
validates_presence_of :title
+
+ # Returns a title that's geared for selection lists.
+ def selectable_title
+ "#{title} (#{priority})"
+ end
end
diff --git a/app/models/user_story.rb b/app/models/user_story.rb
index ba6c648..81a1675 100644
--- a/app/models/user_story.rb
+++ b/app/models/user_story.rb
@@ -35,6 +35,7 @@ class UserStory < ActiveRecord::Base
:minimum => 1
belongs_to :product
+ belongs_to :epic
has_many :backlog_items
# Returns whether the user can edit this user story.
diff --git a/app/views/stories/_edit.html.erb b/app/views/stories/_edit.html.erb
index cc290ce..22e606c 100644
--- a/app/views/stories/_edit.html.erb
+++ b/app/views/stories/_edit.html.erb
@@ -38,6 +38,14 @@
</td>
</tr>
+ <tr>
+ <td class="label">Epic Story</td>
+ <td class="value">
+ <%= collection_select :user_story, :epic_id, @epics, :id,
+ :selectable_title, {:include_blank => true} %>
+ </td>
+ </tr>
+
<tr>
<td class="label-required">Priority</td>
<td class="value">
diff --git a/app/views/stories/_list.html.erb b/app/views/stories/_list.html.erb
index 7ccd44b..bf9a3ad 100644
--- a/app/views/stories/_list.html.erb
+++ b/app/views/stories/_list.html.erb
@@ -1,8 +1,9 @@
<table class="list">
<colgroup>
<col class="row_id" />
- <col class="priority" />
+ <col class="number" />
<col class="description" />
+ <col class="number" />
<col class="actions" />
</colgroup>
@@ -12,8 +13,9 @@
</tr>
<tr>
<th>#</th>
- <th>!</th>
+ <th>Rank</th>
<th>Title</th>
+ <th>Epic</th>
<th>Actions</th>
</tr>
</thead>
@@ -21,16 +23,17 @@
<tbody>
<% if @user_stories.empty? %>
<tr>
- <td colspan="4">No user stories found...</td>
+ <td colspan="5">No user stories found...</td>
</tr>
<% else %>
<% @user_stories.each_with_index do |story, index| %>
<% row_class = index%2 == 0 ? 'even' : 'odd' %>
<tr class="<%= row_class %>">
- <td><%= "#{story.id}" %></td>
+ <td><%= story.id %></td>
<td><%= story.priority %></td>
<td><%= story.title %></td>
+ <td><%= story.epic.id %></td>
<td>
<%= link_to(image_tag("icons/view.png", :title => "View
this user story..."),
product_story_path(story.product,story)) %>
diff --git a/db/migrate/031_add_epic_id_to_user_stories.rb
b/db/migrate/031_add_epic_id_to_user_stories.rb
new file mode 100644
index 0000000..6e80f82
--- /dev/null
+++ b/db/migrate/031_add_epic_id_to_user_stories.rb
@@ -0,0 +1,29 @@
+# 031_add_epic_id_to_user_stories.rb
+# Copyright (C) 2009, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <
http://www.gnu.org/licenses/>.
+#
+
+class AddEpicIdToUserStories < ActiveRecord::Migration
+ def self.up
+ add_column :user_stories, :epic_id, :integer, :null => true
+
+ execute 'alter table user_stories add constraint fk_user_story_epic_id
+ foreign key (epic_id) references epics(id)'
+ end
+
+ def self.down
+ remove_column :user_stories, :epic_id
+ end
+end
diff --git a/test/fixtures/epics.yml b/test/fixtures/epics.yml
index 492bf61..664454c 100644
--- a/test/fixtures/epics.yml
+++ b/test/fixtures/epics.yml
@@ -1,4 +1,9 @@
-user_stories:
+user_stories_epic:
project_id: <%= Fixtures.identify(:projxp) %>
priority: 1
title: User stories capture individual features.
+
+login_epic:
+ project_id: <%= Fixtures.identify(:projxp) %>
+ priority: 2
+ title: Covers all login attempts.
diff --git a/test/fixtures/user_stories.yml b/test/fixtures/user_stories.yml
index 50e29e0..7f97e71 100644
--- a/test/fixtures/user_stories.yml
+++ b/test/fixtures/user_stories.yml
@@ -4,6 +4,7 @@ create_user_story:
title: Create the user story subsystem.
description: Create the data model and relationships for user stories.
closed: true
+ epic: <%= Fixtures.identify(:user_story_epic) %>
create_user_story_web_service_api:
product_id: <%= Fixtures.identify(:projxp_web_services) %>
@@ -11,6 +12,7 @@ create_user_story_web_service_api:
title: Create web services for the user story system.
description: Create a wrapper.
closed: false
+ epic: <%= Fixtures.identify(:user_story_epic) %>
create_login:
product_id: <%= Fixtures.identify(:projxp_web) %>
@@ -18,6 +20,7 @@ create_login:
title: Enable users to log into the system.
description: Create a user model, login controller and required pages.
closed: false
+ epic: <%= Fixtures.identify(:login_epic) %>
create_logout:
product_id: <%= Fixtures.identify(:projxp_web) %>
@@ -25,6 +28,7 @@ create_logout:
title: Users can log out of the system.
description: stuff.
closed: true
+ epic: <%= Fixtures.identify(:login_epic) %>
create_update_profile:
product_id: <%= Fixtures.identify(:projxp_web) %>
diff --git a/test/functional/stories_controller_test.rb
b/test/functional/stories_controller_test.rb
index e0b4c32..fe5d42e 100644
--- a/test/functional/stories_controller_test.rb
+++ b/test/functional/stories_controller_test.rb
@@ -113,6 +113,7 @@ class StoriesControllerTest < ActionController::TestCase
assert_response :success
assert assigns['user_story'], "Failed to create a new user story."
+ assert assigns['epics'], "Failed to load any epic stories for this
project."
end
# Ensures that anonymous users can't edit a user story.
@@ -165,6 +166,7 @@ class StoriesControllerTest < ActionController::TestCase
assert_response :success
assert assigns['user_story'], "Failed to load a user story to
edit."
+ assert assigns['epics'], "Failed to load the set of epics."
assert_equal @existing_story.id, assigns['user_story'].id,
"Failed to load the correct user story."
end
--
1.6.0.6