merged: changed after_save hook of issues to only update issues that are stories or tasks

pull/6827/head
Stephan Eckardt 14 years ago
commit 22bbc5efab
  1. 6
      app/controllers/rb_impediments_controller.rb
  2. 2
      app/controllers/rb_tasks_controller.rb
  3. 2
      app/controllers/rb_updated_items_controller.rb
  4. 18
      app/helpers/rb_common_helper.rb
  5. 109
      app/models/impediment.rb
  6. 14
      app/models/sprint.rb
  7. 68
      app/models/task.rb
  8. 4
      app/views/layouts/rb.html.erb
  9. 2
      app/views/rb_impediments/_impediment.html.erb
  10. 4
      app/views/rb_master_backlogs/show.html.erb
  11. 16
      app/views/rb_taskboards/show.html.erb
  12. 4
      app/views/shared/_backlogs_header.html.erb
  13. 10
      assets/javascripts/app/model.js
  14. 0
      assets/stylesheets/global.css
  15. 0
      assets/stylesheets/global_print.css
  16. 0
      assets/stylesheets/images/ajax.gif
  17. 0
      assets/stylesheets/images/bouncer.gif
  18. 0
      assets/stylesheets/images/error.png
  19. 0
      assets/stylesheets/images/indicator.gif
  20. 0
      assets/stylesheets/images/ui-bg_flat_0_aaaaaa_40x100.png
  21. 0
      assets/stylesheets/images/ui-bg_flat_75_ffffff_40x100.png
  22. 0
      assets/stylesheets/images/ui-bg_glass_55_fbf9ee_1x400.png
  23. 0
      assets/stylesheets/images/ui-bg_glass_65_ffffff_1x400.png
  24. 0
      assets/stylesheets/images/ui-bg_glass_75_dadada_1x400.png
  25. 0
      assets/stylesheets/images/ui-bg_glass_75_e6e6e6_1x400.png
  26. 0
      assets/stylesheets/images/ui-bg_glass_95_fef1ec_1x400.png
  27. 0
      assets/stylesheets/images/ui-bg_highlight-soft_75_cccccc_1x100.png
  28. 0
      assets/stylesheets/images/ui-icons_222222_256x240.png
  29. 0
      assets/stylesheets/images/ui-icons_2e83ff_256x240.png
  30. 0
      assets/stylesheets/images/ui-icons_454545_256x240.png
  31. 0
      assets/stylesheets/images/ui-icons_888888_256x240.png
  32. 0
      assets/stylesheets/images/ui-icons_cd0a0a_256x240.png
  33. 0
      assets/stylesheets/images/warning.png
  34. 0
      assets/stylesheets/jquery-ui.css
  35. 2
      assets/stylesheets/master_backlog.css
  36. 0
      assets/stylesheets/master_backlog_print.css
  37. 0
      assets/stylesheets/statistics.css
  38. 0
      assets/stylesheets/taskboard.css
  39. 0
      assets/stylesheets/taskboard_print.css
  40. 5
      config/locales/de.yml
  41. 5
      config/locales/en-GB.yml
  42. 7
      config/locales/en.yml
  43. 120
      features/scrum_master.feature
  44. 19
      features/step_definitions/_given_steps.rb
  45. 24
      features/step_definitions/_then_steps.rb
  46. 11
      features/step_definitions/_when_steps.rb
  47. 42
      lib/backlogs_issue_patch.rb
  48. 4
      spec/factories/impediment_factory.rb
  49. 3
      spec/factories/task_factory.rb
  50. 300
      spec/models/impediment_spec.rb
  51. 54
      spec/models/tasks_spec.rb

@ -2,7 +2,7 @@ class RbImpedimentsController < RbApplicationController
unloadable
def create
@impediment = Task.create_with_relationships(params, User.current.id, @project.id, true)
@impediment = Impediment.create_with_relationships(params, @project.id)
result = @impediment.errors.length
status = (result == 0 ? 200 : 400)
@include_meta = true
@ -13,8 +13,8 @@ class RbImpedimentsController < RbApplicationController
end
def update
@impediment = Task.find_by_id(params[:id])
result = @impediment.update_with_relationships(params, true)
@impediment = Impediment.find_by_id(params[:id])
result = @impediment.update_with_relationships(params)
status = (result ? 200 : 400)
@include_meta = true

@ -2,7 +2,7 @@ class RbTasksController < RbApplicationController
unloadable
def create
@task = Task.create_with_relationships(params, User.current.id, @project.id)
@task = Task.create_with_relationships(params, @project.id)
result = @task.errors.length
status = (result == 0 ? 200 : 400)
@include_meta = true

@ -26,7 +26,7 @@ class RbUpdatedItemsController < RbApplicationController
end
if only.include? :impediments
@items[:impediments] = Task.find_all_updated_since(params[:since], @project.id, true)
@items[:impediments] = Impediment.find_all_updated_since(params[:since], @project.id)
if @items[:impediments].length > 0
latest_updates << @items[:impediments].sort{ |a,b| a.updated_on <=> b.updated_on }.last
end

@ -9,8 +9,8 @@ module RbCommonHelper
story.blank? || story.assigned_to.blank? ? "" : "#{story.assigned_to.firstname} #{story.assigned_to.lastname}"
end
def blocked_ids(blocked)
blocked.map{|b| b.id }.join(',')
def blocks_ids(ids)
ids.sort.join(',')
end
def build_inline_style(task)
@ -81,20 +81,6 @@ module RbCommonHelper
story.new_record? ? "" : h(story.description).gsub(/&lt;(\/?pre)&gt;/, '<\1>')
end
def theme_name
'rb_default'
end
# TODO: get rid of theme_stylesheet_link_tag, no themeing neccessary, when
# view becomes part of default layout
def theme_stylesheet_link_tag(*args)
themed_args = args.select{ |a| a.class!=Hash }.map{ |s| "#{theme_name}/#{s.to_s}"}
options = args.select{ |a| a.class==Hash}.first || { }
options[:plugin] = 'redmine_backlogs'
themed_args << options
stylesheet_link_tag *themed_args
end
def tracker_id_or_empty(story)
story.new_record? ? "" : story.tracker_id
end

