[PATCH] Added the E/A/R hours to the sprint list page.
by Darryl L. Pierce
The hours are shown as a part of the sprint description.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/views/sprints/index.html.erb | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
diff --git a/app/views/sprints/index.html.erb b/app/views/sprints/index.html.erb
index 9998156..690faca 100644
--- a/app/views/sprints/index.html.erb
+++ b/app/views/sprints/index.html.erb
@@ -23,6 +23,7 @@
<td class="name">
<%= link_to "#{sprint.title} (#{Sprint::STATUS_TEXT[sprint.status][0]})",
sprint_path(sprint) %>
+ <%= "Hours: #{show_hours_as_ear(sprint)}" %>
<%= RedCloth.new(get_first_sentence(sprint.goals)).to_html %>
<% unless @product %>
<%= link_to "For product: #{sprint.product.name}", product_path(sprint.product) %>
--
1.6.2
14 years, 11 months
[PATCH] The sprint list shows the number of backlog items per sprint. #179
by Darryl L. Pierce
Added a column to the sprint list page that is the number of backlog
items for each sprint. That number is a link to the backlog items for
that sprint.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/views/sprints/index.html.erb | 2 ++
doc/ChangeLog | 1 +
2 files changed, 3 insertions(+), 0 deletions(-)
diff --git a/app/views/sprints/index.html.erb b/app/views/sprints/index.html.erb
index 9998156..c776c78 100644
--- a/app/views/sprints/index.html.erb
+++ b/app/views/sprints/index.html.erb
@@ -10,6 +10,7 @@
<th scope="col">#</th>
<th scope="col" class="name">Title</th>
<th scope="col">Team Lead</th>
+ <th scope="col">Items</th>
<th scope="col">Members</th>
<th scope="col">Status</th>
<th scope="col">Starts</th>
@@ -32,6 +33,7 @@
<%= link_to sprint.team_lead.display_name,
user_path(sprint.team_lead) %>
</td>
+ <td><%= link_to "#{sprint.backlog_items.size}", items_path(:sprint => sprint) %></td>
<td><%= sprint.members.size %></td>
<td><%= sprint.status_text %></td>
<td><%= "#{show_date(sprint.start)} (#{sprint.duration} days)" %></td>
diff --git a/doc/ChangeLog b/doc/ChangeLog
index 807063d..4d70c6b 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -3,6 +3,7 @@ Change Log (0.3.0):
* #176 - Added a breadcrumb trail to the navigation bar.
* #177 - Epics cannot be created for unapproved projects.
* #178 - A product with no user stories cannot define a sprint.
+ * #179 - The sprint list page shows the number of backlog items per sprint.
* #199 - Admins can approve or reject proposed projects.
* #202 - Project admins are emailed when a new project is requested.
--
1.6.2
14 years, 11 months
[PATCH] Admins can approve/reject propose projects. #199
by Darryl L. Pierce
Added a style to unapproved projects to make them standout in the
project list. Also added a callout on the project details page to
indicate the project is unapproved.
Added support for cascading deletes from project down the object
graph.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/projects_controller.rb | 4 ++--
app/models/product.rb | 9 +++------
app/models/project.rb | 2 +-
app/views/projects/index.html.erb | 6 ++++--
app/views/projects/show.html.erb | 14 ++++++++++++++
config/routes.rb | 6 +++++-
doc/ChangeLog | 1 +
public/stylesheets/tables.css | 4 ++++
8 files changed, 34 insertions(+), 12 deletions(-)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 49f4f5b..accb038 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -127,7 +127,7 @@ class ProjectsController < ApplicationController
end
end
- # POST /project/1/approve
+ # PUT /project/1/approve
def approve
Project.transaction do
@project.approved = true
@@ -141,7 +141,7 @@ class ProjectsController < ApplicationController
end
end
- # POST /project/1/reject
+ # PUT /project/1/reject
def reject
Project.transaction do
@project.destroy
diff --git a/app/models/product.rb b/app/models/product.rb
index fe85d5f..32888ab 100644
--- a/app/models/product.rb
+++ b/app/models/product.rb
@@ -38,13 +38,10 @@ class Product < ActiveRecord::Base
:class_name => 'User',
:foreign_key => 'owner_id'
- has_many :backlog,
- :class_name => 'UserStory',
- :order => 'priority ASC'
- has_many :product_roles
+ has_many :product_roles, :dependent => :destroy
has_many :users, :through => :product_roles
- has_many :user_stories
- has_many :sprints
+ has_many :user_stories, :dependent => :destroy
+ has_many :sprints, :dependent => :destroy
named_scope :for_project, lambda { |project_id|
{
diff --git a/app/models/project.rb b/app/models/project.rb
index 7db7c35..96ddf10 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -35,7 +35,7 @@ class Project < ActiveRecord::Base
belongs_to :owner, :class_name => 'User', :foreign_key => :owner_id
has_many :epics
- has_many :products
+ has_many :products, :dependent => :destroy
named_scope :approved, lambda{ |show_all|
{
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb
index eda8396..80b47a8 100644
--- a/app/views/projects/index.html.erb
+++ b/app/views/projects/index.html.erb
@@ -17,11 +17,13 @@
</thead>
<tbody>
<% @projects.each do |project| %>
- <tr class="<%= cycle('odd', 'even') %>">
+ <% row_class = cycle('odd', 'even') %>
+ <% row_class = "unapproved" unless project.approved? %>
+ <tr class="<%= row_class %>">
<td><%= project.id %></td>
<td class="name">
<div class="small-icon"><%= image_tag project.logo_url %></div>
- <%= link_to project.name, project_path(project) %>
+ <%= link_to "#{project.name}#{project.approved? ? '' : ' (Pending Approval)'}", project_path(project) %>
<% if project.description %>
<%= RedCloth.new(get_first_sentence(project.description)).to_html %>
<% end %>
diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb
index a40ae45..dc48906 100644
--- a/app/views/projects/show.html.erb
+++ b/app/views/projects/show.html.erb
@@ -6,6 +6,7 @@
<div>
<dl>
+ <dt><%= "#{@project.name}#{(a)project.approved? ? '' : ' (Pending Approved)'}" %></dt>
<dt>Owner</dt>
<dd><%= link_to @project.owner.display_name, user_path((a)project.owner) %></dd>
@@ -45,4 +46,17 @@
<%= link_to "Show all products (#{(a)project.products.size} total)",
products_path(:project => @project), :class => "command" %>
+
+ <% if !(a)project.approved? && @project.can_approve?(@user) %>
+
+ <%= link_to "Approve this project...",
+ approve_project_path(@project), :class => "command",
+ :confirm => "Approve this project? Are you sure?", :method => :put %>
+
+ <%= link_to "Reject this project...",
+ reject_project_path(@project), :class => "command",
+ :confirm => "Reject this project? Are you sure?", :method => :put %>
+
+ <% end %>
+
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 6692db9..4f19aec 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -21,7 +21,11 @@ ActionController::Routing::Routes.draw do |map|
admin.resource :server, :controller => 'ServerSettings'
end
- map.resources :projects
+ map.resources :projects, :member =>
+ {
+ :approve => :put,
+ :reject => :put
+ }
map.resources :epics, :member =>
{
:close => :put,
diff --git a/doc/ChangeLog b/doc/ChangeLog
index 705afe8..2280657 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -2,6 +2,7 @@ Change Log (0.3.0):
* #176 - Added a breadcrumb trail to the navigation bar.
* #177 - Epics cannot be created for unapproved projects.
* #178 - A product with no user stories cannot define a sprint.
+ * #199 - Admins can approve or reject proposed projects.
Change Log (0.2.0):
* #97 - Users can request new projects.
diff --git a/public/stylesheets/tables.css b/public/stylesheets/tables.css
index aceddbd..5b2ef1d 100644
--- a/public/stylesheets/tables.css
+++ b/public/stylesheets/tables.css
@@ -34,6 +34,10 @@ table tr.odd {
table tr.even {
}
+table tr.unapproved {
+ background: #66ccff;
+}
+
table tr.odd:hover, table tr.even:hover {
background: #ff0;
color: #000;
--
1.6.2
14 years, 11 months
[PATCH] Products are displayed in a group, by project.
by Darryl L. Pierce
This is only done if the products are not already being filtered by
project.
The group header contains a link that drills down, showing only the
that specific project.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/models/product.rb | 1 +
app/views/products/index.html.erb | 10 ++++++++--
public/stylesheets/tables.css | 19 ++++++++++++++++++-
3 files changed, 27 insertions(+), 3 deletions(-)
diff --git a/app/models/product.rb b/app/models/product.rb
index fe85d5f..0f9b53f 100644
--- a/app/models/product.rb
+++ b/app/models/product.rb
@@ -46,6 +46,7 @@ class Product < ActiveRecord::Base
has_many :user_stories
has_many :sprints
+ named_scope :default, { :group => 'project_id' }
named_scope :for_project, lambda { |project_id|
{
:conditions => project_id ? ["project_id = ?", project_id] : []
diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb
index 46d5301..966eb2d 100644
--- a/app/views/products/index.html.erb
+++ b/app/views/products/index.html.erb
@@ -23,10 +23,16 @@
</thead>
<tbody>
- <% last_project = 0 %>
+ <% last_project_id = 0 %>
<% @products.each do |product| %>
- <% if last_project != product.project_id %>
+ <% unless @project %>
+ <% if last_project_id != product.project_id %>
+ <tr class="group-header">
+ <td colspan="6">For project: <%= link_to product.project.name, products_path(:project => product.project) %>.</td>
+ <tr />
+ <% last_project_id = product.project_id %>
+ <% end %>
<% end %>
<tr class="<%= cycle('odd', 'even') %>">
diff --git a/public/stylesheets/tables.css b/public/stylesheets/tables.css
index aceddbd..b278384 100644
--- a/public/stylesheets/tables.css
+++ b/public/stylesheets/tables.css
@@ -27,8 +27,25 @@ table .name {
width: 50%;
}
+table tr.group-header {
+ font-size; smaller;
+ background-color: #7f7f7f;
+ color: #ff0;
+}
+
+table tr.group-header td {
+ text-align: left;
+ padding-left: 25px;
+}
+
+table tr.group-header td a {
+ text-decoration: none;
+ color: fff;
+ display: inline;
+}
+
table tr.odd {
- background: #f0f0f0;
+ background-color: #f0f0f0;
}
table tr.even {
--
1.6.2
14 years, 11 months
[PATCH] Moved items to the top level, out from under sprints.
by Darryl L. Pierce
Changed the routing to get items out from under sprints.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/items_controller.rb | 253 ++++++++++++------------------
app/models/backlog_item.rb | 5 +
app/views/items/_list.html.erb | 5 +-
app/views/items/show.html.erb | 20 +--
app/views/sprints/plan.html.erb | 2 +-
app/views/sprints/show.html.erb | 2 +-
app/views/tasks/show.html.erb | 2 +-
config/routes.rb | 16 +-
test/functional/items_controller_test.rb | 196 ++++-------------------
9 files changed, 160 insertions(+), 341 deletions(-)
diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb
index 587e783..80a09cc 100644
--- a/app/controllers/items_controller.rb
+++ b/app/controllers/items_controller.rb
@@ -17,30 +17,28 @@
# +ItemsController+ handles CRUD operations on instances of +BacklogItem+.
class ItemsController < ApplicationController
- before_filter :unsupported, :only => [:new, :edit, :create, :update]
- before_filter :authenticated, :except => [:index, :show]
- before_filter :load_product
- before_filter :load_sprint
- before_filter :load_backlog_item, :except => [:index]
- before_filter :load_hours, :only => [:estimate]
- before_filter :path_to_list, :only => [:index]
- before_filter :path_to_one, :only => [:show]
-
- # GET /products/1/sprints/1/items
+ before_filter :unsupported, :only => [:new, :edit, :create, :update]
+ before_filter :authenticated, :except => [:index, :show]
+ before_filter :load_backlog_item, :except => [:index]
+ before_filter :verify_can_delete, :only => [:destroy]
+ before_filter :verify_sprint_is_active, :only => [:accept, :estimate, :drop, :complete, :reopen]
+ before_filter :verify_is_member, :only => [:accept, :drop, :complete, :reopen]
+ before_filter :load_hours, :only => [:estimate]
+ before_filter :path_to_list, :only => [:index]
+ before_filter :path_to_one, :only => [:show]
+
+ # GET /items
def index
- @title = "Sprint Backlog For #{(a)sprint.title}"
- @backlog_items = BacklogItem.paginate(
- :conditions => ['sprint_id = ?', @sprint.id],
- :order => '(select priority from user_stories where id = user_story_id) ASC',
- :page => params[:page],
- :per_page => 10)
+ @title = "Backlog Items"
+ @backlog_items = BacklogItem.for_sprint(params[:sprint]).paginate(:page => params[:page],
+ :per_page => 10)
respond_to do |format|
format.html
end
end
- # GET /products/1/sprints/1/items/1
+ # GET /items/1
def show
@title = "Backlog Item (#{(a)backlog_item.user_story.title})"
respond_to do |format|
@@ -48,211 +46,162 @@ class ItemsController < ApplicationController
end
end
- # DELETE /products/1/sprints/1/items/1
+ # DELETE /items/1
def destroy
- respond_to do |format|
- if @backlog_item.can_delete?(@user)
- if @backlog_item.sprint.pending?
- UserStory.transaction do
- if @backlog_item.destroy
- flash[:message] = "Backlog item deleted successfully."
- format.html { redirect_to product_sprint_items_path(@product, @sprint) }
- else
- flash[:error] = "Unable to delete that backlog item."
- format.html { redirect_to product_sprint_items_path(@product, @sprint) }
- end
- end
- else
- flash[:error] = "You cannot delete backlog items after a sprint is made active."
- format.html { redirect_to product_sprint_items_path(@product, @sprint) }
+ UserStory.transaction do
+ if @backlog_item.destroy
+ respond_to do |format|
+ flash[:message] = "Backlog item deleted successfully."
+ format.html { redirect_to items_path(:sprint => @sprint) }
end
else
- flash[:error] = "You are not allowed to delete backlog items for this sprint."
- format.html { redirect_to product_sprint_items_path(@product, @sprint) }
+ report_error "Unable to delete that backlog item."
end
end
end
- # GET /products/1/sprints/1/items/1/accept
+ # GET /items/1/accept
def accept
- respond_to do |format|
- if @product.is_member?(@user)
- if !(a)backlog_item.owned?
- if @sprint.active?
- @backlog_item.accept(@user)
- if @backlog_item.save
- flash[:message] = "Owner changed to #{(a)backlog_item.owner.display_name}."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_item_path(@product, @sprint, @backlog_item) }
- else
- flash[:error] = "Unable to set ownership."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
- end
- else
- flash[:error] = "You cannot accept items from an inactive sprint."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
- end
- else
- flash[:error] = "That item is already owned by #{(a)backlog_item.owner.display_name}."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
+ if !(a)backlog_item.owned?
+ @backlog_item.accept(@user)
+ if @backlog_item.save
+ respond_to do |format|
+ flash[:message] = "Owner changed to #{(a)backlog_item.owner.display_name}."
+ format.html { redirect_to params[:source] ? params[:source] : item_path(@backlog_item) }
end
else
- flash[:error] = "You are not a member of this product team."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
+ report_error "Unable to set ownership."
end
+ else
+ report_error "That item is already owned by #{(a)backlog_item.owner.display_name}."
end
end
- # GET /products/1/sprints/1/items/1/drop
+ # GET /items/1/drop
def drop
- respond_to do |format|
- if @backlog_item.can_drop?(@user)
- @backlog_item.drop
+ if @backlog_item.can_drop?(@user)
+ @backlog_item.drop
- if @backlog_item.save
+ if @backlog_item.save
+ respond_to do |format|
flash[:message] = "Item was dropped from your backlog."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
- else
- flash[:error] = "There was an error dropping this backlog item."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
+ format.html { redirect_to params[:source] ? params[:source] : items_path(:sprint => @sprint) }
end
else
- flash[:error] = "You cannot drop that backlog item."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
+ report_error "Unable to drop item at this time."
end
+ else
+ report_error "You cannot drop that backlog item."
end
end
- # GET /products/1/sprints/1/items/1/complete
+ # GET /items/1/complete
def complete
- respond_to do |format|
- if @backlog_item.can_complete?(@user)
- @backlog_item.completed
+ if @backlog_item.can_complete?(@user)
+ @backlog_item.completed
- if @backlog_item.save
+ if @backlog_item.save
+ respond_to do |format|
flash[:message] = "Item marked as completed."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
- else
- flash[:error] = "Unable to complete this backlog item."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
+ format.html { redirect_to params[:source] ? params[:source] : items_path(:sprint => @sprint) }
end
else
- flash[:error] = "You are not allowed to complete this backlog item."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
+ report_error "Unable to complete item at this time."
end
+ else
+ report_error "You are not allowed to complete this backlog item."
end
end
- # GET /products/1/sprints/1/items/1/reopen
+ # GET /items/1/reopen
def reopen
- respond_to do |format|
- if @backlog_item.can_reopen?(@user)
- @backlog_item.reopen
+ if @backlog_item.can_reopen?(@user)
+ @backlog_item.reopen
- if @backlog_item.save
+ if @backlog_item.save
+ respond_to do |format|
flash[:message] = "Item successfully reopened."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
- else
- flash[:error] = "Unable to reopen this backlog item."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint )}
+ format.html { redirect_to params[:source] ? params[:source] : items_path(:sprint => @sprint) }
end
else
- flash[:error] = "You cannot reopen this backlog item."
- format.html { redirect_to params[:source] ? params[:source] : product_sprint_items_path(@product, @sprint) }
+ report_error "Unable to reopen this backlog item."
end
+ else
+ report_error "You cannot reopen this backlog item."
end
end
- # POST /products/1/sprints/1/items/1/estimate
+ # POST /items/1/estimate
def estimate
- respond_to do |format|
- if @sprint.active?
- if @backlog_item.can_estimate?(@user)
- old_hours = @backlog_item.remaining_hours
- @backlog_item.update_remaining_hours((a)hours.to_f, @user)
- BacklogItem.transaction do
- @backlog_item.save!
-
+ if @backlog_item.can_estimate?(@user)
+ old_hours = @backlog_item.remaining_hours
+ @backlog_item.update_remaining_hours((a)hours.to_f, @user)
+ BacklogItem.transaction do
+ if @backlog_item.save
+ respond_to do |format|
flash[:message] = "Estimated hours updated from #{old_hours} to #{@hours} hours."
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
+ format.html { redirect_to item_path(@backlog_item) }
end
else
- flash[:error] = "You are not allowed to update the estimated hours for this item."
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
+ report_error "Unable to update the estimated hours at this time."
end
- else
- flash[:error] = "You cannot estimate for items that are not in an active sprint."
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
end
+ else
+ report_error "You are not allowed to update the estimated hours for this item."
end
end
private
def unsupported
- flash[:error] = "That function is not supported for backlog items."
- respond_to do |format|
- format.html { redirect_to products_path }
- end
- end
-
- def load_product
- @product = Product.find_by_id(params[:product_id])
- @project = @product.project if @product
-
- unless @product
- flash[:error] = "Missing or invalid product."
- respond_to do |format|
- format.html { redirect_to products_path }
- end
- end
- end
-
- def load_sprint
- @sprint = Sprint.find_by_id(params[:sprint_id])
-
- unless @sprint && (@sprint.product_id == @product.id)
- flash[:error] = "Missing or invalid sprint."
- respond_to do |format|
- format.html { redirect_to product_sprints_path(@product) }
- end
- end
+ report_error "That function is not supported for backlog items."
end
def load_backlog_item
@backlog_item = BacklogItem.find_by_id(params[:id])
@tasks = @backlog_item.tasks if @backlog_item
+ @sprint = @backlog_item.sprint if @backlog_item
- unless @backlog_item && (@backlog_item.sprint_id == @sprint.id)
- flash[:error] = "Missing or invalid backlog item."
- respond_to do |format|
- format.html { redirect_to product_sprint_items_path(@product, @sprint) }
- end
- end
+ report_error "Missing or invalid backlog item." unless @backlog_item && (@backlog_item.sprint_id == @sprint.id)
end
def load_hours
@hours = params[:hours]
- unless @hours && @hours.to_f > 0.0
- flash[:error] = "Missing or invalid hours."
- respond_to do |format|
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
- end
- end
+ report_error "Missing or invalid hours." unless @hours && @hours.to_f > 0.0
+ end
+
+ def verify_can_delete
+ report_error "You are not allowed to delete this item." unless @backlog_item.can_delete?(@user)
+ end
+
+ def verify_sprint_is_active
+ report_error "This sprint is not in the active state." unless @sprint.active?
+ end
+ def verify_is_member
+ report_error "You must be a member of the sprint team." unless @sprint.is_member?(@user)
end
def path_to_list
- add_breadcrumb "Projects", projects_path
- add_breadcrumb "#{(a)project.id}", project_path(@project)
- add_breadcrumb "Products", products_path(:project => @project.id)
- add_breadcrumb "#{(a)product.id}", product_path(@product)
- add_breadcrumb "Sprints", product_sprints_path(@product)
- add_breadcrumb "#{(a)sprint.id}", product_sprint_path(@product, @sprint)
- add_breadcrumb "Items", product_sprint_items_path(@product, @sprint)
+ if @sprint || params[:sprint]
+ @sprint = Sprint.find_by_id(params[:sprint]) unless @sprint
+ @product = @sprint.product
+ @project = @product.project
+
+ add_breadcrumb "Projects", projects_path
+ add_breadcrumb "#{(a)project.id}", project_path(@project)
+ add_breadcrumb "Products", products_path(:project => @project.id)
+ add_breadcrumb "#{(a)product.id}", product_path(@product)
+ add_breadcrumb "Sprints", product_sprints_path(@product)
+ add_breadcrumb "#{(a)sprint.id}", product_sprint_path(@product, @sprint)
+ add_breadcrumb "Items", items_path(:sprint => @sprint)
+ else
+ add_breadcrumb "Items", items_path
+ end
end
def path_to_one
path_to_list
- add_breadcrumb "#{(a)backlog_item.id}", product_sprint_item_path(@product, @sprint, @backlog_item)
+ add_breadcrumb "#{(a)backlog_item.id}", item_path(@backlog_item)
end
end
diff --git a/app/models/backlog_item.rb b/app/models/backlog_item.rb
index 0604575..5562286 100644
--- a/app/models/backlog_item.rb
+++ b/app/models/backlog_item.rb
@@ -51,6 +51,11 @@ class BacklogItem < ActiveRecord::Base
STATE_CANCELED => 'Canceled'
}
+ named_scope :default, { :order => 'priority ASC' }
+ named_scope :for_sprint, lambda { |sprint_id|
+ { :conditions => sprint_id ? ['sprint_id = ?', sprint_id] : [] }
+ }
+
def state_text
STATE_TEXT[state]
end
diff --git a/app/views/items/_list.html.erb b/app/views/items/_list.html.erb
index d333450..bb95333 100644
--- a/app/views/items/_list.html.erb
+++ b/app/views/items/_list.html.erb
@@ -29,10 +29,9 @@
<td><%= "#{item.id}" %></td>
<td><%= "#{item.user_story.priority}" %></td>
<td class="name">
- <%= link_to "#{item.user_story.title} (#{item.state_text})",
- product_sprint_item_path(item.user_story.product, item.sprint, item) %>
+ <%= link_to "#{item.user_story.title} (#{item.state_text})", item_path(item) %>
<%= RedCloth.new(get_first_sentence(item.user_story.description)).to_html %>
- <%= link_to "View Sprint", product_sprint_path(item.sprint.product, item.sprint) %>
+ <%= link_to "For sprint: #{sprint.title}", product_sprint_path(item.sprint.product, item.sprint) %>
</td>
<% unless @this_user %>
diff --git a/app/views/items/show.html.erb b/app/views/items/show.html.erb
index 6ab3bb8..6d511e3 100644
--- a/app/views/items/show.html.erb
+++ b/app/views/items/show.html.erb
@@ -28,26 +28,22 @@
<% end %>
<% if @backlog_item.can_accept?(@user) %>
- <%= link_to "Accept this item...",
- accept_product_sprint_item_path(@product, @sprint, @backlog_item,
- :url => request.request_uri), :confirm => "Accept this item? Are you sure?", :class => "command" %>
+ <%= link_to "Accept this item...", accept_item_path(@backlog_item,:url => request.request_uri),
+ :confirm => "Accept this item? Are you sure?", :class => "command" %>
<% end %>
<% if @backlog_item.can_drop?(@user) %>
- <%= link_to "Drop this item...",
- drop_product_sprint_item_path(@product, @sprint,@backlog_item,
- :url => request.request_uri), :confirm => "Drop this item? Are you sure?", :class => "command" %>
+ <%= link_to "Drop this item...", drop_item_path(@backlog_item, :url => request.request_uri),
+ :confirm => "Drop this item? Are you sure?", :class => "command" %>
<% end %>
<% if @backlog_item.can_complete?(@user) %>
- <%= link_to"Mark this item completed...",
- complete_product_sprint_item_path(@product, @sprint, @backlog_item,
- :url => request.request_uri), :confirm => "Mark as completed? Are you sure?", :class => "command" %>
+ <%= link_to"Mark this item completed...", complete_item_path(@backlog_item, :url => request.request_uri),
+ :confirm => "Mark as completed? Are you sure?", :class => "command" %>
<% end %>
<% if @backlog_item.can_reopen?(@user) %>
- <%= link_to "Reopen this item...",
- reopen_product_sprint_item_path(@product, @sprint, @backlog_item,
- :url => request.request_uri), :confirm => "Reopen this item? Are you sure?", :class => "command" %>
+ <%= link_to "Reopen this item...", reopen_item_path(@backlog_item, :url => request.request_uri),
+ :confirm => "Reopen this item? Are you sure?", :class => "command" %>
<% end %>
<% end %>
diff --git a/app/views/sprints/plan.html.erb b/app/views/sprints/plan.html.erb
index 7c1461c..d1478ac 100644
--- a/app/views/sprints/plan.html.erb
+++ b/app/views/sprints/plan.html.erb
@@ -1,5 +1,5 @@
<div id="content">
- <%= form_tag populate_sprint_path(@sprint), :html => {:method => :post} %>
+ <%= form_tag populate_product_sprint_path(@product, @sprint), :html => {:method => :post} %>
<%= submit_tag "Populate" %>
<table class="main-list">
<caption><%= "Plan #{(a)sprint.title}" %><?caption>
diff --git a/app/views/sprints/show.html.erb b/app/views/sprints/show.html.erb
index 2de2c3f..e11a26e 100644
--- a/app/views/sprints/show.html.erb
+++ b/app/views/sprints/show.html.erb
@@ -26,7 +26,7 @@
<%= link_to "View sprint members (#{(a)sprint.members.size} total members)...",
members_product_sprint_path(@product, @sprint), :class => "command" %>
<%= link_to "View sprint backlog (#{(a)sprint.backlog_items.size} total items)...",
- product_sprint_items_path(@product, @sprint), :class => "command" %>
+ items_path(:sprint => @sprint), :class => "command" %>
<% if @sprint.can_edit?(@user) %>
<% if @sprint.can_populate?(@user) %>
diff --git a/app/views/tasks/show.html.erb b/app/views/tasks/show.html.erb
index fc491ed..9f73e50 100644
--- a/app/views/tasks/show.html.erb
+++ b/app/views/tasks/show.html.erb
@@ -15,7 +15,7 @@
<% render :layout => 'home/sidebar', :locals => {:title => 'Task Commands'} do %>
<%= link_to "View backlog item",
- product_sprint_item_path(@product, @sprint, @backlog_item), :class => "command" %>
+ item_path(@backlog_item), :class => "command" %>
<% if @task.can_edit?(@user) %>
<%= link_to "Edit this task",
diff --git a/config/routes.rb b/config/routes.rb
index 23b0cb6..4f8486a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -47,16 +47,16 @@ ActionController::Routing::Routes.draw do |map|
:members => :get
}
) do |sprint|
- sprint.resources :items, :member =>
- {
- :accept => :get,
- :drop => :get,
- :complete => :get,
- :reopen => :get,
- :estimate => :post
- }
end
end
+ map.resources :items, :member =>
+ {
+ :accept => :get,
+ :drop => :get,
+ :complete => :get,
+ :reopen => :get,
+ :estimate => :post
+ }
map.resources :users, :member =>
{
:backlog => :get,
diff --git a/test/functional/items_controller_test.rb b/test/functional/items_controller_test.rb
index 959ec90..589dfa6 100644
--- a/test/functional/items_controller_test.rb
+++ b/test/functional/items_controller_test.rb
@@ -54,79 +54,30 @@ class ItemsControllerTest < ActionController::TestCase
raise "Item owner and nonowner cannot be the same person!" if @item_owner.id == @item_nonowner.id
end
- # Ensures that a valid product is required.
- def test_index_with_invalid_product
- get :index
-
- assert_redirected_to products_path
- end
-
- # Ensures that a valid sprint is required.
- def test_index_with_invalid_sprint
- get :index, {:product_id => @product.id}
-
- assert_redirected_to product_sprints_path(@product)
- end
-
- # Ensures that the product and sprint match.
- def test_index_with_sprint_product_mismatch
- get :index,
- {:product_id => @other_product.id, :sprint => @sprint.id}
-
- assert_redirected_to product_sprints_path(@other_product)
- end
-
# Ensures that viewing an index works as expected.
- def test_index
- get :index,
- {:product_id => @product.id, :sprint_id => @sprint.id}
+ def test_index_with_sprint
+ get :index, { :sprint_id => @sprint.id }
assert_response :success
- assert assigns['backlog_items'], "Failed to load the backlog items."
end
- # Ensures that showing a backlog item requires a product.
- def test_show_with_invalid_product
- get :show
-
- assert_redirected_to products_path
- end
-
- # Ensures that a valid sprint is required.
- def test_show_with_invalid_sprint
- get :show, {:product_id => @product.id}
-
- assert_redirected_to product_sprints_path(@product)
- end
-
- # Ensures that the sprint must belong to the product.
- def test_show_with_product_sprint_mismatch
- get :show,
- {:product_id => @other_product.id, :sprint_id => @sprint.id}
+ # Ensures that a valid product is required.
+ def test_index
+ get :index
- assert_redirected_to product_sprints_path(@other_product)
+ assert_response :success
end
# Ensures that a valid backlog item is required.
def test_show_with_invalid_item
- get :show,
- {:product_id => @product.id, :sprint_id => @sprint.id}
-
- assert_redirected_to product_sprint_items_path(@product, @sprint)
- end
-
- # Ensures the item must belong to the sprint.
- def test_show_with_sprint_item_mismatch
- get :show,
- {:product_id => @product.id, :sprint_id => @other_sprint.id, :id => @item.id}
+ get :show, { }
- assert_redirected_to product_sprint_items_path(@product, @other_sprint)
+ assert_redirected_to error_path
end
# Ensures the item can be shown.
def test_show
- get :show,
- {:product_id => @product.id, :sprint_id => @sprint.id, :id => @item.id}
+ get :show, { :id => @item.id }
assert_response :success
assert assigns['backlog_item'], "Failed to load a backlog item."
@@ -142,80 +93,35 @@ class ItemsControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
- # Ensures that a valid product is required.
- def test_destroy_with_invalid_product
- delete :destroy, {}, {:user_id => @owner.id}
-
- assert_redirected_to products_path
- end
-
- # Ensures that a valid sprint is required.
- def test_destroy_with_invalid_sprint
- delete :destroy, {:product_id => @product.id}, {:user_id => @owner.id}
-
- assert_redirected_to product_sprints_path(@product)
- end
-
- # Ensures that the sprint has to belong to the product.
- def test_destroy_with_sprint_product_mismatch
- delete :destroy,
- {:product_id => @other_product.id, :sprint_id => @sprint.id},
- {:user_id => @owner.id}
-
- assert_redirected_to product_sprints_path(@other_product)
- end
-
# Ensures that the item is required.
def test_destroy_with_invalid_item
- delete :destroy,
- {:product_id => @product.id, :sprint_id => @sprint.id},
- {:user_id => @owner.id}
-
- assert_redirected_to product_sprint_items_path(@product, @sprint)
- end
-
- # Ensures that the item must belong to the sprint.
- def test_destroy_with_item_sprint_mismatch
- delete :destroy,
- {:product_id => @product.id, :sprint_id => @other_sprint.id, :id => @item.id},
- {:user_id => @owner.id}
+ delete :destroy, { }, { :user_id => @owner.id }
- assert_redirected_to product_sprint_items_path(@product, @other_sprint)
- assert BacklogItem.find_by_id((a)item.id),
- "Item should not have been deleted."
+ assert_redirected_to error_path
end
# Ensures that only the owner can delete.
def test_destroy_as_nonowner
- get :destroy,
- {:product_id => @product.id, :sprint_id => @sprint.id, :id => @item.id},
- {:user_id => @nonowner.id}
+ get :destroy, { :id => @item.id }, { :user_id => @nonowner.id }
- assert_redirected_to product_sprint_items_path(@product, @sprint)
- assert BacklogItem.find_by_id((a)item.id),
- "Item should not have been deleted."
+ assert_redirected_to error_path
+ assert BacklogItem.find_by_id((a)item.id), "Item should not have been deleted."
end
# Ensures that only an item on a pending sprint can be deleted.
def test_destroy_for_active_sprint
- get :destroy,
- {:product_id => @product.id, :sprint_id => @sprint.id, :id => @item.id},
- {:user_id => @owner.id}
+ get :destroy, { :id => @item.id }, { :user_id => @owner.id }
- assert_redirected_to product_sprint_items_path(@product, @sprint)
- assert BacklogItem.find_by_id((a)item.id),
- "Item should not have been deleted."
+ assert_redirected_to error_path
+ assert BacklogItem.find_by_id((a)item.id), "Item should not have been deleted."
end
# Ensures that an item on a pending sprint can be deleted.
def test_destroy
- get :destroy,
- {:product_id => @product.id, :sprint_id => @pending_sprint.id, :id => @pending_item.id},
- {:user_id => @owner.id}
+ get :destroy, { :id => @pending_item.id }, { :user_id => @owner.id }
- assert_redirected_to product_sprint_items_path(@product, @pending_sprint)
- assert !BacklogItem.find_by_id((a)pending_item.id),
- "Item should have been deleted."
+ assert_redirected_to items_path(:sprint => @pending_item.sprint_id)
+ assert !BacklogItem.find_by_id((a)pending_item.id), "Item should have been deleted."
end
# Ensures that anonymous users can't update estimaetes.
@@ -225,81 +131,45 @@ class ItemsControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
- # Ensures that a valid product is required.
- def test_estimate_with_invalid_product
- post :estimate, {}, {:user_id => @item_owner.id}
-
- assert_redirected_to products_path
- end
-
- # Ensures that a valid sprint is required.
- def test_estimate_with_invalid_sprint
- post :estimate, {:product_id => @product.id}, {:user_id => @item_owner.id}
-
- assert_redirected_to product_sprints_path(@product)
- end
+ # Ensures that a valid item id is required.
+ def test_estimate_with_invalid_item
+ post :estimate, { }, { :user_id => @item_owner.id }
- # Ensures that a valid backlog item is required.
- def test_estimate_with_invalid_backlog_item
- post :estimate,
- {:product_id => @product.id, :sprint_id => @sprint.id},
- {:user_id => @item_owner.id}
-
- assert_redirected_to product_sprint_items_path(@product, @sprint)
+ assert_redirected_to error_path
end
- # Ensures that a postive estimate value is required.
- def test_estimate_with_invalid_backlog_item
- post :estimate,
- {:product_id => @product.id, :sprint_id => @sprint.id, :id => @item.id, :hours => -3},
- {:user_id => @item_owner.id}
-
- assert_redirected_to product_sprint_item_path(@product, @sprint, @item)
- assert_equal @item.remaining_hours, BacklogItem.find_by_id((a)item.id).remaining_hours,
- "Hours should not have been updated."
- end
-
- # Ensures that an hours estimate is required.
+ # Ensures that an hours estimate is required.
def test_estimate_without_hours
- post :estimate,
- {:product_id => @product.id, :sprint_id => @sprint.id, :id => @item.id},
- {:user_id => @item_owner.id}
+ post :estimate, { :id => @item.id }, { :user_id => @item_owner.id }
- assert_redirected_to product_sprint_item_path(@product, @sprint, @item)
+ assert_redirected_to error_path
assert_equal @item.remaining_hours, BacklogItem.find_by_id((a)item.id).remaining_hours,
"Hours should not have been updated."
end
# Ensures that the sprint must be active for an estimate to be updated.
def test_estimate_on_inactive_sprint
- post :estimate,
- {:product_id => @product.id, :sprint_id => @pending_sprint.id, :id => @pending_item.id,
- :estimate => @pending_item.remaining_hours + 1},
- {:user_id => @item_owner.id}
+ post :estimate, { :id => @pending_item.id }, { :user_id => @item_owner.id }
- assert_redirected_to product_sprint_item_path(@product, @pending_sprint, @pending_item)
+ assert_redirected_to error_path
assert_equal @pending_item.remaining_hours, BacklogItem.find_by_id((a)pending_item.id).remaining_hours,
"Remaining hours should not have been updated!"
end
# Ensures that only the item owner can update the estimate hours.
def test_estimate_as_nonowner
- post :estimate,
- {:product_id => @product.id, :sprint_id => @sprint.id, :id => @item.id, :hours => @item.remaining_hours + 1},
- {:user_id => @item_nonowner.id}
+ post :estimate, { :id => @item.id, :hours => @item.remaining_hours + 1 }, { :user_id => @item_nonowner.id }
- assert_redirected_to product_sprint_item_path(@product, @sprint, @item)
+ assert_redirected_to error_path
assert_equal @item.remaining_hours, BacklogItem.find_by_id((a)item.id).remaining_hours,
"Remaining hours should not have been updated."
end
# Ensures that updating an estimate works as expected.
def test_estimate
- post :estimate,
- {:product_id => @product.id, :sprint_id => @sprint.id, :id => @item.id, :hours => 1.7},
- {:user_id => @item_owner.id}
+ post :estimate, { :id => @item.id, :hours => 1.7 }, { :user_id => @item_owner.id }
- assert_redirected_to product_sprint_item_path(@product, @sprint, @item)
+ assert_redirected_to item_path(@item)
assert_equal 1.7, BacklogItem.find_by_id((a)item.id).remaining_hours.to_f,
"Hours should have been updated."
end
--
1.6.2
14 years, 11 months
[PATCH] Moved user stories to the top level.
by Darryl L. Pierce
Fixed all pages that had the old path macro.
User stories can now be listed entirely, or on a per product basis.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/application.rb | 1 +
app/controllers/products_controller.rb | 2 +-
app/controllers/stories_controller.rb | 210 ++++++++++++-------------
app/models/user_story.rb | 7 +-
app/views/layouts/default.html.erb | 1 +
app/views/products/_product.html.erb | 65 --------
app/views/products/index.html.erb | 2 +-
app/views/products/show.html.erb | 2 +-
app/views/sprints/plan.html.erb | 4 +-
app/views/stories/_edit.html.erb | 4 +-
app/views/stories/index.html.erb | 17 ++-
app/views/stories/show.html.erb | 2 +-
config/routes.rb | 3 +-
test/functional/stories_controller_test.rb | 235 +++++++---------------------
14 files changed, 189 insertions(+), 366 deletions(-)
delete mode 100644 app/views/products/_product.html.erb
diff --git a/app/controllers/application.rb b/app/controllers/application.rb
index c07e5f9..1e9b8f5 100644
--- a/app/controllers/application.rb
+++ b/app/controllers/application.rb
@@ -56,6 +56,7 @@ class ApplicationController < ActionController::Base
def load_counts
@project_count = Project.find(:all).size
@product_count = Product.find(:all).size
+ @user_story_count = UserStory.find(:all).size
@user_count = User.find(:all).size
end
diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb
index 964fb95..77801c4 100644
--- a/app/controllers/products_controller.rb
+++ b/app/controllers/products_controller.rb
@@ -52,7 +52,7 @@ class ProductsController < ApplicationController
@project = @product.project
@sprints = Sprint.for_product(@product.id).paginate(:page => params[:sprint_page],
:per_page => 10)
- @user_stories = UserStory.by_product(@product.id).paginate(:order => 'priority ASC',
+ @user_stories = UserStory.for_product(@product.id).paginate(:order => 'priority ASC',
:page => params[:user_story_page],
:per_page => 10)
diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
index b08aeb2..19f08b5 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -16,26 +16,27 @@
# +StoriesController+ performs CRUD operations for instances of +UserStory+.
class StoriesController < ApplicationController
- before_filter :authenticated, :except => [:index, :show]
- before_filter :load_product
- before_filter :load_user_story, :except => [:index, :new, :create]
- before_filter :path_to_list, :only => [:index, :new, :create]
- before_filter :path_to_one, :only => [:show, :edit, :update]
-
+ before_filter :authenticated, :except => [:index, :show]
+ before_filter :load_product, :only => [:new, :create]
+ before_filter :load_user_story, :except => [:index, :new, :create]
+ before_filter :load_epics, :only => [:new, :edit, :create, :update]
+ before_filter :verify_can_create, :only => [:new, :create]
+ before_filter :verify_can_edit, :only => [:edit, :update]
+ before_filter :verify_can_delete, :only => [:destroy]
+ before_filter :path_to_list, :only => [:index, :new, :create]
+ before_filter :path_to_one, :only => [:show, :edit, :update]
+
+ # GET /stories
def index
- @title = "All User Stories for #{(a)product.name}"
- @user_stories = UserStory.paginate(
- :page => params[:page],
- :per_page => 10,
- :conditions => ['product_id = ?', @product.id],
- :order => 'priority ASC')
-
+ @title = "User Stories"
+ @user_stories = UserStory.for_product(params[:product]).paginate(:page => params[:page],
+ :per_page => 10)
respond_to do |format|
format.html
end
end
- # GET /products/1/stories/1
+ # GET /stories/1
def show
@title = "User Story - #{(a)user_story.title}"
respond_to do |format|
@@ -43,111 +44,81 @@ class StoriesController < ApplicationController
end
end
- # GET /products/1/stories/new
+ # GET /stories/new
def new
add_breadcrumb "New"
- if @product.can_create_user_stories?(@user)
- @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)
- end
+ @title = "Write User Story"
+ @user_story = UserStory.new(:product_id => @product.id)
+ @source = params[:source]
end
- # GET /products/1/stories/1/edit
+ # GET /stories/1/edit
def edit
add_breadcrumb "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)
- end
- else
- flash[:error] = "You are not allowed to edit this user story."
- redirect_to params[:source] ? params[:source] : product_story_path(@product, @user_story)
- end
+ @title = "User Story - #{(a)user_story.title} (Edit)"
end
- # POST /products/1/stories
+ # POST /stories
def create
add_breadcrumb "New"
respond_to do |format|
- if @product.can_create_user_stories?(@user)
- UserStory.transaction do
- @user_story = UserStory.new(params[:user_story])
- @user_story.product = @product
- @source = params[:source]
-
- if @user_story.save
- flash[:message] = "The user story was successfully created."
-
- if params[:add_another]
- format.html {
- redirect_to params[:source] ?
- new_product_story_url(@product, :source => params[:source]) :
- new_product_story_path(@product)
- }
- else
- format.html { redirect_to params[:source] ? params[:source] : product_stories_path(@product) }
- end
+ UserStory.transaction do
+ @user_story = UserStory.new(params[:user_story])
+ @user_story.product = @product
+ @source = params[:source]
+
+ puts "@user_story=#{@user_story}"
+ if @user_story.save
+ flash[:message] = "The user story was successfully created."
+
+ if params[:add_another]
+ format.html {
+ redirect_to params[:source] ?
+ new_story_url(:product => @product, :source => params[:source]) :
+ new_story_path(:product => @product)
+ }
else
- @title = "User Story (New)"
- @user_story.valid?
- load_epics
- format.html { render :action => :edit }
+ format.html { redirect_to params[:source] ? params[:source] : stories_path(:product => @product) }
end
+ else
+ @title = "User Story (New)"
+ @user_story.valid?
+ load_epics
+ format.html { render :action => :edit }
end
- else
- flash[:error] = "You are not allowed to write user stories for #{(a)product.name}."
- format.html { redirect_to params[:source] ? params[:source] : product_stories_path(@product) }
end
end
end
- # PUT /products/1/stories/1
+ # PUT /stories/1
def update
add_breadcrumb "Edit"
respond_to do |format|
- if @user_story.can_edit?(@user)
- UserStory.transaction do
- @user_story.update_attributes(params[:user_story])
+ UserStory.transaction do
+ @user_story.update_attributes(params[:user_story])
- if @user_story.save
- flash[:message] = "The user story was successfully updated."
- format.html { redirect_to params[:source] ? params[:source] : product_stories_path(@product) }
- else
- @title = "User Story - #{(a)user_story.title} (Edit)"
- @user_story.valid?
- load_epics
- format.html { render :action => :edit }
- end
+ if @user_story.save
+ flash[:message] = "The user story was successfully updated."
+ format.html { redirect_to params[:source] ? params[:source] : stories_path(:product => @product) }
+ else
+ @title = "User Story - #{(a)user_story.title} (Edit)"
+ @user_story.valid?
+ load_epics
+ format.html { render :action => :edit }
end
- else
- flash[:error] = "You are not authorized to edit user stories for #{(a)product.name}."
- format.html { redirect_to params[:source] ? params[:source] : product_story_path(@product, @user_story) }
end
end
end
- # DELETE /products/1/stories/1
+ # DELETE /stories/1
def destroy
respond_to do |format|
- if @user_story.can_delete?(@user) && @user_story.can_be_deleted?
- if @user_story.destroy
- flash[:message] = "The user story has been deleted."
- format.html { redirect_to params[:source] ? params[:source] : product_stories_path(@product) }
- else
- flash[:error] = "Unable to delete this user story."
- format.html { redirect_to params[:source] ? params[:source] : product_story_path(@product, @user_story) }
- end
+ if @user_story.destroy
+ flash[:message] = "The user story has been deleted."
+ format.html { redirect_to params[:source] ? params[:source] : stories_path(:product => @product) }
else
- flash[:error] = "You are not authorized to delete user stories for #{(a)product.name}."
- format.html { redirect_to params[:source] ? params[:source] : product_stories_path(@product) }
+ flash[:error] = "Unable to delete this user story."
+ format.html { redirect_to params[:source] ? params[:source] : story_path(@user_story) }
end
end
end
@@ -155,51 +126,78 @@ class StoriesController < ApplicationController
private
def load_product
- @product = Product.find_by_id(params[:product_id])
+ @product = Product.find_by_id(params[:product])
@project = @product.project if @product
unless @product
flash[:error] = "Missing or invalid product."
respond_to do |format|
- format.html { redirect_to products_url }
+ format.html { redirect_to error_path }
end
end
end
def load_user_story
@user_story = UserStory.find_by_id(params[:id])
+ @product = @user_story.product if @user_story
+ @project = @product.project if @product
- if @user_story
- unless @user_story.product_id == @product.id
- flash[:error] = "This user story does not belong to #{(a)product.name}."
- respond_to do |format|
- format.html { redirect_to product_stories_path(@product) }
- end
- end
- else
+ unless @user_story
flash[:error] = "Missing or invalid user story id."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+ def verify_can_create
+ unless @product.can_create_user_stories?(@user)
+ flash[:error] = "You are not allowed to create user stories for this product."
respond_to do |format|
- format.html { redirect_to product_stories_path(@product) }
+ format.html { redirect_to error_path }
end
end
end
+ def verify_can_edit
+ unless @user_story.can_edit?(@user)
+ flash[:error] = "You are not allowed to edit this user story."
+
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ def verify_can_delete
+ unless @user_story.can_delete?(@user)
+ flash[:error] = "You are not allowed to deslete this user story."
+
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
def load_epics
@epics = Epic.find_all_by_project_id((a)project.id, :order => 'priority', :conditions => "closed = false")
end
def path_to_list
- add_breadcrumb "Projects", projects_path
- add_breadcrumb "#{(a)project.id}", project_path(@project)
- add_breadcrumb "Products", products_path(:project => @project.id)
- add_breadcrumb "#{(a)product.id}", product_path(@product)
- add_breadcrumb "User Stories", product_stories_path(@product)
+ if @product || params[:product]
+ @product = Product.find_by_id(params[:product]) unless @product
+ project = @product.project if @product
+
+ add_breadcrumb "Projects", projects_path
+ add_breadcrumb "#{project.id}", project_path(project)
+ add_breadcrumb "Products", products_path(:project => project.id)
+ add_breadcrumb "#{(a)product.id}", product_path(@product)
+ end
+ add_breadcrumb "User Stories", stories_path(:product => @product)
end
def path_to_one
path_to_list
- add_breadcrumb "#{(a)user_story.id}", product_story_path(@product, @user_story)
+ add_breadcrumb "#{(a)user_story.id}", story_path(@user_story)
end
end
diff --git a/app/models/user_story.rb b/app/models/user_story.rb
index 7916497..4007790 100644
--- a/app/models/user_story.rb
+++ b/app/models/user_story.rb
@@ -38,8 +38,9 @@ class UserStory < ActiveRecord::Base
belongs_to :epic
has_many :backlog_items
- named_scope :by_product, lambda { |product_id|
- {:conditions => ["product_id = ?", product_id]}
+ named_scope :default, { :order => 'priority ASC' }
+ named_scope :for_product, lambda { |product_id|
+ { :conditions => product_id ? ["product_id = ?", product_id] : [] }
}
# Returns whether the user can edit this user story.
@@ -49,7 +50,7 @@ class UserStory < ActiveRecord::Base
# Returns whether the user can delete this user story.
def can_delete?(user)
- user && (user.id == product.owner_id)
+ user && (user.id == product.owner_id) && can_be_deleted?
end
# Returns whether the user story can be deleted.
diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb
index 72b5693..b506363 100644
--- a/app/views/layouts/default.html.erb
+++ b/app/views/layouts/default.html.erb
@@ -52,6 +52,7 @@
<li><%= link_to "Home", :controller => :home %></li>
<li><%= link_to "Projects (#{@project_count})", projects_path %></li>
<li><%= link_to "Products (#{@product_count})", products_path %></li>
+ <li><%= link_to "User Stories (#{@user_story_count})", stories_path %></li>
<li><%= link_to "Users (#{@user_count})", users_path %></li>
<li><%= link_to "Reports", :controller => :report, :action => :index %></li>
</ul>
diff --git a/app/views/products/_product.html.erb b/app/views/products/_product.html.erb
deleted file mode 100644
index cfdec46..0000000
--- a/app/views/products/_product.html.erb
+++ /dev/null
@@ -1,65 +0,0 @@
-<table class="detail">
- <colgroup>
- <col class="label" />
- <col class="value" />
- </colgroup>
-
- <thead>
- <tr>
- <th class="title" colspan="2"><%= "Details For #{(a)product.name}" %></th>
- </tr>
- </thead>
-
-
- <tbody>
- <tr>
- <td class="toolbar" colspan="2">
- <%= link_to(image_tag("icons/product_members.png", :title => "View the product team..."),
- product_roles_path(@product)) %>
- <%= link_to(image_tag("icons/product_join.png", :title => "Join this product team..."),
- new_product_role_path(@product)) unless @product.is_member?(@user) %>
- <%= link_to(image_tag("icons/edit.png", :title => "Edit this product..."),
- edit_product_path(@product, :url => request.request_uri)) if @product.can_edit?(@user) %>
- <%= link_to(image_tag("icons/story_add.png", :title => "Create a new user story..."),
- new_product_story_path(@product, :source => request.request_uri)) if @product.can_create_user_stories?(@user) %>
- <%= link_to(image_tag("icons/sprint_add.png", :title => "Create a new sprint..."),
- new_product_sprint_path(@product)) if @product.can_create_sprints?(@user) %>
- </td>
- </tr>
-
- <tr>
- <td class="label">Owner:</td>
- <td class="value"><%= link_to @product.owner.display_name, user_path((a)product.owner) %></td>
- </tr>
-
- <% if @product.has_mailing_list? %>
- <tr>
- <td class="label">Mailing list:</td>
- <td class="value"><%= mail_to @product.mailing_list, @product.mailing_list %>
- </tr>
- <% end %>
-
- <tr>
- <td class="label">For Project:</td>
- <td class="value"><%= link_to @product.project.name, project_path((a)product.project) %></td>
- </tr>
-
- <tr>
- <td class="label">Members:</td>
- <td class="value">
- <%= link_to "#{(a)product.active_roles.size > 0 ? @product.active_roles.size : 'No'} Active Members",
- product_roles_path(@product) %>
- <% if @product.is_owner?(@user) %>
- <%= "(#{(a)product.pending_roles.size > 0 ? @product.pending_roles.size : 'No'} Role Requests)" %>
- <% end %>
- </td>
- </tr>
-
- <tr>
- <td class="text" colspan="2">
- <%= RedCloth.new((a)product.description).to_html %>
- </td>
- </tr>
- </tbody>
-</table>
-
diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb
index f4fbcce..462c208 100644
--- a/app/views/products/index.html.erb
+++ b/app/views/products/index.html.erb
@@ -38,7 +38,7 @@
<%= RedCloth.new(get_first_sentence(product.description)).to_html %>
<%= link_to product.owner.display_name,user_path(product.owner) %>
</td>
- <td><%= link_to "#{product.backlog.size}", product_stories_path(product) %></td>
+ <td><%= link_to "#{product.backlog.size}", stories_path(:product => product) %></td>
<td><%= link_to "#{product.sprints.size}", product_sprints_path(product) %></td>
<td><%= link_to "#{product.product_roles.size}", product_roles_path(product) %>
<td><%= show_date product.created_at %></td>
diff --git a/app/views/products/show.html.erb b/app/views/products/show.html.erb
index 1bce3a3..ff7229d 100644
--- a/app/views/products/show.html.erb
+++ b/app/views/products/show.html.erb
@@ -37,7 +37,7 @@
<% end %>
<%= link_to "View user stories (#{(a)product.user_stories.size} total)",
- product_stories_path(@product), :class => "command" %>
+ stories_path(:product => @product), :class => "command" %>
<%= link_to "View sprints (#{(a)product.sprints.size} total)",
product_sprints_path(@product), :class => "command" %>
diff --git a/app/views/sprints/plan.html.erb b/app/views/sprints/plan.html.erb
index 4dbeab6..7c1461c 100644
--- a/app/views/sprints/plan.html.erb
+++ b/app/views/sprints/plan.html.erb
@@ -1,5 +1,5 @@
<div id="content">
- <%= form_tag populate_product_sprint_path(@product, @sprint), :html => {:method => :post} %>
+ <%= form_tag populate_sprint_path(@sprint), :html => {:method => :post} %>
<%= submit_tag "Populate" %>
<table class="main-list">
<caption><%= "Plan #{(a)sprint.title}" %><?caption>
@@ -18,7 +18,7 @@
<td><%= story.id %></td>
<td><%= story.priority %></td>
<td class="name">
- <%= link_to story.title, product_story_path(@product, story), :target => "_other" %>
+ <%= link_to story.title, story_path(story), :target => "_other" %>
<%= RedCloth.new(get_first_sentence(story.description)).to_html %>
</td>
<td>
diff --git a/app/views/stories/_edit.html.erb b/app/views/stories/_edit.html.erb
index e571418..af03027 100644
--- a/app/views/stories/_edit.html.erb
+++ b/app/views/stories/_edit.html.erb
@@ -2,9 +2,9 @@
<% html = @user_story.new_record? ? {:method => :post} : {:method => :put} %>
<% form_for(:user_story, @user_story,
- :url => product_story_path(@product, @user_story), :html => html) do |form| %>
+ :url => story_path(@user_story), :html => html) do |form| %>
- <%= hidden_field_tag :product_id, @product.id %>
+ <%= hidden_field_tag :product, @product.id %>
<% if @source %>
<%= hidden_field_tag :source, @source %>
diff --git a/app/views/stories/index.html.erb b/app/views/stories/index.html.erb
index 48a7c8a..d281d83 100644
--- a/app/views/stories/index.html.erb
+++ b/app/views/stories/index.html.erb
@@ -1,10 +1,10 @@
-<div id="content">
+<div id="<% if @product && @product.can_create_user_stories?(@user) %>content<% else %>content-no-sidebar<% end %>">
<div>
<%= will_paginate @user_stories %>
</div>
<div>
<table class="main-list">
- <caption><%= "User Stories For #{(a)product.name}" %></caption>
+ <caption><%= @title %></caption>
<thead>
<tr>
<th scope="col">#</th>
@@ -22,8 +22,11 @@
<td><%= story.priority %></td>
<td class="name">
<%= link_to story.title,
- product_story_path(@product, story) %>
+ story_path(story) %>
<%= RedCloth.new(get_first_sentence(story.description)).to_html %>
+ <% unless @product %>
+ <%= link_to "For product: #{story.product.name}", product_path(story.product) %>
+ <% end %>
</td>
<td>
<% if story.epic %>
@@ -39,9 +42,11 @@
</div>
</div>
+<% if @product && @product.can_create_user_stories?(@user) %>
+
<% render :layout => 'home/sidebar', :locals => {:title => 'Commands'} do %>
- <% if @product.can_create_user_stories?(@user) %>
<%= link_to "Write user stories...",
- new_product_story_path(@product), :class => "command" %>
- <% end %>
+ new_story_path(:product => @product), :class => "command" %>
+<% end %>
+
<% end %>
diff --git a/app/views/stories/show.html.erb b/app/views/stories/show.html.erb
index 8642430..9afd9f6 100644
--- a/app/views/stories/show.html.erb
+++ b/app/views/stories/show.html.erb
@@ -10,6 +10,6 @@
<% render :layout => 'home/sidebar', :locals => {:title => 'User Story Commands'} do %>
<%if @user_story.can_edit?(@user) %>
<%= link_to "Edit user story...",
- edit_product_story_path(@product, @user_story), :class => "command" %>
+ edit_story_path(@user_story), :class => "command" %>
<% end %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 21d82f4..1881ba7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -33,10 +33,9 @@ ActionController::Routing::Routes.draw do |map|
:approve => :put,
:reject => :put
}
-
+ map.resources :stories
map.resources :products do |product|
product.resources :roles
- product.resources :stories
product.resource :sprint, :member =>
{
:show => :post
diff --git a/test/functional/stories_controller_test.rb b/test/functional/stories_controller_test.rb
index fe5d42e..1f17bf1 100644
--- a/test/functional/stories_controller_test.rb
+++ b/test/functional/stories_controller_test.rb
@@ -44,28 +44,22 @@ class StoriesControllerTest < ActionController::TestCase
}
end
- # Ensures that viewing user stories requires a product.
- def test_index_without_product
- get :index
+ # Ensures that all stories by product are viewable.
+ def test_index_for_product
+ get :index, { :product => @product.id }
- assert_redirected_to products_url
+ assert_response :success
+ assert_template 'stories/index'
+ assert assigns['user_stories'], "Failed to load stories."
+ assigns['user_stories'].each { |story| assert_equal @product.id, story.product_id, "Loaded stories for the wrong product." }
end
# Ensures that viewing all user stories works.
def test_index
- get :index, {:product_id => @product.id}
+ get :index
assert_response :success
- assert assigns['user_stories'], "Failed to load any user stories."
- assert_equal UserStory.find_all_by_product_id((a)product.id).size,assigns['user_stories'].size,
- "Did not load the right set of user stories."
- end
-
- # Ensures that a product is required to view a user story.
- def test_show_without_product_id
- get :show
-
- assert_redirected_to products_path
+ assert_template 'stories/index'
end
# Ensures that without a user story id, the user is sent back to the user
@@ -73,12 +67,12 @@ class StoriesControllerTest < ActionController::TestCase
def test_show_without_user_story_id
get :show, {:product_id => @product.id}
- assert_redirected_to product_stories_path(@product)
+ assert_redirected_to error_path
end
# Ensures that showing a user story works.
def test_show
- get :show, {:product_id => @product.id, :id => @existing_story.id}
+ get :show, {:id => @existing_story.id}
assert_response :success
assert assigns['user_story'], "Failed to load a user story."
@@ -95,21 +89,21 @@ class StoriesControllerTest < ActionController::TestCase
# Ensures that a product id is required to create a new story.
def test_new_with_invalid_product_id
- get :new, {}, {:user_id => @owner.id}
+ get :new, { }, {:user_id => @owner.id}
- assert_redirected_to products_path
+ assert_redirected_to error_path
end
# Ensures that only the product owner can create a new user story.
def test_new_as_nonowner
- get :new, {:product_id => @product.id}, {:user_id => @nonowner.id}
+ get :new, {:product => @product.id}, {:user_id => @nonowner.id}
- assert_redirected_to product_path(@product)
+ assert_redirected_to error_path
end
# Ensures that creating a new user story works as expected.
def test_new
- get :new, {:product_id => @product.id}, {:user_id => @owner.id}
+ get :new, {:product => @product.id}, {:user_id => @owner.id}
assert_response :success
assert assigns['user_story'], "Failed to create a new user story."
@@ -123,46 +117,23 @@ class StoriesControllerTest < ActionController::TestCase
assert_redirected_to login_url
end
- # Ensures that a valid product id is required.
- def test_edit_with_invalid_product_id
- get :edit, {}, {:user_id => @owner.id}
-
- assert_redirected_to products_path
- end
-
# Ensures that a user story id is required.
def test_edit_with_invalid_user_story_id
- get :edit, {:product_id => @product.id}, {:user_id => @owner.id}
+ get :edit, { }, {:user_id => @owner.id}
- assert_redirected_to product_stories_path(@product)
+ assert_redirected_to error_path
end
# Ensures that only the product owner can edit a user story.
- def test_edit_with_invalid_user_story_id
- get :edit,
- {:product_id => @product.id,
- :id => @existing_story.id},
- {:user_id => @nonowner.id}
+ def test_edit_as_nonowner
+ get :edit, {:id => @existing_story.id}, {:user_id => @nonowner.id}
- assert_redirected_to product_story_path(@product,@existing_story)
- end
-
- # Ensures that the user story belongs to the product.
- def test_edit_with_product_mismatch
- get :edit,
- {:product_id => @other_product.id,
- :id => @existing_story.id},
- {:user_id => @owner.id}
-
- assert_redirected_to product_stories_path(@other_product)
+ assert_redirected_to error_path
end
# Ensures that editing a user story works as expected.
def test_edit
- get :edit,
- {:product_id => @product.id,
- :id => @existing_story.id},
- {:user_id => @owner.id}
+ get :edit, { :id => @existing_story.id}, {:user_id => @owner.id}
assert_response :success
assert assigns['user_story'], "Failed to load a user story to edit."
@@ -180,26 +151,21 @@ class StoriesControllerTest < ActionController::TestCase
# Ensures that a valid product id is required to create a user story.
def test_create_with_invalid_product_id
- post :create, {}, {:user_id => @owner.id}
+ post :create, { }, {:user_id => @owner.id}
- assert_redirected_to products_path
+ assert_redirected_to error_path
end
# Ensures that only the product owner can create a user story.
def test_create_as_nonowner
- post :create,
- {:product_id => @product.id},
- {:user_id => @nonowner.id}
+ post :create, {:product_id => @product.id}, {:user_id => @nonowner.id}
- assert_redirected_to product_stories_path(@product)
+ assert_redirected_to error_path
end
# Ensures that an invalid user story gets sent back to the edit page.
def test_create_with_invalid_user_story
- post :create,
- {:product_id => @product.id,
- :user_story => {}},
- {:user_id => @owner.id}
+ post :create, {:product => @product.id, :user_story => {}}, {:user_id => @owner.id}
assert_response :success
assert assigns['user_story'].new_record?, "Should not have saved the user story."
@@ -207,40 +173,28 @@ class StoriesControllerTest < ActionController::TestCase
# Ensures that creating a user story story works as expected.
def test_create
- post :create,
- {:product_id => @product.id,
- :user_story => @new_story},
- {:user_id => @owner.id}
+ post :create, {:product => @product.id, :user_story => @new_story}, {:user_id => @owner.id}
- assert_redirected_to product_stories_path(@product)
+ assert_redirected_to stories_path(:product => @product)
assert UserStory.find_by_title(@new_story[:title]),
"User story was not saved."
end
# Ensures that creating a user story with a URL returns to the URL.
def test_create_with_source
- post :create,
- {:product_id => @product.id,
- :user_story => @new_story, :source => "/farkle"},
- {:user_id => @owner.id}
+ post :create, {:product => @product.id, :user_story => @new_story, :source => "/farkle"}, {:user_id => @owner.id}
assert_redirected_to "/farkle"
- assert UserStory.find_by_title(@new_story[:title]),
- "User story was not saved."
+ assert UserStory.find_by_title(@new_story[:title]), "User story was not saved."
end
# Ensures that specifying to add another the user is returned to the
# new action
def test_create_and_add_another
- post :create,
- {:product_id => @product.id,
- :user_story => @new_story,
- 'add_another' => 'yup'},
- {:user_id => @owner.id}
+ post :create, {:product => @product.id, :user_story => @new_story, 'add_another' => 'yup'}, {:user_id => @owner.id}
- assert_redirected_to new_product_story_path(@product)
- assert UserStory.find_by_title(@new_story[:title]),
- "User story was not saved."
+ assert_redirected_to new_story_path(:product => @product)
+ assert UserStory.find_by_title(@new_story[:title]), "User story was not saved."
end
# Ensures that anonymous users can't update stories.
@@ -250,86 +204,49 @@ class StoriesControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
- # Ensures that updates require a valid product id.
- def test_update_with_invalid_product_id
- put :update, {}, {:user_id => @owner.id}
-
- assert_redirected_to products_path
- end
-
# Ensures that a valid user story id is required.
def test_update_with_invalid_user_story_id
- put :update, {:product_id => @product.id}, {:user_id => @owner.id}
-
- assert_redirected_to product_stories_path(@product)
- end
+ put :update, { }, {:user_id => @owner.id}
- # Ensures that the user story must be from the specified product.
- def test_update_with_product_mismatch
- put :update,
- {:product_id => @other_product.id,
- :id => @existing_story.id},
- {:user_id => @owner.id}
-
- assert_redirected_to product_stories_path(@other_product)
+ assert_redirected_to error_path
end
# Ensures that only the product owner can update a user story.
def test_update_as_nonowner
- put :update,
- {:product_id => @product.id,
- :id => @existing_story.id,
- :user_story => {:title => 'Updated'}},
- {:user_id => @nonowner.id}
+ put :update, { :id => @existing_story.id, :user_story => {:title => 'Updated'}}, {:user_id => @nonowner.id}
- assert_redirected_to product_story_path(@product, @existing_story)
+ assert_redirected_to error_path
result = UserStory.find_by_id((a)existing_story.id)
- assert_equal @existing_story.title, result.title,
- "User story should not have been updated."
+ assert_equal @existing_story.title, result.title, "User story should not have been updated."
end
# Ensures that an invalid user story gets sent back for editing.
def test_update_with_invalid_user_story
- put :update,
- {:product_id => @product.id,
- :id => @existing_story.id,
- :user_story => {:title => ''}},
- {:user_id => @owner.id}
+ put :update, { :id => @existing_story.id, :user_story => {:title => ''}}, {:user_id => @owner.id}
assert_response :success
result = UserStory.find_by_id((a)existing_story.id)
- assert_equal @existing_story.title, result.title,
- "User story should not have been updated."
+ assert_equal @existing_story.title, result.title, "User story should not have been updated."
end
# Ensures that updates work as expected.
def test_update
update = {:title => "This ia the new title for an existing story"}
- put :update,
- {:product_id => @product.id,
- :id => @existing_story.id,
- :user_story => update},
- {:user_id => @owner.id}
+ put :update, { :id => @existing_story.id, :user_story => update}, {:user_id => @owner.id}
- assert_redirected_to product_stories_path(@product)
+ assert_redirected_to stories_path(:product => @product)
result = UserStory.find_by_id((a)existing_story.id)
- assert_equal update[:title], result.title,
- "User story should have been updated."
+ assert_equal update[:title], result.title, "User story should have been updated."
end
# Ensures that updates return to a url when one is supplied.
def test_update_with_source
update = {:title => "This ia the new title for an existing story"}
- put :update,
- {:product_id => @product.id,
- :id => @existing_story.id,
- :user_story => update, :source => "/farkle"},
- {:user_id => @owner.id}
+ put :update,{ :id => @existing_story.id, :user_story => update, :source => "/farkle"}, {:user_id => @owner.id}
assert_redirected_to "/farkle"
result = UserStory.find_by_id((a)existing_story.id)
- assert_equal update[:title], result.title,
- "User story should have been updated."
+ assert_equal update[:title], result.title, "User story should have been updated."
end
# Ensures anonymous users can't delete user stories.
@@ -339,76 +256,42 @@ class StoriesControllerTest < ActionController::TestCase
assert_redirected_to login_url
end
- # Ensures that destroy requires a valid product id.
- def test_destroy_with_invalid_product_id
- delete :destroy, {}, {:user_id => @owner.id}
-
- assert_redirected_to products_path
- end
-
# Ensures that destroy requires a valid user story id.
def test_destroy_with_invalid_user_story_id
- delete :destroy, {:product_id => @product.id},{:user_id => @owner.id}
-
- assert_redirected_to product_stories_path(@product)
- end
-
- # Ensures that destroy requires the product and user story to match.
- def test_destroy_with_product_mismatch
- delete :destroy,
- {:product_id => @other_product.id,
- :id => @existing_story.id},
- {:user_id => @owner.id}
+ delete :destroy, { },{:user_id => @owner.id}
- assert_redirected_to product_stories_path(@other_product)
+ assert_redirected_to error_path
end
# Ensures that only the owner can delete a user story.
def test_destroy_as_nonowner
- delete :destroy,
- {:product_id => @product.id,
- :id => @existing_story.id},
- {:user_id => @nonowner.id}
+ delete :destroy, { :id => @existing_story.id}, {:user_id => @nonowner.id}
- assert_redirected_to product_stories_path(@product)
- assert UserStory.find_by_id((a)existing_story.id),
- "User story should not have been deleted."
+ assert_redirected_to error_path
+ assert UserStory.find_by_id((a)existing_story.id), "User story should not have been deleted."
end
# Ensures that user stories associated with backlog items can't be deleted.
def test_destroy_for_stories_with_backlog_items
- delete :destroy,
- {:product_id => @product.id,
- :id => @story_with_backlog_items.id},
- {:user_id => @owner.id}
+ delete :destroy, { :id => @story_with_backlog_items.id}, {:user_id => @owner.id}
- assert_redirected_to product_stories_path(@product)
- assert UserStory.find_by_id((a)story_with_backlog_items.id),
- "User story should not have been deleted."
+ assert_redirected_to error_path
+ assert UserStory.find_by_id((a)story_with_backlog_items.id), "User story should not have been deleted."
end
# Ensures that deleting a user story works as expected.
def test_destroy
- delete :destroy,
- {:product_id => @product.id,
- :id => @existing_story.id},
- {:user_id => @owner.id}
+ delete :destroy, {:id => @existing_story.id}, {:user_id => @owner.id}
- assert_redirected_to product_stories_path(@product)
- assert !UserStory.find_by_id((a)existing_story.id),
- "User story should have been deleted."
+ assert_redirected_to stories_path(:product => @product)
+ assert !UserStory.find_by_id((a)existing_story.id), "User story should have been deleted."
end
# Ensures that deleting returns to the url if one is supplied.
def test_destroy_with_source
- delete :destroy,
- {:product_id => @product.id,
- :id => @existing_story.id,
- :source => "/farkle"},
- {:user_id => @owner.id}
+ delete :destroy,{ :id => @existing_story.id, :source => "/farkle"}, {:user_id => @owner.id}
assert_redirected_to "/farkle"
- assert !UserStory.find_by_id((a)existing_story.id),
- "User story should have been deleted."
+ assert !UserStory.find_by_id((a)existing_story.id), "User story should have been deleted."
end
end
--
1.6.0.6
14 years, 11 months
[PATCH] Moved epics to its own top level path, out from under projects.
by Darryl L. Pierce
Added a named scope, for_project, to constrain epics that are viewed.
To create a new epic, the url is "/epics/new?project=[id]". To do any
other function on an epic (edit, update, delete, close, reopen), you
would specify "/epics/[id]/action".
Updated all functional tests.
Fixed all pages that reference the old actions.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/application.rb | 1 +
app/controllers/epics_controller.rb | 223 ++++++++++++++++--------------
app/models/epic.rb | 7 +-
app/views/epics/_edit.html.erb | 5 +-
app/views/epics/index.html.erb | 15 ++-
app/views/epics/show.html.erb | 8 +-
app/views/layouts/default.html.erb | 1 +
app/views/projects/index.html.erb | 2 +-
app/views/projects/show.html.erb | 4 +-
app/views/stories/index.html.erb | 2 +-
config/routes.rb | 13 +-
test/functional/epics_controller_test.rb | 189 ++++++++-----------------
12 files changed, 217 insertions(+), 253 deletions(-)
diff --git a/app/controllers/application.rb b/app/controllers/application.rb
index c07e5f9..9728fb1 100644
--- a/app/controllers/application.rb
+++ b/app/controllers/application.rb
@@ -55,6 +55,7 @@ class ApplicationController < ActionController::Base
def load_counts
@project_count = Project.find(:all).size
+ @epic_count = Epic.find(:all).size
@product_count = Product.find(:all).size
@user_count = User.find(:all).size
end
diff --git a/app/controllers/epics_controller.rb b/app/controllers/epics_controller.rb
index 4f8f2c2..97509ba 100644
--- a/app/controllers/epics_controller.rb
+++ b/app/controllers/epics_controller.rb
@@ -16,156 +16,133 @@
# +EpicsController+ allows users to work with +Epic+ stories.
class EpicsController < ApplicationController
- before_filter :authenticated, :except => [:index, :show]
- before_filter :load_project
- before_filter :load_epic, :except => [:index, :new, :create]
- before_filter :path_to_list, :only => [:index, :new, :create]
- before_filter :path_to_one, :only => [:show, :edit, :update]
-
- # GET /projects/1/epics
+ before_filter :authenticated, :except => [:index, :show]
+ before_filter :load_project, :only => [:new, :create]
+ before_filter :load_epic, :except => [:index, :new, :create]
+ before_filter :verify_can_create, :only => [:new, :create]
+ before_filter :verify_can_edit, :only => [:edit, :update]
+ before_filter :verify_can_delete, :only => [:destroy]
+ before_filter :verify_can_close, :only => [:close]
+ before_filter :verify_can_reopen, :only => [:reopen]
+ before_filter :path_to_list, :only => [:index, :new, :create]
+ before_filter :path_to_one, :only => [:show, :edit, :update]
+
+ # GET /epics
def index
- @title = "Epic stories for #{(a)project.name}"
- @epics = Epic.paginate(:page => params[:page],
- :per_page => 10,
- :conditions => ["project_id = ?", @project.id],
- :order => "priority")
+ @title = "Epic Stories"
+ @title = "Epic Stories For #{(a)project.name}" if @project
+ @project = Project.find_by_id(params[:project])
+ @epics = Epic.for_project(@project).paginate(:page => params[:page],
+ :per_page => 10)
respond_to do |format|
format.html
end
end
- # GET/projects/1/epics/1
+ # GET /epics/1
def show
@title = "Epic:#{@epic.title}"
end
- # GET /projects/1/epics/new
+ # GET /epics/new
def new
add_breadcrumb "New"
- if @project.can_create_epics?(@user)
- @title = "Create new epic for #{(a)project.name}"
- @epic = Epic.new(:project => @project)
- else
- flash[:message] = "You are not authorized to create epics for this project."
- redirect_to project_path(@project)
- end
+ @title = "Create new epic for #{(a)project.name}"
+ @epic = Epic.new(:project => @project)
end
- # GET /projects/1/epics/1/edit
+ # GET /epics/1/edit
def edit
add_breadcrumb "Edit"
- if @project.can_create_epics?(@user)
- @title = "Edit epic for #{(a)project.name}"
- else
- flash[:error] = "You are not authorized to edit epics for this project."
- redirect_to project_path(@project)
- end
+ @title = "Edit epic for #{(a)project.name}"
end
- # POST /projects/1/epics
+ # POST /epics
def create
add_breadcrumb "New"
respond_to do |format|
- if @project.can_create_epics?(@user)
- Epic.transaction do
- @epic = Epic.new(params[:epic])
- if @epic.save
- flash[:message] = "Epic successfully created."
- format.html { redirect_to project_epic_path(@project, @epic) }
- else
- @title = "Create new epic for #{(a)project.name}"
- @epic.valid?
- format.html { render :action => :edit }
- end
+ Epic.transaction do
+ @epic = Epic.new(params[:epic])
+ @epic.project = @project
+ if @epic.save
+ flash[:message] = "Epic successfully created."
+ format.html { redirect_to epic_path(@epic) }
+ else
+ @title = "Create new epic for #{(a)project.name}"
+ @epic.valid?
+ format.html { render :action => :edit }
end
- else
- flash[:message] = "You are not authorized to create epics for this project."
- format.html { redirect_to project_path(@project) }
end
end
end
- # PUT /projects/1/epics/1
+ # PUT /epics/1
def update
add_breadcrumb "Edit"
respond_to do |format|
- if @epic.can_edit?(@user)
- Epic.transaction do
- @epic.update_attributes(params[:epic])
- if @epic.save
- flash[:message] = "Epic was updated."
- format.html { redirect_to project_epic_path(@project, @epic) }
- else
- @title = "Edit epic for #{(a)project.name}"
- @epic.valid?
- format.html { render :action => :edit }
- end
+ Epic.transaction do
+ @epic.update_attributes(params[:epic])
+ if @epic.save
+ flash[:message] = "Epic was updated."
+ format.html { redirect_to epic_path(@epic) }
+ else
+ @title = "Edit epic for #{(a)project.name}"
+ @epic.valid?
+ format.html { render :action => :edit }
end
- else
- flash[:message] = "You are not authorized to update epics for this project."
- format.html { redirect_to project_epic_path(@project, @epic) }
end
end
end
- # DELETE /projects/1/epics/1
+ # DELETE /epics/1
def destroy
respond_to do |format|
- if @epic.can_delete?(@user)
- Epic.transaction do
- if @epic.destroy
- flash[:message] = "Epic was deleted."
- format.html { redirect_to project_epics_path(@project) }
- else
- flash[:error] = "Unable to delete this epic."
- format.html { redirect_to.project_epic_path(@project, @epic) }
- end
+ Epic.transaction do
+ if @epic.destroy
+ flash[:message] = "Epic was deleted."
+ format.html { redirect_to epics_path(:project => @project) }
+ else
+ flash[:error] = "Unable to delete this epic."
+ format.html { redirect_to.epic_path(@epic) }
end
- else
- flash[:message] = "You can not delete this epic."
- format.html { redirect_to project_epic_path(@project, @epic) }
end
end
end
- # PUT /projects/1/epics/1/close
+ # PUT /epics/1/close
def close
respond_to do |format|
- if @epic.can_close?(@user)
- Epic.transaction do
- @epic.closed = true
- @epic.save!
+ Epic.transaction do
+ @epic.closed = true
+ if @epic.save
flash[:message] = "Epic is now marked as closed."
+ format.html { redirect_to epics_path(:project => @project) }
+ else
+ flash[:error] = "Unable to close this epic at this time."
+ format.html { redirect_to error_path }
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
+ # PUT /epics/1/reopen
def reopen
respond_to do |format|
- if @epic.can_reopen?(@user)
- @epic.closed = false
- @epic.save!
+ @epic.closed = false
+ if @epic.save
flash[:message] = "Epic now reopened."
+ format.html { redirect_to epic_path(@epic) }
else
flash[:error] = "You may not reopen this epic."
+ format.html { redirect_to error_path }
end
-
- format.html { redirect_to project_epic_path(@project, @epic) }
-
end
end
private
def load_project
- @project = Project.find_by_id(params[:project_id])
+ @project = Project.find_by_id(params[:project])
unless @project
flash[:message] = "Missing or invalid project id."
respond_to do |format|
@@ -176,14 +153,8 @@ class EpicsController < ApplicationController
def load_epic
@epic = Epic.find_by_id(params[:id])
- if @epic
- unless @epic.project_id == @project.id
- respond_to do |format|
- flash[:error] = "The epic requested does not belong to the project specified."
- format.html { redirect_to error_path }
- end
- end
- else
+ @project = @epic.project if @epic
+ unless @epic
respond_to do |format|
format[:error] = "Invalid or missing epic id."
format.html { redirect_to error_path }
@@ -191,14 +162,64 @@ class EpicsController < ApplicationController
end
end
+ def verify_can_create
+ unless @project.can_create_epics?(@user)
+ flash[:error] = "You are not allowed to create epics for this project."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ def verify_can_edit
+ unless @epic.can_edit?(@user)
+ flash[:error] = "You are not allowed to edit this epic."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ def verify_can_delete
+ unless @epic.can_delete?(@user)
+ flash[:error] = "You are not allowed to delete this epic."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ def verify_can_close
+ unless @epic.can_close?(@user)
+ flash[:error] = "You are not allowed to close this epic."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ def verify_can_reopen
+ unless @epic.can_reopen?(@user)
+ flash[:error] = "You are not allowed to reopen this epic."
+ respond_to do |format|
+ format.html { redirect_to error }
+ end
+ end
+ end
+
def path_to_list
- add_breadcrumb "Projects", projects_path
- add_breadcrumb "#{(a)project.id}", project_path(@project)
- add_breadcrumb "Epics", project_epics_path(@project)
+ if @project || params[:project]
+ @project = Project.find_by_id(params[:project]) unless @project
+ add_breadcrumb "Projects", projects_path
+ add_breadcrumb "#{(a)project.id}", project_path(@project)
+ add_breadcrumb "Epics", epics_path(:project => @project)
+ else
+ add_breadcrumb "Epics", epics_path
+ end
end
def path_to_one
path_to_list
- add_breadcrumb "#{(a)epic.id}", project_epic_path(@project, @epic)
+ add_breadcrumb "#{(a)epic.id}", epic_path(@epic)
end
end
diff --git a/app/models/epic.rb b/app/models/epic.rb
index a1620c5..8e080c0 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -30,11 +30,16 @@ class Epic < ActiveRecord::Base
:only_integer => true,
:greater_than => 0
- validates_presence_of :title
+ validates_presence_of :title, :message => "A title must be given to all epics."
belongs_to :project
has_many :user_stories
+ named_scope :for_project, lambda { |project_id|
+ {
+ :conditions => project_id ? ['project_id = ?', project_id] : []
+ }
+ }
# Returns a title that's geared for selection lists.
def selectable_title
"#{title} (#{priority})"
diff --git a/app/views/epics/_edit.html.erb b/app/views/epics/_edit.html.erb
index 04800df..f3dc09c 100644
--- a/app/views/epics/_edit.html.erb
+++ b/app/views/epics/_edit.html.erb
@@ -1,6 +1,9 @@
<div id="content">
<% html = @epic.new_record? ? {:method => :post} : {:method => :put} %>
- <% form_for :epic, @epic, :url => project_epic_path(@project, @epic), :html => html do |form| %>
+ <% form_for :epic, @epic, :url => epic_path(@epic), :html => html do |form| %>
+
+ <%= hidden_field_tag :project, @project.id %>
+
<table class="edit">
<tr>
<td class="label">For project:</td>
diff --git a/app/views/epics/index.html.erb b/app/views/epics/index.html.erb
index 3bfe53d..b1032f7 100644
--- a/app/views/epics/index.html.erb
+++ b/app/views/epics/index.html.erb
@@ -1,10 +1,10 @@
-<div id="content">
+<div id="<% if @project && @project.can_create_epics?(@user) %>content<% else %>content-no-sidebar<% end %>">
<div>
<%= will_paginate @epics %>
</div>
<div>
<table class="main-list">
- <caption><%= "Epic Stories For #{(a)project.name}" %></caption>
+ <caption><%= @title %></caption>
<thead>
<tr>
<th scope="col">#</th>
@@ -20,8 +20,11 @@
<td><%= epic.id %></td>
<td><%= epic.priority %></td>
<td class="name">
- <%= link_to "#{epic.title}", project_epic_path(@project, epic) %>
+ <%= link_to "#{epic.title}", epic_path(epic) %>
<%= RedCloth.new(get_first_sentence(epic.description)).to_html %>
+ <% unless @project %>
+ <%= link_to "For project: #{epic.project.name}", project_path(epic.project) %>
+ <% end %>
</td>
<td><%= epic.user_stories.size %></td>
<td><%= show_date epic.created_at %></td>
@@ -32,9 +35,13 @@
</div>
</div>
+<% if @project %>
+
<% render :layout => 'home/sidebar', :locals => {:title => "Commands"} do %>
<% if @project.can_create_epics?(@user) %>
<%= link_to "Write new epic...",
- new_project_epic_path(@project), :class => "command" %>
+ new_epic_path(:project => @project), :class => "command" %>
<% end %>
<% end %>
+
+<% end %>
diff --git a/app/views/epics/show.html.erb b/app/views/epics/show.html.erb
index 470813a..ea14757 100644
--- a/app/views/epics/show.html.erb
+++ b/app/views/epics/show.html.erb
@@ -13,17 +13,17 @@
<% if @epic.can_edit?(@user) %>
<%= link_to "Edit this epic",
- edit_project_epic_path(@project, @epic), :class => "command" %>
+ edit_epic_path(@epic), :class => "command" %>
<% if @epic.can_close?(@user) %>
<%= link_to "Close epic...",
- close_project_epic_path(@project, @epic), :class => "command",
+ close_epic_path(@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",
+ reopen_epic_path(@epic), :class => "command",
:confirm => "Reopen this epic? Are you sure?", :method => :put %>
<% end %>
@@ -31,7 +31,7 @@
<% if @epic.can_delete?(@user) %>
<%= link_to "Delete this epic...",
- project_epic_path(@project, @epic), :class => "command",
+ epic_path(@epic), :class => "command",
:confirm => "Delete this epic? Are you sure?", :method => :delete %>
<% end %>
<% end %>
diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb
index 72b5693..34d15d2 100644
--- a/app/views/layouts/default.html.erb
+++ b/app/views/layouts/default.html.erb
@@ -51,6 +51,7 @@
<ul id="site-nav">
<li><%= link_to "Home", :controller => :home %></li>
<li><%= link_to "Projects (#{@project_count})", projects_path %></li>
+ <li><%= link_to "Epics (#{@epic_count})", epics_path %></li>
<li><%= link_to "Products (#{@product_count})", products_path %></li>
<li><%= link_to "Users (#{@user_count})", users_path %></li>
<li><%= link_to "Reports", :controller => :report, :action => :index %></li>
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb
index 5d25268..eda8396 100644
--- a/app/views/projects/index.html.erb
+++ b/app/views/projects/index.html.erb
@@ -27,7 +27,7 @@
<% end %>
<% if project.url %><%= link_to "Homepage", project.url %><% end %>
</td>
- <td><%= link_to "#{project.epics.size}", project_epics_path(project) %></td>
+ <td><%= link_to "#{project.epics.size}", epics_path(:project => project) %></td>
<td><%= link_to "#{project.products.count}", products_path(:project => project) %></td>
<td><%= link_to project.owner.display_name, user_path(project.owner) %></td>
<td><%= show_date project.created_at %></td>
diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb
index b8ca717..a40ae45 100644
--- a/app/views/projects/show.html.erb
+++ b/app/views/projects/show.html.erb
@@ -36,11 +36,11 @@
<% end %>
<%= link_to "Show all epics (#{(a)project.epics.size} total)",
- project_epics_path(@project), :class => "command" %>
+ epics_path(:project => @project), :class => "command" %>
<% if @project.can_create_epics?(@user) %>
<%= link_to "Write new epic...",
- new_project_epic_path(@project), :class => "command" %>
+ new_epic_path(:project => @project), :class => "command" %>
<% end %>
<%= link_to "Show all products (#{(a)project.products.size} total)",
diff --git a/app/views/stories/index.html.erb b/app/views/stories/index.html.erb
index 48a7c8a..214db57 100644
--- a/app/views/stories/index.html.erb
+++ b/app/views/stories/index.html.erb
@@ -28,7 +28,7 @@
<td>
<% if story.epic %>
<%= link_to "#{story.epic.id}",
- project_epic_path(@project, story.epic) %>
+ epic_path(story.epic) %>
<% end %>
</td>
<td><%= show_date story.created_at %></td>
diff --git a/config/routes.rb b/config/routes.rb
index 21d82f4..67a584d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -21,13 +21,12 @@ ActionController::Routing::Routes.draw do |map|
admin.resource :server, :controller => 'ServerSettings'
end
- map.resources :projects do |project|
- project.resources(:epics, :member =>
- {
- :close => :put,
- :reopen => :put
- })
- end
+ map.resources :projects
+ map.resources :epics, :member =>
+ {
+ :close => :put,
+ :reopen => :put
+ }
map.resources :projects, :member =>
{
:approve => :put,
diff --git a/test/functional/epics_controller_test.rb b/test/functional/epics_controller_test.rb
index b7aa513..011489b 100644
--- a/test/functional/epics_controller_test.rb
+++ b/test/functional/epics_controller_test.rb
@@ -45,48 +45,46 @@ class EpicsControllerTest < ActionController::TestCase
@nonowner = users(:mcpierce)
raise "Nonowner and owner cannot be the same user!" if @owner.id == @nonowner.id
- @new_epic = {:project_id => @project.id, :title => 'This is a new epic', :priority => 1}
+ @new_epic = { :title => 'This is a new epic', :priority => 1 }
end
# Ensures that viewing a list of epics requires a valid project.
def test_index_with_invalid_project
get :index, {:project => 9999}
- assert_redirected_to error_path
+ assert_response :success
+ assert assigns['epics'], "Failed to load any epics."
+ assert assigns['epics'].empty?, "Epics should have been empty."
end
- # Ensures that viewing a list of epics works as expected.
- def test_index
- get :index, {:project_id => @project.id}
+ # Ensures that viewing epics for a specific project limits those displayed.
+ def test_index_with_project
+ get :index, { :project => @project.id }
assert_response :success
- assert assigns['project'], "Failed to load the project."
+ assert_template 'epics/index'
+ assert assigns['epics'], "Failed to load any epics."
+ assigns['epics'].each { |epic| assert_equal @project.id, epic.project_id, "Only those epics for the specified project should have been loaded." }
end
- # Ensures that viewing an epic requires a valid project.
- def test_show_with_invalid_project
- get :show, { }
+ # Ensures that viewing a list of epics works as expected.
+ def test_index
+ get :index
- assert_redirected_to error_path
+ assert_response :success
+ assert assigns['epics'], "Failed to load any epics."
end
# Ensures that a valid epic id is required.
def test_show_with_invalid_epic
- get :show, {:project_id => @project.id}
-
- assert_redirected_to error_path
- end
-
- # Ensures that the epic and project must match.
- def test_show_with_project_epic_mismatch
- get :show, {:project_id => @other_project.id, :id => @epic.id}
+ get :show, { }
assert_redirected_to error_path
end
# Ensures that showing an epic works as expected.
def test_show
- get :show, {:project_id => @project.id, :id => @epic.id}
+ get :show, { :id => @epic.id }
assert_response :success
assert assigns['epic'], "Failed to load the epic."
@@ -109,14 +107,14 @@ class EpicsControllerTest < ActionController::TestCase
# Ensures that non-owners can't create new epics.
def test_new_as_nonowner
- get :new, {:project_id => @project.id}, {:user_id => @nonowner.id}
+ get :new, {:project => @project.id}, {:user_id => @nonowner.id}
- assert_redirected_to project_path(@project)
+ assert_redirected_to error_path
end
# Ensures that creating a new epic works as expected.
def test_new
- get :new, {:project_id => @project.id}, {:user_id => @owner.id}
+ get :new, { :project => @project.id }, {:user_id => @owner.id}
assert_response :success
assert assigns['epic'], "Failed to create a new epic."
@@ -131,29 +129,28 @@ class EpicsControllerTest < ActionController::TestCase
# Ensures that a valid project is required to create a new epic.
def test_create_with_invalid_project
- post :create, {:epic => @new_epic }, {:user_id => @owner.id}
+ post :create, { }, {:user_id => @owner.id}
assert_redirected_to error_path
end
# Ensures that an unapproved project cannot have epics created.
def test_create_for_unapproved_project
- @new_epic[:project_id] = @unapproved_project.id
- post :create, {:epic => @new_epic}, {:user_id => @owner.id}
+ post :create, { :project => @unapproved_project.id, :epic => @new_epic}, {:user_id => @owner.id}
assert_redirected_to error_path
end
# Ensures that a non-owner cannot create a new epic.
def test_create_as_nonowner
- post :create, {:epic => @new_epic, :project_id => @project.id}, {:user_id => @nonowner.id}
+ post :create, {:project => @project.id, :epic => @new_epic }, {:user_id => @nonowner.id}
- assert_redirected_to project_path(@project)
+ assert_redirected_to error_path
end
- # Ensures that a mal-formed epic redisplays the edit window.
- def test_create_with_malformed_epic
- post :create, {:epic => {:title => "Test"}, :project_id => @project.id}, {:user_id => @owner.id}
+ # Ensures that a malformed epic redisplays the edit window.
+ def test_create_with_invalid_epic
+ post :create, {:project => @project.id, :epic => {:title => "Test"} }, {:user_id => @owner.id}
assert_response :success
assert_template "edit"
@@ -161,11 +158,11 @@ class EpicsControllerTest < ActionController::TestCase
# Ensures that a well-formed epic is saved.
def test_create
- post :create, {:epic => @new_epic, :project_id => @project.id}, {:user_id => @owner.id}
+ post :create, { :project => @project.id, :epic => @new_epic }, {:user_id => @owner.id}
result = Epic.find_by_title(@new_epic[:title])
assert result, "The new epic was not saved."
- assert_redirected_to project_epic_path(@project, result)
+ assert_redirected_to epic_path(result)
end
# Ensures that anonymous users cannot edit epics.
@@ -175,37 +172,23 @@ class EpicsControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
- # Ensures that a valid project is required.
- def test_edit_with_invalid_project
- get :edit, { }, {:user_id => @owner.id}
-
- assert_redirected_to error_path
- end
-
# Ensures that a valid epic id is required.
def test_edit_with_invalid_epic
- get :edit, {:project_id => @project.id}, {:user_id => @owner.id}
-
- assert_redirected_to error_path
- end
-
- # Ensures that the epic and project must be a match.
- def test_edit_with_epic_project_mismatch
- get :edit, {:id => @epic.id, :project_id => @other_project.id}, {:user_id => @other_owner.id}
+ get :edit, { }, {:user_id => @owner.id}
assert_redirected_to error_path
end
# Ensures that a non-owner cannot edit an epic.
def test_edit_as_nonowner
- get :edit, {:id => @epic.id, :project_id => @project.id}, {:user_id => @nonowner.id}
+ get :edit, {:id => @epic.id}, {:user_id => @nonowner.id}
- assert_redirected_to project_path(@project)
+ assert_redirected_to error_path
end
# Ensures that editing loads the epic story correctly.
def test_edit
- get :edit, {:id => @epic.id, :project_id => @project.id}, {:user_id => @owner.id}
+ get :edit, {:id => @epic.id}, {:user_id => @owner.id}
assert_response :success
assert assigns['epic'], "Failed to load the epic."
@@ -219,23 +202,9 @@ class EpicsControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
- # Ensures that a valid project is required.
- def test_update_with_invalid_project
- put :update, { }, {:user_id => @owner.id}
-
- assert_redirected_to error_path
- end
-
# Ensures that a valid epic id is required.
def test_update_with_invalid_epic
- put :update, {:project_id => @project.id}, {:user_id => @owner.id}
-
- assert_redirected_to error_path
- end
-
- # Ensures that the epic belongs to the specified project.
- def test_update_with_epic_project_mismatch
- put :update, {:project_id => @other_project.id, :id => @epic.id}, {:user_id => @other_owner.id}
+ put :update, {}, {:user_id => @owner.id}
assert_redirected_to error_path
end
@@ -246,7 +215,7 @@ class EpicsControllerTest < ActionController::TestCase
{:project_id => @project.id, :id => @epic.id, :epic => {:title => "test"}},
{:user_id => @nonowner.id}
- assert_redirected_to project_epic_path(@project, @epic)
+ assert_redirected_to error_path
result = Epic.find_by_id((a)epic.id)
assert_equal @epic.title, result.title, "Epic should not have been updated."
end
@@ -254,7 +223,7 @@ class EpicsControllerTest < ActionController::TestCase
# Ensures that a malformed epic sends the user back to the edit page.
def test_update_with_invalid_epic
put :update,
- {:project_id => @project.id,:id => @epic.id, :epic => {:title => ''}},
+ {:id => @epic.id, :epic => {:title => ''}},
{:user_id => @owner.id}
assert_response :success
@@ -266,10 +235,10 @@ class EpicsControllerTest < ActionController::TestCase
# Ensures that epics are updated as expected.
def test_update
put :update,
- {:project_id => @project.id, :id => @epic.id, :epic => {:title => "test"}},
+ { :id => @epic.id, :epic => {:title => "test"}},
{:user_id => @owner.id}
- assert_redirected_to project_epic_path(@project, @epic)
+ assert_redirected_to epic_path(@epic)
result = Epic.find_by_id((a)epic.id)
assert_equal "test", result.title, "Epic was not updated as expected."
end
@@ -281,41 +250,27 @@ class EpicsControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
- # Ensures that valid project id is required.
- def test_destroy_with_invalid_project
- delete :destroy, { }, {:user_id => @owner.id}
-
- assert_redirected_to error_path
- end
-
# Ensures that a valid epic is required.
def test_destroy_with_invalid_epic
- delete :destroy, {:project_id => @project.id}, {:user_id => @owner.id}
-
- assert_redirected_to error_path
- end
-
- # Ensures that the epic and project must match.
- def test_destroy_with_epic_project_mismatch
- delete :destroy, {:project_id => @other_project.id, :id => @epic.id}, {:user_id => @owner.id}
+ delete :destroy, {}, {:user_id => @owner.id}
assert_redirected_to error_path
end
# Ensures that epics cannot be deleted by non-owners.
def test_destroy_as_nonowner
- delete :destroy, {:project_id => @project.id, :id => @epic.id}, {:user_id => @nonowner.id}
+ delete :destroy, {:id => @epic.id}, {:user_id => @nonowner.id}
- assert_redirected_to project_epic_path(@project, @epic)
+ assert_redirected_to error_path
result = Epic.find_by_id((a)epic.id)
assert result, "Epic should not have been deleted."
end
# Ensures that an epic can be deleted.
def test_destroy
- delete :destroy, {:project_id => @project.id, :id => @epic.id}, {:user_id => @owner.id}
+ delete :destroy, {:id => @epic.id}, {:user_id => @owner.id}
- assert_redirected_to project_epics_path(@project)
+ assert_redirected_to epics_path(:project => @project.id)
result = Epic.find_by_id((a)epic.id)
assert_nil result, "Epic should have been deleted."
end
@@ -327,13 +282,6 @@ class EpicsControllerTest < ActionController::TestCase
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}
@@ -341,32 +289,25 @@ class EpicsControllerTest < ActionController::TestCase
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}
+ def test_close_as_nonowner
+ put :close, { :id => @epic.id}, {:user_id => @nonowner.id}
- assert_redirected_to project_epic_path(@project, @epic)
+ assert_redirected_to error_path
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}
+ put :close, {:id => @closed_epic.id}, {:user_id => @owner.id}
- assert_redirected_to project_epic_path(@project, @closed_epic)
+ assert_redirected_to error_path
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}
+ put :close, {:id => @epic.id}, {:user_id => @owner.id}
- assert_redirected_to project_epic_path(@project, @epic)
+ assert_redirected_to epics_path(:project => @project.id)
assert Epic.find_by_id((a)epic.id).closed, "Epic should have been marked as closed."
end
@@ -377,46 +318,32 @@ class EpicsControllerTest < ActionController::TestCase
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}
+ put :reopen, { }, {:user_id => @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}
+ def test_reopen_as_nonowner
+ put :reopen, { :id => @closed_epic.id}, {:user_id => @nonowner.id}
- assert_redirected_to project_epic_path(@project, @closed_epic)
+ assert_redirected_to error_path
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}
+ put :reopen, { :id => @epic.id}, {:user_id => @owner.id}
- assert_redirected_to project_epic_path(@project, @epic)
+ assert_redirected_to error_path
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}
+ put :reopen, {:id => @closed_epic.id}, {:user_id => @owner.id}
- assert_redirected_to project_epic_path(@project, @closed_epic)
+ assert_redirected_to epic_path(@closed_epic)
assert !Epic.find_by_id((a)closed_epic.id).closed, "Epic should have been reopened."
end
end
--
1.6.0.6
14 years, 11 months
[PATCH] Moved tasks to its own top level path, out from under users.
by Darryl L. Pierce
To list tasks, simply going to the "/tasks" shows all recent tasks is
descending order by date entered.
To list tasks for a specific item, use "/tasks?item=[backlog item id]"
to show them.
To list tasks for a specific user, use "/tasks?user=[user id]" to show
them.
To create a task, the url is "/tasks/new?item=[backlog item id]". The
primary on the task will be the current user, and that user must be the
owner for the backlog item.
The create and edit methods required the creation of a new named scope
for User, for_sprint, which limits the users returned to those who are
members of the specified sprint.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/tasks_controller.rb | 211 ++++++++++++++--------------
app/models/task.rb | 6 +
app/models/user.rb | 5 +
app/views/items/show.html.erb | 2 +-
app/views/tasks/_edit.html.erb | 2 +-
app/views/tasks/_list.html.erb | 2 +-
app/views/tasks/show.html.erb | 4 +-
config/routes.rb | 7 +-
test/functional/tasks_controller_test.rb | 222 +++++++++++++-----------------
9 files changed, 220 insertions(+), 241 deletions(-)
diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb
index fa08add..dfc5817 100644
--- a/app/controllers/tasks_controller.rb
+++ b/app/controllers/tasks_controller.rb
@@ -19,21 +19,21 @@
# item.
#
class TasksController < ApplicationController
- before_filter :authenticated, :except => [:index, :show]
+ before_filter :authenticated, :except => [:index, :show]
before_filter :load_this_user
- before_filter :load_task, :except => [:index, :new, :create]
- before_filter :load_backlog_item, :only => [:new, :create]
- before_filter :path_to_list, :only => [:index, :new, :create]
- before_filter :path_to_one, :only => [:show, :edit, :update]
+ before_filter :load_backlog_item, :only => [:new, :create]
+ before_filter :load_task, :only => [:show, :edit, :update, :destroy]
+ before_filter :verify_can_create, :only => [:new, :create]
+ before_filter :verify_can_edit, :only => [:edit, :update]
+ before_filter :verify_can_delete, :only => [:destroy]
+ before_filter :load_sprint_members, :only => [:new, :create, :edit, :update]
+ before_filter :path_to_list, :only => [:index, :new, :create]
+ before_filter :path_to_one, :only => [:show, :edit, :update]
# GET /tasks
def index
- @tasks = Task.paginate(
- :conditions => ['(primary_id = ? or backup_id = ?)', @this_user.id, @this_user.id],
- :order => 'when_entered ASC',
- :page => params[:page],
- :per_page => 10)
-
+ @tasks = Task.for_user(params[:user]).paginate(:page => params[:page],
+ :per_page => 10)
respond_to do |format|
format.html
end
@@ -49,101 +49,69 @@ class TasksController < ApplicationController
# GET /tasks/new
def new
- if @user.id == @this_user.id
- if @backlog_item.can_add_tasks?(@user)
- @title = "New Task For \"#{(a)backlog_item.user_story.title}\""
- @task = Task.new(:primary_id => @this_user.id, :backlog_item_id => @backlog_item.id)
- @users = load_product_members
- else
- flash[:error] = "You cannot create tasks on this backlog item."
- redirect_to product_sprint_item_path((a)backlog_item.sprint.product,
- @backlog_item.sprint, @backlog_item)
- end
- else
- flash[:error] = "You cannot create tasks for #{(a)this_user.display_name}."
- redirect_to user_path(@this_user)
- end
+ @title = "New Task For \"#{(a)backlog_item.user_story.title}\""
+ @task = Task.new(:primary_id => @user.id, :backlog_item_id => @backlog_item.id)
end
# GET /tasks/1/edit
def edit
@title = "Task For Backlog Item #{(a)task.backlog_item.id} (Edit)"
- @users = load_product_members
- unless @task.can_edit?(@user)
- flash[:error] = "You are not allowed to edit this task."
- redirect_to product_sprint_item_path(@product, @sprint, @backlog_item)
- end
end
# POST /tasks
def create
- respond_to do |format|
- if @backlog_item.can_add_tasks?(@user)
- Task.transaction do
- @task = Task.new(params[:task])
- @task.primary = @this_user
- @task.backlog_item = @backlog_item
-
- if !(a)task.backup_id || @product.is_member?((a)task.backup)
- if @task.save
- flash[:message] = "Recorded #{(a)task.hours} hours against this backlog item."
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
- else
- @title = "New Task For \"#{(a)backlog_item.user_story.title}\""
- @task.valid?
- @users = load_product_members
- format.html { render :action => :new }
- end
+ Task.transaction do
+ @task = Task.new(params[:task])
+ @task.primary = @user
+ @task.backlog_item = @backlog_item
+
+ respond_to do |format|
+ if @task.backup_id && !@sprint.is_team_member?((a)task.backup)
+ flash[:error] = "Backup must be a member of the sprint team."
+ format.html { redirect_to error_path }
+ else
+ if @task.save
+ flash[:message] = "Recorded #{(a)task.hours} hours against this backlog item."
+ format.html { redirect_to task_path(@task) }
else
- flash[:error] = "#{(a)task.backup.display_name} is not a member of #{(a)product.name}."
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
+ @title = "New Task For \"#{(a)backlog_item.user_story.title}\""
+ @task.valid?
+ format.html { render :action => :new }
end
end
- else
- flash[:error] = "You are not allowed to add tasks to this backlog item."
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
end
end
end
# PUT /tasks/1
def update
- respond_to do |format|
- if @task.can_edit?(@user)
- Task.transaction do
- @task.update_attributes(params[:task])
+ Task.transaction do
+ @task.update_attributes(params[:task])
- if @task.save
- else
- @title = "Task For Backlog Item #{(a)task.backlog_item.id} (Edit)"
- @users = load_product_members
- @task.valid?
- format.html { render :action => :edit }
- end
+ respond_to do |format|
+ if @task.save
+ flash[:message] = "Task successfully updated."
+ format.html { redirect_to task_path(@task) }
+ else
+ @title = "Task For Backlog Item #{(a)task.backlog_item.id} (Edit)"
+ @task.valid?
+ format.html { render :action => :edit }
end
-
- flash[:message] = "Task updated."
-
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
- else
- flash[:error] = "You are not allowed to update that task."
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
end
end
end
# DELETE /tasks/1
def destroy
- respond_to do |format|
- if @task.can_delete?(@user)
- Task.transaction do
- @task.destroy
+ Task.transaction do
+ respond_to do |format|
+ if @task.destroy
+ flash[:message] = "Task has been deleted."
+ format.html { redirect_to tasks_path(:user => @user) }
+ else
+ flash[:error] = "Unable to delete task at this time."
+ format.html { redirect_to error_path }
end
- flash[:message] = "Task has been deleted."
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
- else
- flash[:error] = 'You are not allowed to delete this task.'
- format.html { redirect_to product_sprint_item_path(@product, @sprint, @backlog_item) }
end
end
end
@@ -151,61 +119,90 @@ class TasksController < ApplicationController
private
def load_this_user
- @this_user = User.find_by_id(params[:user_id])
+ if params[:user]
+ @this_user = User.find_by_id(params[:user])
- unless @this_user
- respond_to do |format|
- flash[:error] = "Missing or invalid user."
- format.html {redirect_to users_path }
+ unless @this_user
+ flash[:error] = 'Invalid user specified.'
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
end
end
end
def load_task
@task = Task.find_by_id(params[:id])
- @backlog_item = @task.backlog_item if @task
- @sprint = @backlog_item.sprint if @backlog_item
- @product = @sprint.product if @sprint
-
- unless @task &&
- (((a)task.primary_id == @this_user.id) ||
- (@task.backup_id == @this_user.id))
+ if @task
+ @primary = @task.primary
+ @backup = @task.backup
+ @backlog_item = @task.backlog_item
+ @sprint = @backlog_item.sprint
+ @product = @sprint.product
+ else
flash[:error] = 'Missing or invalid task.'
respond_to do |format|
- format.html { redirect_to user_tasks_path(@this_user) }
+ format.html { redirect_to error_path }
end
end
end
def load_backlog_item
@backlog_item = BacklogItem.find_by_id(params[:item])
- @sprint = @backlog_item.sprint if @backlog_item
- @product = @sprint.product if @sprint
-
- unless @backlog_item
+ if @backlog_item
+ @sprint = @backlog_item.sprint
+ @product = @sprint.product
+ else
flash[:error] = "Missing or invalid backlog item."
respond_to do |format|
- format.html { redirect_to user_path(@this_user) }
+ format.html { redirect_to error_path }
end
end
end
- def load_product_members
- User.find(:all,
- :conditions =>
- ["id in (select user_id from product_roles where product_id = ? )",
- @backlog_item.sprint.product_id],
- :order => "display_name ASC")
+ def verify_can_create
+ unless @backlog_item.can_add_tasks?(@user)
+ flash[:error] = "You are not allowed to add tasks to this item."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ def verify_can_edit
+ unless @task.can_edit?(@user)
+ flash[:error] = "You are not allowed to edit this task."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ def verify_can_delete
+ unless @task.can_delete?(@user)
+ flash[:error] = "You are not allowed to delete this task."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ def load_sprint_members
+ @users = User.for_sprint((a)sprint.id)
end
def path_to_list
- add_breadcrumb "Users", users_path
- add_breadcrumb "#{(a)this_user.id}", user_path(@this_user)
- add_breadcrumb "Tasks", user_tasks_path(@this_user)
+ if @this_user
+ add_breadcrumb "Users", users_path
+ add_breadcrumb "#{(a)this_user.id}", user_path(@this_user)
+ add_breadcrumb "Tasks", tasks_path(:user => @this_user)
+ else
+ add_breadcrumb "Tasks", tasks_path
+ end
end
def path_to_one
path_to_list
- add_breadcrumb "#{(a)task.id}", user_task_path(@this_user, @task)
+ add_breadcrumb "#{(a)task.id}", task_path(@task)
end
end
diff --git a/app/models/task.rb b/app/models/task.rb
index a3c621b..75b2451 100644
--- a/app/models/task.rb
+++ b/app/models/task.rb
@@ -43,6 +43,12 @@ class Task < ActiveRecord::Base
belongs_to :backlog_item
+ named_scope :default, { :order => 'when_created DESC' }
+ named_scope :for_user, lambda { |user_id|
+ {
+ :conditions => user_id ? ['primary_id = ?', user_id] : []
+ }
+ }
# Returns whether the user can edit the task.
def can_edit?(user)
user &&
diff --git a/app/models/user.rb b/app/models/user.rb
index 6486b25..eaf2b3d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -38,6 +38,11 @@ class User < ActiveRecord::Base
validates_confirmation_of :password,
:message => 'Passwords do not match.'
+ named_scope :for_sprint, lambda { |sprint_id|
+ {
+ :conditions => ['id in (select member_id from sprint_members where sprint_id = ?)', sprint_id]
+ }
+ }
def validate
errors.add_to_base("Missing password") if hashed_password.blank?
end
diff --git a/app/views/items/show.html.erb b/app/views/items/show.html.erb
index 9bc2fce..6ab3bb8 100644
--- a/app/views/items/show.html.erb
+++ b/app/views/items/show.html.erb
@@ -23,7 +23,7 @@
<% if @backlog_item.can_add_tasks?(@user) %>
<%= link_to "Add completed task...",
- new_user_task_path(@user, :item => @backlog_item),
+ new_task_path(:item => @backlog_item),
:url => request.request_uri, :class => "command" %>
<% end %>
diff --git a/app/views/tasks/_edit.html.erb b/app/views/tasks/_edit.html.erb
index c28da9c..e3d58d5 100644
--- a/app/views/tasks/_edit.html.erb
+++ b/app/views/tasks/_edit.html.erb
@@ -1,6 +1,6 @@
<div id="content">
<% html = @task.new_record? ? {:method => :post} : {:method => :put} %>
- <% form_for(:task, @task, :url => user_task_path(@this_user, @task), :html => html) do |form| %>
+ <% form_for(:task, @task, :url => task_path(@task), :html => html) do |form| %>
<%= hidden_field_tag :item, @backlog_item.id %>
<table class="edit">
<caption><%= "#{(a)task.new_record? ? 'Create' : 'Edit'} A Task" %></caption>
diff --git a/app/views/tasks/_list.html.erb b/app/views/tasks/_list.html.erb
index df4f657..4660370 100644
--- a/app/views/tasks/_list.html.erb
+++ b/app/views/tasks/_list.html.erb
@@ -13,7 +13,7 @@
<tbody>
<% @tasks.each do |task| %>
<tr class="<%= cycle('odd', 'even') %>">
- <td><%= link_to "#{task.id}", user_task_path(task.primary, task) %></td>
+ <td><%= link_to "#{task.id}", task_path(task) %></td>
<td><%= link_to task.primary.display_name, user_path(task.primary) %></td>
<td>
<% if task.backup %>
diff --git a/app/views/tasks/show.html.erb b/app/views/tasks/show.html.erb
index 23f7091..fc491ed 100644
--- a/app/views/tasks/show.html.erb
+++ b/app/views/tasks/show.html.erb
@@ -19,12 +19,12 @@
<% if @task.can_edit?(@user) %>
<%= link_to "Edit this task",
- edit_user_task_path((a)task.primary, @task), :class => "command" %>
+ edit_task_path(@task), :class => "command" %>
<% end %>
<% if @task.can_delete?(@user) %>
<%= link_to "Delete this task...",
- user_task_path((a)task.primary, @task), :method => :delete,
+ task_path(@task), :method => :delete,
:confirm => "Delete this task? Are you sure?", :class => "command" %>
<% end %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 21d82f4..13803b1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -66,9 +66,10 @@ ActionController::Routing::Routes.draw do |map|
:notifications => :post,
:password => :get,
:change_password => :post
- } do |user|
- user.resources :tasks
- end
+ }
+
+ map.resources :tasks
+
# You can have the root of your site routed with map.root -- just remember to
# delete public/index.html.
diff --git a/test/functional/tasks_controller_test.rb b/test/functional/tasks_controller_test.rb
index e2ed4a5..01894ef 100644
--- a/test/functional/tasks_controller_test.rb
+++ b/test/functional/tasks_controller_test.rb
@@ -57,54 +57,36 @@ class TasksControllerTest < ActionController::TestCase
}
end
- # Ensures that a user is required.
- def test_index_without_user
- get :index
+ # Ensures that specifying a user shows only that user's tasks.
+ def test_index_with_user
+ get :index, { :user_id => @user.id }
- assert_redirected_to users_path
+ assert_response :success
+ assert_template 'tasks/index'
+ assigns['tasks'].each { |task| assert_equal @user.id, task.primary_id, "Should not load tasks for other users." }
end
- # Ensures that an index works.
+ # Ensures that a list of the most recent tasks is displayed.
def test_index
- get :index, {:user_id => @user.id}
+ get :index
assert_response :success
- assert assigns['tasks'], "Failed to load any tasks."
- end
-
- # Ensures that a user is required.
- def test_show_without_user
- get :show
-
- assert_redirected_to users_path
+ assert_template 'tasks/index'
end
# Ensures that a task is required.
def test_show_without_task
- get :show, {:user_id => @user.id}
-
- assert_redirected_to user_tasks_path(@user)
- end
-
- # Ensures the task was done by teh user.
- def test_show_with_user_task_mismatch
- get :show, {:user_id => @other_user.id, :id => @task.id}
-
- assert_redirected_to user_tasks_path(@other_user)
- end
-
- # Ensures that the backup for a task is a valid user.
- def test_show_with_backup_user
- get :show, {:user_id => @shared_task.backup_id, :id => @shared_task.id}
+ get :show
- assert_response :success
+ assert_redirected_to error_path
end
# Ensures that show works.
def test_show
- get :show, {:user_id => @user.id, :id => @task.id}
+ get :show, { :id => @task.id }
assert_response :success
+ assert_template 'tasks/show'
end
# Ensures anonymous users can't create tasks.
@@ -116,40 +98,28 @@ class TasksControllerTest < ActionController::TestCase
# Ensures that a user is required.
def test_new_with_invalid_user
- get :new, {}, {:user_id => @user.id}
-
- assert_redirected_to users_path
- end
+ get :new, { :user => 9999 }, { :user_id => @user.id }
- # Ensures that the user must match the logged in used.
- def test_new_with_user_mismatch
- get :new, {:user_id => @other_user.id}, {:user_id => @user.id}
-
- assert_redirected_to user_path(@other_user)
+ assert_redirected_to error_path
end
# Ensures that a valid backlog item was specified.
def test_new_with_invalid_backlog_item
- get :new, {:user_id => @user.id}, {:user_id => @user.id}
+ get :new, { :item => 9999 }, { :user_id => @user.id }
- assert_redirected_to user_path(@user)
+ assert_redirected_to error_path
end
# Ensures that only the backlog owner can add a task.
def test_new_as_nonowner
- get :new,
- {:user_id => @user.id, :item => @item.id},
- {:user_id => @nonowner.id}
+ get :new, { :item => @item.id }, { :user_id => @nonowner.id }
- assert_redirected_to
- product_sprint_item_path(@product,@sprint, @item)
+ assert_redirected_to error_path
end
# Ensures that only an open item can get a task.
def test_new_on_completed_item
- get :new,
- {:user_id => @user.id, :item => @closed_item.id},
- {:user_id => @owner.id}
+ get :new, { :item => @closed_item.id }, { :user_id => @owner.id }
assert_redirected_to
product_sprint_item_path(@product, @sprint, @item)
@@ -157,11 +127,10 @@ class TasksControllerTest < ActionController::TestCase
# Ensures that new works.
def test_new
- get :new,
- {:user_id => @user.id, :item => @item.id},
- {:user_id => @owner.id}
+ get :new, { :item => @item.id }, { :user_id => @owner.id }
assert_response :success
+ assert_template 'tasks/new'
assert assigns['users'], "Failed to load a list of users."
end
@@ -174,33 +143,32 @@ class TasksControllerTest < ActionController::TestCase
# Ensures that a user is required.
def test_edit_with_invalid_user
- get :edit, {}, {:user_id => @owner.id}
+ get :edit, { }, { :user_id => @owner.id }
- assert_redirected_to users_path
+ assert_redirected_to error_path
end
# Ensures that a backlog item is required.
- def test_edit_with_invalid_backlog_item
- get :edit, {:user_id => @user.id}, {:user_id => @owner.id}
+ def test_edit_with_invalid_task_item
+ get :edit, { }, { :user_id => @owner.id }
- assert_redirected_to user_tasks_path(@user)
+ assert_redirected_to error_path
end
# Ensures the owner is the only one who can edit tasks.
def test_edit_as_nonowner
- get :edit, {:user_id => @user.id, :item => @item.id},
- {:user_id => @nonowner.id}
+ get :edit, { :id => @task.id }, { :user_id => @nonowner.id }
- assert_redirected_to user_tasks_path(@user)
+ assert_redirected_to error_path
end
# Ensures that editing works as expected.
def test_edit
- get :edit, {:user_id => @owner.id, :item => @item.id, :id => @task.id},
- {:user_id => @owner.id}
+ get :edit, { :id => @task.id }, { :user_id => @owner.id }
assert_response :success
- assert assigns['users'], "Failed to load a list for backup."
+ assert_template 'tasks/edit'
+ assert assigns['users'], 'Failed to load the list of members.'
end
# Ensures anonymous users can't create tasks.
@@ -210,67 +178,55 @@ class TasksControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
- # Ensures that a user is required.
- def test_create_with_invalid_user
- post :create, {}, {:user_id => @user.id}
-
- assert_redirected_to users_path
- end
-
- # Ensures that the user must match the logged in used.
- def test_create_with_user_mismatch
- post :create, {:user_id => @other_user.id}, {:user_id => @user.id}
-
- assert_redirected_to user_path(@other_user)
- end
-
# Ensures that a valid backlog item was specified.
- def test_create_with_invalid_backlog_item
- post:create, {:user_id => @user.id}, {:user_id => @user.id}
+ def test_create_with_invalid_item
+ post:create, { }, { :user_id => @user.id }
- assert_redirected_to user_path(@user)
+ assert_redirected_to error_path
end
# Ensures that only the backlog owner can add a task.
def test_create_as_nonowner
- post :create,
- {:user_id => @user.id, :item => @item.id},
- {:user_id => @nonowner.id}
+ post :create, { :item => @item.id }, { :user_id => @nonowner.id }
- assert_redirected_to product_sprint_item_path(@product,@sprint, @item)
+ assert_redirected_to error_path
end
# Ensures that only product members can add tasks.
def test_create_for_non_member
@new_task[:backup_id] = @nonmember.id
- post :create,
- {:user_id => @user.id, :item => @item.id,
- :task => @new_task},
- {:user_id => @owner.id}
+ post :create, { :item => @item.id, :task => @new_task }, { :user_id => @owner.id }
- assert_redirected_to product_sprint_item_path(@product, @sprint, @item)
+ assert_redirected_to error_path
assert !Task.find_by_description(@new_task[:description]),
"Task should not have been saved!"
end
# Ensures that only an open item can get a task.
def test_create_on_completed_item
- post :create,
- {:user_id => @user.id, :item => @closed_item.id},
- {:user_id => @owner.id}
+ post :create, { :item => @closed_item.id }, { :user_id => @owner.id }
- assert_redirected_to product_sprint_item_path(@product, @sprint, @closed_item)
+ assert_redirected_to error_path
+ end
+
+ # Ensure that an invalid task returns the user to the edit page.
+ def test_create_with_invalid_task
+ @new_task[:hours] = nil
+ post :create, { :item => @item.id, :task => @new_task }, { :user_id => @owner.id }
+
+ assert_response :success
+ assert_template 'tasks/new'
+ result = Task.find_by_description(@new_task[:description])
+ assert !result, "Task should not have been saved."
end
# Ensures that creating a task works as expected.
def test_create
- post :create,
- {:user_id => @user.id, :item => @item.id, :task => @new_task},
- {:user_id => @owner.id}
+ post :create, { :item => @item.id, :task => @new_task }, { :user_id => @owner.id }
- assert_redirected_to product_sprint_item_path(@product, @sprint, @item)
- assert Task.find_all_by_description(@new_task[:description]),
- "Tasks should have been created."
+ result = Task.find_by_description(@new_task[:description])
+ assert result, "Task should have been created."
+ assert_redirected_to task_path(result)
end
# Ensures anonymous users can't update a task.
@@ -280,51 +236,65 @@ class TasksControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
- # Ensures that a user is required.
- def test_update_with_invalid_user
- put :update, {}, {:user_id => @owner.id}
-
- assert_redirected_to users_path
- end
+ # Ensures that a task id is required.
+ def test_update_with_invalid_task_id
+ put :update, { }, { :user_id => @owner.id }
- # Ensures that a backlog item is required.
- def test_update_with_invalid_backlog_item
- put :update, {:user_id => @user.id}, {:user_id => @owner.id}
-
- assert_redirected_to user_tasks_path(@user)
+ assert_redirected_to error_path
end
# Ensures the owner is the only one who can update tasks.
def test_update_as_nonowner
- put :update,
- {:user_id => @user.id, :id => @task.id},
- {:user_id => @nonowner.id}
+ put :update, { :id => @task.id }, { :user_id => @nonowner.id }
- assert_redirected_to product_sprint_item_path(@product, @sprint, @item)
+ assert_redirected_to error_path
end
# Ensures that an invalid update returns to the edit view.
- def test_update_with_invalid_update
- put :update,
- {:user_id => @owner.id, :id => @task.id,
- :task => {:hours => nil}},
- {:user_id => @owner.id}
+ def test_update_with_invalid_task
+ put :update, { :id => @task.id, :task => { :hours => nil }}, { :user_id => @owner.id }
assert_response :success
- assert_template 'edit'
+ assert_template 'tasks/edit'
assert_equal @task.hours, Task.find_by_id((a)task.id).hours,
"Hours should not have been updated."
end
# Ensures that an update works.
def test_update
- put :update,
- {:user_id => @owner.id, :id => @task.id,
- :task => {:hours => 12.5}},
- {:user_id => @owner.id}
+ put :update, { :id => @task.id, :task => { :hours => 12.5 }}, { :user_id => @owner.id }
- assert_redirected_to product_sprint_item_path(@product, @sprint, @item)
+ assert_redirected_to task_path(@task)
assert_equal 12.5, Task.find_by_id((a)task.id).hours,
"Hours hould have been updated."
end
+
+ # Ensures that anonymous users cannot delete tasks.
+ def test_delete_as_anonymous
+ delete :destroy
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid task id is required.
+ def test_delete_with_invalid_task_id
+ delete :destroy, { }, { :user_id => @owner.id }
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that only the owner can delete a task.
+ def test_delete_as_nonowner
+ delete :destory, { :id => @task.id }, { :user_id => @nonowner.id }
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that delete a task works as expected.
+ def test_delete
+ delete :destroy, { :id => @task.id }, { :user_id => @owner.id }
+
+ assert_redirected_to tasks_path(:user => @owner.id)
+ assert !Task.find_by_id((a)task.id), "Task should have been deleted."
+ end
end
--
1.6.0.6
14 years, 11 months
[PATCH] Cannot create a sprint when there are no user stories. #178
by Darryl L. Pierce
Added a functional test to sprints_controller to check for this rule.
Added a unit test to product to enforce this business rule.
Changed the redirection from the sprint page to the error page whenever
a sprint cannot be defined.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/sprints_controller.rb | 12 ++++++------
app/models/product.rb | 2 +-
doc/ChangeLog | 1 +
test/functional/sprints_controller_test.rb | 19 +++++++++++++++----
test/unit/product_test.rb | 9 +++++++++
5 files changed, 32 insertions(+), 11 deletions(-)
diff --git a/app/controllers/sprints_controller.rb b/app/controllers/sprints_controller.rb
index 023dd43..823f0eb 100644
--- a/app/controllers/sprints_controller.rb
+++ b/app/controllers/sprints_controller.rb
@@ -54,9 +54,9 @@ class SprintsController < ApplicationController
@title = "New Sprint For #{(a)product.name}"
@sprint = Sprint.new(:product_id => @product.id)
else
- flash[:error] = "You are not authorized to create sprints for #{(a)product.name}."
+ flash[:error] = "Cannot create a sprint at this time."
- redirect_to product_sprints_path(@product)
+ redirect_to error_path
end
end
@@ -86,8 +86,8 @@ class SprintsController < ApplicationController
end
end
else
- flash[:error] = "You are not allowed to create sprints for #{(a)product.name}."
- format.html { redirect_to product_sprints_path(@product) }
+ flash[:error] = "Cannot create a sprint at this time."
+ format.html { redirect_to error_path }
end
end
end
@@ -115,7 +115,7 @@ class SprintsController < ApplicationController
end
else
flash[:error] = "You are now allowed to edit sprints for #{(a)product.name}."
- format.html { redirect_to product_sprints_path(@product) }
+ format.html { redirect_to error_path }
end
end
end
@@ -156,7 +156,7 @@ class SprintsController < ApplicationController
end
else
flash[:error] = "You are not allowed to delete this sprint."
- format.html { redirect_to product_sprints_path(@product) }
+ format.html { redirect_to error_path }
end
end
end
diff --git a/app/models/product.rb b/app/models/product.rb
index 7f612ad..fe85d5f 100644
--- a/app/models/product.rb
+++ b/app/models/product.rb
@@ -96,7 +96,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 && (user.id == owner.id) && !user_stories.empty?
end
# Returns whether the user specified is the product owner.
diff --git a/doc/ChangeLog b/doc/ChangeLog
index 25c5b57..df04680 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -1,4 +1,5 @@
Change Log (0.3.0):
+ * #178 - A product with no user stories cannot define a sprint.
Change Log (0.2.0):
* #97 - Users can request new projects.
diff --git a/test/functional/sprints_controller_test.rb b/test/functional/sprints_controller_test.rb
index cf15b40..64946af 100644
--- a/test/functional/sprints_controller_test.rb
+++ b/test/functional/sprints_controller_test.rb
@@ -46,6 +46,9 @@ class SprintsControllerTest < ActionController::TestCase
@other_member = users(:team_lead)
raise "Other member must be a part of the same product team!" unless @product.is_member?(@other_member)
+ @product_with_no_stories = products(:teatime_midp)
+ raise "Product cannot have user stories!" unless @product_with_no_stories.user_stories.empty?
+
@new_sprint = {
:title => "New Sprint",
:start => Date.today,
@@ -132,7 +135,7 @@ class SprintsControllerTest < ActionController::TestCase
def test_new_as_nonowner
get :new, {:product_id => @product.id}, {:user_id => @nonowner.id}
- assert_redirected_to product_sprints_path(@product)
+ assert_redirected_to error_path
end
# Ensures that creating a new sprint works as expected.
@@ -214,7 +217,7 @@ class SprintsControllerTest < ActionController::TestCase
{:product_id => @product.id},
{:user_id => @nonowner.id}
- assert_redirected_to product_sprints_path(@product)
+ assert_redirected_to error_path
end
# Ensures that an invalid sprint redirects to the edit page.
@@ -249,6 +252,14 @@ class SprintsControllerTest < ActionController::TestCase
assert !Sprint.find_by_title(@new_sprint[:title]), "Sprint should not have been saved!"
end
+ # Ensures that a sprint cannot be created on a product with no user stories.
+ def test_create_for_product_with_no_stories
+ post :create, {:product_id => @product_with_no_stories.id, :sprint => @new_sprint},
+ {:user_id => @product_with_no_stories.owner_id}
+
+ assert_redirected_to error_path
+ end
+
# Ensures that creating a sprint works as expected.
def test_create
post :create,
@@ -299,7 +310,7 @@ class SprintsControllerTest < ActionController::TestCase
:sprint => {:title => "Don't do it"}},
{:user_id => @nonowner.id}
- assert_redirected_to product_sprints_path(@product)
+ assert_redirected_to error_path
result = Sprint.find_by_id((a)active_sprint.id)
assert_equal @active_sprint.title, result.title,
"Sprint should not have been updated."
@@ -382,7 +393,7 @@ class SprintsControllerTest < ActionController::TestCase
{:product_id => @product.id, :id => @active_sprint.id},
{:user_id => @nonowner.id}
- assert_redirected_to product_sprints_path(@product)
+ assert_redirected_to error_path
assert Sprint.find_by_id((a)active_sprint.id),
"Sprint should not have been deleted."
end
diff --git a/test/unit/product_test.rb b/test/unit/product_test.rb
index 69b4e5c..e6cf5c3 100644
--- a/test/unit/product_test.rb
+++ b/test/unit/product_test.rb
@@ -40,6 +40,9 @@ class ProductTest < ActiveSupport::TestCase
raise "Product must have a logo!" unless @product_with_logo.logo_name
@product_without_logo = products(:projxp_web_services)
raise "Product must not have a logo!" if @product_without_logo.logo_name
+
+ @product_with_no_stories = products(:teatime_midp)
+ raise "Product must not have stories!" unless @product_with_no_stories.user_stories.empty?
end
# Ensures that a project is required.
@@ -96,4 +99,10 @@ class ProductTest < ActiveSupport::TestCase
assert_equal "#{ConfigProperty.fetch('image.product_logo_path')}/#{(a)product_with_logo.logo_name}",
@product_with_logo.logo_url, "Did not return the correct logo url."
end
+
+ # 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."
+ end
end
--
1.6.0.6
14 years, 11 months
[PATCH] Ensures epics cannot be created for an unapproved project. #177
by Darryl L. Pierce
Added a test to ensure the feature is present, even though it was
already implicitly present.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
doc/ChangeLog | 1 +
test/functional/epics_controller_test.rb | 12 ++++++++++++
2 files changed, 13 insertions(+), 0 deletions(-)
diff --git a/doc/ChangeLog b/doc/ChangeLog
index 25c5b57..39602f1 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -1,4 +1,5 @@
Change Log (0.3.0):
+ * #177 - Epics cannot be created for unapproved projects.
Change Log (0.2.0):
* #97 - Users can request new projects.
diff --git a/test/functional/epics_controller_test.rb b/test/functional/epics_controller_test.rb
index 0d41b19..b7aa513 100644
--- a/test/functional/epics_controller_test.rb
+++ b/test/functional/epics_controller_test.rb
@@ -38,6 +38,10 @@ class EpicsControllerTest < ActionController::TestCase
@other_project = projects(:teatime)
@other_owner = @other_project.owner
+ @unapproved_project = projects(:unapproved_project)
+ raise "Project cannot be approved!" if @unapproved_project.approved
+ raise "Project owner is wrong!" unless @unapproved_project.owner_id == @owner.id
+
@nonowner = users(:mcpierce)
raise "Nonowner and owner cannot be the same user!" if @owner.id == @nonowner.id
@@ -132,6 +136,14 @@ class EpicsControllerTest < ActionController::TestCase
assert_redirected_to error_path
end
+ # Ensures that an unapproved project cannot have epics created.
+ def test_create_for_unapproved_project
+ @new_epic[:project_id] = @unapproved_project.id
+ post :create, {:epic => @new_epic}, {:user_id => @owner.id}
+
+ assert_redirected_to error_path
+ end
+
# Ensures that a non-owner cannot create a new epic.
def test_create_as_nonowner
post :create, {:epic => @new_epic, :project_id => @project.id}, {:user_id => @nonowner.id}
--
1.6.0.6
14 years, 11 months