├── 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 |
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 | - <%= link_to h(story), [@project, story] %>
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 |
18 | -
19 | <%= link_to l(:new_story), contextual_new_story_path %>
20 |
21 | -
22 | <%= link_to l(:plan_iteration), new_project_iteration_path(@project) %>
23 |
24 |
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 | -
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 |
7 | -
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 |
12 | -
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 |
17 | -
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 |
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 |
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 |
31 | <% story.acceptance_criteria.each do |criterion| %>
32 | - <%= h criterion %>
33 | <% end %>
34 |
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('');
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('');
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 |
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 |
27 | <%= render @story.team_members %>
28 |
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 |
62 | -
63 | <%= link_to h(@story.iteration.name), project_iteration_path(@project, @story.iteration) %>
64 |
65 |
66 | <% end %>
67 |
68 | <% if @story.iteration_id.blank? || @story.iteration.pending? %>
69 |
<%= l(:next_steps) %>
70 |
71 | -
72 | <%= link_to l(:new_story), contextual_new_story_path %>
73 |
74 | -
75 | <%= link_to l(:backlog), backlog_project_stories_path(@project) %>
76 |
77 |
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 |
28 | <% ea_tabs.each do |tab| -%>
29 | - <%= link_to l(tab[:label]), tab[:path],
30 | :id => "tab-#{tab[:id]}",
31 | :class => (tab[:id] != selected_tab ? nil : 'selected'),
32 | :onclick => "showTab('#{tab[:id]}'); this.blur(); return false;" %>
33 | <% end -%>
34 |
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 |
62 | <%= yield :actions %>
63 |
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 += '- '+content+'
';
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('');
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 = $('');
168 | var io = $io[0];
169 |
170 | $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
171 |
172 | var xhr = { // mock object
173 | aborted: 0,
174 | responseText: null,
175 | responseXML: null,
176 | status: 0,
177 | statusText: 'n/a',
178 | getAllResponseHeaders: function() {},
179 | getResponseHeader: function() {},
180 | setRequestHeader: function() {},
181 | abort: function() {
182 | this.aborted = 1;
183 | $io.attr('src','about:blank'); // abort op in progress
184 | }
185 | };
186 |
187 | var g = opts.global;
188 | // trigger ajax global events so that activity/block indicators work like normal
189 | if (g && ! $.active++) $.event.trigger("ajaxStart");
190 | if (g) $.event.trigger("ajaxSend", [xhr, opts]);
191 |
192 | if (s.beforeSend && s.beforeSend(xhr, s) === false) {
193 | s.global && jQuery.active--;
194 | return;
195 | }
196 | if (xhr.aborted)
197 | return;
198 |
199 | var cbInvoked = 0;
200 | var timedOut = 0;
201 |
202 | // add submitting element to data if we know it
203 | var sub = form.clk;
204 | if (sub) {
205 | var n = sub.name;
206 | if (n && !sub.disabled) {
207 | options.extraData = options.extraData || {};
208 | options.extraData[n] = sub.value;
209 | if (sub.type == "image") {
210 | options.extraData[name+'.x'] = form.clk_x;
211 | options.extraData[name+'.y'] = form.clk_y;
212 | }
213 | }
214 | }
215 |
216 | // take a breath so that pending repaints get some cpu time before the upload starts
217 | setTimeout(function() {
218 | // make sure form attrs are set
219 | var t = $form.attr('target'), a = $form.attr('action');
220 |
221 | // update form attrs in IE friendly way
222 | form.setAttribute('target',id);
223 | if (form.getAttribute('method') != 'POST')
224 | form.setAttribute('method', 'POST');
225 | if (form.getAttribute('action') != opts.url)
226 | form.setAttribute('action', opts.url);
227 |
228 | // ie borks in some cases when setting encoding
229 | if (! options.skipEncodingOverride) {
230 | $form.attr({
231 | encoding: 'multipart/form-data',
232 | enctype: 'multipart/form-data'
233 | });
234 | }
235 |
236 | // support timout
237 | if (opts.timeout)
238 | setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
239 |
240 | // add "extra" data to form if provided in options
241 | var extraInputs = [];
242 | try {
243 | if (options.extraData)
244 | for (var n in options.extraData)
245 | extraInputs.push(
246 | $('')
247 | .appendTo(form)[0]);
248 |
249 | // add iframe to doc and submit the form
250 | $io.appendTo('body');
251 | io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
252 | form.submit();
253 | }
254 | finally {
255 | // reset attrs and remove "extra" input elements
256 | form.setAttribute('action',a);
257 | t ? form.setAttribute('target', t) : $form.removeAttr('target');
258 | $(extraInputs).remove();
259 | }
260 | }, 10);
261 |
262 | var nullCheckFlag = 0;
263 |
264 | function cb() {
265 | if (cbInvoked++) return;
266 |
267 | io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
268 |
269 | var ok = true;
270 | try {
271 | if (timedOut) throw 'timeout';
272 | // extract the server response from the iframe
273 | var data, doc;
274 |
275 | doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
276 |
277 | if ((doc.body == null || doc.body.innerHTML == '') && !nullCheckFlag) {
278 | // in some browsers (cough, Opera 9.2.x) the iframe DOM is not always traversable when
279 | // the onload callback fires, so we give them a 2nd chance
280 | nullCheckFlag = 1;
281 | cbInvoked--;
282 | setTimeout(cb, 100);
283 | return;
284 | }
285 |
286 | xhr.responseText = doc.body ? doc.body.innerHTML : null;
287 | xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
288 | xhr.getResponseHeader = function(header){
289 | var headers = {'content-type': opts.dataType};
290 | return headers[header];
291 | };
292 |
293 | if (opts.dataType == 'json' || opts.dataType == 'script') {
294 | var ta = doc.getElementsByTagName('textarea')[0];
295 | xhr.responseText = ta ? ta.value : xhr.responseText;
296 | }
297 | else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
298 | xhr.responseXML = toXml(xhr.responseText);
299 | }
300 | data = $.httpData(xhr, opts.dataType);
301 | }
302 | catch(e){
303 | ok = false;
304 | $.handleError(opts, xhr, 'error', e);
305 | }
306 |
307 | // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
308 | if (ok) {
309 | opts.success(data, 'success');
310 | if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
311 | }
312 | if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
313 | if (g && ! --$.active) $.event.trigger("ajaxStop");
314 | if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
315 |
316 | // clean up
317 | setTimeout(function() {
318 | $io.remove();
319 | xhr.responseXML = null;
320 | }, 100);
321 | };
322 |
323 | function toXml(s, doc) {
324 | if (window.ActiveXObject) {
325 | doc = new ActiveXObject('Microsoft.XMLDOM');
326 | doc.async = 'false';
327 | doc.loadXML(s);
328 | }
329 | else
330 | doc = (new DOMParser()).parseFromString(s, 'text/xml');
331 | return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
332 | };
333 | };
334 | };
335 |
336 | /**
337 | * ajaxForm() provides a mechanism for fully automating form submission.
338 | *
339 | * The advantages of using this method instead of ajaxSubmit() are:
340 | *
341 | * 1: This method will include coordinates for elements (if the element
342 | * is used to submit the form).
343 | * 2. This method will include the submit element's name/value data (for the element that was
344 | * used to submit the form).
345 | * 3. This method binds the submit() method to the form for you.
346 | *
347 | * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
348 | * passes the options argument along after properly binding events for submit elements and
349 | * the form itself.
350 | */
351 | $.fn.ajaxForm = function(options) {
352 | return this.ajaxFormUnbind().bind('submit.form-plugin',function() {
353 | $(this).ajaxSubmit(options);
354 | return false;
355 | }).each(function() {
356 | // store options in hash
357 | $(":submit,input:image", this).bind('click.form-plugin',function(e) {
358 | var form = this.form;
359 | form.clk = this;
360 | if (this.type == 'image') {
361 | if (e.offsetX != undefined) {
362 | form.clk_x = e.offsetX;
363 | form.clk_y = e.offsetY;
364 | } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
365 | var offset = $(this).offset();
366 | form.clk_x = e.pageX - offset.left;
367 | form.clk_y = e.pageY - offset.top;
368 | } else {
369 | form.clk_x = e.pageX - this.offsetLeft;
370 | form.clk_y = e.pageY - this.offsetTop;
371 | }
372 | }
373 | // clear form vars
374 | setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10);
375 | });
376 | });
377 | };
378 |
379 | // ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
380 | $.fn.ajaxFormUnbind = function() {
381 | this.unbind('submit.form-plugin');
382 | return this.each(function() {
383 | $(":submit,input:image", this).unbind('click.form-plugin');
384 | });
385 |
386 | };
387 |
388 | /**
389 | * formToArray() gathers form element data into an array of objects that can
390 | * be passed to any of the following ajax functions: $.get, $.post, or load.
391 | * Each object in the array has both a 'name' and 'value' property. An example of
392 | * an array for a simple login form might be:
393 | *
394 | * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
395 | *
396 | * It is this array that is passed to pre-submit callback functions provided to the
397 | * ajaxSubmit() and ajaxForm() methods.
398 | */
399 | $.fn.formToArray = function(semantic) {
400 | var a = [];
401 | if (this.length == 0) return a;
402 |
403 | var form = this[0];
404 | var els = semantic ? form.getElementsByTagName('*') : form.elements;
405 | if (!els) return a;
406 | for(var i=0, max=els.length; i < max; i++) {
407 | var el = els[i];
408 | var n = el.name;
409 | if (!n) continue;
410 |
411 | if (semantic && form.clk && el.type == "image") {
412 | // handle image inputs on the fly when semantic == true
413 | if(!el.disabled && form.clk == el)
414 | a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
415 | continue;
416 | }
417 |
418 | var v = $.fieldValue(el, true);
419 | if (v && v.constructor == Array) {
420 | for(var j=0, jmax=v.length; j < jmax; j++)
421 | a.push({name: n, value: v[j]});
422 | }
423 | else if (v !== null && typeof v != 'undefined')
424 | a.push({name: n, value: v});
425 | }
426 |
427 | if (!semantic && form.clk) {
428 | // input type=='image' are not found in elements array! handle them here
429 | var inputs = form.getElementsByTagName("input");
430 | for(var i=0, max=inputs.length; i < max; i++) {
431 | var input = inputs[i];
432 | var n = input.name;
433 | if(n && !input.disabled && input.type == "image" && form.clk == input)
434 | a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
435 | }
436 | }
437 | return a;
438 | };
439 |
440 | /**
441 | * Serializes form data into a 'submittable' string. This method will return a string
442 | * in the format: name1=value1&name2=value2
443 | */
444 | $.fn.formSerialize = function(semantic) {
445 | //hand off to jQuery.param for proper encoding
446 | return $.param(this.formToArray(semantic));
447 | };
448 |
449 | /**
450 | * Serializes all field elements in the jQuery object into a query string.
451 | * This method will return a string in the format: name1=value1&name2=value2
452 | */
453 | $.fn.fieldSerialize = function(successful) {
454 | var a = [];
455 | this.each(function() {
456 | var n = this.name;
457 | if (!n) return;
458 | var v = $.fieldValue(this, successful);
459 | if (v && v.constructor == Array) {
460 | for (var i=0,max=v.length; i < max; i++)
461 | a.push({name: n, value: v[i]});
462 | }
463 | else if (v !== null && typeof v != 'undefined')
464 | a.push({name: this.name, value: v});
465 | });
466 | //hand off to jQuery.param for proper encoding
467 | return $.param(a);
468 | };
469 |
470 | /**
471 | * Returns the value(s) of the element in the matched set. For example, consider the following form:
472 | *
473 | *
481 | *
482 | * var v = $(':text').fieldValue();
483 | * // if no values are entered into the text inputs
484 | * v == ['','']
485 | * // if values entered into the text inputs are 'foo' and 'bar'
486 | * v == ['foo','bar']
487 | *
488 | * var v = $(':checkbox').fieldValue();
489 | * // if neither checkbox is checked
490 | * v === undefined
491 | * // if both checkboxes are checked
492 | * v == ['B1', 'B2']
493 | *
494 | * var v = $(':radio').fieldValue();
495 | * // if neither radio is checked
496 | * v === undefined
497 | * // if first radio is checked
498 | * v == ['C1']
499 | *
500 | * The successful argument controls whether or not the field element must be 'successful'
501 | * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
502 | * The default value of the successful argument is true. If this value is false the value(s)
503 | * for each element is returned.
504 | *
505 | * Note: This method *always* returns an array. If no valid value can be determined the
506 | * array will be empty, otherwise it will contain one or more values.
507 | */
508 | $.fn.fieldValue = function(successful) {
509 | for (var val=[], i=0, max=this.length; i < max; i++) {
510 | var el = this[i];
511 | var v = $.fieldValue(el, successful);
512 | if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
513 | continue;
514 | v.constructor == Array ? $.merge(val, v) : val.push(v);
515 | }
516 | return val;
517 | };
518 |
519 | /**
520 | * Returns the value of the field element.
521 | */
522 | $.fieldValue = function(el, successful) {
523 | var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
524 | if (typeof successful == 'undefined') successful = true;
525 |
526 | if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
527 | (t == 'checkbox' || t == 'radio') && !el.checked ||
528 | (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
529 | tag == 'select' && el.selectedIndex == -1))
530 | return null;
531 |
532 | if (tag == 'select') {
533 | var index = el.selectedIndex;
534 | if (index < 0) return null;
535 | var a = [], ops = el.options;
536 | var one = (t == 'select-one');
537 | var max = (one ? index+1 : ops.length);
538 | for(var i=(one ? index : 0); i < max; i++) {
539 | var op = ops[i];
540 | if (op.selected) {
541 | var v = op.value;
542 | if (!v) // extra pain for IE...
543 | v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
544 | if (one) return v;
545 | a.push(v);
546 | }
547 | }
548 | return a;
549 | }
550 | return el.value;
551 | };
552 |
553 | /**
554 | * Clears the form data. Takes the following actions on the form's input fields:
555 | * - input text fields will have their 'value' property set to the empty string
556 | * - select elements will have their 'selectedIndex' property set to -1
557 | * - checkbox and radio inputs will have their 'checked' property set to false
558 | * - inputs of type submit, button, reset, and hidden will *not* be effected
559 | * - button elements will *not* be effected
560 | */
561 | $.fn.clearForm = function() {
562 | return this.each(function() {
563 | $('input,select,textarea', this).clearFields();
564 | });
565 | };
566 |
567 | /**
568 | * Clears the selected form elements.
569 | */
570 | $.fn.clearFields = $.fn.clearInputs = function() {
571 | return this.each(function() {
572 | var t = this.type, tag = this.tagName.toLowerCase();
573 | if (t == 'text' || t == 'password' || tag == 'textarea')
574 | this.value = '';
575 | else if (t == 'checkbox' || t == 'radio')
576 | this.checked = false;
577 | else if (tag == 'select')
578 | this.selectedIndex = -1;
579 | });
580 | };
581 |
582 | /**
583 | * Resets the form data. Causes all form elements to be reset to their original value.
584 | */
585 | $.fn.resetForm = function() {
586 | return this.each(function() {
587 | // guard against an input with the name of 'reset'
588 | // note that IE reports the reset function as an 'object'
589 | if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
590 | this.reset();
591 | });
592 | };
593 |
594 | /**
595 | * Enables or disables any matching elements.
596 | */
597 | $.fn.enable = function(b) {
598 | if (b == undefined) b = true;
599 | return this.each(function() {
600 | this.disabled = !b
601 | });
602 | };
603 |
604 | /**
605 | * Checks/unchecks any matching checkboxes or radio buttons and
606 | * selects/deselects and matching option elements.
607 | */
608 | $.fn.selected = function(select) {
609 | if (select == undefined) select = true;
610 | return this.each(function() {
611 | var t = this.type;
612 | if (t == 'checkbox' || t == 'radio')
613 | this.checked = select;
614 | else if (this.tagName.toLowerCase() == 'option') {
615 | var $sel = $(this).parent('select');
616 | if (select && $sel[0] && $sel[0].type == 'select-one') {
617 | // deselect all other options
618 | $sel.find('option').selected(false);
619 | }
620 | this.selected = select;
621 | }
622 | });
623 | };
624 |
625 | // helper fn for console logging
626 | // set $.fn.ajaxSubmit.debug to true to enable debug logging
627 | function log() {
628 | if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
629 | window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
630 | };
631 |
632 | })(jQuery);
633 |
--------------------------------------------------------------------------------
/assets/javascripts/jquery-ui-1.7.custom.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery UI 1.7
3 | *
4 | * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
5 | * Dual licensed under the MIT (MIT-LICENSE.txt)
6 | * and GPL (GPL-LICENSE.txt) licenses.
7 | *
8 | * http://docs.jquery.com/UI
9 | */
jQuery.ui||(function(c){var i=c.fn.remove,d=c.browser.mozilla&&(parseFloat(c.browser.version)<1.9);c.ui={version:"1.7",plugin:{add:function(k,l,n){var m=c.ui[k].prototype;for(var j in n){m.plugins[j]=m.plugins[j]||[];m.plugins[j].push([l,n[j]])}},call:function(j,l,k){var n=j.plugins[l];if(!n||!j.element[0].parentNode){return}for(var m=0;m0){return true}m[j]=1;l=(m[j]>0);m[j]=0;return l},isOverAxis:function(k,j,l){return(k>j)&&(k<(j+l))},isOver:function(o,k,n,m,j,l){return c.ui.isOverAxis(o,n,j)&&c.ui.isOverAxis(k,m,l)},keyCode:{BACKSPACE:8,CAPS_LOCK:20,COMMA:188,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38}};if(d){var f=c.attr,e=c.fn.removeAttr,h="http://www.w3.org/2005/07/aaa",a=/^aria-/,b=/^wairole:/;c.attr=function(k,j,l){var m=l!==undefined;return(j=="role"?(m?f.call(this,k,j,"wairole:"+l):(f.apply(this,arguments)||"").replace(b,"")):(a.test(j)?(m?k.setAttributeNS(h,j.replace(a,"aaa:"),l):f.call(this,k,j.replace(a,"aaa:"))):f.apply(this,arguments)))};c.fn.removeAttr=function(j){return(a.test(j)?this.each(function(){this.removeAttributeNS(h,j.replace(a,""))}):e.call(this,j))}}c.fn.extend({remove:function(){c("*",this).add(this).each(function(){c(this).triggerHandler("remove")});return i.apply(this,arguments)},enableSelection:function(){return this.attr("unselectable","off").css("MozUserSelect","").unbind("selectstart.ui")},disableSelection:function(){return this.attr("unselectable","on").css("MozUserSelect","none").bind("selectstart.ui",function(){return false})},scrollParent:function(){var j;if((c.browser.msie&&(/(static|relative)/).test(this.css("position")))||(/absolute/).test(this.css("position"))){j=this.parents().filter(function(){return(/(relative|absolute|fixed)/).test(c.curCSS(this,"position",1))&&(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}else{j=this.parents().filter(function(){return(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}return(/fixed/).test(this.css("position"))||!j.length?c(document):j}});c.extend(c.expr[":"],{data:function(l,k,j){return !!c.data(l,j[3])},focusable:function(k){var l=k.nodeName.toLowerCase(),j=c.attr(k,"tabindex");return(/input|select|textarea|button|object/.test(l)?!k.disabled:"a"==l||"area"==l?k.href||!isNaN(j):!isNaN(j))&&!c(k)["area"==l?"parents":"closest"](":hidden").length},tabbable:function(k){var j=c.attr(k,"tabindex");return(isNaN(j)||j>=0)&&c(k).is(":focusable")}});function g(m,n,o,l){function k(q){var p=c[m][n][q]||[];return(typeof p=="string"?p.split(/,?\s+/):p)}var j=k("getter");if(l.length==1&&typeof l[0]=="string"){j=j.concat(k("getterSetter"))}return(c.inArray(o,j)!=-1)}c.widget=function(k,j){var l=k.split(".")[0];k=k.split(".")[1];c.fn[k]=function(p){var n=(typeof p=="string"),o=Array.prototype.slice.call(arguments,1);if(n&&p.substring(0,1)=="_"){return this}if(n&&g(l,k,p,o)){var m=c.data(this[0],k);return(m?m[p].apply(m,o):undefined)}return this.each(function(){var q=c.data(this,k);(!q&&!n&&c.data(this,k,new c[l][k](this,p))._init());(q&&n&&c.isFunction(q[p])&&q[p].apply(q,o))})};c[l]=c[l]||{};c[l][k]=function(o,n){var m=this;this.namespace=l;this.widgetName=k;this.widgetEventPrefix=c[l][k].eventPrefix||k;this.widgetBaseClass=l+"-"+k;this.options=c.extend({},c.widget.defaults,c[l][k].defaults,c.metadata&&c.metadata.get(o)[k],n);this.element=c(o).bind("setData."+k,function(q,p,r){if(q.target==o){return m._setData(p,r)}}).bind("getData."+k,function(q,p){if(q.target==o){return m._getData(p)}}).bind("remove",function(){return m.destroy()})};c[l][k].prototype=c.extend({},c.widget.prototype,j);c[l][k].getterSetter="option"};c.widget.prototype={_init:function(){},destroy:function(){this.element.removeData(this.widgetName).removeClass(this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").removeAttr("aria-disabled")},option:function(l,m){var k=l,j=this;if(typeof l=="string"){if(m===undefined){return this._getData(l)}k={};k[l]=m}c.each(k,function(n,o){j._setData(n,o)})},_getData:function(j){return this.options[j]},_setData:function(j,k){this.options[j]=k;if(j=="disabled"){this.element[k?"addClass":"removeClass"](this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").attr("aria-disabled",k)}},enable:function(){this._setData("disabled",false)},disable:function(){this._setData("disabled",true)},_trigger:function(l,m,n){var p=this.options[l],j=(l==this.widgetEventPrefix?l:this.widgetEventPrefix+l);m=c.Event(m);m.type=j;if(m.originalEvent){for(var k=c.event.props.length,o;k;){o=c.event.props[--k];m[o]=m.originalEvent[o]}}this.element.trigger(m,n);return !(c.isFunction(p)&&p.call(this.element[0],m,n)===false||m.isDefaultPrevented())}};c.widget.defaults={disabled:false};c.ui.mouse={_mouseInit:function(){var j=this;this.element.bind("mousedown."+this.widgetName,function(k){return j._mouseDown(k)}).bind("click."+this.widgetName,function(k){if(j._preventClickEvent){j._preventClickEvent=false;k.stopImmediatePropagation();return false}});if(c.browser.msie){this._mouseUnselectable=this.element.attr("unselectable");this.element.attr("unselectable","on")}this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName);(c.browser.msie&&this.element.attr("unselectable",this._mouseUnselectable))},_mouseDown:function(l){l.originalEvent=l.originalEvent||{};if(l.originalEvent.mouseHandled){return}(this._mouseStarted&&this._mouseUp(l));this._mouseDownEvent=l;var k=this,m=(l.which==1),j=(typeof this.options.cancel=="string"?c(l.target).parents().add(l.target).filter(this.options.cancel).length:false);if(!m||j||!this._mouseCapture(l)){return true}this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet){this._mouseDelayTimer=setTimeout(function(){k.mouseDelayMet=true},this.options.delay)}if(this._mouseDistanceMet(l)&&this._mouseDelayMet(l)){this._mouseStarted=(this._mouseStart(l)!==false);if(!this._mouseStarted){l.preventDefault();return true}}this._mouseMoveDelegate=function(n){return k._mouseMove(n)};this._mouseUpDelegate=function(n){return k._mouseUp(n)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);(c.browser.safari||l.preventDefault());l.originalEvent.mouseHandled=true;return true},_mouseMove:function(j){if(c.browser.msie&&!j.button){return this._mouseUp(j)}if(this._mouseStarted){this._mouseDrag(j);return j.preventDefault()}if(this._mouseDistanceMet(j)&&this._mouseDelayMet(j)){this._mouseStarted=(this._mouseStart(this._mouseDownEvent,j)!==false);(this._mouseStarted?this._mouseDrag(j):this._mouseUp(j))}return !this._mouseStarted},_mouseUp:function(j){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=(j.target==this._mouseDownEvent.target);this._mouseStop(j)}return false},_mouseDistanceMet:function(j){return(Math.max(Math.abs(this._mouseDownEvent.pageX-j.pageX),Math.abs(this._mouseDownEvent.pageY-j.pageY))>=this.options.distance)},_mouseDelayMet:function(j){return this.mouseDelayMet},_mouseStart:function(j){},_mouseDrag:function(j){},_mouseStop:function(j){},_mouseCapture:function(j){return true}};c.ui.mouse.defaults={cancel:null,distance:1,delay:0}})(jQuery);;/*
10 | * jQuery UI Draggable 1.7
11 | *
12 | * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
13 | * Dual licensed under the MIT (MIT-LICENSE.txt)
14 | * and GPL (GPL-LICENSE.txt) licenses.
15 | *
16 | * http://docs.jquery.com/UI/Draggables
17 | *
18 | * Depends:
19 | * ui.core.js
20 | */
(function(a){a.widget("ui.draggable",a.extend({},a.ui.mouse,{_init:function(){if(this.options.helper=="original"&&!(/^(?:r|a|f)/).test(this.element.css("position"))){this.element[0].style.position="relative"}(this.options.addClasses&&this.element.addClass("ui-draggable"));(this.options.disabled&&this.element.addClass("ui-draggable-disabled"));this._mouseInit()},destroy:function(){if(!this.element.data("draggable")){return}this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy()},_mouseCapture:function(b){var c=this.options;if(this.helper||c.disabled||a(b.target).is(".ui-resizable-handle")){return false}this.handle=this._getHandle(b);if(!this.handle){return false}return true},_mouseStart:function(b){var c=this.options;this.helper=this._createHelper(b);this._cacheHelperProportions();if(a.ui.ddmanager){a.ui.ddmanager.current=this}this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(b);this.originalPageX=b.pageX;this.originalPageY=b.pageY;if(c.cursorAt){this._adjustOffsetFromHelper(c.cursorAt)}if(c.containment){this._setContainment()}this._trigger("start",b);this._cacheHelperProportions();if(a.ui.ddmanager&&!c.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,b)}this.helper.addClass("ui-draggable-dragging");this._mouseDrag(b,true);return true},_mouseDrag:function(b,d){this.position=this._generatePosition(b);this.positionAbs=this._convertPositionTo("absolute");if(!d){var c=this._uiHash();this._trigger("drag",b,c);this.position=c.position}if(!this.options.axis||this.options.axis!="y"){this.helper[0].style.left=this.position.left+"px"}if(!this.options.axis||this.options.axis!="x"){this.helper[0].style.top=this.position.top+"px"}if(a.ui.ddmanager){a.ui.ddmanager.drag(this,b)}return false},_mouseStop:function(c){var d=false;if(a.ui.ddmanager&&!this.options.dropBehaviour){d=a.ui.ddmanager.drop(this,c)}if(this.dropped){d=this.dropped;this.dropped=false}if((this.options.revert=="invalid"&&!d)||(this.options.revert=="valid"&&d)||this.options.revert===true||(a.isFunction(this.options.revert)&&this.options.revert.call(this.element,d))){var b=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){b._trigger("stop",c);b._clear()})}else{this._trigger("stop",c);this._clear()}return false},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?true:false;a(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==b.target){c=true}});return c},_createHelper:function(c){var d=this.options;var b=a.isFunction(d.helper)?a(d.helper.apply(this.element[0],[c])):(d.helper=="clone"?this.element.clone():this.element);if(!b.parents("body").length){b.appendTo((d.appendTo=="parent"?this.element[0].parentNode:d.appendTo))}if(b[0]!=this.element[0]&&!(/(fixed|absolute)/).test(b.css("position"))){b.css("position","absolute")}return b},_adjustOffsetFromHelper:function(b){if(b.left!=undefined){this.offset.click.left=b.left+this.margins.left}if(b.right!=undefined){this.offset.click.left=this.helperProportions.width-b.right+this.margins.left}if(b.top!=undefined){this.offset.click.top=b.top+this.margins.top}if(b.bottom!=undefined){this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top}},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])){b.left+=this.scrollParent.scrollLeft();b.top+=this.scrollParent.scrollTop()}if((this.offsetParent[0]==document.body)||(this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)){b={top:0,left:0}}return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var b=this.element.position();return{top:b.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:b.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else{return{top:0,left:0}}},_cacheMargins:function(){this.margins={left:(parseInt(this.element.css("marginLeft"),10)||0),top:(parseInt(this.element.css("marginTop"),10)||0)}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e=this.options;if(e.containment=="parent"){e.containment=this.helper[0].parentNode}if(e.containment=="document"||e.containment=="window"){this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(e.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(e.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]}if(!(/^(document|window|parent)$/).test(e.containment)&&e.containment.constructor!=Array){var c=a(e.containment)[0];if(!c){return}var d=a(e.containment).offset();var b=(a(c).css("overflow")!="hidden");this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(b?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(b?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}else{if(e.containment.constructor==Array){this.containment=e.containment}}},_convertPositionTo:function(f,h){if(!h){h=this.position}var c=f=="absolute"?1:-1;var e=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=(/(html|body)/i).test(b[0].tagName);return{top:(h.top+this.offset.relative.top*c+this.offset.parent.top*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(g?0:b.scrollTop()))*c)),left:(h.left+this.offset.relative.left*c+this.offset.parent.left*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:b.scrollLeft())*c))}},_generatePosition:function(e){var h=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,i=(/(html|body)/i).test(b[0].tagName);if(this.cssPosition=="relative"&&!(this.scrollParent[0]!=document&&this.scrollParent[0]!=this.offsetParent[0])){this.offset.relative=this._getRelativeOffset()}var d=e.pageX;var c=e.pageY;if(this.originalPosition){if(this.containment){if(e.pageX-this.offset.click.leftthis.containment[2]){d=this.containment[2]+this.offset.click.left}if(e.pageY-this.offset.click.top>this.containment[3]){c=this.containment[3]+this.offset.click.top}}if(h.grid){var g=this.originalPageY+Math.round((c-this.originalPageY)/h.grid[1])*h.grid[1];c=this.containment?(!(g-this.offset.click.topthis.containment[3])?g:(!(g-this.offset.click.topthis.containment[2])?f:(!(f-this.offset.click.left').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1000}).css(a(this).offset()).appendTo("body")})},stop:function(b,c){a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});a.ui.plugin.add("draggable","opacity",{start:function(c,d){var b=a(d.helper),e=a(this).data("draggable").options;if(b.css("opacity")){e._opacity=b.css("opacity")}b.css("opacity",e.opacity)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._opacity){a(c.helper).css("opacity",d._opacity)}}});a.ui.plugin.add("draggable","scroll",{start:function(c,d){var b=a(this).data("draggable");if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!="HTML"){b.overflowOffset=b.scrollParent.offset()}},drag:function(d,e){var c=a(this).data("draggable"),f=c.options,b=false;if(c.scrollParent[0]!=document&&c.scrollParent[0].tagName!="HTML"){if(!f.axis||f.axis!="x"){if((c.overflowOffset.top+c.scrollParent[0].offsetHeight)-d.pageY=0;v--){var s=g.snapElements[v].left,n=s+g.snapElements[v].width,m=g.snapElements[v].top,A=m+g.snapElements[v].height;if(!((s-y=p&&n<=k)||(m>=p&&m<=k)||(nk))&&((e>=g&&e<=c)||(d>=g&&d<=c)||(ec));break;default:return false;break}};a.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,g){var b=a.ui.ddmanager.droppables[e.options.scope];var f=g?g.type:null;var h=(e.currentItem||e.element).find(":data(droppable)").andSelf();droppablesLoop:for(var d=0;d=0;b--){this.items[b].item.removeData("sortable-item")}},_mouseCapture:function(e,f){if(this.reverting){return false}if(this.options.disabled||this.options.type=="static"){return false}this._refreshItems(e);var d=null,c=this,b=a(e.target).parents().each(function(){if(a.data(this,"sortable-item")==c){d=a(this);return false}});if(a.data(e.target,"sortable-item")==c){d=a(e.target)}if(!d){return false}if(this.options.handle&&!f){var g=false;a(this.options.handle,d).find("*").andSelf().each(function(){if(this==e.target){g=true}});if(!g){return false}}this.currentItem=d;this._removeCurrentsFromItems();return true},_mouseStart:function(e,f,b){var g=this.options,c=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(e);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");a.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(e);this.originalPageX=e.pageX;this.originalPageY=e.pageY;if(g.cursorAt){this._adjustOffsetFromHelper(g.cursorAt)}this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};if(this.helper[0]!=this.currentItem[0]){this.currentItem.hide()}this._createPlaceholder();if(g.containment){this._setContainment()}if(g.cursor){if(a("body").css("cursor")){this._storedCursor=a("body").css("cursor")}a("body").css("cursor",g.cursor)}if(g.opacity){if(this.helper.css("opacity")){this._storedOpacity=this.helper.css("opacity")}this.helper.css("opacity",g.opacity)}if(g.zIndex){if(this.helper.css("zIndex")){this._storedZIndex=this.helper.css("zIndex")}this.helper.css("zIndex",g.zIndex)}if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){this.overflowOffset=this.scrollParent.offset()}this._trigger("start",e,this._uiHash());if(!this._preserveHelperProportions){this._cacheHelperProportions()}if(!b){for(var d=this.containers.length-1;d>=0;d--){this.containers[d]._trigger("activate",e,c._uiHash(this))}}if(a.ui.ddmanager){a.ui.ddmanager.current=this}if(a.ui.ddmanager&&!g.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,e)}this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(e);return true},_mouseDrag:function(f){this.position=this._generatePosition(f);this.positionAbs=this._convertPositionTo("absolute");if(!this.lastPositionAbs){this.lastPositionAbs=this.positionAbs}if(this.options.scroll){var g=this.options,b=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if((this.overflowOffset.top+this.scrollParent[0].offsetHeight)-f.pageY=0;d--){var e=this.items[d],c=e.item[0],h=this._intersectsWithPointer(e);if(!h){continue}if(c!=this.currentItem[0]&&this.placeholder[h==1?"next":"prev"]()[0]!=c&&!a.ui.contains(this.placeholder[0],c)&&(this.options.type=="semi-dynamic"?!a.ui.contains(this.element[0],c):true)){this.direction=h==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(e)){this._rearrange(f,e)}else{break}this._trigger("change",f,this._uiHash());break}}this._contactContainers(f);if(a.ui.ddmanager){a.ui.ddmanager.drag(this,f)}this._trigger("sort",f,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(c,d){if(!c){return}if(a.ui.ddmanager&&!this.options.dropBehaviour){a.ui.ddmanager.drop(this,c)}if(this.options.revert){var b=this;var e=b.placeholder.offset();b.reverting=true;a(this.helper).animate({left:e.left-this.offset.parent.left-b.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:e.top-this.offset.parent.top-b.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){b._clear(c)})}else{this._clear(c,d)}return false},cancel:function(){var b=this;if(this.dragging){this._mouseUp();if(this.options.helper=="original"){this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else{this.currentItem.show()}for(var c=this.containers.length-1;c>=0;c--){this.containers[c]._trigger("deactivate",null,b._uiHash(this));if(this.containers[c].containerCache.over){this.containers[c]._trigger("out",null,b._uiHash(this));this.containers[c].containerCache.over=0}}}if(this.placeholder[0].parentNode){this.placeholder[0].parentNode.removeChild(this.placeholder[0])}if(this.options.helper!="original"&&this.helper&&this.helper[0].parentNode){this.helper.remove()}a.extend(this,{helper:null,dragging:false,reverting:false,_noFinalSort:null});if(this.domPosition.prev){a(this.domPosition.prev).after(this.currentItem)}else{a(this.domPosition.parent).prepend(this.currentItem)}return true},serialize:function(d){var b=this._getItemsAsjQuery(d&&d.connected);var c=[];d=d||{};a(b).each(function(){var e=(a(d.item||this).attr(d.attribute||"id")||"").match(d.expression||(/(.+)[-=_](.+)/));if(e){c.push((d.key||e[1]+"[]")+"="+(d.key&&d.expression?e[1]:e[2]))}});return c.join("&")},toArray:function(d){var b=this._getItemsAsjQuery(d&&d.connected);var c=[];d=d||{};b.each(function(){c.push(a(d.item||this).attr(d.attribute||"id")||"")});return c},_intersectsWith:function(m){var e=this.positionAbs.left,d=e+this.helperProportions.width,k=this.positionAbs.top,j=k+this.helperProportions.height;var f=m.left,c=f+m.width,n=m.top,i=n+m.height;var o=this.offset.click.top,h=this.offset.click.left;var g=(k+o)>n&&(k+o)f&&(e+h)m[this.floating?"width":"height"])){return g}else{return(f0?"down":"up")},_getDragHorizontalDirection:function(){var b=this.positionAbs.left-this.lastPositionAbs.left;return b!=0&&(b>0?"right":"left")},refresh:function(b){this._refreshItems(b);this.refreshPositions()},_connectWith:function(){var b=this.options;return b.connectWith.constructor==String?[b.connectWith]:b.connectWith},_getItemsAsjQuery:function(b){var l=this;var g=[];var e=[];var h=this._connectWith();if(h&&b){for(var d=h.length-1;d>=0;d--){var k=a(h[d]);for(var c=k.length-1;c>=0;c--){var f=a.data(k[c],"sortable");if(f&&f!=this&&!f.options.disabled){e.push([a.isFunction(f.options.items)?f.options.items.call(f.element):a(f.options.items,f.element).not(".ui-sortable-helper"),f])}}}}e.push([a.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):a(this.options.items,this.element).not(".ui-sortable-helper"),this]);for(var d=e.length-1;d>=0;d--){e[d][0].each(function(){g.push(this)})}return a(g)},_removeCurrentsFromItems:function(){var d=this.currentItem.find(":data(sortable-item)");for(var c=0;c=0;e--){var m=a(l[e]);for(var d=m.length-1;d>=0;d--){var g=a.data(m[d],"sortable");if(g&&g!=this&&!g.options.disabled){f.push([a.isFunction(g.options.items)?g.options.items.call(g.element[0],b,{item:this.currentItem}):a(g.options.items,g.element),g]);this.containers.push(g)}}}}for(var e=f.length-1;e>=0;e--){var k=f[e][1];var c=f[e][0];for(var d=0,n=c.length;d=0;d--){var e=this.items[d];if(e.instance!=this.currentContainer&&this.currentContainer&&e.item[0]!=this.currentItem[0]){continue}var c=this.options.toleranceElement?a(this.options.toleranceElement,e.item):e.item;if(!b){e.width=c.outerWidth();e.height=c.outerHeight()}var f=c.offset();e.left=f.left;e.top=f.top}if(this.options.custom&&this.options.custom.refreshContainers){this.options.custom.refreshContainers.call(this)}else{for(var d=this.containers.length-1;d>=0;d--){var f=this.containers[d].element.offset();this.containers[d].containerCache.left=f.left;this.containers[d].containerCache.top=f.top;this.containers[d].containerCache.width=this.containers[d].element.outerWidth();this.containers[d].containerCache.height=this.containers[d].element.outerHeight()}}},_createPlaceholder:function(d){var b=d||this,e=b.options;if(!e.placeholder||e.placeholder.constructor==String){var c=e.placeholder;e.placeholder={element:function(){var f=a(document.createElement(b.currentItem[0].nodeName)).addClass(c||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!c){f.style.visibility="hidden"}return f},update:function(f,g){if(c&&!e.forcePlaceholderSize){return}if(!g.height()){g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10))}if(!g.width()){g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||0,10))}}}}b.placeholder=a(e.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);e.placeholder.update(b,b.placeholder)},_contactContainers:function(d){for(var c=this.containers.length-1;c>=0;c--){if(this._intersectsWith(this.containers[c].containerCache)){if(!this.containers[c].containerCache.over){if(this.currentContainer!=this.containers[c]){var h=10000;var g=null;var e=this.positionAbs[this.containers[c].floating?"left":"top"];for(var b=this.items.length-1;b>=0;b--){if(!a.ui.contains(this.containers[c].element[0],this.items[b].item[0])){continue}var f=this.items[b][this.containers[c].floating?"left":"top"];if(Math.abs(f-e)this.containment[2]){d=this.containment[2]+this.offset.click.left}if(e.pageY-this.offset.click.top>this.containment[3]){c=this.containment[3]+this.offset.click.top}}if(h.grid){var g=this.originalPageY+Math.round((c-this.originalPageY)/h.grid[1])*h.grid[1];c=this.containment?(!(g-this.offset.click.topthis.containment[3])?g:(!(g-this.offset.click.topthis.containment[2])?f:(!(f-this.offset.click.left=0;c--){if(a.ui.contains(this.containers[c].element[0],this.currentItem[0])&&!e){f.push((function(g){return function(h){g._trigger("receive",h,this._uiHash(this))}}).call(this,this.containers[c]));f.push((function(g){return function(h){g._trigger("update",h,this._uiHash(this))}}).call(this,this.containers[c]))}}}for(var c=this.containers.length-1;c>=0;c--){if(!e){f.push((function(g){return function(h){g._trigger("deactivate",h,this._uiHash(this))}}).call(this,this.containers[c]))}if(this.containers[c].containerCache.over){f.push((function(g){return function(h){g._trigger("out",h,this._uiHash(this))}}).call(this,this.containers[c]));this.containers[c].containerCache.over=0}}if(this._storedCursor){a("body").css("cursor",this._storedCursor)}if(this._storedOpacity){this.helper.css("opacity",this._storedOpacity)}if(this._storedZIndex){this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex)}this.dragging=false;if(this.cancelHelperRemoval){if(!e){this._trigger("beforeStop",d,this._uiHash());for(var c=0;c *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1000}})})(jQuery);;
--------------------------------------------------------------------------------