@ -0,0 +1,109 @@
class Impediment < Task
unloadable
acts_as_list :scope => :project
after_save :update_blocks_list
safe_attributes "blocks_ids",
:if => lambda {|impediment, user|
(impediment.new_record? && user.allowed_to?(:create_impediments, impediment.project)) ||
user.allowed_to?(:update_impediments, impediment.project)
}
def self.find(*args)
if args[1] && args[1][:conditions]
if args[1][:conditions].is_a?(Hash)
args[1][:conditions][:parent_id] = nil
args[1][:conditions][:tracker_id] = self.tracker
elsif args[1][:conditions].is_a?(Array)
args[1][:conditions][0] += " AND parent_id is NULL AND tracker_id = #{self.tracker}"
end
else
args << {:conditions => {:parent_id => nil, :tracker_id => self.tracker}}
end
super
end
def self.find_all_updated_since(since, project_id)
super(since, project_id, true)
end
def blocks_ids=(ids)
@blocks_ids_list = [ids] if ids.is_a?(Integer)
@blocks_ids_list = ids.split(/\D+/).map{|id| id.to_i} if ids.is_a?(String)
@blocks_ids_list = ids.map {|id| id.to_i} if ids.is_a?(Array)
end
def blocks_ids
@blocks_ids_list ||= relations_from.select{ |rel| rel.relation_type == IssueRelation::TYPE_BLOCKS }.collect(&:issue_to_id)
end
def move_after(prev_id)
#our super's, move after is using awesome_nested_set which is inapproriate for impediments.
#Impediments always have a parent_id of nil, it is their defining criteria.
#Therefore we use the acts_as_list method
#TODO: create a module to be included by story and impediment, for now this code is just copy&paste
# remove so the potential 'prev' has a correct position
remove_from_list
begin
prev = self.class.find(prev_id)
rescue ActiveRecord::RecordNotFound
prev = nil
end
# if it's the first story, move it to the 1st position
if prev.blank?
insert_at
move_to_top
# if its predecessor has no position (shouldn't happen), make it
# the last story
elsif !prev.in_list?
insert_at
move_to_bottom
# there's a valid predecessor
else
insert_at(prev.position + 1)
end
end
private
def update_blocks_list
relations_from = [] if relations_from.nil?
remove_from_blocks_list
add_to_blocks_list
end
def remove_from_blocks_list
self.relations_from.delete(self.relations_from.select{|rel| rel.relation_type == IssueRelation::TYPE_BLOCKS && !blocks_ids.include?(rel.issue_to_id) })
end
def add_to_blocks_list
currently_blocking = relations_from.select{|rel| rel.relation_type == IssueRelation::TYPE_BLOCKS}.collect(&:issue_to_id)
(self.blocks_ids - currently_blocking).each{ |id|
rel = IssueRelation.new(:relation_type => IssueRelation::TYPE_BLOCKS, :issue_from => self)
rel.issue_to_id = id #attr_protected
self.relations_from << rel
}
end
def validate
validate_blocks_list
end
def validate_blocks_list
if blocks_ids.size == 0
errors.add :blocks_ids, :must_block_at_least_one_issue
else
issues = Issue.find_all_by_id(blocks_ids)
errors.add :blocks_ids, :can_only_contain_issues_of_current_sprint if issues.size == 0 || issues.any?{|i| i.fixed_version != self.fixed_version }
end
end
end

@ -274,19 +274,7 @@ class Sprint < Version
end
def impediments
return Issue.find(:all,
:conditions => ["id in (
select issue_from_id
from issue_relations ir
join issues blocked
on blocked.id = ir.issue_to_id
and blocked.tracker_id in (?)
and blocked.fixed_version_id = (?)
where ir.relation_type = 'blocks'
)",
Story.trackers + [Task.tracker],
self.id]
) #.sort {|a,b| a.closed? == b.closed? ? a.updated_on <=> b.updated_on : (a.closed? ? 1 : -1) }
Impediment.find(:all, :conditions => {:fixed_version_id => self})
end
end

