Added a controller method and test to mark an epic as closed.
Modified the epic details page to show a close epic method.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/epics_controller.rb | 38 ++++++++++-
app/models/epic.rb | 20 +++++-
app/views/epics/show.html.erb | 16 +++++
config/routes.rb | 6 ++-
test/fixtures/epics.yml | 8 ++
test/functional/epics_controller_test.rb | 108 ++++++++++++++++++++++++++++++
test/unit/epic_test.rb | 34 +++++++++
7 files changed, 225 insertions(+), 5 deletions(-)
diff --git a/app/controllers/epics_controller.rb b/app/controllers/epics_controller.rb
index 6f7f819..fa251db 100644
--- a/app/controllers/epics_controller.rb
+++ b/app/controllers/epics_controller.rb
@@ -16,9 +16,9 @@
# +EpicsController+ allows users to work with +Epic+ stories.
class EpicsController < ApplicationController
- before_filter :authenticated, :only => [:new, :edit, :create, :update, :destroy]
+ before_filter :authenticated, :except => [:index, :show]
before_filter :load_project
- before_filter :load_epic, :only => [:show, :edit, :update, :destroy]
+ before_filter :load_epic, :except => [:index, :new, :create]
# GET /projects/1/epics
def index
@@ -122,6 +122,40 @@ class EpicsController < ApplicationController
end
end
+ # PUT /projects/1/epics/1/close
+ def close
+ respond_to do |format|
+ if @epic.can_close?(@user)
+ Epic.transaction do
+ @epic.closed = true
+ @epic.save!
+ flash[:message] = "Epic is now marked as closed."
+ end
+ else
+ flash[:error] = "You may not mark this epic completed."
+ end
+
+ format.html { redirect_to project_epic_path(@project, @epic) }
+
+ end
+ end
+
+ # PUT /projects/1/epics/1/reopen
+ def reopen
+ respond_to do |format|
+ if @epic.can_reopen?(@user)
+ @epic.closed = false
+ @epic.save!
+ flash[:message] = "Epic now reopened."
+ else
+ flash[:error] = "You may not reopen this epic."
+ end
+
+ format.html { redirect_to project_epic_path(@project, @epic) }
+
+ end
+ end
+
private
def load_project
diff --git a/app/models/epic.rb b/app/models/epic.rb
index e2e33b6..a1620c5 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -42,11 +42,27 @@ class Epic < ActiveRecord::Base
# Returns whether the user can edit this epic.
def can_edit?(user)
- (user != nil) && (user.id == project.owner_id)
+ is_owner? user
end
# Returns whether this epic can be deleted.
def can_delete?(user)
- (user != nil) && (user.id == project.owner_id) &&
user_stories.empty?
+ is_owner?(user) && user_stories.empty?
+ end
+
+ # Returns whether the user can close this epic.
+ def can_close?(user)
+ is_owner?(user) && !closed
+ end
+
+ # Returns whether the user can reopen this epic.
+ def can_reopen?(user)
+ is_owner?(user) && closed
+ end
+
+ private
+
+ def is_owner?(user)
+ (user != nil) && (user.id == project.owner_id)
end
end
diff --git a/app/views/epics/show.html.erb b/app/views/epics/show.html.erb
index 08aa7fa..75db19c 100644
--- a/app/views/epics/show.html.erb
+++ b/app/views/epics/show.html.erb
@@ -3,6 +3,8 @@
<dl>
<dt><%= "#{(a)epic.title} (Priority: #{(a)epic.priority})"
%></dt>
<dd><%= RedCloth.new((a)epic.description).to_html %></dd>
+
+ <dt><%= "This epic is #{(a)epic.closed ? 'closed' :
'open'}." %></dt>
</dl>
</div>
</div>
@@ -15,8 +17,22 @@
project_epics_path(@project), :class => "command" %>
<% if @epic.can_edit?(@user) %>
+
<%= link_to "Edit this epic",
edit_project_epic_path(@project, @epic), :class => "command" %>
+
+ <% if @epic.can_close?(@user) %>
+ <%= link_to "Close epic...",
+ close_project_epic_path(@project, @epic), :class => "command",
+ :confirm => "Close this epic? Are you sure?", :method => :put %>
+ <% end %>
+
+ <% if @epic.can_reopen?(@user) %>
+ <%= link_to "Reopen epic...",
+ reopen_project_epic_path(@project, @epic), :class => "command",
+ :confirm => "Reopen this epic? Are you sure?", :method => :put %>
+ <% end %>
+
<% end %>
<% if @epic.can_delete?(@user) %>
diff --git a/config/routes.rb b/config/routes.rb
index 6fe5168..21d82f4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -22,7 +22,11 @@ ActionController::Routing::Routes.draw do |map|
end
map.resources :projects do |project|
- project.resources :epics
+ project.resources(:epics, :member =>
+ {
+ :close => :put,
+ :reopen => :put
+ })
end
map.resources :projects, :member =>
{
diff --git a/test/fixtures/epics.yml b/test/fixtures/epics.yml
index 3654b8d..dfcb78c 100644
--- a/test/fixtures/epics.yml
+++ b/test/fixtures/epics.yml
@@ -15,3 +15,11 @@ no_user_story_epic:
priority: 3
title: Has no user stories.
description: Epic with no user stories.
+
+closed_epic:
+ project_id: <%= Fixtures.identify(:projxp) %>
+ priority: 4
+ title: This epic is completed.
+ description: Nope, not open at all.
+ closed: true
+
diff --git a/test/functional/epics_controller_test.rb
b/test/functional/epics_controller_test.rb
index 5660f32..0d41b19 100644
--- a/test/functional/epics_controller_test.rb
+++ b/test/functional/epics_controller_test.rb
@@ -27,6 +27,14 @@ class EpicsControllerTest < ActionController::TestCase
@epic = epics(:no_user_story_epic)
raise "Epic is not in this project!" unless @epic.project_id ==
@project.id
+ @open_epic = @epic
+ raise "Open epic cannot be closed!" if @open_epic.closed
+ raise "Open epic is not in this project!" unless @open_epic.project_id ==
@project.id
+
+ @closed_epic = epics(:closed_epic)
+ raise "Closed epic must be closed!" unless @closed_epic.closed
+ raise "Closed epic is not in this project!" unless @closed_epic.project_id
== @project.id
+
@other_project = projects(:teatime)
@other_owner = @other_project.owner
@@ -299,4 +307,104 @@ class EpicsControllerTest < ActionController::TestCase
result = Epic.find_by_id((a)epic.id)
assert_nil result, "Epic should have been deleted."
end
+
+ # Ensures that anonymous users cannot mark epics completed.
+ def test_close_as_anonymous
+ put :close
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid project must be required.
+ def test_close_with_invalid_project
+ put :close, { }, {:user_id => @owner.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that a valid epic id is required.
+ def test_close_with_invalid_epic
+ put :close, {:project_id => @project.id}, {:user_id => @owner.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that the project and epic are a match.
+ def test_close_with_project_epic_mismatch
+ put :close, {:project_id => @other_project.id, :id => @epic.id}, {:user_id
=> @other_owner.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that only the owner can mark a project completed.
+ def test_close_with_nonowner
+ put :close, {:project_id => @project.id, :id => @epic.id}, {:user_id =>
@nonowner.id}
+
+ assert_redirected_to project_epic_path(@project, @epic)
+ end
+
+ # Ensures that only a closed epic cannot be closed again.
+ def test_closed_with_closed_epic
+ put :close, {:project_id => @project.id, :id => @closed_epic.id}, {:user_id
=> @owner.id}
+
+ assert_redirected_to project_epic_path(@project, @closed_epic)
+ end
+
+ # Ensures that closing an epic works as expected.
+ def test_close
+ put :close, {:project_id => @project.id, :id => @epic.id}, {:user_id =>
@owner.id}
+
+ assert_redirected_to project_epic_path(@project, @epic)
+ assert Epic.find_by_id((a)epic.id).closed, "Epic should have been marked as
closed."
+ end
+
+ # Ensures that anonymous users cannot reopen a closed epic.
+ def test_reopen_as_anonymous
+ put :reopen
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid project must be required.
+ def test_reopen_with_invalid_project
+ put :reopen, { }, {:user_id => @owner.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that a valid epic id is required.
+ def test_reopen_with_invalid_epic
+ put :reopen, {:project_id => @project.id}, {:user_id => @owner.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that the project and epic are a match.
+ def test_reopen_with_project_epic_mismatch
+ put :reopen, {:project_id => @other_project.id, :id => @closed_epic.id},
{:user_id => @other_owner.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that only the owner can reopen a closed epic.
+ def test_reopen_with_nonowner
+ put :reopen, {:project_id => @project.id, :id => @closed_epic.id}, {:user_id
=> @nonowner.id}
+
+ assert_redirected_to project_epic_path(@project, @closed_epic)
+ end
+
+ # Ensures that only an open epic cannot be reopened.
+ def test_reopen_with_open_epic
+ put :reopen, {:project_id => @project.id, :id => @epic.id}, {:user_id =>
@owner.id}
+
+ assert_redirected_to project_epic_path(@project, @epic)
+ end
+
+ # Ensures that reopening an epic works as expected.
+ def test_reopen
+ put :reopen, {:project_id => @project.id, :id => @closed_epic.id}, {:user_id
=> @owner.id}
+
+ assert_redirected_to project_epic_path(@project, @closed_epic)
+ assert !Epic.find_by_id((a)closed_epic.id).closed, "Epic should have been
reopened."
+ end
end
diff --git a/test/unit/epic_test.rb b/test/unit/epic_test.rb
index bd13745..dbe5998 100644
--- a/test/unit/epic_test.rb
+++ b/test/unit/epic_test.rb
@@ -31,6 +31,10 @@ class EpicTest < ActiveSupport::TestCase
@new_epic = Epic.new(:project => @project, :priority => 1, :title =>
"This is an epic")
@epic = epics(:user_stories_epic)
raise "Epic must be part of the project!" unless @epic.project_id ==
@project.id
+ raise "Epic must be open!" if @epic.closed
+
+ @closed_epic = epics(:closed_epic)
+ raise "Epic must be closed!" unless @closed_epic.closed
end
# Ensures the project_id field is required.
@@ -89,4 +93,34 @@ class EpicTest < ActiveSupport::TestCase
def test_can_edit
flunk "Admins must be able to edit epics." unless @epic.can_edit?(@owner)
end
+
+ # Ensures that non-admins cannot close epics.
+ def test_can_close_as_nonadmin
+ flunk "Non-admins cannot close epics." if @epic.can_close?(@nonadmin)
+ end
+
+ # Ensures that a closed epic cannot be closed again.
+ def test_can_close_when_already_closed
+ flunk "Closed epics cannot be closed again." if
@closed_epic.can_close?(@owner)
+ end
+
+ # Ensures that an epic can be closed by the admin.
+ def test_can_close
+ flunk "Admins must be able to close epics." unless
@epic.can_close?(@owner)
+ end
+
+ # Ensures that closed epics cannot be reopened.
+ def test_can_reopen_an_open_epic
+ flunk "Users cannot reopen an open epic." if @epic.can_reopen?(@owner)
+ end
+
+ # Ensures non-admins cannot reopen epics.
+ def test_can_reopen_as_nonadmin
+ flunk "Non-admins cannot reopen epics." if
@closed_epic.can_reopen?(@nonadmin)
+ end
+
+ # Ensures reopening an epic works as expected..
+ def test_can_reopen
+ flunk "Closed tasks should be able to reopen." unless
@closed_epic.can_reopen?(@owner)
+ end
end
--
1.6.0.6