├── VERSION ├── app ├── helpers │ ├── burndowns_helper.rb │ ├── iterations_helper.rb │ ├── active_iterations_helper.rb │ ├── story_team_members_helper.rb │ ├── acceptance_criteria_helper.rb │ ├── easy_agile_helper.rb │ ├── stories_helper.rb │ └── application_helper.rb ├── controllers │ ├── application_controller.rb │ ├── easy_agile_controller.rb │ ├── easy_agile_common_controller.rb │ ├── active_iterations_controller.rb │ ├── burndowns_controller.rb │ ├── story_team_members_controller.rb │ ├── iterations_controller.rb │ ├── acceptance_criteria_controller.rb │ └── stories_controller.rb ├── views │ ├── iterations │ │ ├── _list_item.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── new_guidance.html.erb │ │ ├── _form.erb │ │ ├── _iteration.erb │ │ ├── planned.html.erb │ │ ├── finished.html.erb │ │ ├── index.html.erb │ │ ├── show_active.html.erb │ │ └── show.html.erb │ ├── stories │ │ ├── _list_item.erb │ │ ├── show.feature.erb │ │ ├── _form.erb │ │ ├── edit.html.erb │ │ ├── new_with_project.html.erb │ │ ├── _estimating_story.erb │ │ ├── backlog_guidance.html.erb │ │ ├── index.html.erb │ │ ├── _prioritising_story.erb │ │ ├── new_with_iteration.html.erb │ │ ├── backlog.html.erb │ │ ├── _status_form.erb │ │ ├── _story.html.erb │ │ └── show.html.erb │ ├── layouts │ │ ├── request.html.erb │ │ └── ea_base.html.erb │ ├── story_team_members │ │ └── _story_team_member.erb │ ├── acceptance_criteria │ │ ├── edit.html.erb │ │ ├── edit.js.erb │ │ ├── _list.erb │ │ └── _criterion.erb │ ├── easy_agile │ │ ├── show_guidance.html.erb │ │ └── show.html.erb │ └── my │ │ └── blocks │ │ ├── _active_work.erb │ │ └── _easy_agile_home.erb └── models │ ├── story_action.rb │ ├── acceptance_criterion_observer.rb │ ├── burndown_data_point.rb │ ├── story_team_member.rb │ ├── acceptance_criterion.rb │ ├── story_action_observer.rb │ ├── burndown.rb │ ├── story.rb │ └── iteration.rb ├── assets ├── stylesheets │ ├── iteration │ │ ├── typography.css │ │ ├── layout.css │ │ └── colours.css │ ├── iteration_planning │ │ ├── typography.css │ │ ├── colours.css │ │ └── layout.css │ ├── story │ │ ├── typography.css │ │ ├── colours.css │ │ └── layout.css │ ├── iteration_active │ │ ├── typography.css │ │ ├── colours.css │ │ └── layout.css │ ├── application │ │ ├── colours.css │ │ ├── typography.css │ │ └── layout.css │ └── landing │ │ ├── colours.css │ │ ├── typography.css │ │ └── layout.css ├── images │ ├── burndown.png │ ├── new_story.png │ ├── story_card.png │ ├── fella_testing.gif │ ├── fella_testing.png │ ├── simply_agile.png │ ├── try_it_free.png │ ├── arrow_from_grey.png │ ├── underline_green.png │ ├── active_iteration.png │ ├── arrow_large_left.png │ ├── arrow_large_right.png │ ├── arrow_small_right.gif │ ├── arrow_small_right.png │ ├── button_background.png │ ├── fella_in_progress.gif │ ├── fella_in_progress.png │ ├── planning_iteration.png │ ├── simply_agile_large.png │ ├── new_story_highlight.png │ ├── simply_agile_twitter.png │ ├── arrow_large_left_highlighted.png │ ├── arrow_large_right_highlighted.png │ ├── arrow_small_right_grey_background.png │ ├── arrow_small_right_green_background.png │ ├── arrow_small_right_orange_background.png │ └── a_simple_tool_for_effective_agile_teams.png └── javascripts │ ├── backlog_prioritisation.js │ ├── flash.js │ ├── application.js │ ├── request.js │ ├── story.js │ ├── acceptance_criteria.js │ ├── iteration_planning.js │ ├── iteration_active.js │ ├── jquery.form.js │ └── jquery-ui-1.7.custom.min.js ├── db └── migrate │ ├── 20090325133236_create_story_team_members.rb │ ├── 20090310175005_create_burndown_data_points.rb │ ├── 20090327110522_create_story_actions.rb │ ├── 20090304193819_create_acceptance_criteria.rb │ ├── 20090303213012_create_iterations.rb │ └── 20090304004418_create_stories.rb ├── lib ├── my_controller_patch.rb ├── project_patch.rb └── user_patch.rb ├── config ├── locales │ ├── numbers.yml │ ├── activerecord.yml │ ├── en.yml │ ├── ru.yml │ ├── it.yml │ └── pt-BR.yml └── routes.rb ├── LICENSE ├── LICENSE.simply_agile ├── init.rb └── README.rdoc /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.5 -------------------------------------------------------------------------------- /app/helpers/burndowns_helper.rb: -------------------------------------------------------------------------------- 1 | module BurndownsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/iterations_helper.rb: -------------------------------------------------------------------------------- 1 | module IterationsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/active_iterations_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveIterationsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/story_team_members_helper.rb: -------------------------------------------------------------------------------- 1 | module StoryTeamMembersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/acceptance_criteria_helper.rb: -------------------------------------------------------------------------------- 1 | module AcceptanceCriteriaHelper 2 | end 3 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration/typography.css: -------------------------------------------------------------------------------- 1 | #content .iteration a { text-align:center } 2 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /assets/images/burndown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/burndown.png -------------------------------------------------------------------------------- /app/views/iterations/_list_item.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to h(list_item), [list_item.project, list_item] %> 3 |
  • 4 | -------------------------------------------------------------------------------- /app/views/stories/_list_item.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to h(list_item), [list_item.project, list_item] %> 3 |
  • 4 | -------------------------------------------------------------------------------- /assets/images/new_story.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/new_story.png -------------------------------------------------------------------------------- /assets/images/story_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/story_card.png -------------------------------------------------------------------------------- /assets/images/fella_testing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/fella_testing.gif -------------------------------------------------------------------------------- /assets/images/fella_testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/fella_testing.png -------------------------------------------------------------------------------- /assets/images/simply_agile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/simply_agile.png -------------------------------------------------------------------------------- /assets/images/try_it_free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/try_it_free.png -------------------------------------------------------------------------------- /assets/images/arrow_from_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_from_grey.png -------------------------------------------------------------------------------- /assets/images/underline_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/underline_green.png -------------------------------------------------------------------------------- /app/views/stories/show.feature.erb: -------------------------------------------------------------------------------- 1 | <%= l(:feature) %>: <%= @story.name %> 2 | <%= @story.content.gsub(/^/, ' ') %> 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/images/active_iteration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/active_iteration.png -------------------------------------------------------------------------------- /assets/images/arrow_large_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_large_left.png -------------------------------------------------------------------------------- /assets/images/arrow_large_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_large_right.png -------------------------------------------------------------------------------- /assets/images/arrow_small_right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_small_right.gif -------------------------------------------------------------------------------- /assets/images/arrow_small_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_small_right.png -------------------------------------------------------------------------------- /assets/images/button_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/button_background.png -------------------------------------------------------------------------------- /assets/images/fella_in_progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/fella_in_progress.gif -------------------------------------------------------------------------------- /assets/images/fella_in_progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/fella_in_progress.png -------------------------------------------------------------------------------- /assets/images/planning_iteration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/planning_iteration.png -------------------------------------------------------------------------------- /assets/images/simply_agile_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/simply_agile_large.png -------------------------------------------------------------------------------- /assets/images/new_story_highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/new_story_highlight.png -------------------------------------------------------------------------------- /assets/images/simply_agile_twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/simply_agile_twitter.png -------------------------------------------------------------------------------- /app/views/layouts/request.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= yield :h1 %>

    2 |
    3 | <%= yield %> 4 |
    5 | -------------------------------------------------------------------------------- /app/models/story_action.rb: -------------------------------------------------------------------------------- 1 | class StoryAction < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :story 4 | belongs_to :iteration 5 | end 6 | -------------------------------------------------------------------------------- /assets/images/arrow_large_left_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_large_left_highlighted.png -------------------------------------------------------------------------------- /assets/images/arrow_large_right_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_large_right_highlighted.png -------------------------------------------------------------------------------- /assets/images/arrow_small_right_grey_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_small_right_grey_background.png -------------------------------------------------------------------------------- /assets/images/arrow_small_right_green_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_small_right_green_background.png -------------------------------------------------------------------------------- /assets/images/arrow_small_right_orange_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/arrow_small_right_orange_background.png -------------------------------------------------------------------------------- /assets/images/a_simple_tool_for_effective_agile_teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/HEAD/assets/images/a_simple_tool_for_effective_agile_teams.png -------------------------------------------------------------------------------- /app/views/stories/_form.erb: -------------------------------------------------------------------------------- 1 |

    2 | <%= form.label :name %> 3 | <%= form.text_field :name, :class => 'auto_focus' %> 4 |

    5 |

    6 | <%= form.label :content %> 7 | <%= form.text_area :content, :rows => 5, :cols => 28 %> 8 |

    9 | -------------------------------------------------------------------------------- /app/models/acceptance_criterion_observer.rb: -------------------------------------------------------------------------------- 1 | class AcceptanceCriterionObserver < ActiveRecord::Observer 2 | def after_save(acceptance_criterion) 3 | acceptance_criterion.story.update_status_from_acceptance_criteria 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/story_team_members/_story_team_member.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_user story_team_member.user %> 3 | <% if story_team_member.user == User.current %> 4 | <%= button_to l(:remove_me), [story_team_member.story.project, story_team_member], :method => :delete %> 5 | <% end %> 6 |
  • 7 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration/layout.css: -------------------------------------------------------------------------------- 1 | /* iteration lists */ 2 | #iterations div.section .guidance { padding:30px 0 0 15px } 3 | #content .iteration { padding:0 15px; float:left; margin-bottom:15px } 4 | #content .iteration a { display:block; margin:0 auto } 5 | /* end iteration lists */ 6 | -------------------------------------------------------------------------------- /app/models/burndown_data_point.rb: -------------------------------------------------------------------------------- 1 | class BurndownDataPoint < ActiveRecord::Base 2 | belongs_to :iteration 3 | 4 | named_scope :for_iteration, lambda { |iteration| 5 | { 6 | :conditions => { :iteration_id => iteration.id }, 7 | :order => 'date' 8 | } 9 | } 10 | end 11 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration_planning/typography.css: -------------------------------------------------------------------------------- 1 | div.iteration_planning .include a { text-indent:-20000px; outline:none; 2 | text-underline:none } 3 | div.iteration_planning .display h4 { font-weight:bold } 4 | div.iteration_planning .javascript #stories span.estimate { text-align:center } 5 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration/colours.css: -------------------------------------------------------------------------------- 1 | /* iteration lists */ 2 | #content .iteration a img { border:2px solid #fff } 3 | #content .iteration a:hover img, 4 | #content .iteration a:focus img { border:2px solid #999 } 5 | #iterations .active { background:#e4e5f3 } 6 | #iterations .planned { background:#f1f1f8 } 7 | /* end iteration lists */ 8 | -------------------------------------------------------------------------------- /app/views/stories/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{l(:editing)} #{@story}" %> 2 | <% content_for :h2, h("#{l(:editing)} #{@story}") %> 3 | <% form_for [@project, @story] do |form| %> 4 | <%= form.error_messages %> 5 | <%= render form %> 6 |

    7 | <%= form.submit l(:update_story) %> 8 |

    9 | <% end %> 10 | -------------------------------------------------------------------------------- /app/controllers/easy_agile_controller.rb: -------------------------------------------------------------------------------- 1 | class EasyAgileController < EasyAgileCommonController 2 | before_filter :find_optional_project 3 | 4 | helper :stories 5 | 6 | def show 7 | if @project.stories.empty? 8 | render :template => 'easy_agile/show_guidance' 9 | end 10 | end 11 | 12 | def my_page 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/stories/new_with_project.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, l(:new_story) %> 2 | <% content_for :h2, l(:new_story) %> 3 | <% form_for @story, :url => project_stories_path do |form| %> 4 | <%= form.error_messages %> 5 | <%= render form %> 6 |

    7 | <%= form.submit l(:create_story) %> 8 |

    9 | <% end %> 10 | -------------------------------------------------------------------------------- /assets/stylesheets/story/typography.css: -------------------------------------------------------------------------------- 1 | .story ol li { font-size:100%; margin:0 } 2 | .story h3, .story h4{ font-weight:bold } 3 | .story h3{ padding:4px } 4 | .story h4{ padding:6px 0 0 4px !important } 5 | #content .story p.estimate { margin:0; text-align:center; 6 | line-height:1.2em; font-weight:bold; font-size:100% } 7 | -------------------------------------------------------------------------------- /app/helpers/easy_agile_helper.rb: -------------------------------------------------------------------------------- 1 | module EasyAgileHelper 2 | def ea_tabs 3 | [{:id => 'easy_agile', :path => :project_easy_agile, :label => :dashboard}, 4 | {:id => 'stories', :path => :backlog_project_stories, :label => :backlog}, 5 | {:id => 'iterations', :path => :project_iterations, :label => :iteration_plural}, 6 | ] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/stories/_estimating_story.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= label_tag "stories_#{estimating_story.id}_estimate", l(:estimate) %> 3 | <%= text_field_tag("stories[#{estimating_story.id}][estimate]", 4 | estimating_story.estimate, 5 | :class => 'numeric', 6 | :size => 2) %> 7 |
    8 | -------------------------------------------------------------------------------- /app/controllers/easy_agile_common_controller.rb: -------------------------------------------------------------------------------- 1 | class EasyAgileCommonController < ApplicationController 2 | layout 'ea_base' 3 | helper :easy_agile 4 | 5 | alias_method :tab_name, :controller_name 6 | 7 | def controller_name 8 | "easy_agile" 9 | end 10 | 11 | # show tabs in a layout? 12 | def has_tabs? 13 | true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/stories/backlog_guidance.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@project} Backlog" %> 2 | <% content_for :h2, h("#{@project} Backlog") %> 3 | <% body_classes << 'guidance' %> 4 | 5 |

    <%= l(:empty_backlog) %>

    6 |

    7 | <%= l(:new_story_in_backlog) %> 8 | <%= link_to l(:new_story_accusative), new_project_story_path(@project) %>. 9 |

    10 | -------------------------------------------------------------------------------- /app/views/stories/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{l(:story_plural)} #{@project}" %> 2 | <% content_for :h2, h("#{l(:story_plural)} #{@project}") %> 3 | <% unless @project.stories.empty? %> 4 |
      5 | <% @project.stories.each do |story| %> 6 |
    1. <%= link_to h(story), [@project, story] %>
    2. 7 | <% end %> 8 |
    9 | <% end %> 10 | -------------------------------------------------------------------------------- /db/migrate/20090325133236_create_story_team_members.rb: -------------------------------------------------------------------------------- 1 | class CreateStoryTeamMembers < ActiveRecord::Migration 2 | def self.up 3 | create_table :story_team_members do |t| 4 | t.integer :user_id 5 | t.integer :story_id 6 | 7 | t.timestamps 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :story_team_members 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/acceptance_criteria/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, @acceptance_criterion %> 2 | <% content_for :h2, h(@acceptance_criterion) %> 3 | 4 | <% form_for [@project, @story, @acceptance_criterion] do |f| %> 5 |

    6 | <%= f.label :criterion %> 7 | <%= f.text_field :criterion, :class => 'auto_focus' %> 8 | <%= f.submit 'Update' %> 9 |

    10 | <% end %> 11 | 12 | -------------------------------------------------------------------------------- /db/migrate/20090310175005_create_burndown_data_points.rb: -------------------------------------------------------------------------------- 1 | class CreateBurndownDataPoints < ActiveRecord::Migration 2 | def self.up 3 | create_table :burndown_data_points do |t| 4 | t.integer :iteration_id 5 | t.integer :story_points 6 | t.date :date 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :burndown_data_points 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/easy_agile/show_guidance.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@project} | Projects" %> 2 | <% content_for :h2, h(@project) %> 3 | <% body_classes << 'guidance' %> 4 | 5 | <%= simple_format h(@project.description) %> 6 | 7 |

    <%= l(:no_stories) %>

    8 |

    9 | <%= l(:you_can_start_by_new_story) %> <%= link_to l(:creating_new_story), [:new, @project, :story] %>. 10 |

    11 | -------------------------------------------------------------------------------- /db/migrate/20090327110522_create_story_actions.rb: -------------------------------------------------------------------------------- 1 | class CreateStoryActions < ActiveRecord::Migration 2 | def self.up 3 | create_table :story_actions do |t| 4 | t.integer :user_id 5 | t.integer :story_id 6 | t.integer :iteration_id 7 | 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :story_actions 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/stories/_prioritising_story.erb: -------------------------------------------------------------------------------- 1 |

    2 | <%= label_tag "priorities_#{prioritising_story.id}", 'Priority' %> 3 | <%= text_field_tag("project[priorities[#{prioritising_story.id}]]", 4 | prioritising_story.priority, 5 | :id => "priorities_#{prioritising_story.id}", 6 | :class => 'numeric', 7 | :size => 2) %> 8 |

    9 | -------------------------------------------------------------------------------- /app/views/iterations/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, l(:planning_iteration) + " #{@iteration}" %> 2 | <% content_for :h2, l(:planning_iteration) + " #{h(@iteration)}" %> 3 | <% body_classes << 'iteration_planning' %> 4 | 5 | <% form_for [@project, @iteration] do |form| %> 6 | <%= render form %> 7 |

    8 | <%= form.submit l(:update_iteration) %> 9 |

    10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/iterations/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, l(:planning_iteration_for) + " #{@project}" %> 2 | <% content_for :h2, l(:planning_iteration_for) + " #{h(@project)}" %> 3 | <% body_classes << 'iteration_planning' %> 4 | 5 | <% form_for [@project, @iteration] do |form| %> 6 | <%= render form %> 7 |

    8 | <%= form.submit l(:create_iteration) %> 9 |

    10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/iterations/new_guidance.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, l(:new_iteration_for) + " #{@project}" %> 2 | <% content_for :h2, l(:no_stories_for) + " #{h(@project)}" %> 3 | <% body_classes << 'guidance' %> 4 | 5 |

    6 | <%= l(:no_stories_no_iteration) %> 7 |

    8 |

    9 | <%= l(:you_might_want) %> <%= link_to l(:create_story_for_project), new_project_story_path(@project) %>. 10 |

    11 | -------------------------------------------------------------------------------- /db/migrate/20090304193819_create_acceptance_criteria.rb: -------------------------------------------------------------------------------- 1 | class CreateAcceptanceCriteria < ActiveRecord::Migration 2 | def self.up 3 | create_table :acceptance_criteria do |t| 4 | t.integer :story_id 5 | t.string :criterion 6 | t.datetime :fulfilled_at 7 | 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :acceptance_criteria 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/stories_helper.rb: -------------------------------------------------------------------------------- 1 | module StoriesHelper 2 | 3 | def story_classes(story) 4 | "story #{story.status} #{story.team_members.empty? ? '' : 'with_team'}" 5 | end 6 | 7 | def story_breadcrumbs(story) 8 | crumbs = [ link_to(h(@project), @project) ] 9 | 10 | if @story.iteration_id? 11 | crumbs << link_to(@story.iteration, [@project, @story.iteration]) 12 | end 13 | 14 | crumbs 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration_active/typography.css: -------------------------------------------------------------------------------- 1 | div.iteration_active .javascript #stories .guidance p { max-width:none; margin:1em 0px } 2 | div.iteration_active .javascript #stories .guidance, 3 | div.iteration_active .javascript #burndown, 4 | div.iteration_active .javascript #headings ol li { text-align:center } 5 | div.iteration_active h1 { margin-bottom:0 } 6 | div.iteration_active #content .story li { font-size:93%; padding:2px 5px 1px } 7 | -------------------------------------------------------------------------------- /lib/my_controller_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'my_controller' 2 | 3 | module MyControllerPatch 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | base.send(:include, InstanceMethods) 7 | 8 | base.class_eval do 9 | helper :stories 10 | end 11 | end 12 | 13 | module ClassMethods 14 | end 15 | 16 | module InstanceMethods 17 | end 18 | end 19 | 20 | MyController.send(:include, MyControllerPatch) 21 | 22 | -------------------------------------------------------------------------------- /db/migrate/20090303213012_create_iterations.rb: -------------------------------------------------------------------------------- 1 | class CreateIterations < ActiveRecord::Migration 2 | def self.up 3 | create_table :iterations do |t| 4 | t.integer :project_id 5 | t.string :name 6 | t.integer :duration 7 | t.integer :initial_estimate 8 | t.date :start_date 9 | t.date :end_date 10 | 11 | t.timestamps 12 | end 13 | end 14 | 15 | def self.down 16 | drop_table :iterations 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration_planning/colours.css: -------------------------------------------------------------------------------- 1 | div.iteration_planning .include a{ background-repeat:no-repeat } 2 | div.iteration_planning #stories_available .include a { background-image:url(../../images/arrow_large_left_highlighted.png) } 3 | div.iteration_planning #stories_iteration .include a { background-image:url(../../images/arrow_large_right_highlighted.png) } 4 | div.iteration_planning .display .content { background:#fff } 5 | div.iteration_planning .story .estimate { border:none } 6 | -------------------------------------------------------------------------------- /app/models/story_team_member.rb: -------------------------------------------------------------------------------- 1 | class StoryTeamMember < ActiveRecord::Base 2 | attr_protected :user_id 3 | belongs_to :user 4 | belongs_to :story 5 | 6 | validates_presence_of :user_id, :story_id 7 | 8 | validates_uniqueness_of :story_id, :scope => :user_id, 9 | :message => 'is already assigned to you' 10 | 11 | def validate 12 | if story && !user.projects.include?(story.project) 13 | errors.add(:story, "must belong to one of your projects") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20090304004418_create_stories.rb: -------------------------------------------------------------------------------- 1 | class CreateStories < ActiveRecord::Migration 2 | def self.up 3 | create_table :stories do |t| 4 | t.integer :project_id 5 | t.integer :iteration_id 6 | t.string :name 7 | t.text :content 8 | t.integer :estimate 9 | t.string :status, :default => 'pending' 10 | t.integer :priority, :default => 1 11 | 12 | t.timestamps 13 | end 14 | end 15 | 16 | def self.down 17 | drop_table :stories 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/stories/new_with_iteration.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, l(:new_story) %> 2 | <% content_for :h2, l(:new_story) %> 3 | <% form_for [@project, @story] do |form| %> 4 | <%= form.error_messages %> 5 |

    6 | <%= form.check_box :iteration_id, {}, @iteration.id, nil %> 7 | <%= form.label :iteration_id, l(:add_to_iteration) + " '#{h(@iteration)}'" %> 8 |

    9 | <%= render form %> 10 |

    11 | <%= form.submit l(:create_story) %> 12 |

    13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/controllers/active_iterations_controller.rb: -------------------------------------------------------------------------------- 1 | class ActiveIterationsController < ApplicationController 2 | 3 | helper :stories 4 | before_filter :get_iteration 5 | 6 | def create 7 | if @iteration.start 8 | redirect_to [@iteration.project, @iteration] 9 | else 10 | @project = @iteration.project 11 | render :template => 'iterations/show' 12 | end 13 | end 14 | 15 | protected 16 | 17 | def get_iteration 18 | @iteration = Iteration.find(params[:iteration_id]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/views/acceptance_criteria/edit.js.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% form_for [@project, @story, @acceptance_criterion] do |f| %> 4 |

    5 | <%= 6 | f.text_field :criterion, 7 | :id => 'edit_acceptance_criterion_criterion', 8 | :class => 'auto_focus' 9 | %> 10 | <%= f.submit l(:button_update) %> 11 |

    12 | <% end %> 13 | 14 | 15 | <%= button_to l(:button_delete), [@project, @story, @acceptance_criterion], 16 | :method => :delete %> 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/views/iterations/_form.erb: -------------------------------------------------------------------------------- 1 |

    <%= l(:iteration_plan_guidance) %>

    2 | <%= form.error_messages %> 3 |

    4 | <%= form.label :name %> 5 | <%= form.text_field :name %> 6 |

    7 |

    8 | <%= form.label :duration, l(:duration_days) %> 9 | <%= form.text_field :duration, :class => 'numeric', :size => 2 %> 10 |

    11 | 12 |
    13 |
    14 |

    <%= l(:backlog) %>

    15 |
      16 | <%= render @stories %> 17 |
    18 |
    19 |
    20 | -------------------------------------------------------------------------------- /app/controllers/burndowns_controller.rb: -------------------------------------------------------------------------------- 1 | class BurndownsController < ApplicationController 2 | before_filter :get_iteration 3 | before_filter :get_burndown 4 | 5 | def show 6 | send_data( 7 | @burndown.to_png, 8 | :disposition => 'inline', 9 | :type => 'image/png', 10 | :filename => "#{@iteration.name} Burndown.png" 11 | ) 12 | end 13 | 14 | protected 15 | 16 | def get_iteration 17 | @iteration = Iteration.find(params[:iteration_id]) 18 | end 19 | 20 | def get_burndown 21 | @burndown = @iteration.burndown(params[:width]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/iterations/_iteration.erb: -------------------------------------------------------------------------------- 1 | <% 2 | width ||= 300 3 | ratio = 600 / width 4 | height = 450 / ratio 5 | %> 6 | 7 |
  • 8 | <%= 9 | image = iteration.pending? ? 'burndown.png' : project_iteration_burndown_path(iteration.project, iteration, :width => width) 10 | options = {:width => width, :height => height, :alt => l(:burndown_for) + " #{h iteration.name}"} 11 | options[:plugin] = 'easy_agile' if iteration.pending? 12 | link_to(image_tag(image, options), [iteration.project, iteration]) %> 13 | <%= link_to h(iteration), [iteration.project, iteration] %> 14 |
  • 15 | -------------------------------------------------------------------------------- /assets/javascripts/backlog_prioritisation.js: -------------------------------------------------------------------------------- 1 | var BacklogPrioritisation = { 2 | init: function() { 3 | // enable sorting 4 | $('ol.stories').sortable({ 5 | axis: 'y', 6 | stop: function(event, ui) { 7 | var ids = $(this).sortable('toArray'); 8 | $(ids).each( function(i) { 9 | var priority = i + 1; 10 | var field = $('#' + this + ' input[type="text"]'); 11 | 12 | // set new field value 13 | $(field).val(priority); 14 | }); 15 | 16 | // submit form 17 | $('form.edit_project').ajaxSubmit(); 18 | } 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /assets/javascripts/flash.js: -------------------------------------------------------------------------------- 1 | function Flash(messages) { 2 | this.messages = messages; 3 | this.draw(); 4 | } 5 | 6 | Flash.prototype = { 7 | draw: function() { 8 | var insert_after, actions; 9 | actions = $('#actions'); 10 | insert_after = actions[0] ? actions : $('h1'); 11 | 12 | // remove previous messages 13 | this.remove(); 14 | 15 | insert_after.after('
    ') 16 | if (this.messages.notice && $.trim(this.messages.notice) != '') { 17 | $('.flash').append('

    Notice

    '+this.messages.notice+'

    '); 18 | } 19 | }, 20 | 21 | remove: function() { 22 | $('.flash').remove(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/locales/numbers.yml: -------------------------------------------------------------------------------- 1 | en: 2 | number: 3 | format: 4 | separator: "." 5 | delimiter: "," 6 | precision: 2 7 | 8 | currency: 9 | format: 10 | format: "%u%n" 11 | unit: "£" 12 | precision: 0 13 | date: 14 | abbr_day_names: [Sun, Mon, Tues, Wed, Thurs, Fri, Sat] 15 | day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] 16 | abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] 17 | month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] 18 | formats: 19 | default: "%A %d %B %Y" 20 | month: "%B %Y" 21 | -------------------------------------------------------------------------------- /assets/stylesheets/application/colours.css: -------------------------------------------------------------------------------- 1 | 2 | #actions a { color:#444; display:block; background:url(../../images/button_background.png) no-repeat } 3 | 4 | div.section, div.section h3 { border:2px solid #e4e5f3 } 5 | div.section .errorExplanation h3 { border: none } 6 | div.section h3 { background:#fff } 7 | 8 | #headings .complete { background:#3dfd4f } 9 | #acceptance_criteria #completed, .aside { color: #aaa } 10 | 11 | /* /\* home/show *\/ */ 12 | div#home_show #recent_work .section>ol>li { background:#e4e5f3; 13 | border-bottom:2px solid #fff } 14 | div#home_show #recent_work .section>ol>li:last-child { border:none } 15 | /* /\* end home/show *\/ */ 16 | 17 | -------------------------------------------------------------------------------- /lib/project_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'project' 2 | 3 | module ProjectPatch 4 | def self.included(base) 5 | base.send(:include, InstanceMethods) 6 | 7 | base.class_eval do 8 | has_many :iterations, :dependent => :destroy 9 | has_many :stories, :dependent => :destroy 10 | has_many(:available_stories, 11 | :class_name => 'Story', 12 | :conditions => 'iteration_id IS NULL') 13 | end 14 | end 15 | 16 | module InstanceMethods 17 | 18 | def priorities=(priorities) 19 | priorities.each_pair do |id, priority| 20 | stories.update(id, :priority => priority) 21 | end 22 | end 23 | 24 | end 25 | end 26 | 27 | Project.send(:include, ProjectPatch) 28 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration_active/colours.css: -------------------------------------------------------------------------------- 1 | div.iteration_active #burndown p { background:#fff } 2 | div.iteration_active .draggables .ui-droppable { background:#fff; border-color:#fff } 3 | 4 | div.iteration_active #headings .pending { background:#ddd } 5 | div.iteration_active #headings .pending { border-color:#ddd } 6 | div.iteration_active #headings .in_progress { background:#3dc0fd } 7 | div.iteration_active #headings .in_progress { border-color:#3dc0fd } 8 | div.iteration_active #headings .testing { background:#fde03d } 9 | div.iteration_active #headings .testing { border-color:#fde03d } 10 | div.iteration_active #headings .complete { background:#3dfd4f } 11 | div.iteration_active #headings .complete { border-color:#3dfd4f } 12 | 13 | -------------------------------------------------------------------------------- /app/views/stories/backlog.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@project} #{l(:backlog)}" %> 2 | <% content_for :h2, h("#{@project} #{l(:backlog)}") %> 3 | 4 | <% form_for :project, @project, :url => { :action => 'edit' } do |f| %> 5 | 6 |

    <%= l(:backlog_drag_stories) %>

    7 |
      8 | <%= render @project.stories.backlog %> 9 |
    10 |

    11 | <%= f.submit 'Save Priorities' %> 12 |

    13 | <% end %> 14 | 15 |
    16 |

    <%= l(:next_steps) %>

    17 | 25 |
    26 | -------------------------------------------------------------------------------- /assets/stylesheets/story/colours.css: -------------------------------------------------------------------------------- 1 | .draggables .pending .story_header { border-color:#ddd } 2 | .draggables .in_progress .story_header { border-color:#3dc0fd } 3 | .draggables .testing .story_header { border-color:#fde03d } 4 | .draggables .complete .story_header { border-color:#3dfd4f } 5 | .draggables .story { cursor:pointer } 6 | 7 | .story .story_content { border-color:#aaa } 8 | .story, 9 | .story h4 { background:#fff } 10 | .story h4 a, .story h4 { color:#333 } 11 | .story .less_more a { outline:none } 12 | 13 | .story .story_header { border-bottom:2px solid #99f } 14 | .story ol li{ border-bottom:1px solid #ccf } 15 | .story ol li:last-child { border-bottom:none } 16 | 17 | .story .story_content p.estimate { background:#eee; border-color:#ddd } 18 | 19 | -------------------------------------------------------------------------------- /app/views/acceptance_criteria/_list.erb: -------------------------------------------------------------------------------- 1 | <% if @story.acceptance_criteria.empty? %> 2 |

    3 | <%= l(:story_no_acceptance_criteria) %> 4 |

    5 | <% else %> 6 |
    7 | <% unless @story.acceptance_criteria.uncompleted.empty? %> 8 | 9 | <%= render(:partial => 'acceptance_criteria/criterion', 10 | :collection => @story.acceptance_criteria.uncompleted) %> 11 |
    12 | <% end %> 13 |
    14 | 15 |
    16 | <% unless @story.acceptance_criteria.completed.empty? %> 17 | 18 | <%= render(:partial => 'acceptance_criteria/criterion', 19 | :collection => @story.acceptance_criteria.completed) %> 20 |
    21 | <% end %> 22 |
    23 | <% end %> 24 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | map.with_options :name_prefix => 'project_', :path_prefix => 'projects/:project_id' do |project| 3 | project.resource :easy_agile, :controller => 'easy_agile', :member => { :my_page => :get } 4 | 5 | project.resources :iterations, :collection => { :finished => :get, :planned => :get } do |iteration| 6 | iteration.resource :burndown 7 | iteration.resource :active_iteration 8 | iteration.resources :stories 9 | end 10 | 11 | project.resources :stories, :member => { :estimate => :get }, 12 | :collection => { :backlog => :get, :finished => :get } do |story| 13 | story.resources :acceptance_criteria 14 | end 15 | 16 | project.resources :story_team_members 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/views/acceptance_criteria/_criterion.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% form_for [@project, @story, criterion] do |f| -%> 4 |

    5 | <%= hidden_field_tag "acceptance_criterion[complete]", 6 | ! criterion.complete?, :id => "complete_#{criterion.id}" %> 7 | <%= f.submit criterion.complete? ? l(:uncomplete) : l(:complete), :id => nil, :class => 'submit_complete' %> 8 |

    9 | <% end -%> 10 | 11 | <%= h criterion %> 12 | <%= link_to l(:button_edit), 13 | edit_project_story_acceptance_criterion_url(@project, @story, criterion) 14 | %> 15 | <%= button_to l(:button_delete), [@project, @story, criterion], 16 | :method => :delete, :class => "delete" %> 17 | 18 | -------------------------------------------------------------------------------- /app/models/acceptance_criterion.rb: -------------------------------------------------------------------------------- 1 | class AcceptanceCriterion < ActiveRecord::Base 2 | belongs_to :story 3 | 4 | validates_presence_of :criterion, :story_id 5 | 6 | validates_uniqueness_of :criterion, :scope => :story_id, 7 | :message => 'already assigned to story' 8 | 9 | named_scope :completed, { :conditions => 'fulfilled_at IS NOT NULL' } 10 | named_scope :uncompleted, { :conditions => 'fulfilled_at IS NULL' } 11 | 12 | def to_s 13 | criterion || "New Acceptance Criterion" 14 | end 15 | 16 | def complete? 17 | ! fulfilled_at.nil? 18 | end 19 | alias :complete :complete? 20 | 21 | def complete=(value) 22 | if value.kind_of?(TrueClass) || value == "true" 23 | self.fulfilled_at ||= Time.now 24 | else 25 | self.fulfilled_at = nil 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /config/locales/activerecord.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | attributes: 4 | story: 5 | content: "Story content" 6 | errors: 7 | messages: 8 | invalid: "is invalid" 9 | not_routable: "does not appear to exist" 10 | taken: "has been taken" 11 | blank: "is blank" 12 | greater_than_or_equal_to: "must be at least {{count}}" 13 | not_a_number: "must be a number" 14 | expired: "has passed" 15 | models: 16 | iteration: 17 | attributes: 18 | stories: 19 | not_estimated: "must have some story points (estimations). You should re-plan this iteration and add story points." 20 | template: 21 | header: 22 | one: "Error" 23 | other: "{{count}} errors" 24 | body: "" 25 | -------------------------------------------------------------------------------- /app/views/my/blocks/_active_work.erb: -------------------------------------------------------------------------------- 1 | <% 2 | width ||= 300 3 | ratio = 600 / width 4 | height = 450 / ratio 5 | 6 | iteration = active_work 7 | unless iteration.nil? || iteration == 0 8 | %> 9 |
  • 10 |
    11 | <%= link_to( 12 | image_tag(iteration.pending? ? 'burndown.png' : project_iteration_burndown_path(iteration.project, iteration, :width => width), 13 | :width => width, :height => height, :alt => "Burndown chart for #{h iteration.name}"), 14 | [iteration.project, iteration]) %> 15 | <%= link_to h(iteration), [iteration.project, iteration] %> 16 |
    17 | 18 |
    19 |
      20 | <%= render @user.active_stories_worked_on.select { |s| s.iteration == iteration } %> 21 |
    22 |
    23 |
  • 24 | <% 25 | end 26 | %> 27 | -------------------------------------------------------------------------------- /assets/stylesheets/landing/colours.css: -------------------------------------------------------------------------------- 1 | #container { background:url(/images/simply_agile_large.png) center top no-repeat } 2 | #header { text-indent:-20000px; 3 | background:#ff7f2a url(/images/a_simple_tool_for_effective_agile_teams.png) center 19px no-repeat } 4 | #footer, #footer a { color:#666 } 5 | #existing_users, 6 | #new_users, 7 | #new_user { background:#e5e5e5 } 8 | .flash { background:#ffbe93 } 9 | #existing_users form, 10 | #new_users p, 11 | form.new_user .container, 12 | .flash p { background:#fff } 13 | 14 | ul li { list-style-type:square } 15 | ol li { list-style-type:decimal } 16 | ul#images li { list-style-type:none } 17 | ul#images li p { color:#444 } 18 | ul#images img { border:1px solid #ccc } 19 | 20 | #main p { color:#444 } 21 | 22 | #user_story_description { } 23 | #why_not_tasks { background:#e5e5e5 url(/images/arrow_from_grey.png) no-repeat } 24 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration_active/layout.css: -------------------------------------------------------------------------------- 1 | div.iteration_active #burndown { position:fixed; right:30px; top:180px } 2 | 3 | div.iteration_active .javascript #content .story, 4 | div.iteration_active .ui-droppable { height:10em } 5 | 6 | div.iteration_active #headings { overflow:hidden; margin-bottom:9px } 7 | div.iteration_active #headings ol { width:100% } 8 | div.iteration_active #headings li { height:2em; line-height:2em } 9 | 10 | div.iteration_active #headings li, 11 | div.iteration_active .ui-droppable { float:left; border-width:1px; 12 | border-style:solid } 13 | 14 | div.iteration_active #headings li, 15 | div.iteration_active .ui-droppable { width:24.5% } 16 | 17 | div.iteration_active .draggables, 18 | .draggables .story{ overflow:visible } 19 | 20 | div.iteration_active .javascript #stories h2, 21 | div.iteration_active .javascript ol.stories { display:none } 22 | -------------------------------------------------------------------------------- /app/views/iterations/planned.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, l(:planned_iterations) %> 2 | <% content_for :h2, l(:planned_iterations) %> 3 | <% content_for :actions do %> 4 |
  • <%= link_to l(:active), project_iterations_path %>
  • 5 |
  • <%= link_to l(:finished), finished_project_iterations_path %>
  • 6 | <% end %> 7 | 8 | <% if @iterations.empty? -%> 9 |

    10 | <%= l(:page_only_planned_iterations) %> 11 |

    12 |

    13 | <%= l(:no_planned_iterations) %> 14 |

    15 | <% else -%> 16 |
    17 |
    18 |

    <%= %>

    19 |
    20 | <% width = @iterations.size == 1 ? 600 : 300 -%> 21 | <% height = @iterations.size == 1 ? 450 : 225 -%> 22 |
      23 | <%= render @iterations, :width => width %> 24 |
    25 |
    26 |
    27 |
    28 | <% end -%> 29 | -------------------------------------------------------------------------------- /app/views/iterations/finished.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, l(:finished_iterations) %> 2 | <% content_for :h2, l(:finished_iterations) %> 3 | <% content_for :actions do %> 4 |
  • <%= link_to l(:active), project_iterations_path %>
  • 5 |
  • <%= link_to l(:planned), planned_project_iterations_path %>
  • 6 | <% end %> 7 | 8 | <% if @iterations.empty? -%> 9 |

    10 | <%= l(:page_only_finished_iterations) %> 11 |

    12 |

    13 | <%= l(:no_finished_iterations) %> 14 |

    15 | <% else -%> 16 |
    17 |
    18 |

    <%= l(:finished) %>

    19 |
    20 | <% width = @iterations.size == 1 ? 600 : 300 -%> 21 | <% height = @iterations.size == 1 ? 450 : 225 -%> 22 |
      23 | <%= render @iterations, :width => width %> 24 |
    25 |
    26 |
    27 |
    28 | <% end -%> 29 | -------------------------------------------------------------------------------- /app/views/iterations/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, l(:active_iterations) %> 2 | <% content_for :h2, l(:active_iterations) %> 3 | <% content_for :actions do %> 4 |
  • <%= link_to l(:finished), finished_project_iterations_path %>
  • 5 |
  • <%= link_to l(:planned), planned_project_iterations_path %>
  • 6 | <% end %> 7 | 8 | <% if @iterations.empty? -%> 9 |

    10 | <%= l(:page_only_active_iterations) %> 11 |

    12 |

    13 | <%= l(:no_active_iterations) %> 14 |

    15 | <% else -%> 16 |
    17 |
    18 |

    <%= l(:active) %>

    19 |
    20 | <% width = @iterations.size == 1 ? 600 : 300 -%> 21 | <% height = @iterations.size == 1 ? 450 : 225 -%> 22 |
      23 | <%= render @iterations, :width => width %> 24 |
    25 |
    26 |
    27 |
    28 | <% end -%> 29 | -------------------------------------------------------------------------------- /app/views/iterations/show_active.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@iteration}" %> 2 | <% content_for :h2, h(@iteration) %> 3 | <% body_classes << 'iteration_active' %> 4 | 5 |
    6 |

    <%= l(:story_plural) %>

    7 |

    <%= l(:iteration_drag_stories) %>

    8 |
      9 | <%= render @iteration.stories %> 10 |
    11 |
    12 | 13 |
    14 | <%= image_tag(project_iteration_burndown_path(@project, @iteration, :width => 350), 15 | :alt => 'Burndown Chart', 16 | :width => 350) %> 17 |

    18 | <% if @iteration.finished? %> 19 | <%= l(:iteration_finished_on) %> <%= @iteration.end_date %> 20 | <% else %> 21 | <%= @iteration.start_date %> - <%= @iteration.end_date %>. <%= l(:iteration_days_left) %>: <%= @iteration.days_remaining.to_s %>. 22 | <% end %> 23 |

    24 |
    25 | 26 | -------------------------------------------------------------------------------- /app/models/story_action_observer.rb: -------------------------------------------------------------------------------- 1 | class StoryActionObserver < ActiveRecord::Observer 2 | observe :story_team_member, :acceptance_criterion, :story 3 | 4 | def before_save(obj) 5 | return if change_should_be_ignored?(obj) 6 | 7 | story = nil 8 | if obj.is_a? Story 9 | story = obj 10 | elsif obj.respond_to? :story 11 | story = obj.story 12 | else 13 | raise "don't know what to do with #{obj.inspect}" 14 | end 15 | 16 | user = User.current 17 | iteration_id = story.iteration_id 18 | 19 | StoryAction.find_or_create_by_user_id_and_story_id_and_iteration_id( 20 | user.id, 21 | story.id, 22 | iteration_id 23 | ) 24 | end 25 | 26 | def change_should_be_ignored?(obj) 27 | ignore = false 28 | 29 | ignore ||= obj.is_a?(AcceptanceCriterion) && 30 | ! obj.changes.keys.include?("fulfilled_at") 31 | 32 | ignore ||= obj.is_a?(Story) && 33 | ! obj.changes.keys.include?("status") 34 | 35 | ignore 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/views/iterations/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@iteration} | #{@project} #{l(:iteration_plural)}" %> 2 | <% content_for :h2, h(@iteration) %> 3 | <% content_for :actions do %> 4 |
  • <%= link_to l(:re_plan), edit_project_iteration_path(@project, @iteration) %>
  • 5 | <% end %> 6 | 7 | <% form_for(@iteration, 8 | :url => project_iteration_active_iteration_path(@project, @iteration), 9 | :html => {:method => :post}) do |f| %> 10 |

    11 | <%= f.submit l(:start_iteration) %> 12 |

    13 | <%= f.error_messages %> 14 | <% end %> 15 | 16 |
    17 |
    18 |

    <%= l(:story_plural) %>

    19 | <% if @iteration.stories.empty? %> 20 |

    <%= l(:iteration_no_stories) %>

    21 | <% else %> 22 |

    23 | <%= l(:total_story_points) %>: <%= @iteration.stories.sum(:estimate) %> 24 |

    25 |
      26 | <%= render @iteration.stories %> 27 |
    28 | <% end %> 29 |
    30 |
    31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Sphere Consulting inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /assets/stylesheets/landing/typography.css: -------------------------------------------------------------------------------- 1 | h1 { font-size:267%; float:left; margin-bottom:.5em } 2 | h2 { font-size:197% } 3 | #main h2 { margin:2em 0 .5em 0; clear:left } 4 | #main .flash h2 { margin-top:0 } 5 | #footer { font-size:77% } 6 | .errorExplanation h2 { font-size:123.1%; font-weight:bold } 7 | 8 | #content p.back { float:right; margin:0; clear:none; margin-left:3em } 9 | #content p.bottom { clear:both } 10 | #content p.back a { display:block; text-align:right } 11 | 12 | li { margin:0 0 0 3em } 13 | .errorExplanation li { margin-left:1em } 14 | 15 | #main p { clear:left; margin:1em 0 } 16 | #main .flash p { margin:0 } 17 | 18 | #content p, 19 | li { font-size:138.5% } 20 | #main p, 21 | li { line-height:1.5em; max-width:28em } 22 | 23 | p.description { font-size:108%; margin-bottom:1em } 24 | strong { font-weight:bold } 25 | ul#images li p { font-size:85%; text-align:center; font-weight:bold } 26 | 27 | #sign_up_login p { text-align:center; margin-top:.2em } 28 | 29 | p a { white-space:nowrap } 30 | 31 | #why_not_tasks { font-size:75% } 32 | #why_not_tasks h2 { margin-top:0 } 33 | 34 | #have_a_go label { width:10em; float:left; margin-top:0 } 35 | -------------------------------------------------------------------------------- /app/controllers/story_team_members_controller.rb: -------------------------------------------------------------------------------- 1 | class StoryTeamMembersController < ApplicationController 2 | before_filter :find_optional_project 3 | before_filter :new_story_team_member, :only => [:create] 4 | before_filter :get_story_team_member, :only => [:destroy] 5 | 6 | helper :stories 7 | 8 | def create 9 | if @story_team_member.save 10 | redirect_to [ 11 | @story_team_member.story.project, 12 | @story_team_member.story 13 | ] 14 | else 15 | @story = @story_team_member.story 16 | render :template => 'stories/show' 17 | end 18 | end 19 | 20 | def destroy 21 | @story_team_member.destroy 22 | redirect_to [ 23 | @story_team_member.story.project, 24 | @story_team_member.story 25 | ] 26 | end 27 | 28 | protected 29 | 30 | def new_story_team_member 31 | @story_team_member = 32 | StoryTeamMember.new(params[:story_team_member]. 33 | merge(:user => User.current)) 34 | 35 | end 36 | 37 | def get_story_team_member 38 | @story_team_member = 39 | StoryTeamMember.find(params[:id], 40 | :conditions => ['user_id = ?', User.current.id]) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/views/stories/_status_form.erb: -------------------------------------------------------------------------------- 1 |
      2 |
    1. 3 | <%= status_form.radio_button(:status, 'pending', 4 | :id => "story_status_pending_#{status_form.object.id}") %> 5 | <%= status_form.label "status_pending_#{status_form.object.id}", l(:status_pending) %> 6 |
    2. 7 |
    3. 8 | <%= status_form.radio_button(:status, 'in_progress', 9 | :id => "story_status_in_progress_#{status_form.object.id}") %> 10 | <%= status_form.label "status_in_progress_#{status_form.object.id}", l(:status_in_progress) %> 11 |
    4. 12 |
    5. 13 | <%= status_form.radio_button(:status, 'testing', 14 | :id => "story_status_testing_#{status_form.object.id}") %> 15 | <%= status_form.label "status_testing_#{status_form.object.id}", l(:status_testing) %> 16 |
    6. 17 |
    7. 18 | <%= status_form.radio_button(:status, 'complete', 19 | :id => "story_status_complete_#{status_form.object.id}") %> 20 | <%= status_form.label "status_complete_#{status_form.object.id}", l(:status_complete) %> 21 |
    8. 22 |
    23 |

    24 | <%= status_form.submit l(:update_status), :id => nil %> 25 |

    26 | -------------------------------------------------------------------------------- /assets/stylesheets/application/typography.css: -------------------------------------------------------------------------------- 1 | p { margin:1em 0 1em 0 } 2 | table p { margin:0 } 3 | table th { font-weight:bold } 4 | a:hover { text-decoration:none } 5 | 6 | .numeric { text-align:right } 7 | 8 | legend { font-weight:bold; padding-top:2em } 9 | 10 | #container { text-align:center } /* largely for ie6 centring but reused */ 11 | #content { text-align:left } 12 | 13 | .important_message { font-size:93%; font-weight:bold } 14 | 15 | #navigation { font-weight:bold; font-size:123.1%; text-align:center } 16 | #navigation a { text-decoration:none } 17 | #navigation a:hover, 18 | #navigation a:focus { text-decoration:underline } 19 | 20 | #actions a { line-height:29px; text-align:center; text-decoration:none; 21 | font-weight:bold } 22 | 23 | #footer { font-size:77% } 24 | 25 | div.section div.subsection h4 { text-align:right } 26 | 27 | /* iterations/new */ 28 | div.iterations .include a { text-indent:-20000px; outline:none; text-underline:none } 29 | div.iterations .display h3 { font-weight:bold } 30 | .javascript #request_container { text-align:left } 31 | .javascript #stories span.estimate { text-align:center } 32 | /* end iterations/new */ 33 | 34 | /* iterations/show */ 35 | div#iterations_show .javascript #stories .guidance, 36 | div#iterations_show .javascript #headings ol li { text-align:center } 37 | /* end iterations/show */ 38 | -------------------------------------------------------------------------------- /assets/stylesheets/landing/layout.css: -------------------------------------------------------------------------------- 1 | #container { text-align:center; margin:20px 0 0 0; padding-top:148px } 2 | #header { height:60px } 3 | #content { width:950px; margin:27px auto } 4 | #footer { clear:both; padding:30px 0 } 5 | #main, 6 | #new_existing, 7 | body.non_session #content { text-align:left } 8 | #main { float:left; width:100% } 9 | #public_show #main, 10 | .sessions #main, 11 | .users #main{ width:38em } 12 | #sign_up_login { float:right; margin-right:40px } 13 | ul#images, 14 | #new_existing { width:40%; float:right } 15 | ul#images { margin-top:3em } 16 | ul#images li p { margin:0 0 1.5em 0 } 17 | .flash, 18 | #existing_users, 19 | #new_users, 20 | #new_user { padding:10px 15px 15px 15px; margin-bottom:15px } 21 | #new_user form, 22 | #existing_users form, 23 | #new_users p, 24 | form.new_user .container, 25 | .flash p { padding:10px 14px } 26 | label { display:block; margin-top:.5em } 27 | 28 | p.submit { margin-top:1em } 29 | 30 | #privacy_policies_show dt { float:left; clear:left; width:10em } 31 | #privacy_policies_show dd { display:block; margin-left:10em } 32 | 33 | ul#images li { margin:0 } 34 | 35 | .flash { clear:both; margin-top:15px; width:50% } 36 | body.sessions .flash { width:auto } 37 | 38 | .important_message { display:none } 39 | 40 | #user_story_description { float:left; clear:left; width:56% } 41 | #why_not_tasks { clear:right; float:right; width:40%; overflow:hidden; padding:10px 5px 0 30px } 42 | -------------------------------------------------------------------------------- /lib/user_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'user' 2 | 3 | module UserPatch 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | base.send(:include, InstanceMethods) 7 | 8 | base.class_eval do 9 | has_many :story_team_members 10 | has_many :stories, :through => :story_team_members 11 | has_many :story_actions 12 | has_many :stories_worked_on, :through => :story_actions, :source => 'story' 13 | has_many :iterations_worked_on, :through => :story_actions, :source => 'iteration' 14 | end 15 | end 16 | 17 | module ClassMethods 18 | end 19 | 20 | module InstanceMethods 21 | def active_iterations_worked_on 22 | iterations_worked_on.active.select do |iteration| 23 | self.projects.include?(iteration.project) 24 | end.uniq 25 | end 26 | 27 | def active_stories_worked_on 28 | active_iterations = active_iterations_worked_on 29 | stories_worked_on.delete_if do |story| 30 | ! active_iterations.include?(story.iteration) 31 | end 32 | end 33 | 34 | def recently_finished_iterations_worked_on 35 | iterations_worked_on.recently_finished.select do |iteration| 36 | self.projects.include?(iteration.project) 37 | end.uniq 38 | end 39 | 40 | def active_iterations 41 | Iteration.active.find_all_by_project_id(self.projects) 42 | end 43 | end 44 | end 45 | 46 | User.send(:include, UserPatch) 47 | 48 | -------------------------------------------------------------------------------- /LICENSE.simply_agile: -------------------------------------------------------------------------------- 1 | Copyright © 2010 Andrew Bruce and John Cinnamond. All Rights Reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | 3. The name of the author may not be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY ANDREW BRUCE AND JOHN CINNAMOND "AS IS" AND ANY 17 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 22 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'redmine' 2 | require 'dispatcher' 3 | require 'project_patch' 4 | require 'user_patch' 5 | 6 | Redmine::Plugin.register :easy_agile do 7 | name 'Redmine Easy Agile plugin' 8 | author 'Sphere Consulting Inc.' 9 | description 'Simple scrum board for agile teams' 10 | version '1.0.3' 11 | url 'http://github.com/SphereConsultingInc/easy_agile' 12 | author_url 'http://sphereinc.com' 13 | 14 | project_module :easy_agile do 15 | permission :easy_agile_manage_iterations, :iterations => [:index, :new, :create, :show, :edit, :update, :planned, :finished] 16 | permission :easy_agile_view_home, :easy_agile => [:show] 17 | permission :easy_agile_manage_stories, :stories => [:index, :new, :create, :show, :edit, :update, :backlog] 18 | permission :easy_agile_manage_acceptance_criteria, :acceptance_criteria => [:create, :edit, :update, :destroy] 19 | permission :easy_agile_manage_story_team_members, :story_team_members => [:create, :destroy] 20 | end 21 | 22 | Dispatcher.to_prepare do 23 | Project.send(:include, ProjectPatch) unless Project.included_modules.include? ProjectPatch 24 | User.send(:include, UserPatch) unless User.included_modules.include? UserPatch 25 | MyController.send(:include, MyControllerPatch) unless MyController.included_modules.include? MyControllerPatch 26 | end 27 | 28 | # observer 29 | ActiveRecord::Base.observers << :acceptance_criterion_observer << :story_action_observer 30 | 31 | menu :project_menu, :easy_agile, { :controller => 'easy_agile', :action => 'show' }, :caption => 'Easy Agile', :before => :calendar, :param => :project_id 32 | 33 | # feature 34 | Mime::Type.register "text/plain", :feature 35 | 36 | # cretiria inflections 37 | ActiveSupport::Inflector.inflections do |inflect| 38 | inflect.irregular 'criterion', 'criteria' 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /app/views/stories/_story.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <% if body_classes.include?('iteration_planning') %> 3 |
    4 | <%= check_box_tag("stories[#{story.id}][include]", 5 | '1', 6 | story.iteration_id? || (@iteration && @iteration.stories.include?(story))) %> 7 | <%= label_tag "stories_#{story.id}_include", l(:include) %> 8 |
    9 | <% end %> 10 | 11 |
    12 |
    13 | <% if body_classes.include?('iteration_planning') %> 14 |

    <%= h(story) %>

    15 | <%= render :partial => 'stories/estimating_story', :object => story %> 16 | <% else %> 17 |

    <%= link_to h(story), [story.project, story] %>

    18 | <% if story.estimate? %> 19 |

    <%= l(:story_points) %>: <%= story.estimate %>

    20 | <% end %> 21 | <% end %> 22 |
    23 | <%= story_format story.content %> 24 |
    25 | 26 | <% if (!body_classes.include?('iteration_active') && 27 | !story.acceptance_criteria.empty?) %> 28 |
    29 |

    <%= l(:acceptance_criteria) %>

    30 | 35 |
    36 | <% end %> 37 | 38 | <% if body_classes.include?('iteration_active') %> 39 | <% form_for [story.project, story] do |form| %> 40 | <%= render :partial => 'stories/status_form', :object => form %> 41 | <% end %> 42 | <% end %> 43 | 44 | <% if controller.action_name == 'backlog' %> 45 | <%= render :partial => 'stories/prioritising_story', :object => story %> 46 | <% end %> 47 |
  • 48 | -------------------------------------------------------------------------------- /app/models/burndown.rb: -------------------------------------------------------------------------------- 1 | class Burndown 2 | attr_accessor :iteration 3 | attr_accessor :width 4 | 5 | DEFAULT_WIDTH = 600 6 | 7 | def initialize(iteration, options = {}) 8 | self.iteration = iteration 9 | self.width = options[:width] || DEFAULT_WIDTH 10 | self.width = 600 if width.to_i > 600 11 | end 12 | 13 | def to_png 14 | gruff = Gruff::Line.new(width.to_i) 15 | 16 | gruff.theme = { 17 | :colors => %w(grey darkorange), 18 | :marker_color => 'black', 19 | :background_colors => 'white' 20 | } 21 | 22 | gruff.data("Baseline", baseline_data) 23 | gruff.data("Actual", actual_data) 24 | 25 | gruff.minimum_value = 0 26 | gruff.y_axis_label = "Story Points" 27 | gruff.x_axis_label = "Day" 28 | gruff.labels = labels 29 | 30 | gruff.to_blob 31 | end 32 | 33 | def baseline_data 34 | points = iteration.initial_estimate 35 | duration = iteration.duration 36 | points_per_day = points.to_f / duration 37 | 38 | data = [points] 39 | iteration.duration.times do 40 | data << points -= points_per_day 41 | end 42 | data 43 | end 44 | 45 | def actual_data 46 | data = [iteration.initial_estimate] 47 | 48 | data_points = BurndownDataPoint.for_iteration(iteration).inject({}) do |data_points, point| 49 | data_points[point.date] = point.story_points 50 | data_points 51 | end 52 | 53 | today = [Date.today, iteration.end_date].min 54 | start = iteration.start_date 55 | previous_points = data.last 56 | (0...(today - start).to_i).each do |d| 57 | previous_points = data_points[start + d.days] || previous_points 58 | data << previous_points 59 | end 60 | 61 | data << iteration.story_points_remaining if today < iteration.end_date 62 | data 63 | end 64 | 65 | def labels 66 | labels = {} 67 | (1..iteration.duration).each { |v| labels[v] = v.to_s } 68 | labels 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | 3 | def javascript_includes 4 | javascript_include_tag('jquery-1.3.2.min.js', 5 | 'jquery-ui-1.7.custom.min.js', 6 | 'jquery.form.js', 7 | 'application', 8 | 'flash', 9 | 'story', 10 | 'request', 11 | 'acceptance_criteria', 12 | 'iteration_planning', 13 | 'iteration_active', 14 | 'backlog_prioritisation', 15 | :plugin => 'easy_agile') 16 | end 17 | 18 | def next_steps(&block) 19 | content = '

    Next Steps

    ' 20 | content += yield if block_given? 21 | content += '
    ' 22 | end 23 | 24 | def story_format(content) 25 | return nil if content.blank? 26 | items = content.split("\n") 27 | 28 | xml = Builder::XmlMarkup.new 29 | xml.ol do 30 | items.each do |item| 31 | xml.li do |li| 32 | li << h(item) 33 | end 34 | end 35 | end 36 | end 37 | 38 | def contextual_new_story_path 39 | if @iteration && !@iteration.new_record? && @iteration.pending? 40 | [:new, @project, @iteration, :story] 41 | elsif @project && !@project.new_record? 42 | [:new, @project, :story] 43 | else 44 | new_story_path 45 | end 46 | end 47 | 48 | def body_classes 49 | @body_classes ||= [controller.controller_name] 50 | end 51 | 52 | def render_flash 53 | return nil if flash.keys.empty? 54 | xml = Builder::XmlMarkup.new 55 | xml.div :class => 'flash' do 56 | flash.each do |type, message| 57 | next if message.blank? 58 | xml.div :class => type do 59 | xml.h2 type.to_s.titleize 60 | xml.p do |p| 61 | p << message 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /app/controllers/iterations_controller.rb: -------------------------------------------------------------------------------- 1 | class IterationsController < EasyAgileCommonController 2 | before_filter :find_optional_project 3 | before_filter :get_iterations, :only => [:index] 4 | before_filter :get_finished_iterations, :only => [:finished] 5 | before_filter :get_pending_iterations, :only => [:planned] 6 | before_filter :get_iteration, :only => [:edit, :show, :update] 7 | before_filter :new_iteration, :only => [:new, :create] 8 | before_filter :get_stories, :only => [:edit, :new] 9 | 10 | helper :stories 11 | 12 | def create 13 | @iteration.save_with_planned_stories_attributes! params[:stories] 14 | redirect_to [@project, @iteration] 15 | 16 | rescue ActiveRecord::RecordInvalid => e 17 | @stories = @iteration.planned_stories 18 | render :template => 'iterations/new' 19 | end 20 | 21 | def new 22 | if @stories.empty? 23 | render :template => 'iterations/new_guidance' 24 | end 25 | end 26 | 27 | def update 28 | @iteration.attributes = params[:iteration] 29 | @iteration.save_with_planned_stories_attributes! params[:stories] 30 | redirect_to [@project, @iteration] 31 | 32 | rescue ActiveRecord::RecordInvalid => e 33 | @stories = e.record.planned_stories 34 | render :template => 'iterations/edit' 35 | end 36 | 37 | def show 38 | if @iteration.active? 39 | render :template => 'iterations/show_active' 40 | end 41 | end 42 | 43 | protected 44 | 45 | def get_iterations 46 | @iterations = @project.iterations.active 47 | end 48 | 49 | def get_pending_iterations 50 | @iterations = @project.iterations.pending 51 | end 52 | 53 | def get_finished_iterations 54 | @iterations = @project.iterations.finished 55 | end 56 | 57 | def get_iteration 58 | @iteration = @project.iterations.find(params[:id]) 59 | end 60 | 61 | def get_stories 62 | @stories = @project.stories.assigned_or_available_for(@iteration) 63 | end 64 | 65 | def new_iteration 66 | @iteration = @project.iterations.build(params[:iteration]) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /app/views/easy_agile/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@project} | Projects" %> 2 | <% content_for :h2, l(:dashboard) %> 3 | 4 |
    5 |
    6 |

    <%= l(:backlog) %>

    7 | <% if @project.stories.backlog.empty? %> 8 |

    9 | <%= l(:project_has_no_backlog) %> 10 |

    11 | <% else %> 12 |
      13 | <%= render @project.stories.backlog.all(:limit => 10) %> 14 |
    15 | <% if @project.stories.backlog.size > 10 -%> 16 |

    17 | <%= l(:the_rest_stories) %> <%= @project.stories.backlog.size - 10 %> <%= l(:in) %> 18 | <%= link_to l(:full_backlog), 19 | backlog_project_stories_path(@project) %>. 20 |

    21 | <% end %> 22 | <% end -%> 23 |
    24 |
    25 | 26 |
    27 |
    28 |

    <%= l(:iteration_plural) %>

    29 | <% if (@project.iterations.active + @project.iterations.pending).empty? %> 30 |
    31 |

    32 | <%= l(:no_active_or_planned_iterations) %> 33 |

    34 | <% unless @project.iterations.finished.empty? %> 35 |

    36 | <%= link_to l(:finished_iterations), finished_project_iterations_path(@project) %> 37 |

    38 | <% end %> 39 |
    40 | <% else %> 41 | <% unless @project.iterations.active.empty? %> 42 |
    43 |

    <%= l(:active) %>

    44 |
      45 | <%= render @project.iterations.active, :width => 150 %> 46 |
    47 |
    48 | <% end %> 49 | <% unless @project.iterations.pending.empty? %> 50 |
    51 |

    <%= l(:planned) %>

    52 |
      53 | <%= render @project.iterations.pending, :width => 150 %> 54 |
    55 |
    56 | <% end %> 57 | <% end %> 58 |
    59 |
    60 | -------------------------------------------------------------------------------- /assets/stylesheets/iteration_planning/layout.css: -------------------------------------------------------------------------------- 1 | div.iteration_planning #stories { clear:both; margin-top:30px; 2 | overflow:hidden; /*ie6*/ height:200px } 3 | div.iteration_planning form p.checkbox { margin:15px 0 } 4 | div.iteration_planning form>#stories, 5 | div.iteration_planning div>#stories { min-height:200px; height:auto } 6 | div.iteration_planning .javascript #stories_iteration_container { float:left } 7 | div.iteration_planning .javascript #stories_available_container { float:right } 8 | div.iteration_planning .javascript #stories_available_container, 9 | div.iteration_planning .javascript #stories_iteration_container { /*ie6*/ width:47% } 10 | div.iteration_planning .javascript div>#stories_available_container, 11 | div.iteration_planning .javascript div>#stories_iteration_container { width:49% } 12 | div.iteration_planning .javascript #stories_iteration, 13 | div.iteration_planning .javascript #stories_available { width:70%; height:auto } 14 | div.iteration_planning .javascript #stories_iteration { /*ie6*/ margin-right:6% } 15 | div.iteration_planning .javascript #stories_available { /*ie6*/ margin-left:1% } 16 | div.iteration_planning .javascript div>#stories_iteration { margin-right:11% } 17 | div.iteration_planning .javascript div>#stories_available { margin-left:11% } 18 | 19 | div.iteration_planning .javascript #stories span.estimate { position:absolute; top:15px; right:0; 20 | width:140px } 21 | 22 | div.iteration_planning .javascript #stories .include input, 23 | div.iteration_planning .javascript #stories .include label { display:none } 24 | 25 | div.iteration_planning .javascript .include a { display:block; width:60px; height:42px } 26 | div.iteration_planning .javascript .include>a { position:absolute; top:0 } 27 | div.iteration_planning .javascript #stories_available .include>a { left:-79px } 28 | div.iteration_planning .javascript #stories_iteration .include>a { right:-79px } 29 | 30 | div.iteration_planning .javascript #stories .estimate { float:right } 31 | div.iteration_planning .javascript #stories .estimate label { display:none } 32 | 33 | div.iteration_planning p.submit { clear:both } 34 | -------------------------------------------------------------------------------- /assets/stylesheets/story/layout.css: -------------------------------------------------------------------------------- 1 | ol.stories{ list-style:none } 2 | body.iteration_planning ol.stories { overflow:visible } 3 | ol.stories li.story{ margin-bottom:15px } 4 | 5 | /* #content .story{ height:100%; max-width:400px; position:relative } */ 6 | #content .story{ max-width:400px; position:relative } 7 | #content .story .story_content { border-width:1px; border-style:solid } 8 | #content .story ol, .story ul{ margin:4px 0 0 0 } 9 | #content .story li{ display:block; padding:0 5px; max-width:none } 10 | 11 | #content .story .story_header { position:relative } 12 | #content .story h4 { overflow:hidden } 13 | div.iteration_active .javascript #content .story h3 { margin-right:53px } 14 | 15 | #content .story img { position:absolute; bottom:1px; right:2.4em } 16 | div.iteration_active #content .story img { right:29px } 17 | 18 | .story .story_content * { margin:0; padding:0 } 19 | 20 | /* less/more links */ 21 | div#stories_backlog #content .story .less_more, 22 | div#projects_show #backlog .story .less_more { margin-right:2.4em } 23 | #content .story .less_more { float:right; margin-top:6px; margin-right:45px; } 24 | #content .story .less_more a { display:block } 25 | #content .story .less_more a.less { margin-right:.5em; float:left } 26 | #content .story .less_more a.more { float:left } 27 | /* end less/more links */ 28 | 29 | #content .story .estimate { margin:0; position:absolute; border-style:solid; 30 | border-width:1px; padding:0 3px; top:3px; right:3px } 31 | #content .story .estimate span { display:none } 32 | .story ol{ clear:both } 33 | .story h4 { border: none } 34 | 35 | div.iteration_active .javascript form.edit_story input, 36 | div.iteration_active .javascript form.edit_story label, 37 | div.iteration_active .javascript li .content, 38 | div.iteration_active .javascript form.edit_story p.submit{ display:none } 39 | 40 | /* page-dependent default states */ 41 | #content .story .acceptance_criteria, 42 | div#stories_backlog #content .story ol, 43 | div#stories_show .story .less_more { display:none } 44 | #stories_available_container .story .less_more, #stories_iteration_container .story .less_more { margin-right:100px } 45 | /* end page-dependent default states */ 46 | 47 | 48 | -------------------------------------------------------------------------------- /assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // no JS support for shit browsers 3 | if (!$.support.boxModel) { 4 | var message = "

    Your browser does not support modern web standards. "+ 5 | "Please upgrade to a more recent browser for a better experience.

    "; 6 | 7 | if ($('.important_message')[0]) { 8 | $('.important_message div').prepend(message); 9 | } else { 10 | $('#content').after('
    '+message+'
    '); 11 | } 12 | 13 | $('.important_message') 14 | .css({position: 'absolute', textAlign: 'center', opacity:0.9}); 15 | $('.important_message div') 16 | .css({width:'auto', 17 | margin:0}); 18 | return false; 19 | } 20 | $('div.ea_container').addClass('javascript'); 21 | 22 | // highlight first erroneous field / auto focus field 23 | var first_error_field = $('.field_with_errors')[0]; 24 | if (first_error_field) first_error_field.focus(); 25 | else $('.auto_focus').focus(); 26 | 27 | // stories/show 28 | if ($('div#stories_show')) AcceptanceCriteria.init(); 29 | 30 | // iterations/new 31 | if ($('#stories_available')[0]) { 32 | // start swapper 33 | StorySwapper.init(); 34 | } 35 | 36 | // iterations/show when active 37 | if ($('div').hasClass('iteration_active')) { 38 | new DraggableStories(); 39 | // don't enhance stories 40 | } else if (!$('div#home_show')[0]) { 41 | // normal story enhancements 42 | $('#content .story').each( function() { new Story(this) }); 43 | } 44 | 45 | // backlog 46 | if ($('div#stories_backlog')[0]) { 47 | BacklogPrioritisation.init(); 48 | } 49 | 50 | }); 51 | 52 | // source: http://www.hunlock.com/blogs/Mastering_Javascript_Arrays 53 | Array.prototype.compare = function(testArr) { 54 | if (this.length != testArr.length) return false; 55 | for (var i = 0; i < testArr.length; i++) { 56 | if (this[i].compare) { 57 | if (!this[i].compare(testArr[i])) return false; 58 | } 59 | if (this[i] !== testArr[i]) return false; 60 | } 61 | return true; 62 | } 63 | 64 | // add header to AJAX requests to play nice with Rails' content negotiation 65 | jQuery.ajaxSetup({ 66 | 'beforeSend': function(xhr) { 67 | xhr.setRequestHeader("Accept", "text/javascript") 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = easy_agile 2 | 3 | Easy Agile is a Redmine[http://www.redmine.org] plugin for agile development teams. Based on the original {Simply Agile}[http://github.com/camelpunch/simply_agile]. 4 | 5 | == Install 6 | {Install Redmine}[http://www.redmine.org/wiki/redmine/RedmineInstall]. Follow the Redmine plugin installation manual[http://www.redmine.org/wiki/redmine/Plugins]. Or go this way (inside the Redmine directory). Make sure you have git installed: 7 | ./script/plugin install git@github.com:SphereConsultingInc/easy_agile.git 8 | or: 9 | cd vendor/plugins 10 | git clone git://github.com/SphereConsultingInc/easy_agile.git 11 | Start migration: 12 | rake db:migrate_plugins RAILS_ENV=production (or simply "rake db:migrate_plugins" for development mode) 13 | 14 | Install nested_layouts[https://github.com/jwigal/nested_layouts] plugin (from Redmine root directory): 15 | ./script/plugin install git://github.com/jwigal/nested_layouts.git 16 | 17 | Install ImageMagick and RMagick. See the RMagick's {Installation FAQ}[http://rmagick.rubyforge.org/install-faq.html] for details. Windows users might need to reinstall RMagick and ImageMagick in case of any problems. 18 | 19 | Install {gruffs library}[https://github.com/topfunky/gruff] (if you want your Redmine installation to be portable): 20 | gem install gruff 21 | cd vendor/plugins && gem unpack gruff 22 | or (if you want to use gruff from gems): 23 | gem install gruff 24 | and add the following line to your config/environment.rb file: 25 | config.gem "gruff" 26 | 27 | Then restart Redmine. In project's setting in Modules include 'Easy Agile' or enable it globally for all projects from Administration area. It will be available under 'Easy Agile' tab. 28 | 29 | Recent activities summary is available inside 'My Page' area. 30 | 31 | == Description 32 | Easy Agile is a simple task board which allows you to define stories and track their statuses through iteration. The application is quite straigtforward for the people familiar with the SCRUM and Agile methodology. 33 | 34 | == Credits 35 | Easy Agile based on the original {Simply Agile}[http://github.com/camelpunch/simply_agile] standalone application by {Jandaweb}[http://www.jandaweb.com/] (see LICENSE.simply_agile). 36 | 37 | === Project team 38 | * Sphere Consulting Inc Development Team 39 | 40 | Copyright (c) 2010 {Sphere Consulting Inc.}[http://www.sphereinc.com], released under the MIT license (see LICENSE). 41 | -------------------------------------------------------------------------------- /app/models/story.rb: -------------------------------------------------------------------------------- 1 | class Story < ActiveRecord::Base 2 | module Status 3 | PENDING = "pending" 4 | IN_PROGRESS = "in_progress" 5 | TESTING = "testing" 6 | COMPLETE = "complete" 7 | end 8 | 9 | attr_accessor :include 10 | belongs_to :iteration 11 | belongs_to :project 12 | 13 | has_many(:acceptance_criteria, 14 | :order => 'criterion', 15 | :dependent => :destroy) 16 | 17 | has_many :team_members, :class_name => 'StoryTeamMember' 18 | has_many :users, :through => :team_members 19 | 20 | default_scope :order => 'priority, created_at DESC' 21 | 22 | named_scope :assigned_or_available_for, lambda {|iteration| 23 | { 24 | :conditions => [ 25 | 'status = ? AND (iteration_id = ? OR iteration_id IS NULL)', 26 | 'pending', iteration.id 27 | ] 28 | } 29 | } 30 | 31 | named_scope :backlog, 32 | :conditions => ['status = ? AND iteration_id IS NULL', 'pending'] 33 | 34 | validates_presence_of :name, :content, :project_id 35 | validates_uniqueness_of :name, :scope => :project_id 36 | validates_numericality_of :estimate, 37 | :only_integer => true, 38 | :allow_nil => true, 39 | :greater_than_or_equal_to => 0, 40 | :less_than => 101 41 | 42 | def validate 43 | if iteration && project && (iteration.project_id != project_id) 44 | errors.add(:iteration_id, "does not belong to the story's project") 45 | end 46 | 47 | # iteration id changed on a story 48 | if iteration_id_changed? && !changes['iteration_id'][0].nil? 49 | errors.add(:iteration_id, "cannot be changed") 50 | end 51 | end 52 | 53 | named_scope :incomplete, :conditions => ['status != ?', 'complete'] 54 | 55 | after_update :update_iteration_points 56 | 57 | def to_s 58 | name || "New Story" 59 | end 60 | 61 | def update_status_from_acceptance_criteria 62 | return if iteration.nil? 63 | if acceptance_criteria.uncompleted.empty? 64 | if (status == Status::PENDING || status == Status::IN_PROGRESS) 65 | users.clear 66 | self.update_attributes(:status => Status::TESTING) 67 | end 68 | elsif status == Status::TESTING || status == Status::COMPLETE 69 | users.clear 70 | self.update_attributes(:status => Status::IN_PROGRESS) 71 | end 72 | end 73 | 74 | private 75 | 76 | def update_iteration_points 77 | iteration.try(:update_burndown_data_points) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /app/views/my/blocks/_easy_agile_home.erb: -------------------------------------------------------------------------------- 1 | <%= stylesheet_link_tag( 2 | 'story/layout', 3 | 'story/colours', 4 | 'story/typography', 5 | 6 | 'iteration/layout', 7 | 'iteration/typography', 8 | 'iteration/colours', 9 | 'iteration_planning/layout', 10 | 'iteration_planning/typography', 11 | 'iteration_planning/colours', 12 | 'iteration_active/layout', 13 | 'iteration_active/typography', 14 | 'iteration_active/colours', 15 | 16 | 'application/layout', 17 | 'application/colours', 18 | 'application/typography', 19 | :plugin => 'easy_agile') %> 20 |

    <%= l(:easy_agile_home) %>

    21 |
    23 |
    24 | <% unless @user.active_iterations_worked_on.empty? -%> 25 |
    26 |
    27 |

    <%= l(:recent_work) %>

    28 |
      29 | <%= render :partial => 'my/blocks/active_work', 30 | :collection => @user.active_iterations_worked_on %> 31 |
    32 |
    33 |
    34 | <% end -%> 35 | 36 | <% unless @user.recently_finished_iterations_worked_on.empty? -%> 37 |
    38 |
    39 |

    <%= l(:recently_finished_iterations) %>

    40 |
    41 |
      42 | <%= render @user.recently_finished_iterations_worked_on %> 43 |
    44 |
    45 |
    46 |
    47 | <% end -%> 48 | 49 | 50 | <% unless @user.active_iterations.empty? -%> 51 |
    52 |
    53 |

    <%= l(:active_iterations) %>

    54 |
    55 |
      56 | <%= render @user.active_iterations, :width => 300 %> 57 |
    58 |
    59 |
    60 |
    61 | <% end -%> 62 |
    63 |
    64 | -------------------------------------------------------------------------------- /assets/javascripts/request.js: -------------------------------------------------------------------------------- 1 | function Request(options) { 2 | this.url = options.url; 3 | 4 | var request = this; 5 | 6 | $.ajax({ 7 | url: this.url, 8 | success: function(html) { request.draw(html) } 9 | }); 10 | 11 | if (options.beforeClose) this.beforeClose = options.beforeClose; 12 | if (options.final) this.final = options.final; 13 | } 14 | 15 | Request.prototype = { 16 | autoFocus: function() { 17 | $('#request .auto_focus').focus(); 18 | }, 19 | 20 | bindForms: function() { 21 | var request = this; 22 | 23 | // special cases - make modular when we have > 1 24 | if ($('input#acceptance_criterion_criterion')[0]) { 25 | AcceptanceCriteria.formInit(); 26 | 27 | } else { 28 | // generic form binding 29 | $('#request form').ajaxForm({ 30 | error: function(xhr, status) { request.handleFormError(xhr, status) }, 31 | success: function(data, status) { request.handleFormSuccess(data, status) }, 32 | complete: function(xhr, status) { request.handleFormCompletion(xhr, status) } 33 | }); 34 | } 35 | }, 36 | 37 | draw: function(html) { 38 | $('#container').prepend('
    '+html+'
    '); 39 | this.createCloseLink(); 40 | this.bindForms(); 41 | this.autoFocus(); 42 | }, 43 | 44 | handleFormError: function(xhr, status) { 45 | $('#request').html(xhr.responseText); 46 | this.bindForms(); 47 | this.createCloseLink(); 48 | this.autoFocus(); 49 | }, 50 | 51 | handleFormSuccess: function(data, status) { 52 | this.close(); 53 | }, 54 | 55 | handleFormCompletion: function(xhr, status) { 56 | if (xhr.status == 201) { 57 | var loc = xhr.getResponseHeader('Location'); 58 | this.close(); 59 | new Request({ url: loc, 60 | beforeClose: this.beforeClose, 61 | final: this.final }); 62 | } 63 | }, 64 | 65 | close: function() { 66 | this.beforeClose(); 67 | $('#request_container').remove(); 68 | return false; 69 | }, 70 | 71 | createCloseLink: function() { 72 | var request = this; 73 | $('#request').prepend('Close'); 74 | 75 | if (this.final && $(this.final.selector)[0]) { 76 | $('#request_body').append(''); 77 | if (this.final.afterOpen) this.final.afterOpen(); 78 | } 79 | 80 | $('a#close_request,button#done_request').click( function() { 81 | request.close(); 82 | return false; 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/views/stories/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@story} | #{@project} " + l(:story_plural) %> 2 | <% content_for :h2, l(:story) + " ##{@story.id}" %> 3 | <% content_for :actions do %> 4 |
  • <%= link_to l(:button_edit), [:edit, @project, @story] %>
  • 5 |
  • <%= link_to l(:feature), project_story_path(@project, @story, :format => 'feature') %>
  • 6 | <% end %> 7 | 8 |
    9 |
    10 |
    11 |
    12 |

    <%= h @story %>

    13 | <% if @story.estimate? %> 14 |

    Story Points: <%= @story.estimate %>

    15 | <% end %> 16 |
    17 | <%= story_format @story.content %> 18 |
    19 |
    20 |
    21 | 22 | <% if @story.iteration && @story.iteration.active? %> 23 |
    24 |

    <%= l(:story_team) %>

    25 | <% unless @story.team_members.empty? %> 26 | 29 | <% end %> 30 | 31 | <% unless @story.users.include?(User.current) %> 32 | <% form_for [@project, StoryTeamMember.new(:story => @story)] do |f| %> 33 |

    34 | <%= f.hidden_field :story_id %> 35 | <%= f.submit l(:work_on_story) %> 36 |

    37 | <% end %> 38 | <% end %> 39 |
    40 | <% end %> 41 | 42 |
    43 |

    <%= l(:acceptance_criteria) %>

    44 | <% form_for [@project, @story, AcceptanceCriterion.new], :html => { :class => "add" } do |f| %> 45 | <%= f.error_messages %> 46 |

    47 | <%= f.label :criterion, l(:new_criterion) %> 48 | <%= f.text_field :criterion, :class => 'auto_focus', :size => 20 %> 49 | <%= f.submit l(:button_add) %> 50 |

    51 | <% end %> 52 | 53 |
    54 | <%= render :partial => 'acceptance_criteria/list' %> 55 |
    56 |
    57 | 58 |
    59 | <% if @story.iteration %> 60 |

    <%= l(:iteration) %>

    61 | 66 | <% end %> 67 | 68 | <% if @story.iteration_id.blank? || @story.iteration.pending? %> 69 |

    <%= l(:next_steps) %>

    70 | 78 | <% end %> 79 |
    80 | -------------------------------------------------------------------------------- /app/views/layouts/ea_base.html.erb: -------------------------------------------------------------------------------- 1 | <% inside_layout 'base' do -%> 2 | <% content_for :header_tags do %> 3 | <%= stylesheet_link_tag( 4 | 'story/layout', 5 | 'story/colours', 6 | 'story/typography', 7 | 8 | 'iteration/layout', 9 | 'iteration/typography', 10 | 'iteration/colours', 11 | 'iteration_planning/layout', 12 | 'iteration_planning/typography', 13 | 'iteration_planning/colours', 14 | 'iteration_active/layout', 15 | 'iteration_active/typography', 16 | 'iteration_active/colours', 17 | 18 | 'application/layout', 19 | 'application/colours', 20 | 'application/typography', 21 | :plugin => 'easy_agile') %> 22 | <% end %> 23 | 24 |
    25 | <% if controller.has_tabs? %> 26 | <% selected_tab = controller.tab_name %> 27 | 35 | <% end %> 36 |
    37 | <%= link_to l(:new_story), 38 | contextual_new_story_path, 39 | :accesskey => 'n', 40 | :id => 'contextual_new_story', 41 | :class => 'icon icon-new-task' %> 42 | <%= link_to l(:plan_iteration), 43 | new_project_iteration_path(@project), 44 | :accesskey => 'i', 45 | :id => 'contextual_new_iteration', 46 | :class => 'icon icon-plan-iteration' %> 47 | <%= link_to l(:dashboard), 48 | project_easy_agile_path(@project), 49 | :accesskey => 'd', 50 | :id => 'contextual_ea_dashboard', 51 | :class => 'icon icon-ea-dashboard' %> 52 |
    53 |
    54 | 55 |
    "> 57 |
    58 | 59 |

    <%= yield(:h2) %>

    60 | <% unless yield(:actions).blank? %> 61 | 64 | <% end %> 65 | 66 | <%= yield %> 67 |
    68 |
    69 | <% unless request.user_agent.include?('iPhone') %> 70 | <%= javascript_includes %> 71 | <% end %> 72 | <% end %> 73 | -------------------------------------------------------------------------------- /app/controllers/acceptance_criteria_controller.rb: -------------------------------------------------------------------------------- 1 | class AcceptanceCriteriaController < ApplicationController 2 | before_filter :find_optional_project 3 | before_filter :get_story 4 | before_filter :new_acceptance_criterion, :only => :create 5 | before_filter :get_acceptance_criterion, :only => [:edit, :update] 6 | 7 | layout :decide_layout 8 | 9 | def create 10 | respond_to do |format| 11 | format.html do 12 | if @acceptance_criterion.update_attributes(params[:acceptance_criterion]) 13 | redirect_to [@project, @story] 14 | else 15 | render(:template => 'stories/show') 16 | end 17 | end 18 | 19 | format.js do 20 | if @acceptance_criterion.update_attributes(params[:acceptance_criterion]) 21 | render(:partial => 'acceptance_criteria/list') 22 | else 23 | render(:text => @acceptance_criterion.errors.full_messages.join("\n"), 24 | :status => :unprocessable_entity) 25 | end 26 | end 27 | end 28 | end 29 | 30 | def update 31 | previous_story_status = @story.status 32 | previous_story_users_empty = @story.users.empty? 33 | 34 | if @acceptance_criterion.update_attributes(params[:acceptance_criterion]) 35 | @story.reload 36 | if previous_story_status != @story.status 37 | new_status = ActiveSupport::Inflector.titleize(@story.status) 38 | message = "Story status has changed to '#{new_status}'. " 39 | end 40 | 41 | if !previous_story_users_empty && @story.users.empty? 42 | message ||= '' 43 | message << "Story team members have been removed." 44 | end 45 | 46 | respond_to do |format| 47 | format.html do 48 | flash[:notice] = message 49 | redirect_to project_story_url(@story.project, @story) 50 | end 51 | 52 | format.js do 53 | render :text => message 54 | end 55 | end 56 | 57 | else 58 | render(:text => @acceptance_criterion.errors.full_messages.join("\n"), 59 | :status => :unprocessable_entity) 60 | end 61 | end 62 | 63 | def destroy 64 | @story.acceptance_criteria.find(params[:id]).destroy 65 | respond_to do |format| 66 | format.html do 67 | redirect_to [@project, @story] 68 | end 69 | 70 | format.js do 71 | render :partial => 'acceptance_criteria/list' 72 | end 73 | end 74 | end 75 | 76 | def edit 77 | respond_to do |format| 78 | format.html 79 | format.js 80 | end 81 | end 82 | 83 | protected 84 | 85 | def decide_layout 86 | layout = '' 87 | respond_to do |format| 88 | format.js do 89 | layout = 'request' 90 | end 91 | end 92 | 93 | layout 94 | end 95 | 96 | def get_story 97 | @story = @project.stories.find(params[:story_id]) 98 | end 99 | 100 | def get_acceptance_criterion 101 | @acceptance_criterion = @story.acceptance_criteria.find(params[:id]) 102 | end 103 | 104 | def new_acceptance_criterion 105 | @acceptance_criterion = @story.acceptance_criteria.build 106 | end 107 | 108 | end 109 | -------------------------------------------------------------------------------- /assets/javascripts/story.js: -------------------------------------------------------------------------------- 1 | function Story(element) { 2 | var instance = this; 3 | this.element = $(element); 4 | this.story_content = this.element.find('ol'); 5 | this.acceptance_criteria = this.element.find('.acceptance_criteria'); 6 | 7 | if (this.element.hasClass('pending')) this.status = 'pending'; 8 | if (this.element.hasClass('in_progress')) this.status = 'in_progress'; 9 | if (this.element.hasClass('testing')) this.status = 'testing'; 10 | if (this.element.hasClass('complete')) this.status = 'complete'; 11 | 12 | this.createContainer(); 13 | this.createLessAnchor(); 14 | 15 | // add 'more' link if needed 16 | if (this.acceptance_criteria[0]) { 17 | this.createMoreAnchor(); 18 | } 19 | 20 | Story.setStatus(this.element, this.status); 21 | } 22 | 23 | Story.setStatus = function(element, status) { 24 | element.removeClass('pending'); 25 | element.removeClass('in_progress'); 26 | element.removeClass('testing'); 27 | element.removeClass('complete'); 28 | element.addClass(status); 29 | 30 | // draw the little fella 31 | if (!element.hasClass('with_team')) return; 32 | 33 | var img = element.find('img').remove(); 34 | 35 | if (status == 'in_progress' || status == 'testing') { 36 | var html = ''; 37 | element.find('.story_header').append(html); 38 | } 39 | } 40 | Story.prototype = { 41 | createContainer: function() { 42 | this.element.find('.less_more').remove(); 43 | this.element.find('.story_header h4').before('
    '); 44 | this.container = this.element.find('.less_more'); 45 | }, 46 | 47 | createMoreAnchor: function() { 48 | var instance = this; 49 | 50 | this.container 51 | .append('More »') 52 | 53 | .find('a.more').click(function() { 54 | instance.acceptance_criteria.toggle(); 55 | instance.setMoreHtml(); 56 | return false; 57 | }); 58 | 59 | this.more = this.container.find('a.more'); 60 | this.setMoreHtml(); 61 | }, 62 | 63 | setMoreHtml: function() { 64 | var html; 65 | 66 | if (this.acceptance_criteria.is(':visible')) { 67 | html = '« Less' 68 | this.less.hide(); 69 | } else if (!this.story_content.is(':visible')) { 70 | this.more.hide(); 71 | } else { 72 | html = 'More »'; 73 | this.less.show(); 74 | } 75 | this.more.html(html); 76 | }, 77 | 78 | createLessAnchor: function() { 79 | var instance = this; 80 | 81 | this.container.find('a.less').remove(); 82 | this.container.append('« Less'); 83 | this.less = this.container.find('a.less'); 84 | 85 | this.less.click(function() { 86 | instance.story_content.toggle(); 87 | instance.setLessHtml(); 88 | return false; 89 | }); 90 | 91 | this.setLessHtml(); 92 | }, 93 | 94 | setLessHtml: function() { 95 | var html; 96 | 97 | if (this.story_content.is(':visible')) { 98 | html = '« Less'; 99 | if (this.more) this.more.show(); 100 | } else { 101 | html = 'More »'; 102 | if (this.more) this.more.hide(); 103 | } 104 | 105 | this.less.html(html); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /assets/javascripts/acceptance_criteria.js: -------------------------------------------------------------------------------- 1 | var AcceptanceCriteria = { 2 | init: function() { 3 | AcceptanceCriteria.formInit(); 4 | AcceptanceCriteria.createMissingTables(); 5 | AcceptanceCriteria.createCheckBoxes(); 6 | AcceptanceCriteria.bindCheckBoxes(); 7 | AcceptanceCriteria.anchorInit(); 8 | }, 9 | 10 | bindCheckBoxes: function(base) { 11 | var form, checked, checked_before_removal, tr; 12 | 13 | if (!base) base = $('body'); 14 | 15 | $(base).find('input[type=checkbox][name=acceptance_criterion[complete]]').click( function() { 16 | checked = $(this).attr('checked'); 17 | form = $(this).parents('form'); 18 | criterion = $(this).parents('tr'); 19 | 20 | if (!checked) { 21 | $(this).before(''); 22 | } 23 | 24 | form.submit(); 25 | }); 26 | }, 27 | 28 | createCheckBoxes: function() { 29 | var id, hidden, checked, name_td, name; 30 | 31 | $('input[name=acceptance_criterion[complete]]').each( function() { 32 | id = $(this).attr('id'); 33 | name_td = $(this).parents('tr').find('td.name'); 34 | name = name_td.html(); 35 | 36 | // checked status needs to be opposite of hidden field value 37 | checked = $(this).val() == 'true' ? '' : ' checked="checked"'; 38 | 39 | // add the checkbox with correct checked status 40 | $(this).before( 41 | '' 42 | ); 43 | 44 | // wrap name in a label 45 | name_td.html(''); 46 | 47 | // remove hidden field 48 | $(this).remove(); 49 | }); 50 | }, 51 | 52 | createMissingTables: function() { 53 | if (!$('#completed table')[0]) $('#completed').prepend('
    '); 54 | if (!$('#uncompleted table')[0]) $('#uncompleted').prepend('
    '); 55 | }, 56 | 57 | formInit: function(base) { 58 | if (!base) base = $('#acceptance_criteria'); 59 | $(base).find('.delete form, form.add').ajaxForm({ 60 | target: '#acceptance_criteria .story_content', 61 | resetForm: true, 62 | error: function(xhr) { alert(xhr.responseText) }, 63 | success: function() { 64 | $('input#acceptance_criterion_criterion').focus(); 65 | AcceptanceCriteria.init(); 66 | } 67 | }); 68 | }, 69 | 70 | anchorInit: function(base) { 71 | if (!base) base = $('#acceptance_criteria tr'); 72 | $(base).each( function() { 73 | var tr = this; 74 | $(tr).find('a').click( function() { 75 | $.ajax({ 76 | url: this.href, 77 | success: function(html) { 78 | // first reset the list 79 | $('tr.display').show(); 80 | $('tr.edit').remove(); 81 | 82 | // then insert new content 83 | $(tr).before(html); 84 | 85 | // select element 86 | $('input#edit_acceptance_criterion_criterion').focus(); 87 | 88 | // hide display version 89 | $(tr).hide(); 90 | 91 | AcceptanceCriteria.formInit(); 92 | } 93 | }); 94 | 95 | return false; 96 | }); 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/controllers/stories_controller.rb: -------------------------------------------------------------------------------- 1 | class StoriesController < EasyAgileCommonController 2 | before_filter :find_optional_project 3 | before_filter :get_iteration, :only => [:new, :create] 4 | before_filter :get_story, :only => [:edit, :update, :show, :estimate] 5 | before_filter :new_story, :only => [:new, :create] 6 | 7 | helper :easy_agile 8 | 9 | 10 | def backlog 11 | @has_tabs = true 12 | if @project.stories.backlog.empty? 13 | render :template => 'stories/backlog_guidance', :layout => 'ea_base' 14 | return 15 | end 16 | render :template => 'stories/backlog', :layout => 'ea_base' 17 | end 18 | 19 | def show 20 | respond_to do |format| 21 | format.html do 22 | if params[:iteration_id] 23 | redirect_to [@project, @story], :status => 301 24 | end 25 | end 26 | 27 | format.feature 28 | end 29 | end 30 | 31 | def create 32 | if @story.save 33 | respond_to do |format| 34 | format.html do 35 | redirect_to [@project, @story] 36 | end 37 | 38 | format.js do 39 | head :created, :location => project_story_url(@project, @story) 40 | end 41 | end 42 | 43 | elsif @story.iteration_id? 44 | @iteration = @story.iteration 45 | render(:status => :unprocessable_entity, 46 | :template => 'stories/new_with_iteration') 47 | else 48 | render(:status => :unprocessable_entity, 49 | :template => 'stories/new_with_project') 50 | end 51 | end 52 | 53 | def estimate 54 | @body_classes = [controller_name, 'iteration_planning'] 55 | render :partial => 'stories/story', :object => @story 56 | end 57 | 58 | def index 59 | respond_to do |format| 60 | format.html 61 | format.json do 62 | get_iteration 63 | render :json => @iteration.stories.to_json(:only => [:status, :id]) 64 | end 65 | end 66 | end 67 | 68 | def new 69 | @story.content = "As a 70 | I want 71 | So that " 72 | 73 | if @iteration 74 | render :template => 'stories/new_with_iteration' 75 | elsif @project 76 | render :template => 'stories/new_with_project' 77 | else 78 | render :template => 'stories/new_guidance' 79 | end 80 | end 81 | 82 | def update 83 | if params[:iteration_id] 84 | @story.update_attributes! params[:story] 85 | 86 | respond_to do |format| 87 | format.html do 88 | if params[:story][:status] 89 | redirect_to project_iteration_url(@project, params[:iteration_id]) 90 | else 91 | redirect_to [@project, @story] 92 | end 93 | end 94 | 95 | format.js do 96 | head :ok 97 | end 98 | end 99 | 100 | else 101 | @story.update_attributes! params[:story] 102 | redirect_to [@project, @story] 103 | end 104 | 105 | rescue ActiveRecord::RecordInvalid => e 106 | render :template => 'stories/edit' 107 | end 108 | 109 | def has_tabs? 110 | @has_tabs 111 | end 112 | 113 | protected 114 | 115 | def get_iteration 116 | if params[:iteration_id] && @project 117 | @iteration = @project.iterations.find(params[:iteration_id]) 118 | end 119 | end 120 | 121 | def get_story 122 | @story = @project.stories.find(params[:id]) 123 | end 124 | 125 | def new_story 126 | if @iteration 127 | @story = @iteration.stories.build(params[:story]) 128 | elsif @project 129 | @story = @project.stories.build(params[:story]) 130 | else 131 | @story = Story.new 132 | end 133 | end 134 | 135 | end 136 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here for Rails i18n 2 | en: 3 | iteration: "Iteration" 4 | iteration_plural: "Iterations" 5 | plan_iteration: "Plan an iteration" 6 | create_iteration: "Create Iteration" 7 | re_plan: "Re-plan" 8 | start_iteration: "Start Iteration" 9 | active: "Active" 10 | planned: "Planned" 11 | finished: "Finished" 12 | active_iterations: "Active Iterations" 13 | planned_iterations: "Planned Iterations" 14 | finished_iterations: "Finished Iterations" 15 | page_only_active_iterations: "This page only shows active iterations." 16 | no_active_iterations: "There are currently no active iterations." 17 | page_only_planned_iterations: "This page only shows planned iterations." 18 | no_planned_iterations: "There are currently no planned iterations." 19 | page_only_finished_iterations: "This page only shows finished iterations." 20 | no_finished_iterations: "There are currently no finished iterations." 21 | no_active_or_planned_iterations: "This project has no active or planned iterations." 22 | 23 | add_to_iteration: "Add to the iteration" 24 | iteration_no_stories: "This iteration does not have any stories." 25 | update_iteration: "Update Iteration" 26 | planning_iteration: "Planning iteration" 27 | planning_iteration_for: "Planning iteration for" 28 | burndown_for: "Burndown chart for" 29 | iteration_days_left: "Days left till iteration end" 30 | iteration_finished_on: "This iteration finished on " 31 | new_iteration_for: "New iteration for" 32 | no_stories_for: "No available stories for" 33 | no_stories_no_iteration: "There are no available stories for this project, so you can't plan an iteration." 34 | you_might_want: "You might want to" 35 | create_story_for_project: "create a story for this project" 36 | iteration_plan_guidance: "Iteration should have a unique name, duration from 1 to 60 days. Story can have upto 100 points." 37 | 38 | new_story: "New story" 39 | new_story_accusative: "new story" 40 | the_rest_stories: "...and the rest" 41 | no_stories: "No Stories" 42 | you_can_start_by_new_story: "You should start by" 43 | creating_new_story: "creating new story" 44 | story: "Story" 45 | story_plural: "Stories" 46 | story_points: "Story Points" 47 | total_story_points: "Total Story Points" 48 | story_team: "Story Team" 49 | create_story: "Create Story" 50 | update_story: "Update Story" 51 | work_on_story: "Work on this story" 52 | feature: "Feature" 53 | story_no_acceptance_criteria: "This story has no acceptance criteria." 54 | backlog_drag_stories: "Drag stories to change priority" 55 | iteration_drag_stories: "Drag stories to set their statuses" 56 | 57 | status_pending: "Pending" 58 | status_in_progress: "In Progress" 59 | status_testing: "Testing" 60 | status_complete: "Complete" 61 | update_status: "Update Status" 62 | 63 | backlog: "Backlog" 64 | new_story_in_backlog: "You don't have a backlog! You might want to write a" 65 | project_has_no_backlog: "This project has no backlog." 66 | full_backlog: "full backlog" 67 | empty_backlog: "Empty Backlog" 68 | 69 | easy_agile_home: "Easy Agile Home" 70 | recent_work: "Recent Work" 71 | recently_finished_iterations: "Recently finished iterations" 72 | 73 | in: "in" 74 | ends_at: "Ends at" 75 | dashboard: "Dashboard" 76 | next_steps: "Next Steps" 77 | estimate: "Estimate" 78 | editing: "Editing" 79 | include: "Include" 80 | remove_me: "Remove Me" 81 | complete: "Complete" 82 | uncomplete: "Uncomplete" 83 | duration_days: "Duration (days)" 84 | 85 | new_criterion: "New Criterion" 86 | acceptance_criteria: "Acceptance Criteria" 87 | 88 | field_duration: "Duration" 89 | field_stories: "Stories" 90 | field_criterion: "Criterion" -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | # Russian strings go here for Rails i18n Translated by @Vorona (admin@vorona.no-ip.org) This is very beta version :) 2 | ru: 3 | iteration: "Итерация" 4 | iteration_plural: "Итерации" 5 | plan_iteration: "План итерации" 6 | create_iteration: "Создать итерацию" 7 | re_plan: "Re-plan" 8 | start_iteration: "Начать итерацию" 9 | active: "Активные" 10 | planned: "Запланированные" 11 | finished: "Законченные" 12 | active_iterations: "Активные итерации" 13 | planned_iterations: "Запланированные итерации" 14 | finished_iterations: "Завершенные итерации" 15 | page_only_active_iterations: "Здесь показаны только активные итерации." 16 | no_active_iterations: "Нет активных итераций." 17 | page_only_planned_iterations: "Здесь показаны только запланированные итерации." 18 | no_planned_iterations: "Нет запланированных итераций." 19 | page_only_finished_iterations: "Здесь показаны только завершенные итерации." 20 | no_finished_iterations: "Нет завершенных итераций." 21 | no_active_or_planned_iterations: "В этом проекте нет активных или запланированных итераций." 22 | 23 | add_to_iteration: "добавить в итерацию" 24 | iteration_no_stories: "В этой игерации нет историй." 25 | update_iteration: "Обновить итерацию" 26 | planning_iteration: "Планирование итерации" 27 | planning_iteration_for: "Планирование итерации для" 28 | burndown_for: "Burndown-диаграмма для" 29 | iteration_days_left: "Дней осталось до конца итерации" 30 | iteration_finished_on: "Эта итерация завершится " 31 | new_iteration_for: "Новая итерация для" 32 | no_stories_for: "Нет доступных историй для" 33 | no_stories_no_iteration: "Нет доступных историй для этого проекта, Вы не можете спланировать итерацию." 34 | you_might_want: "Возможно вы хотели " 35 | create_story_for_project: "создать историю для этого проекта" 36 | iteration_plan_guidance: "Итерация должна иметь уникальное имя, длительность от 1 до 60 дней. Истории могут быть до 100 points." 37 | 38 | new_story: "Новая история" 39 | new_story_accusative: "новую историю" 40 | the_rest_stories: "...and the rest" 41 | no_stories: "Нет Историй" 42 | you_can_start_by_new_story: "Начните с " 43 | creating_new_story: "создания новой истории" 44 | story: "История" 45 | story_plural: "Истории" 46 | story_points: "Точки истории" 47 | total_story_points: "Всего точек историй" 48 | story_team: "Цепочка истории" 49 | create_story: "Создать Историю" 50 | update_story: "Обновить Историю" 51 | work_on_story: "Работать с этой историей" 52 | feature: "Фича" 53 | story_no_acceptance_criteria: "Эта история не имеет критерий одобрения." 54 | backlog_drag_stories: "Переместите истории, чтобы изменить приоритет." 55 | iteration_drag_stories: "Переместите истории, чтобы установить их статусы" 56 | 57 | status_pending: "Ожидание" 58 | status_in_progress: "В процессе" 59 | status_testing: "Тестирование" 60 | status_complete: "Готово" 61 | update_status: "Обновить статус" 62 | 63 | backlog: "Backlog" 64 | new_story_in_backlog: "У вас нет backlog-а! Возможно вы хотели бы написать" 65 | project_has_no_backlog: "У этого проекта нет backlog-а." 66 | full_backlog: "Полный backlog" 67 | empty_backlog: "Пустой Backlog" 68 | 69 | easy_agile_home: "Agile домашняя" 70 | recent_work: "Недавняя работа" 71 | recently_finished_iterations: "Недавно оконченные итерации" 72 | 73 | in: "в" 74 | ends_at: "Завершается" 75 | dashboard: "Панель задач" 76 | next_steps: "Следующие Шаги" 77 | estimate: "Оценка" 78 | editing: "Редавтирование" 79 | include: "Включить" 80 | remove_me: "Переместить меня" 81 | complete: "Завершить" 82 | uncomplete: "Неоконченная" 83 | duration_days: "Длительность (дней)" 84 | 85 | new_criterion: "Новый критерий" 86 | acceptance_criteria: "Критерий приемки" 87 | 88 | field_duration: "Длительность" 89 | field_stories: "Истории" 90 | field_criterion: "Критерий" -------------------------------------------------------------------------------- /config/locales/it.yml: -------------------------------------------------------------------------------- 1 | # Italiano 2 | 3 | it: 4 | iteration: "Iterazione" 5 | iteration_plural: "Iterazioni" 6 | plan_iteration: "Pianifica una iterazione" 7 | create_iteration: "Crea una iterazione" 8 | re_plan: "Pianifica di nuovo" 9 | start_iteration: "Inizia l'iterazione" 10 | active: "Attiva" 11 | planned: "Pianificata" 12 | finished: "Finita" 13 | active_iterations: "Iterazioni attive" 14 | planned_iterations: "Iterazioni pianificate" 15 | finished_iterations: "Iterazioni finite" 16 | page_only_active_iterations: "Questa pagina mostra solo le iterazioni attive." 17 | no_active_iterations: "Non ci sono al momento iterazioni attive." 18 | page_only_planned_iterations: "Questa pagina mostra solo le iterazioni pianificate." 19 | no_planned_iterations: "Non ci sono al momento iterazioni pianificate." 20 | page_only_finished_iterations: "Quest apagina mostra solo le iterazioni finite." 21 | no_finished_iterations: "Non ci sono al momento iterazioni finite." 22 | no_active_or_planned_iterations: "Il progetto non ha iterazioni attive o pianificate." 23 | 24 | add_to_iteration: "Aggiungi all'iterazione" 25 | iteration_no_stories: "Questa iterazione non ha storie." 26 | update_iteration: "Aggiorna l'iterazione" 27 | planning_iteration: "Pianifica l'iterazione." 28 | planning_iteration_for: "Pianifica l'iterazione per" 29 | burndown_for: "Burndown chart per" 30 | iteration_days_left: "Giorni alla fine dell'iterazione" 31 | iteration_finished_on: "Questa iterazione e' finita il" 32 | new_iteration_for: "Nuova iterazione per" 33 | no_stories_for: "Nessuna storia disponibile per" 34 | no_stories_no_iteration: "Non ci sono storie disponibili in questo progetto, quindi non puoi pianificare un'iterazione." 35 | you_might_want: "Forse vuoi" 36 | create_story_for_project: "creare una storia in questo progetto" 37 | iteration_plan_guidance: "Le iterazioni devono avere un nome univoco, una durata da 1 a 100 giorni. Ogni storia puIteration avere fino a 100 punti." 38 | 39 | new_story: "Nuova storia" 40 | new_story_accusative: "nuova storia" 41 | the_rest_stories: "...e tutto il resto" 42 | no_stories: "Nessuna storia" 43 | you_can_start_by_new_story: "Devi cominciare" 44 | creating_new_story: "creando una nuova storia" 45 | story: "Storia" 46 | story_plural: "Storie" 47 | story_points: "Story Points" 48 | total_story_points: "Story Points totali" 49 | story_team: "Story Team" 50 | create_story: "Crea Storia" 51 | update_story: "Aggiorna storia" 52 | work_on_story: "Lavora su questa storia" 53 | feature: "Feature" 54 | story_no_acceptance_criteria: "Questa storia non ha un criterio d'accettazione." 55 | backlog_drag_stories: "Trascina le storie per cambiarne la priorita'" 56 | iteration_drag_stories: "Trascina le storie per cambiarne lo stato." 57 | 58 | status_pending: "In attesa" 59 | status_in_progress: "In Progress" 60 | status_testing: "In test" 61 | status_complete: "Completata" 62 | update_status: "Aggiorna stato" 63 | 64 | backlog: "Backlog" 65 | new_story_in_backlog: "Non hai un backlog!. Forse vuoi scrivere" 66 | project_has_no_backlog: "Questo progetto non ha backlog." 67 | full_backlog: "tutto il backlog" 68 | empty_backlog: "Cancella Backlog" 69 | 70 | easy_agile_home: "Easy Agile Home" 71 | recent_work: "Lavoro recente" 72 | recently_finished_iterations: "Iterazioni completate di recente" 73 | 74 | in: "in" 75 | ends_at: "Finisce il" 76 | dashboard: "Dashboard" 77 | next_steps: "Prossimi passi" 78 | estimate: "Stima" 79 | editing: "Modifica" 80 | include: "Includi" 81 | remove_me: "Cancellami" 82 | complete: "Completa" 83 | uncomplete: "Non completa" 84 | duration_days: "Durata (giorni)" 85 | 86 | new_criterion: "Nuovo Criterio" 87 | acceptance_criteria: "Criterio d'accettazione" 88 | 89 | field_duration: "Durata" 90 | field_stories: "Storie" 91 | field_criterion: "Criterio" -------------------------------------------------------------------------------- /app/models/iteration.rb: -------------------------------------------------------------------------------- 1 | class Iteration < ActiveRecord::Base 2 | # for use after failed save_with_planned_stories_attributes! 3 | attr_accessor :planned_stories 4 | 5 | attr_protected :project_id 6 | belongs_to :project 7 | has_many :stories 8 | has_many :burndown_data_points 9 | validates_presence_of :name, :duration, :project_id 10 | validates_uniqueness_of :name 11 | validates_numericality_of :duration, 12 | :greater_than_or_equal_to => 1, 13 | :less_than_or_equal_to => 60, 14 | :only_integer => true 15 | 16 | named_scope :active, 17 | :conditions => 'start_date IS NOT NULL AND (end_date IS NULL OR end_date > CURRENT_DATE)' 18 | named_scope :pending, :conditions => 'start_date IS NULL' 19 | named_scope :recently_finished, 20 | :conditions => 'end_date <= CURRENT_DATE AND end_date >= CURRENT_DATE - 7' 21 | named_scope :finished, :conditions => 'end_date <= CURRENT_DATE' 22 | 23 | def validate 24 | errors.add(:stories, "must be assigned") if stories.empty? 25 | 26 | if (start_date? && !initial_estimate.nil? && initial_estimate <= 0) 27 | errors.add(:stories, :not_estimated) 28 | end 29 | end 30 | 31 | def save_with_planned_stories_attributes!(attributes) 32 | Iteration.transaction do 33 | stories.clear # no dependent => :destroy or :delete_all 34 | 35 | self.planned_stories = project.stories.find(attributes.keys) 36 | 37 | planned_stories.each do |story| 38 | story_attributes = attributes[story.id.to_s] 39 | included = story_attributes.delete('include') == '1' 40 | 41 | if included 42 | story.attributes = story_attributes 43 | self.stories << story 44 | else 45 | story.update_attributes!(story_attributes) 46 | end 47 | end 48 | 49 | save! 50 | end 51 | end 52 | 53 | def name 54 | if attributes["name"] 55 | attributes["name"] 56 | elsif project 57 | "Iteration #{project.iterations.count + 1}" 58 | end 59 | end 60 | 61 | def to_s 62 | name || 'New Iteration' 63 | end 64 | 65 | def story_points_remaining 66 | stories.incomplete.inject(0) do |sum, st| 67 | sum + st.estimate.to_i 68 | end 69 | end 70 | 71 | def start 72 | unless active? 73 | self.update_attributes( 74 | :start_date => Date.today, 75 | :initial_estimate => story_points_remaining 76 | ) 77 | end 78 | end 79 | 80 | def duration=(value) 81 | super value 82 | if value && start_date? 83 | self.end_date = start_date + value.to_i 84 | end 85 | end 86 | 87 | def start_date=(value) 88 | super value 89 | if value && duration? 90 | self.end_date = value + duration 91 | end 92 | end 93 | 94 | def days_remaining 95 | end_date - Date.today 96 | end 97 | 98 | def pending? 99 | !active? 100 | end 101 | 102 | def active? 103 | ! self.start_date.nil? 104 | end 105 | 106 | def finished? 107 | end_date? && end_date <= Date.today 108 | end 109 | 110 | def burndown(width = nil) 111 | options = {} 112 | options[:width] = width unless width.nil? 113 | Burndown.new(self, options) 114 | end 115 | 116 | def update_burndown_data_points 117 | return if ! active? || end_date <= Date.today 118 | data_point = burndown_data_points.find_by_date(Date.today) 119 | if data_point 120 | data_point.update_attributes(:story_points => story_points_remaining) 121 | else 122 | burndown_data_points.create( 123 | :date => Date.today, 124 | :story_points => story_points_remaining 125 | ) 126 | end 127 | end 128 | 129 | def self.update_burndown_data_points_for_all_active 130 | active.each do |iteration| 131 | iteration.update_burndown_data_points 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /config/locales/pt-BR.yml: -------------------------------------------------------------------------------- 1 | # English strings go here for Rails i18n 2 | pt-BR: 3 | iteration: "Iteração" 4 | iteration_plural: "Iterações" 5 | plan_iteration: "Planejar uma iteração" 6 | create_iteration: "Criar Iteração" 7 | re_plan: "Re-planejar" 8 | start_iteration: "Iniciar Iteração" 9 | active: "Ativo" 10 | planned: "Planejado" 11 | finished: "Terminado" 12 | active_iterations: "Iterações Ativas" 13 | planned_iterations: "Iterações Planejadas" 14 | finished_iterations: "Iterações Terminadas" 15 | page_only_active_iterations: "Essa página só mostra iterações ativas." 16 | no_active_iterations: "Atualmente não existem iterações ativas." 17 | page_only_planned_iterations: "Essa página só mostra iterações planejadas." 18 | no_planned_iterations: "Atualmente não existem iterações planejadas." 19 | page_only_finished_iterations: "Essa página só mostra iterações terminadas." 20 | no_finished_iterations: "Atualmente não existem iterações terminadas." 21 | no_active_or_planned_iterations: "Esse projeto não tem iterações ativas ou planejadas." 22 | 23 | add_to_iteration: "Adicionar à iteração" 24 | iteration_no_stories: "Esta iteração não tem nenhuma estória." 25 | update_iteration: "Atualizar Iteração" 26 | planning_iteration: "Planejamento de Iteração" 27 | planning_iteration_for: "Planejamento de Iteração para" 28 | burndown_for: "Gráfico Burndown para" 29 | iteration_days_left: "Dias que restam até o fim da iteração" 30 | iteration_finished_on: "Esta iteração termina em " 31 | new_iteration_for: "Nova iteração para" 32 | no_stories_for: "Sem estórias para" 33 | no_stories_no_iteration: "Atualmente não existem estórias para esse projeto, então você não pode planejar uma iteração." 34 | you_might_want: "Você pode querer" 35 | create_story_for_project: "criar uma estória para esse projeto" 36 | iteration_plan_guidance: "A Iteração deve ter um nome único, duração de 1 até 60 dias. A Estória pode ter mais de 100 pontos." 37 | 38 | new_story: "Nova estória" 39 | new_story_accusative: "nova estória" 40 | the_rest_stories: "...e o resto" 41 | no_stories: "Sem estórias" 42 | you_can_start_by_new_story: "Você deve começar por" 43 | creating_new_story: "criando uma nova estória" 44 | story: "Estória" 45 | story_plural: "Estórias" 46 | story_points: "Pontos da Estória" 47 | total_story_points: "Total de Pontos da Estória" 48 | story_team: "Time da Estória" 49 | create_story: "Criar Estória" 50 | update_story: "Atualizar Estória" 51 | work_on_story: "Trabalhar nessa estória" 52 | feature: "Feature" 53 | story_no_acceptance_criteria: "Esta história não tem critérios de aceitação." 54 | backlog_drag_stories: "Arraste as estórias para alterar a prioridade" 55 | iteration_drag_stories: "Arraste as estórias para definir seu status" 56 | 57 | status_pending: "Pendente" 58 | status_in_progress: "Em Andamento" 59 | status_testing: "Testando" 60 | status_complete: "Completo" 61 | update_status: "Status da Atualização" 62 | 63 | backlog: "Backlog" 64 | new_story_in_backlog: "Você não tem um backlog! Você pode querer escrever um" 65 | project_has_no_backlog: "Este projeto não tem backlog." 66 | full_backlog: "backlog cheio" 67 | empty_backlog: "Backlog Vazio" 68 | 69 | easy_agile_home: "Easy Agile Início" 70 | recent_work: "Trabalhos Recentes" 71 | recently_finished_iterations: "Iterações terminadas recentemente" 72 | 73 | in: "em" 74 | ends_at: "Termina em" 75 | dashboard: "Painel de Controle" 76 | next_steps: "Próximos Passos" 77 | estimate: "Estimar" 78 | editing: "Edição" 79 | include: "Incluir" 80 | remove_me: "Remova-me" 81 | complete: "Completo" 82 | uncomplete: "Incompleto" 83 | duration_days: "Duração (dias)" 84 | 85 | new_criterion: "Novo Critério" 86 | acceptance_criteria: "Critério de Aceitação" 87 | 88 | field_duration: "Duração" 89 | field_stories: "Estórias" 90 | field_criterion: "Critério" 91 | -------------------------------------------------------------------------------- /assets/javascripts/iteration_planning.js: -------------------------------------------------------------------------------- 1 | var StorySwapper = { 2 | init: function() { 3 | // create an iteration stories div 4 | $('#stories_available') 5 | .before('
    Story Points (0)

    Iteration stories

      '); 6 | 7 | // wrap the available div 8 | var available_div = $('#stories_available').remove(); 9 | $('#stories_iteration_container').after('
      Story Points (0)'+available_div.html()+'
      '); 10 | 11 | StorySwapper.initStories(); 12 | StorySwapper.convertCheckBoxes(); 13 | StorySwapper.bindEstimates(); 14 | StorySwapper.updateEstimates(); 15 | }, 16 | 17 | // add the story to the specified ol, maintaining the original order 18 | appendStory: function(story, ol) { 19 | var stories = ol.find('li.story'); 20 | 21 | var source_index = $.inArray(story.attr('id'), StorySwapper.story_order); 22 | 23 | var inserted = false; 24 | 25 | stories.each( function() { 26 | destination_index = $.inArray(this.id, StorySwapper.story_order); 27 | 28 | var above = source_index <= destination_index; 29 | 30 | if (above) { 31 | $(this).before(story); 32 | inserted = true; 33 | return false; // break 34 | } 35 | }); 36 | 37 | if (!inserted) { 38 | ol.append(story); 39 | } 40 | }, 41 | 42 | convertCheckBoxes: function() { 43 | $('input[type="checkbox"]').each( function() { 44 | // make links that toggle the checkboxes 45 | $(this).before('Move'); 46 | }); 47 | 48 | StorySwapper.bindAnchors(); 49 | }, 50 | 51 | bindAnchors: function() { 52 | $('a.move').click( function() { 53 | var id = this.href.split('#')[1]; 54 | var input = $('input#'+id); 55 | input.click(); 56 | StorySwapper.moveCheckBoxStory(input); 57 | StorySwapper.bindAnchors(); 58 | return false; 59 | }); 60 | }, 61 | 62 | initStories: function() { 63 | // store initial order 64 | StorySwapper.story_order = $.map($('ol li.story'), function(element, i) { 65 | return element.id 66 | }); 67 | 68 | // move stories to correct ols - order is maintained without doing anything 69 | // at this stage 70 | $('ol li.story').each( function() { 71 | if ($(this).find('input[checked]:checked')[0]) { 72 | var story = $(this).remove(); 73 | $('#stories_iteration>ol').append(story); 74 | } 75 | }); 76 | }, 77 | 78 | moveCheckBoxStory: function(checkbox) { 79 | var checked_before_removal = checkbox.attr('checked'); 80 | var story = checkbox.parents('li.story').remove(); 81 | 82 | var append_to = checked_before_removal 83 | ? $('#stories_iteration>ol') 84 | : $('#stories_available>ol'); 85 | 86 | StorySwapper.appendStory(story, append_to); 87 | 88 | // workaround ie6 bug with checkbox values being reset after append 89 | if (checked_before_removal != checkbox.attr('checked')) checkbox.click(); 90 | 91 | StorySwapper.updateEstimates(); 92 | StorySwapper.bindEstimates(); 93 | 94 | new Story(story); 95 | }, 96 | 97 | bindEstimates: function() { 98 | $('div.estimate input').keyup(StorySwapper.updateEstimates); 99 | }, 100 | 101 | updateEstimates: function() { 102 | var points; 103 | var integer; 104 | 105 | $('#stories_iteration,#stories_available').each( function() { 106 | if ($(this).find('li.story').length == 0) { 107 | $(this).find('span.estimate').hide(); 108 | } else { 109 | points = 0; 110 | $(this).find('div.estimate input').each( function() { 111 | integer = parseInt($(this).val()); 112 | if (integer) points += integer; 113 | }); 114 | $(this).find('span.estimate span.numeric').html(points); 115 | $(this).find('span.estimate').show(); 116 | } 117 | }); 118 | 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /assets/stylesheets/application/layout.css: -------------------------------------------------------------------------------- 1 | /* /\* basic layout *\/ */ 2 | /* #container { margin:0 0 0 0; position:relative } */ 3 | /* #header { width:100%; padding-bottom:5px } */ 4 | /* #contextual_new_story { position:absolute; width:118px; height:99px; */ 5 | /* right:30px; top:2px } */ 6 | /* #footer { clear:both; padding:0 0 30px 0 } */ 7 | /* #footer p { max-width:none } */ 8 | 9 | /* .important_message { position:absolute; top:0; left:0; z-index:11; width:100% } */ 10 | /* .important_message div { margin:0 auto; width:52em; padding:4px 0 } */ 11 | /* .important_message p { margin:0; max-width:none } */ 12 | 13 | /* #errorExplanation { margin-bottom:15px } */ 14 | /* /\* end basic layout *\/ */ 15 | 16 | /* /\* navigation *\/ */ 17 | /* #navigation { height:67px; position:relative; width:30.05em } */ 18 | /* #header>#navigation { margin:0 auto } */ 19 | /* #navigation ol { position:absolute; left:0; bottom:0; height:2.5em } */ 20 | /* #navigation li { float:left; height:2.5em; line-height:2.5em; width:6em } */ 21 | /* #navigation li a { display:block; width:6em } */ 22 | /* /\* end navigation *\/ */ 23 | 24 | /* javascript request */ 25 | .javascript #request_container { position:fixed; top:140px; height:100%; width:100%; z-index:11 } 26 | .javascript #request { margin:0 auto; width:40%; height:60%; 27 | min-height:340px; overflow:auto; position:relative } 28 | .javascript #request #request_body { padding:5px 15px 0 15px } 29 | .javascript #request h2 { margin-top:1em } 30 | .javascript #request h2#request_header { margin:0; padding:5px 15px } 31 | .javascript #request a#close_request { position:absolute; top:5px; right:8px } 32 | 33 | .javascript #request .edit_acceptance_criterion, 34 | .javascript #request .edit a, 35 | .javascript #request .delete form, 36 | .javascript #request #add_to_iteration, 37 | .javascript #request #next_steps { display:none } 38 | /* end javascript request */ 39 | 40 | 41 | /* content */ 42 | #actions { position:relative; float:right; width:124px; z-index:20 } 43 | #actions li { width:104px; display:block; margin:0 auto 1em auto } 44 | #actions a { width:104px; height:29px } 45 | 46 | div.section { position:relative; padding:40px 36px 22px 36px; 47 | margin:23px 0 0 11px } 48 | div.with_subsections div.section { padding:0 } 49 | form div.with_subsections div.section { padding-bottom:5px } 50 | form div.with_subsections div.section div.subsection { padding-left:15px } 51 | .section .subsection { position:relative; overflow:hidden } 52 | div.section h3 { position:absolute; z-index:10; top:-25px; padding:12px 23px; 53 | /*ie6*/ left:-50px } 54 | div.section>h3 { left:-13px } 55 | div.section .errorExplanation h3 { position: relative; padding: 0; margin: 0; 56 | top:0; left: 0 } 57 | 58 | .section_container { position:relative; overflow:hidden } 59 | .section_container a.section, 60 | .section_container ul.section_links { position:absolute; right:9px; top:6px; z-index:20 } 61 | .section_container ul.section_links li { float:left; margin-left:1em } 62 | /* end content */ 63 | 64 | /* /\* forms *\/ */ 65 | /* form p { margin:5px 0 } */ 66 | /* form p.submit { margin:15px 0 } */ 67 | /* form p label { display:block; width:10em; float:left; line-height:1.5em } */ 68 | /* form p.checkbox { overflow:hidden } */ 69 | /* form p.checkbox input { float:left } */ 70 | /* form p.checkbox label { display:inline; padding-left:5px; width:20em; */ 71 | /* line-height:1.2em } */ 72 | /* /\* end forms *\/ */ 73 | 74 | /* home/show */ 75 | .section_container.first { width:56%; max-width:472px; 76 | min-width:275px; float:left; 77 | margin-right:30px; 78 | margin-bottom:30px } 79 | div#home_show #iterations { min-width:345px } 80 | div#home_show #iterations div.active { padding-top:45px } 81 | div#home_show #recently_finished_iterations .finished ol, 82 | div#home_show #iterations ol { overflow:hidden } 83 | div#home_show #recent_work .section { padding:0 } 84 | div#home_show #recent_work .section>ol>li { padding:45px 30px 15px 30px } 85 | div#home_show #recent_work .section>ol>li, 86 | div#home_show #recent_work li div.stories { overflow:hidden } 87 | div#home_show #recent_work li div.iteration { margin-right:30px } 88 | /* /\* end home/show *\/ */ 89 | 90 | /* iterations/index */ 91 | div#iterations_index .active, 92 | div#iterations_planned .planned, 93 | div#iterations_finished .finished { padding:45px 30px 30px 30px; 94 | overflow:hidden } 95 | /* end iterations/index */ 96 | 97 | /* stories/show */ 98 | div#stories_show #users ul { width:20em } 99 | div#stories_show #users form.button-to { float:right; margin-top:-4px } 100 | div#stories_show #acceptance_criteria table { width:40%; margin-top:1em } 101 | div#stories_show #acceptance_criteria table td { padding:0 5px } 102 | div#stories_show #acceptance_criteria table td.edit, 103 | div#stories_show #acceptance_criteria table td.delete { width:4em } 104 | div#stories_show #acceptance_criteria table td.complete { width:1em } 105 | 106 | div#stories_show .javascript #acceptance_criteria table .submit_complete { display:none } 107 | div#stories_show #acceptance_criteria form { display:inline } 108 | /* end stories/show */ 109 | 110 | /* stories/backlog */ 111 | div#stories_backlog .ea_container.javascript p.submit, 112 | div#stories_backlog .javascript .story label, 113 | div#stories_backlog .javascript .story input { display:none } 114 | /* end stories/backlog */ 115 | 116 | ol.stories, ol.stories * { margin:0; padding:0 } 117 | .iterations_list, .iteration_active * { list-style:none; margin:0; padding:0 } 118 | 119 | .contextual { position:relative; z-index:50 } 120 | 121 | .col-left { float:left; width:48% } 122 | .clearer { clear:both } 123 | 124 | /* icons */ 125 | .icon-new-task { background-image: url(../../../../images/ticket.png); } 126 | .icon-plan-iteration { background-image: url(../../../../images/table_multiple.png); } 127 | .icon-ea-dashboard { background-image: url(../../../../images/news.png); } 128 | 129 | -------------------------------------------------------------------------------- /assets/javascripts/iteration_active.js: -------------------------------------------------------------------------------- 1 | function objectsFromInput(input) { 2 | this.form = $(input).parents('form'); 3 | this.container = $('#draggables_for_'+this.form.attr('id')); 4 | this.li = $(input).parents('li'); 5 | } 6 | var Burndown = { 7 | refresh: function() { 8 | var location_parts, iteration_id; 9 | location_parts = location.href.split('/'); 10 | iteration_id = location_parts[location_parts.length - 1]; 11 | project_id = location_parts[location_parts.length - 3]; 12 | $('#burndown img').attr('src', 13 | '/projects/' + project_id + 14 | '/iterations/' + iteration_id + 15 | '/burndown?width=350&' + new Date().getTime()); 16 | } 17 | } 18 | function DraggableStories() { 19 | DraggableStories.labelColumns(); 20 | DraggableStories.create(); 21 | 22 | // handle resize event 23 | $(window).resize(function() { 24 | DraggableStories.create(); 25 | DraggableStories.recently_resized = true; 26 | setTimeout('DraggableStories.recently_resized = false', 100); 27 | }); 28 | } 29 | 30 | DraggableStories.statuses = []; 31 | 32 | DraggableStories.create = function() { 33 | var full_width, container, height; 34 | 35 | if (DraggableStories.recently_resized) return false; 36 | 37 | // size div 38 | full_width = $(window).width(); 39 | if (full_width < 831) { 40 | $('#burndown').hide(); 41 | full_width += 370; 42 | } else { 43 | $('#burndown').show(); 44 | } 45 | $('#stories').width(full_width - 430); 46 | 47 | // remove all existing JSy elements 48 | $('#draggables_container').remove(); 49 | 50 | // make a container for all draggables 51 | $('ol.stories').before('
      '); 52 | 53 | container = $('#draggables_container'); 54 | 55 | // make draggable container for each form 56 | $('ol.stories form').each( function() { 57 | container.append('
      '); 58 | }); 59 | 60 | // make droppables for each radio button 61 | DraggableStories.droppables = $.map($('input[name="story[status]"]'), function(input, i) { 62 | return new DroppableStatus(input); 63 | }); 64 | 65 | DraggableStories.draggables = $.map($('input[name="story[status]"]:checked'), function(input, i) { 66 | return new DraggableStory(input); 67 | }); 68 | 69 | // set height of each row to the height of the draggable content 70 | $('.draggables').each( function() { 71 | height = $(this).find('.story .story_content').height() + 9; 72 | 73 | $(this).height(height); 74 | $(this).find('.ui-droppable').height(height); 75 | }); 76 | 77 | // set positions on each draggable 78 | $(DraggableStories.draggables).each( function() { 79 | this.setPosition(); 80 | }); 81 | 82 | DraggableStories.scheduleRefresh(); 83 | } 84 | 85 | DraggableStories.scheduleRefresh = function() { 86 | clearTimeout(DraggableStories.timeout); 87 | if (DraggableStories.refreshedOnce) { 88 | DraggableStories.timeout = setTimeout('DraggableStories.refresh()', 5000); 89 | } else { 90 | DraggableStories.timeout = setTimeout('DraggableStories.refresh()', 0); 91 | DraggableStories.refreshedOnce = true; 92 | } 93 | } 94 | 95 | DraggableStories.refresh = function() { 96 | var url, stories, statuses, droppable, draggable_story; 97 | 98 | if (DraggableStories.refresh_lock) { 99 | DraggableStories.scheduleRefresh(); 100 | return false; 101 | } 102 | 103 | url = window.location.href.split('#')[0] + '/stories.json'; 104 | 105 | $.getJSON(url, 106 | function(stories) { 107 | statuses = $.map(stories, function(obj) { 108 | return obj.status; 109 | }); 110 | if (!DraggableStories.statuses.compare(statuses)) { 111 | DraggableStories.statuses = statuses; 112 | $(stories).each( function(i) { 113 | draggable_story = DraggableStories.draggables[i]; 114 | draggable_story.story.id = this.id; 115 | draggable_story.story.status = this.status; 116 | draggable_story.setStatus(); 117 | draggable_story.setPosition(); 118 | }); 119 | Burndown.refresh(); 120 | } 121 | } 122 | ); 123 | 124 | DraggableStories.scheduleRefresh(); 125 | return false; 126 | } 127 | 128 | // make headings based on first set of labels 129 | DraggableStories.labelColumns = function() { 130 | var html = '
        '; 131 | 132 | $($('form.edit_story')[0]).find('label').each( function() { 133 | var content = $(this).html(); 134 | var label_for = $(this).attr('for'); 135 | var class_name = $('#'+label_for).val(); 136 | 137 | html += '
      1. '+content+'
      2. '; 138 | }); 139 | html += '
      '; 140 | 141 | $('ol.stories').before(html); 142 | } 143 | 144 | function DraggableStory(input) { 145 | var id, id_parts, classes, droppable, droppable_position, objects, content, acceptance_criteria, container; 146 | 147 | this.input = input; 148 | id_parts = this.input.id.split('_'); 149 | 150 | this.story = { 151 | 'id': id_parts[id_parts.length - 1], 152 | 'status': $(input).val() 153 | } 154 | 155 | objects = new objectsFromInput(input); 156 | 157 | content = objects.li.find('.story_content'); 158 | acceptance_criteria = objects.li.find('.acceptance_criteria'); 159 | container = objects.container; 160 | 161 | droppable = this.droppable(); 162 | droppable_position = droppable.position(); 163 | droppable.addClass('ui-state-highlight'); 164 | 165 | classes = 'story'; 166 | if (objects.li.hasClass('with_team')) { 167 | classes += ' with_team'; 168 | } 169 | 170 | container.append('
      '+ 173 | content.html()+ 174 | '
      '); 175 | 176 | this.element = $('#draggable_' + this.input.id); 177 | 178 | if (acceptance_criteria[0]) { 179 | this.element.find('.story_content').append('
      '+ 180 | acceptance_criteria.html()+ 181 | '
      '); 182 | } 183 | 184 | this.element.draggable({ 185 | revert: 'invalid', 186 | axis: 'x', 187 | containment: 'parent', 188 | cursor: 'pointer' 189 | }) 190 | .css('position', 'absolute') 191 | .width(droppable.width()); 192 | 193 | this.setStatus(); 194 | } 195 | DraggableStory.prototype = { 196 | droppable: function() { 197 | return $('#droppable_story_status_' + this.story.status + '_' + this.story.id); 198 | }, 199 | 200 | setPosition: function() { 201 | var droppable_position = this.droppable().position(); 202 | this.element 203 | .css('top', droppable_position.top) 204 | .css('left', droppable_position.left); 205 | }, 206 | 207 | setStatus: function() { 208 | Story.setStatus(this.element, this.story.status); 209 | } 210 | } 211 | 212 | function DroppableStatus(input) { 213 | var instance = this; 214 | this.input = input; 215 | var objects = new objectsFromInput(input); 216 | this.form = objects.form; 217 | this.container = objects.container; 218 | this.li = objects.li; 219 | this.status = $(input).val(); 220 | 221 | this.container.append('
      '); 222 | 223 | this.droppable = $('#droppable_' + input.id); 224 | this.droppable 225 | .droppable({ 226 | drop: function(ev, ui) { 227 | var id_parts = instance.input.id.split('_'); 228 | var story_id = id_parts[id_parts.length - 1]; 229 | 230 | // set the refresh lock to avoid out-of-sync updates 231 | DraggableStories.refresh_lock = true; 232 | 233 | // check the radio button 234 | $('li#story_'+story_id+' ol input').val([instance.status]); 235 | 236 | // send the request 237 | instance.form.ajaxSubmit({ 238 | success: function() { 239 | DroppableStatus.previous_statuses[story_id] = instance.status; 240 | 241 | // clear refresh lock 242 | DraggableStories.refresh_lock = false; 243 | } 244 | }); 245 | 246 | // change class of elements 247 | var draggable = instance.container.find('.ui-draggable'); 248 | Story.setStatus(draggable, instance.status); 249 | 250 | // custom snapping 251 | $(ui.draggable) 252 | .css('left', $(this).position().left) 253 | .css('top', $(this).position().top); 254 | } 255 | }); 256 | } 257 | 258 | DroppableStatus.previous_statuses = {}; 259 | -------------------------------------------------------------------------------- /assets/javascripts/jquery.form.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Form Plugin 3 | * version: 2.21 (08-FEB-2009) 4 | * @requires jQuery v1.2.2 or later 5 | * 6 | * Examples and documentation at: http://malsup.com/jquery/form/ 7 | * Dual licensed under the MIT and GPL licenses: 8 | * http://www.opensource.org/licenses/mit-license.php 9 | * http://www.gnu.org/licenses/gpl.html 10 | */ 11 | ;(function($) { 12 | 13 | /* 14 | Usage Note: 15 | ----------- 16 | Do not use both ajaxSubmit and ajaxForm on the same form. These 17 | functions are intended to be exclusive. Use ajaxSubmit if you want 18 | to bind your own submit handler to the form. For example, 19 | 20 | $(document).ready(function() { 21 | $('#myForm').bind('submit', function() { 22 | $(this).ajaxSubmit({ 23 | target: '#output' 24 | }); 25 | return false; // <-- important! 26 | }); 27 | }); 28 | 29 | Use ajaxForm when you want the plugin to manage all the event binding 30 | for you. For example, 31 | 32 | $(document).ready(function() { 33 | $('#myForm').ajaxForm({ 34 | target: '#output' 35 | }); 36 | }); 37 | 38 | When using ajaxForm, the ajaxSubmit function will be invoked for you 39 | at the appropriate time. 40 | */ 41 | 42 | /** 43 | * ajaxSubmit() provides a mechanism for immediately submitting 44 | * an HTML form using AJAX. 45 | */ 46 | $.fn.ajaxSubmit = function(options) { 47 | // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) 48 | if (!this.length) { 49 | log('ajaxSubmit: skipping submit process - no element selected'); 50 | return this; 51 | } 52 | 53 | if (typeof options == 'function') 54 | options = { success: options }; 55 | 56 | options = $.extend({ 57 | url: this.attr('action') || window.location.toString(), 58 | type: this.attr('method') || 'GET' 59 | }, options || {}); 60 | 61 | // hook for manipulating the form data before it is extracted; 62 | // convenient for use with rich editors like tinyMCE or FCKEditor 63 | var veto = {}; 64 | this.trigger('form-pre-serialize', [this, options, veto]); 65 | if (veto.veto) { 66 | log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); 67 | return this; 68 | } 69 | 70 | // provide opportunity to alter form data before it is serialized 71 | if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { 72 | log('ajaxSubmit: submit aborted via beforeSerialize callback'); 73 | return this; 74 | } 75 | 76 | var a = this.formToArray(options.semantic); 77 | if (options.data) { 78 | options.extraData = options.data; 79 | for (var n in options.data) { 80 | if(options.data[n] instanceof Array) { 81 | for (var k in options.data[n]) 82 | a.push( { name: n, value: options.data[n][k] } ) 83 | } 84 | else 85 | a.push( { name: n, value: options.data[n] } ); 86 | } 87 | } 88 | 89 | // give pre-submit callback an opportunity to abort the submit 90 | if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { 91 | log('ajaxSubmit: submit aborted via beforeSubmit callback'); 92 | return this; 93 | } 94 | 95 | // fire vetoable 'validate' event 96 | this.trigger('form-submit-validate', [a, this, options, veto]); 97 | if (veto.veto) { 98 | log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); 99 | return this; 100 | } 101 | 102 | var q = $.param(a); 103 | 104 | if (options.type.toUpperCase() == 'GET') { 105 | options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; 106 | options.data = null; // data is null for 'get' 107 | } 108 | else 109 | options.data = q; // data is the query string for 'post' 110 | 111 | var $form = this, callbacks = []; 112 | if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); 113 | if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); 114 | 115 | // perform a load on the target only if dataType is not provided 116 | if (!options.dataType && options.target) { 117 | var oldSuccess = options.success || function(){}; 118 | callbacks.push(function(data) { 119 | $(options.target).html(data).each(oldSuccess, arguments); 120 | }); 121 | } 122 | else if (options.success) 123 | callbacks.push(options.success); 124 | 125 | options.success = function(data, status) { 126 | for (var i=0, max=callbacks.length; i < max; i++) 127 | callbacks[i].apply(options, [data, status, $form]); 128 | }; 129 | 130 | // are there files to upload? 131 | var files = $('input:file', this).fieldValue(); 132 | var found = false; 133 | for (var j=0; j < files.length; j++) 134 | if (files[j]) 135 | found = true; 136 | 137 | // options.iframe allows user to force iframe mode 138 | if (options.iframe || found) { 139 | // hack to fix Safari hang (thanks to Tim Molendijk for this) 140 | // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d 141 | if (options.closeKeepAlive) 142 | $.get(options.closeKeepAlive, fileUpload); 143 | else 144 | fileUpload(); 145 | } 146 | else 147 | $.ajax(options); 148 | 149 | // fire 'notify' event 150 | this.trigger('form-submit-notify', [this, options]); 151 | return this; 152 | 153 | 154 | // private function for handling file uploads (hat tip to YAHOO!) 155 | function fileUpload() { 156 | var form = $form[0]; 157 | 158 | if ($(':input[name=submit]', form).length) { 159 | alert('Error: Form elements must not be named "submit".'); 160 | return; 161 | } 162 | 163 | var opts = $.extend({}, $.ajaxSettings, options); 164 | var s = jQuery.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); 165 | 166 | var id = 'jqFormIO' + (new Date().getTime()); 167 | var $io = $('