@ -5,29 +5,20 @@ class Task < Issue
def self.tracker
task_tracker = Setting.plugin_redmine_backlogs[:task_tracker]
return nil if task_tracker.blank?
return Integer(task_tracker)
task_tracker.blank? ? nil : task_tracker.to_i
end
def self.create_with_relationships(params, user_id, project_id, is_impediment = false)
def self.create_with_relationships(params, project_id)
task = new
task.author_id = user_id
task.author = User.current
task.project_id = project_id
task.tracker_id = Task.tracker
task.safe_attributes = params
task.remaining_hours = 0 if IssueStatus.find(params[:status_id]).is_closed?
valid_relationships = if is_impediment
task.validate_blocks_list(params[:blocks])
else
true
end
if valid_relationships && task.save
if task.save
task.move_after params[:prev]
task.update_blocked_list params[:blocks].split(/\D+/) if params[:blocks]
end
return task
@ -51,50 +42,23 @@ class Task < Issue
return tasks
end
def update_with_relationships(params, is_impediment = false)
attribs = params.clone.delete_if { |k, v| !safe_attribute_names.include?(k) }
def status_id=(id)
super
self.remaining_hours = 0 if IssueStatus.find(id).is_closed?
end
attribs[:remaining_hours] = 0 if IssueStatus.find(params[:status_id]).is_closed?
def impediment?
parent_issue_id.nil?
end
valid_relationships = if is_impediment && params[:blocks] #if blocks param was not sent, that means the impediment was just dragged
validate_blocks_list(params[:blocks])
else
true
end
def update_with_relationships(params, is_impediment = false)
attribs = params.reject { |k, v| !safe_attribute_names.include?(k.to_s) }
if valid_relationships && result = journalized_update_attributes!(attribs)
move_after params[:prev]
update_blocked_list params[:blocks].split(/\D+/) if params[:blocks]
result
else
false
end
end
result = journalized_update_attributes(attribs)
def update_blocked_list(for_blocking)
# Existing relationships not in for_blocking should be removed from the 'blocks' list
relations_from.find(:all, :conditions => "relation_type='blocks'").each{ |ir|
ir.destroy unless for_blocking.include?( ir[:issue_to_id] )
}
already_blocking = relations_from.find(:all, :conditions => "relation_type='blocks'").map{|ir| ir.issue_to_id}
# Non-existing relationships that are in for_blocking should be added to the 'blocks' list
for_blocking.select{ |id| !already_blocking.include?(id) }.each{ |id|
ir = relations_from.new(:relation_type=>'blocks')
ir[:issue_to_id] = id
ir.save!
}
reload
end
move_after params[:prev] if result
def validate_blocks_list(list)
if list.split(/\D+/).length==0
errors.add :blocks, :must_have_comma_delimited_list
false
else
true
end
result
end
# assumes the task is already under the same story as 'id'

@ -5,8 +5,8 @@
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="description" content="Redmine Backlogs" />
<meta name="keywords" content="issue,bug,tracker,scrum,agile,plugin" />
<%= theme_stylesheet_link_tag 'global.css', 'jquery-ui.css', :media => 'print,screen' %>
<%= theme_stylesheet_link_tag 'global_print.css', :media => 'print' %>
<%= stylesheet_link_tag 'global.css', 'jquery-ui.css', :media => 'print,screen', :plugin => 'redmine_backlogs' %>
<%= stylesheet_link_tag 'global_print.css', :media => 'print', :plugin => 'redmine_backlogs' %>
<%= javascript_include_tag 'jquery-1.4.2.min.js', 'jquery-ui-1.8rc3.custom.min.js', 'jquery.jeditable.mini.js', 'jquery.cookie.js', 'common.js', :plugin => 'redmine_backlogs' %>
<!-- looks like we don't need this anymore -->

@ -4,7 +4,7 @@
<div class="v"><%= id_or_empty(impediment) %></div>
</div>
<div class="subject editable" fieldtype="textarea" fieldname="subject"><%= impediment.subject %></div>
<div class="blocks editable" fieldname="blocks"><%= blocked_ids(impediment.blocks) %></div>
<div class="blocks editable" fieldname="blocks_ids" fieldlabel="<%= l(:field_blocks_ids) %>"><%= blocks_ids(impediment.blocks_ids) %></div>
<div class="assigned_to_id editable" fieldtype="select" fieldname="assigned_to_id">
<div class="t"><%= assignee_name_or_empty(impediment) %></div>
<div class="v"><%= assignee_id_or_empty(impediment) %></div>

@ -18,8 +18,8 @@
:format => :js) %>
<%= stylesheet_link_tag 'jqplot.css', :plugin => 'redmine_backlogs' %>
<%= theme_stylesheet_link_tag 'master_backlog.css', :media => 'print,screen' %>
<%= theme_stylesheet_link_tag 'master_backlog_print.css', :media => 'print' %>
<%= stylesheet_link_tag 'master_backlog.css', :media => 'print,screen', :plugin => 'redmine_backlogs' %>
<%= stylesheet_link_tag 'master_backlog_print.css', :media => 'print', :plugin => 'redmine_backlogs' %>
<% end %>
<div class='contextual'></div>

@ -17,8 +17,8 @@
:sprint_id => @sprint) %>
<%= stylesheet_link_tag 'jqplot.css', :plugin => 'redmine_backlogs' %>
<%= theme_stylesheet_link_tag 'taskboard.css', :media => 'print,screen' %>
<%= theme_stylesheet_link_tag 'taskboard_print.css', :media => 'print' %>
<%= stylesheet_link_tag 'taskboard.css', :media => 'print,screen', :plugin => 'redmine_backlogs' %>
<%= stylesheet_link_tag 'taskboard_print.css', :media => 'print', :plugin => 'redmine_backlogs' %>
<% end %>
<div class="contextual">
@ -26,17 +26,13 @@
<a id='show_charts'><%= l(:label_burndown) %></a>
<% end %>
<%# TODO i18n %>
<span id="col_width">
<label for="col_width_input">Column width:</label>
<label for="col_width_input"><%= l('backlogs.column_with') %></label>
<input id="col_width_input" name="col_width" type='text' />
</span>
</div>
<div class="breadcrumb">
<%= link_to l(:label_backlogs), rb_master_backlog_path(@project) %>
»
</div>
<%= breadcrumb(link_to l(:label_backlogs), rb_master_backlog_path(@project)) %>
<h2>
<%= link_to @sprint.name, rb_taskboard_path(@sprint) %>
@ -60,7 +56,7 @@
<% @statuses.each do |status| %>
<td class="swimlane list <%= status.is_closed? ? 'closed' : '' %>" id="impcell_<%= status.id %>">
<%= render :partial => "rb_impediments/impediment",
:collection => @sprint.impediments.select { |impediment| impediment.status_id == status.id } %>
:collection => @sprint.impediments.select{ |i| i.status_id == status.id }.sort_by {|i| i.position } %>
</td>
<% end %>
</tr>
@ -102,7 +98,7 @@
<%= render :partial => "rb_tasks/task", :object => Task.new %>
</div>
<div id="impediment_template">
<%= render :partial => "rb_impediments/impediment", :object => Task.new %>
<%= render :partial => "rb_impediments/impediment", :object => Impediment.new %>
</div>
<div id="issue_editor"> </div>

@ -1,6 +1,6 @@
<% content_for :header_tags do %>
<%= theme_stylesheet_link_tag 'global.css', 'jquery-ui.css', :media => 'print,screen' %>
<%= theme_stylesheet_link_tag 'global_print.css', :media => 'print' %>
<%= stylesheet_link_tag 'global.css', 'jquery-ui.css', :media => 'print,screen', :plugin => 'redmine_backlogs' %>
<%= stylesheet_link_tag 'global_print.css', :media => 'print', :plugin => 'redmine_backlogs' %>
<%= javascript_include_tag 'lib/jquery-1.5.1.min.js',
'lib/jquery-ui-1.8.11.custom.min.js',

@ -105,14 +105,18 @@ RB.Model = (function ($) {
this.$.find('.editable').each(function (index) {
var field, fieldType, fieldName, input;
var field, fieldType, fieldLabel, fieldName, input;
field = $(this);
fieldName = field.attr('fieldname');
fieldLabel = field.attr('fieldlabel');
fieldType = field.attr('fieldtype') || 'input';
$("<label></label>").text(fieldName.replace(/_/ig, " ").replace(/ id$/ig, "")).
appendTo(editor);
if (!fieldLabel) {
fieldLabel = fieldName.replace(/_/ig, " ").replace(/ id$/ig, "");
}
$("<label></label>").text(fieldLabel).appendTo(editor);
if (fieldType === 'select') {
input = $('#' + fieldName + '_options').clone(true);

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 529 B

Before

Width:  |  Height:  |  Size: 613 B

After

Width:  |  Height:  |  Size: 613 B

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 613 B

After

Width:  |  Height:  |  Size: 613 B

@ -110,7 +110,7 @@
}
#rb .backlog .header .menu .item.hover,
#rb .backlog .header .menu .item:hover {
background-color:#CC0000;
background-color: #999;
}
#rb .backlog .header .menu .item a {
display:block;

@ -88,6 +88,7 @@ de:
show_statistics: 'Scrum Statistiken anzeigen'
show_burndown_chart: 'Burndown-Chart'
add_new_story: 'Neue Story'
column_with: "Spaltenbreite:"
project_module_backlogs: Backlogs (Beta)
@ -96,3 +97,7 @@ de:
version_settings_display_option_right: rechts
version_settings_display_label: Spalte im Backlog
field_display: Spalte im Backlog
field_blocks_ids: Blockiert (IDs)
error_can_only_contain_issues_of_current_sprint: kann nur IDs von Tickets des aktuellen Sprints enthalten
error_must_block_at_least_one_issue: muss die ID wenigstens eines Tickets enthalten

@ -90,3 +90,8 @@ en-GB:
version_settings_display_label: Column in backlog
field_display: Column in backlog
field_blocks_ids: Blocks (IDs)
error_can_only_contain_issues_of_current_sprint: can only contain the IDs of current sprint's tickets
error_must_block_at_least_one_issue: must contain the ID of at least one ticket

@ -106,6 +106,7 @@ en:
show_statistics: 'Show Scrum statistics'
show_burndown_chart: 'Burndown Chart'
add_new_story: 'New Story'
column_with: "Column width:"
project_module_backlogs: Backlogs (Beta)
@ -114,3 +115,9 @@ en:
version_settings_display_option_right: right
version_settings_display_label: Column in backlog
field_display: Column in backlog
field_blocks_ids: Blocks (IDs)
error_can_only_contain_issues_of_current_sprint: can only contain the IDs of current sprint's tickets
error_must_block_at_least_one_issue: must contain the ID of at least one ticket

@ -1,7 +1,7 @@
Feature: Scrum Master
As a scrum master
I want to manage sprints and their stories
So that they get done according the product owner's requirements
So that they get done according the product owner´s requirements
Background:
Given there is 1 project with:
@ -26,6 +26,8 @@ Feature: Scrum Master
| manage_subtasks |
And there is 1 user with:
| login | markus |
| firstname | Markus |
| Lastname | Master |
And the user "markus" is a "scrum master"
And the project has the following sprints:
| name | sprint_start_date | effective_date |
@ -46,28 +48,116 @@ Feature: Scrum Master
| position | subject | sprint |
| 5 | Story A | Sprint 001 |
| 6 | Story B | Sprint 001 |
| 7 | Story C | Sprint 002 |
And there are the following trackers:
| name |
| Task |
And the tracker "Task" is configured to track tasks
And there are the following issue status:
| name | is_closed | is_default |
| New | false | true |
| In Progress | false | false |
| Resolved | false | false |
| Closed | true | false |
| Rejected | true | false |
And the tracker "Task" has the default workflow for the role "scrum master"
And the project has the following tasks:
| subject | sprint | parent |
| Task 1 | Sprint 001 | Story A |
And the project has the following impediments:
| subject | sprint | blocks |
| Impediment 1 | Sprint 001 | Story A |
| subject | sprint | blocks |
| Impediment 1 | Sprint 001 | Story A |
And I am logged in as "markus"
@javascript
Scenario: Create an impediment
Given I am on the taskboard for "Sprint 001"
And I want to create an impediment for Sprint 001
And I want to set the subject of the impediment to Bad Impediment
And I want to indicate that the impediment blocks Story B
When I create the impediment
Then the request should complete successfully
And the sprint named Sprint 001 should have 2 impediments named Bad Impediment
When I press "td.add_new" within "#impediments"
And I fill in "Bad Company" for "subject"
And I fill in the ids of the tasks "Task 1" for "blocks_ids"
And I select "Markus Master" from "assigned_to_id"
And I press "OK"
Then I should see "Bad Company" within "#impediments"
And the impediment "Bad Company" should signal successful saving
@javascript
Scenario: Create an impediment blocking an issue of another sprint
Given I am on the taskboard for "Sprint 001"
When I press "td.add_new" within "#impediments"
And I fill in "Bad Company" for "subject"
And I fill in the ids of the stories "Story C" for "blocks_ids"
And I select "Markus Master" from "assigned_to_id"
And I press "OK"
Then I should see "Bad Company" within "#impediments"
And the impediment "Bad Company" should signal unsuccessful saving
And the error alert should show "Blocks (IDs) can only contain the IDs of current sprint's tickets"
@javascript
Scenario: Create an impediment blocking a non existent issue
Given I am on the taskboard for "Sprint 001"
When I press "td.add_new" within "#impediments"
And I fill in "Bad Company" for "subject"
And I fill in "0" for "blocks_ids"
And I select "Markus Master" from "assigned_to_id"
And I press "OK"
Then I should see "Bad Company" within "#impediments"
And the impediment "Bad Company" should signal unsuccessful saving
And the error alert should show "Blocks (IDs) can only contain the IDs of current sprint's tickets"
@javascript
Scenario: Create an impediment without specifying what it blocks
Given I am on the taskboard for "Sprint 001"
When I press "td.add_new" within "#impediments"
And I fill in "Bad Company" for "subject"
And I fill in "" for "blocks_ids"
And I select "Markus Master" from "assigned_to_id"
And I press "OK"
Then I should see "Bad Company" within "#impediments"
And the impediment "Bad Company" should signal unsuccessful saving
And the error alert should show "Blocks (IDs) must contain the ID of at least one ticket"
@javascript
Scenario: Update an impediment
Given I am on the taskboard for "Sprint 001"
And I want to edit the impediment named Impediment 1
And I want to set the subject of the impediment to Good Impediment
And I want to indicate that the impediment blocks Story B
When I update the impediment
Then the request should complete successfully
And the sprint named Sprint 001 should have 1 impediment named Good Impediment
When I click on the impediment called "Impediment 1"
And I fill in "Bad Company" for "subject"
And I fill in the ids of the tasks "Task 1" for "blocks_ids"
And I press "OK"
Then I should see "Bad Company" within "#impediments"
And the impediment "Bad Company" should signal successful saving
@javascript
Scenario: Update an impediment to block an issue of another sprint
Given I am on the taskboard for "Sprint 001"
When I click on the impediment called "Impediment 1"
And I fill in "Bad Company" for "subject"
And I fill in the ids of the stories "Story C" for "blocks_ids"
And I press "OK"
Then I should see "Bad Company" within "#impediments"
And the impediment "Bad Company" should signal unsuccessful saving
And the error alert should show "Blocks (IDs) can only contain the IDs of current sprint's tickets"
@javascript
Scenario: Update an impediment to block a non existent issue
Given I am on the taskboard for "Sprint 001"
When I click on the impediment called "Impediment 1"
And I fill in "Bad Company" for "subject"
And I fill in "0" for "blocks_ids"
And I press "OK"
Then I should see "Bad Company" within "#impediments"
And the impediment "Bad Company" should signal unsuccessful saving
And the error alert should show "Blocks (IDs) can only contain the IDs of current sprint's tickets"
@javascript
Scenario: Update an impediment to not block anything
Given I am on the taskboard for "Sprint 001"
When I click on the impediment called "Impediment 1"
And I fill in "Bad Company" for "subject"
And I fill in "" for "blocks_ids"
And I press "OK"
Then I should see "Bad Company" within "#impediments"
And the impediment "Bad Company" should signal unsuccessful saving
And the error alert should show "Blocks (IDs) must contain the ID of at least one ticket"
Scenario: Update sprint details
Given I am on the master backlog

@ -150,36 +150,36 @@ end
Given /^the [pP]roject(?: "([^\"]*)")? has the following tasks:$/ do |project_name, table|
project = get_project(project_name)
author = User.find(:first)
User.current = User.find(:first)
table.hashes.each do |task|
story = Story.find(:first, :conditions => { :subject => task['parent'] })
params = initialize_task_params(project, story, author)
params = initialize_task_params(project, story)
params['subject'] = task['subject']
# NOTE: We're bypassing the controller here because we're just
# setting up the database for the actual tests. The actual tests,
# however, should NOT bypass the controller
Task.create_with_relationships(params, author, project.id)
Task.create_with_relationships(params, project.id)
end
end
Given /^the [pP]roject(?: "([^\"]*)")? has the following impediments:$/ do |project_name, table|
project = get_project(project_name)
author = User.find(:first)
User.current = User.find(:first)
table.hashes.each do |impediment|
sprint = Sprint.find(:first, :conditions => { :name => impediment['sprint'] })
blocks = Story.find(:all, :conditions => { :subject => impediment['blocks'].split(', ') }).map{ |s| s.id }
params = initialize_impediment_params(project, sprint, author)
params = initialize_impediment_params(project, sprint)
params['subject'] = impediment['subject']
params['blocks'] = blocks.join(',')
params['blocks_ids'] = blocks.join(',')
# NOTE: We're bypassing the controller here because we're just
# setting up the database for the actual tests. The actual tests,
# however, should NOT bypass the controller
Task.create_with_relationships(params, author.id, project.id)
Impediment.create_with_relationships(params, project.id)
end
end
@ -249,3 +249,8 @@ end
Given /^the scrum statistics are enabled$/ do
Setting.plugin_redmine_backlogs = Setting.plugin_redmine_backlogs.merge(:show_statistics => true)
end
Given /^the tracker "(.+?)" is configured to track tasks$/ do |tracker_name|
tracker = Tracker.find_by_name(tracker_name)
Setting.plugin_redmine_backlogs = Setting.plugin_redmine_backlogs.merge(:task_tracker => tracker.id)
end

@ -114,6 +114,26 @@ Then /^the sprint named (.+) should have (\d+) impediments? named (.+)$/ do |spr
sprints.first.impediments.map{ |i| i.subject==impediment_subject}.length.should == count.to_i
end
Then /^the impediment "(.+)" should signal( | un)successful saving$/ do |impediment_subject, negative|
negative = !negative.blank?
element = {}
begin
wait_until(5) do
element = page.find(:xpath, "//div[contains(concat(' ',normalize-space(@class),' '),' impediment ') and contains(., '#{impediment_subject}')]")
!element[:class].include?('saving') || element[:class].include?('error')
end
rescue Capybara::TimeoutError
fail "The impediment '#{impediment_subject}' did not finish saving within within 5 sec"
end
if negative
element[:class].should be_include('error')
else
element[:class].should_not be_include('error')
end
end
Then /^the sprint should be updated accordingly$/ do
sprint = Sprint.find(@sprint_params['id'])
@ -172,3 +192,7 @@ Then /^(issue|task|story) (.+) should have (.+) set to (.+)$/ do |type, subject,
issue = Issue.find_by_subject(subject)
issue[attribute].should == value.to_i
end
Then /^the error alert should show "(.+?)"$/ do |msg|
Then %Q{I should see "#{msg}" within "#msgBox"}
end

@ -173,3 +173,14 @@ When /^I confirm the story form$/ do
sleep 1.5
steps 'Then I should not see ".saving"'
end
When /^I fill in the ids of the (tasks|issues|stories) "(.+?)" for "(.+?)"$/ do |model_name, subjects, field|
model = Kernel.const_get(model_name.classify)
ids = subjects.split(/,/).collect { |subject| model.find_by_subject(subject).id }
When %{I fill in "#{ids.join(", ")}" for "#{field}"}
end
When /^I click on the impediment called "(.+?)"$/ do |impediment_name|
When %Q{I click on the text "#{impediment_name}"}
end

@ -5,7 +5,7 @@ module Backlogs
def self.included(base) # :nodoc:
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
@ -15,10 +15,10 @@ module Backlogs
after_save :backlogs_after_save
end
end
module ClassMethods
end
module InstanceMethods
def move_to_project_without_transaction_with_autolink(new_project, new_tracker = nil, options = {})
newissue = move_to_project_without_transaction_without_autolink(new_project, new_tracker, options)
@ -29,33 +29,33 @@ module Backlogs
relation.issue_to = newissue
relation.save
end
return newissue
end
def journalized_update_attributes!(attribs)
self.init_journal(User.current)
return self.update_attributes!(attribs)
end
def journalized_update_attributes(attribs)
self.init_journal(User.current)
return self.update_attributes(attribs)
end
def journalized_update_attribute(attrib, v)
self.init_journal(User.current)
self.update_attribute(attrib, v)
end
def is_story?
return Story.trackers.include?(self.tracker_id)
end
def is_task?
return (self.parent_id && self.tracker_id == Task.tracker && Task.tracker.present?)
end
def story
if self.is_story?
return self
@ -63,13 +63,13 @@ module Backlogs
return self.ancestors.find_by_tracker_id(Story.trackers)
end
end
def blocks
# return issues that I block that aren't closed
return [] if closed?
relations_from.collect {|ir| ir.relation_type == 'blocks' && !ir.issue_to.closed? ? ir.issue_to : nil}.compact
end
def blockers
# return issues that block me
return [] if closed?
@ -78,23 +78,23 @@ module Backlogs
def velocity_based_estimate
return nil if !self.is_story? || ! self.story_points || self.story_points <= 0
dpp = self.project.scrum_statistics.info[:average_days_per_point]
return nil if ! dpp
return Integer(self.story_points * dpp)
end
def recalculate_attributes_for_with_remaining_hours(issue_id)
recalculate_attributes_for_without_remaining_hours(issue_id)
if issue_id && p = Issue.find_by_id(issue_id)
if p.left != (p.right + 1) # this node has children
p.update_attribute(:remaining_hours, p.leaves.sum(:remaining_hours).to_f)
end
end
end
def backlogs_before_validation
if self.tracker_id == Task.tracker
self.estimated_hours = self.remaining_hours if self.estimated_hours.blank? && ! self.remaining_hours.blank?
@ -108,9 +108,9 @@ module Backlogs
## Normally one of the _before_save hooks ought to take
## care of this, but appearantly neither root_id nor
## parent_id are set at that point
touched_sprints = []
if self.is_story?
# raw sql here because it's efficient and not
# doing so causes an update loop when Issue calls
@ -125,7 +125,7 @@ module Backlogs
touched_sprints = [self.fixed_version_id, self.fixed_version_id_was].compact.uniq
touched_sprints = touched_sprints.collect{|s| Sprint.find(s)}.compact
elsif self.is_task?
begin
story = self.story
@ -136,7 +136,7 @@ module Backlogs
touched_sprints = [self.root_id, self.root_id_was].compact.uniq.collect{|s| Story.find(s).fixed_version}.compact
end
end
touched_sprints.each {|sprint|
sprint.touch_burndown
}

@ -0,0 +1,4 @@
Factory.define(:impediment, :parent => :issue, :class => Impediment) do |t|
t.association :tracker, :factory => :tracker_task
t.subject "Impeding progress"
end

@ -0,0 +1,3 @@
Factory.define(:task, :parent => :issue, :class => Task) do |t|
t.association :tracker, :factory => :tracker_task
end

@ -0,0 +1,300 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe Impediment do
let(:user) { @user ||= Factory.create(:user) }
let(:role) { @role ||= Factory.create(:role) }
let(:tracker_feature) { @tracker_feature ||= Factory.create(:tracker_feature) }
let(:tracker_task) { @tracker_task ||= Factory.create(:tracker_task) }
let(:issue_priority) { @issue_priority ||= Factory.create(:priority, :is_default => true) }
let(:task) { Factory.build(:task, :tracker => tracker_task,
:project => project,
:author => user,
:priority => issue_priority,
:status => issue_status1) }
let(:feature) { Factory.build(:issue, :tracker => tracker_feature,
:project => project,
:author => user,
:priority => issue_priority,
:status => issue_status1) }
let(:version) { Factory.create(:version, :project => project) }
let(:project) do
unless @project
@project = Factory.build(:project)
@project.members = [Factory.build(:member, :principal => user,
:project => @project,
:roles => [role])]
end
@project
end
let(:issue_status1) { @status1 ||= Factory.create(:issue_status, :name => "status 1", :is_default => true) }
let(:issue_status2) { @status2 ||= Factory.create(:issue_status, :name => "status 2") }
let(:tracker_workflow) { @workflow ||= Workflow.create(:tracker_id => tracker_task.id,
:old_status => issue_status1,
:new_status => issue_status2,
:role => role) }
let(:impediment) { Factory.build(:impediment, :author => user,
:fixed_version => version,
:assigned_to => user,
:priority => issue_priority,
:project => project,
:tracker => tracker_task,
:status => issue_status1)}
before(:each) do
Setting.plugin_redmine_backlogs = {:points_burn_direction => "down",
:wiki_template => "",
:card_spec => "Sattleford VM-5040",
:story_trackers => [tracker_feature.id.to_s],
:task_tracker => tracker_task.id.to_s }
User.current = user
issue_priority.save
issue_status1.save
project.save
tracker_workflow.save
end
describe "class methods" do
describe :create_with_relationships do
before(:each) do
@impediment_subject = "Impediment A"
role.permissions = [:create_impediments]
role.save
end
shared_examples_for "impediment creation" do
it { @impediment.subject.should eql @impediment_subject }
it { @impediment.author.should eql User.current }
it { @impediment.project.should eql project }
it { @impediment.fixed_version.should eql version }
it { @impediment.priority.should eql issue_priority}
it { @impediment.status.should eql issue_status1 }
it { @impediment.tracker.should eql tracker_task }
it { @impediment.assigned_to.should eql user }
end
shared_examples_for "impediment creation with 1 blocking relationship" do
it_should_behave_like "impediment creation"
it { @impediment.should have(1).relations_from }
it { @impediment.relations_from[0].issue_to.should eql feature }
it { @impediment.relations_from[0].relation_type.should eql IssueRelation::TYPE_BLOCKS }
end
shared_examples_for "impediment creation with no blocking relationship" do
it_should_behave_like "impediment creation"
it { @impediment.should have(0).relations_from }
end
describe "WITH a blocking relationship to a story" do
describe "WITH the story having the same version" do
before(:each) do
feature.fixed_version = version
feature.save
@impediment = Impediment.create_with_relationships({:subject => @impediment_subject,
:assigned_to_id => user.id,
:blocks_ids => feature.id.to_s,
:status_id => issue_status1.id,
:fixed_version_id => version.id},
project.id)
end
it_should_behave_like "impediment creation with 1 blocking relationship"
it { @impediment.should_not be_new_record }
it { @impediment.relations_from[0].should_not be_new_record }
end
describe "WITH the story having another version" do
before(:each) do
feature.fixed_version = Factory.create(:version, :project => project, :name => "another version")
feature.save
@impediment = Impediment.create_with_relationships({:subject => @impediment_subject,
:assigned_to_id => user.id,
:blocks_ids => feature.id.to_s,
:status_id => issue_status1.id,
:fixed_version_id => version.id},
project.id)
end
it_should_behave_like "impediment creation with no blocking relationship"
it { @impediment.should be_new_record }
it { @impediment.errors[:blocks_ids].should eql I18n.t(:can_only_contain_issues_of_current_sprint, :scope => [:activerecord, :errors, :models, :impediment, :attributes, :blocks_ids]) }
end
describe "WITH the story being non existent" do
before(:each) do
@impediment = Impediment.create_with_relationships({:subject => @impediment_subject,
:assigned_to_id => user.id,
:blocks_ids => "0",
:status_id => issue_status1.id,
:fixed_version_id => version.id},
project.id)
end
it_should_behave_like "impediment creation with no blocking relationship"
it { @impediment.should be_new_record }
it { @impediment.errors[:blocks_ids].should eql I18n.t(:can_only_contain_issues_of_current_sprint, :scope => [:activerecord, :errors, :models, :impediment, :attributes, :blocks_ids]) }
end
end
describe "WITHOUT a blocking relationship defined" do
before(:each) do
@impediment = Impediment.create_with_relationships({:subject => @impediment_subject,
:assigned_to_id => user.id,
:blocks_ids => "",
:status_id => issue_status1.id,
:fixed_version_id => version.id},
project.id)
end
it_should_behave_like "impediment creation with no blocking relationship"
it { @impediment.should be_new_record }
it { @impediment.errors[:blocks_ids].should eql I18n.t(:must_block_at_least_one_issue, :scope => [:activerecord, :errors, :models, :impediment, :attributes, :blocks_ids]) }
end
end
end
describe "instance methods" do
describe :update_with_relationships do
before(:each) do
role.permissions = [:update_impediments]
role.save
feature.fixed_version = version
feature.save
@impediment = impediment
@impediment.blocks_ids = feature.id.to_s
@impediment.save
end
shared_examples_for "impediment update" do
it { @impediment.author.should eql user }
it { @impediment.project.should eql project }
it { @impediment.fixed_version.should eql version }
it { @impediment.priority.should eql issue_priority}
it { @impediment.status.should eql issue_status1 }
it { @impediment.tracker.should eql tracker_task }
it { @impediment.blocks_ids.should eql @blocks.split(/\D+/).map{|id| id.to_i} }
end
shared_examples_for "impediment update with changed blocking relationship" do
it_should_behave_like "impediment update"
it { @impediment.should have(1).relations_from }
it { @impediment.relations_from[0].should_not be_new_record }
it { @impediment.relations_from[0].issue_to.should eql @story }
it { @impediment.relations_from[0].relation_type.should eql IssueRelation::TYPE_BLOCKS }
end
shared_examples_for "impediment update with unchanged blocking relationship" do
it_should_behave_like "impediment update"
it { @impediment.should have(1).relations_from }
it { @impediment.relations_from[0].should_not be_changed }
it { @impediment.relations_from[0].issue_to.should eql feature }
it { @impediment.relations_from[0].relation_type.should eql IssueRelation::TYPE_BLOCKS }
end
describe "WHEN changing the blocking relationship to another story" do
before(:each) do
@story = Factory.build(:issue, :subject => "another story",
:tracker => tracker_feature,
:project => project,
:author => user,
:priority => issue_priority,
:status => issue_status1)
end
describe "WITH the story having the same version" do
before(:each) do
@story.fixed_version = version
@story.save
@blocks = @story.id.to_s
@impediment.update_with_relationships({:blocks_ids => @blocks,
:status_id => issue_status1.id.to_s})
end
it_should_behave_like "impediment update with changed blocking relationship"
it { @impediment.should_not be_changed }
end
describe "WITH the story having another version" do
before(:each) do
@story.fixed_version = Factory.create(:version, :project => project, :name => "another version")
@story.save
@blocks = @story.id.to_s
@impediment.update_with_relationships({:blocks_ids => @blocks,
:status_id => issue_status1.id.to_s})
end
it_should_behave_like "impediment update with unchanged blocking relationship"
it { @impediment.should be_changed }
it { @impediment.errors[:blocks_ids].should eql I18n.t(:can_only_contain_issues_of_current_sprint, :scope => [:activerecord, :errors, :models, :impediment, :attributes, :blocks_ids]) }
end
describe "WITH the story beeing non existent" do
before(:each) do
@blocks = "0"
@impediment.update_with_relationships({:blocks_ids => @blocks,
:status_id => issue_status1.id.to_s})
end
it_should_behave_like "impediment update with unchanged blocking relationship"
it { @impediment.should be_changed }
it { @impediment.errors[:blocks_ids].should eql I18n.t(:can_only_contain_issues_of_current_sprint, :scope => [:activerecord, :errors, :models, :impediment, :attributes, :blocks_ids]) }
end
end
describe "WITHOUT a blocking relationship defined" do
before(:each) do
@blocks = ""
@impediment.update_with_relationships({:blocks_ids => @blocks,
:status_id => issue_status1.id.to_s})
end
it_should_behave_like "impediment update with unchanged blocking relationship"
it { @impediment.should be_changed }
it { @impediment.errors[:blocks_ids].should eql I18n.t(:must_block_at_least_one_issue, :scope => [:activerecord, :errors, :models, :impediment, :attributes, :blocks_ids]) }
end
end
describe "blocks_ids=/blocks_ids" do
describe "WITH an integer" do
it do
impediment.blocks_ids = 2
impediment.blocks_ids.should eql [2]
end
end
describe "WITH a string" do
it do
impediment.blocks_ids = "1, 2, 3"
impediment.blocks_ids.should eql [1,2,3]
end
end
describe "WITH an array" do
it do
impediment.blocks_ids = [1,2,3]
impediment.blocks_ids.should eql [1,2,3]
end
end
describe "WITH only prior blockers defined" do
before(:each) do
feature.fixed_version = version
feature.save
task.fixed_version = version
task.save
impediment.relations_from = [IssueRelation.new(:issue_from => impediment, :issue_to => feature, :relation_type => IssueRelation::TYPE_BLOCKS),
IssueRelation.new(:issue_from => impediment, :issue_to => task, :relation_type => IssueRelation::TYPE_BLOCKS)]
true
end
it { impediment.blocks_ids.should eql [feature.id, task.id] }
end
end
end
end

@ -0,0 +1,54 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe Task do
let(:user) { @user ||= Factory.create(:user) }
let(:tracker_feature) { @tracker_feature ||= Factory.create(:tracker_feature) }
let(:tracker_task) { @tracker_task ||= Factory.create(:tracker_task) }
let(:issue_priority) { @issue_priority ||= Factory.create(:priority) }
let(:task) { Factory.build(:task, :tracker => tracker_task,
:project => project,
:author => user,
:priority => issue_priority,
:status => issue_status) }
let(:feature) { Factory.build(:issue, :tracker => tracker_feature,
:project => project,
:author => user,
:priority => issue_priority,
:status => issue_status) }
let(:version) { @version ||= Factory.create(:version, :project => project) }
let(:project) { @project ||= Factory.create(:project) }
let(:issue_status) { @status ||= Factory.create(:issue_status) }
before(:each) do
Setting.plugin_redmine_backlogs = {"points_burn_direction" => "down",
"wiki_template" => "",
"card_spec" => "Sattleford VM-5040",
"story_trackers" => [tracker_feature.id.to_s],
"task_tracker" => tracker_task.id.to_s }
end
describe "Instance Methods" do
before(:each) do
end
describe :impediment? do
describe "WITHOUT parent" do
before(:each) do
end
it { task.should be_impediment }
end
describe "WITH parent" do
before(:each) do
feat = feature
feat.save
task.parent_issue_id = feat.id
end
it { task.should_not be_impediment }
end
end
end
end
Loading…
Cancel
Save