├── LICENSE ├── LICENSE.simply_agile ├── README.rdoc ├── VERSION ├── app ├── controllers │ ├── acceptance_criteria_controller.rb │ ├── active_iterations_controller.rb │ ├── application_controller.rb │ ├── burndowns_controller.rb │ ├── easy_agile_common_controller.rb │ ├── easy_agile_controller.rb │ ├── iterations_controller.rb │ ├── stories_controller.rb │ └── story_team_members_controller.rb ├── helpers │ ├── acceptance_criteria_helper.rb │ ├── active_iterations_helper.rb │ ├── application_helper.rb │ ├── burndowns_helper.rb │ ├── easy_agile_helper.rb │ ├── iterations_helper.rb │ ├── stories_helper.rb │ └── story_team_members_helper.rb ├── models │ ├── acceptance_criterion.rb │ ├── acceptance_criterion_observer.rb │ ├── burndown.rb │ ├── burndown_data_point.rb │ ├── iteration.rb │ ├── story.rb │ ├── story_action.rb │ ├── story_action_observer.rb │ └── story_team_member.rb └── views │ ├── acceptance_criteria │ ├── _criterion.erb │ ├── _list.erb │ ├── edit.html.erb │ └── edit.js.erb │ ├── easy_agile │ ├── show.html.erb │ └── show_guidance.html.erb │ ├── iterations │ ├── _form.erb │ ├── _iteration.erb │ ├── _list_item.erb │ ├── edit.html.erb │ ├── finished.html.erb │ ├── index.html.erb │ ├── new.html.erb │ ├── new_guidance.html.erb │ ├── planned.html.erb │ ├── show.html.erb │ └── show_active.html.erb │ ├── layouts │ ├── ea_base.html.erb │ └── request.html.erb │ ├── my │ └── blocks │ │ ├── _active_work.erb │ │ └── _easy_agile_home.erb │ ├── stories │ ├── _estimating_story.erb │ ├── _form.erb │ ├── _list_item.erb │ ├── _prioritising_story.erb │ ├── _status_form.erb │ ├── _story.html.erb │ ├── backlog.html.erb │ ├── backlog_guidance.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new_with_iteration.html.erb │ ├── new_with_project.html.erb │ ├── show.feature.erb │ └── show.html.erb │ └── story_team_members │ └── _story_team_member.erb ├── assets ├── images │ ├── a_simple_tool_for_effective_agile_teams.png │ ├── active_iteration.png │ ├── arrow_from_grey.png │ ├── arrow_large_left.png │ ├── arrow_large_left_highlighted.png │ ├── arrow_large_right.png │ ├── arrow_large_right_highlighted.png │ ├── arrow_small_right.gif │ ├── arrow_small_right.png │ ├── arrow_small_right_green_background.png │ ├── arrow_small_right_grey_background.png │ ├── arrow_small_right_orange_background.png │ ├── burndown.png │ ├── button_background.png │ ├── fella_in_progress.gif │ ├── fella_in_progress.png │ ├── fella_testing.gif │ ├── fella_testing.png │ ├── new_story.png │ ├── new_story_highlight.png │ ├── planning_iteration.png │ ├── simply_agile.png │ ├── simply_agile_large.png │ ├── simply_agile_twitter.png │ ├── story_card.png │ ├── try_it_free.png │ └── underline_green.png ├── javascripts │ ├── acceptance_criteria.js │ ├── application.js │ ├── backlog_prioritisation.js │ ├── flash.js │ ├── iteration_active.js │ ├── iteration_planning.js │ ├── jquery-1.3.2.min.js │ ├── jquery-ui-1.7.custom.min.js │ ├── jquery.form.js │ ├── request.js │ └── story.js └── stylesheets │ ├── application │ ├── colours.css │ ├── layout.css │ └── typography.css │ ├── iteration │ ├── colours.css │ ├── layout.css │ └── typography.css │ ├── iteration_active │ ├── colours.css │ ├── layout.css │ └── typography.css │ ├── iteration_planning │ ├── colours.css │ ├── layout.css │ └── typography.css │ ├── landing │ ├── colours.css │ ├── layout.css │ └── typography.css │ └── story │ ├── colours.css │ ├── layout.css │ └── typography.css ├── config ├── locales │ ├── activerecord.yml │ ├── en.yml │ ├── it.yml │ ├── numbers.yml │ ├── pt-BR.yml │ └── ru.yml └── routes.rb ├── db └── migrate │ ├── 20090303213012_create_iterations.rb │ ├── 20090304004418_create_stories.rb │ ├── 20090304193819_create_acceptance_criteria.rb │ ├── 20090310175005_create_burndown_data_points.rb │ ├── 20090325133236_create_story_team_members.rb │ └── 20090327110522_create_story_actions.rb ├── init.rb └── lib ├── my_controller_patch.rb ├── project_patch.rb └── user_patch.rb /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.5 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/helpers/acceptance_criteria_helper.rb: -------------------------------------------------------------------------------- 1 | module AcceptanceCriteriaHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/active_iterations_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveIterationsHelper 2 | end 3 | -------------------------------------------------------------------------------- /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/helpers/burndowns_helper.rb: -------------------------------------------------------------------------------- 1 | module BurndownsHelper 2 | end 3 | -------------------------------------------------------------------------------- /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/helpers/iterations_helper.rb: -------------------------------------------------------------------------------- 1 | module IterationsHelper 2 | end 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/helpers/story_team_members_helper.rb: -------------------------------------------------------------------------------- 1 | module StoryTeamMembersHelper 2 | end 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/models/story_action.rb: -------------------------------------------------------------------------------- 1 | class StoryAction < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :story 4 | belongs_to :iteration 5 | end 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /app/views/iterations/_list_item.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to h(list_item), [list_item.project, list_item] %> 3 |
  • 4 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/views/layouts/ea_base.html.erb: -------------------------------------------------------------------------------- 1 | <% inside_layout 'base' do -%> 2 | <% content_for :header_tags do %> 3 | <%= stylesheet_link_tag( 4 | 'story/layout', 5 | 'story/colours', 6 | 'story/typography', 7 | 8 | 'iteration/layout', 9 | 'iteration/typography', 10 | 'iteration/colours', 11 | 'iteration_planning/layout', 12 | 'iteration_planning/typography', 13 | 'iteration_planning/colours', 14 | 'iteration_active/layout', 15 | 'iteration_active/typography', 16 | 'iteration_active/colours', 17 | 18 | 'application/layout', 19 | 'application/colours', 20 | 'application/typography', 21 | :plugin => 'easy_agile') %> 22 | <% end %> 23 | 24 |
    25 | <% if controller.has_tabs? %> 26 | <% selected_tab = controller.tab_name %> 27 | 35 | <% end %> 36 |
    37 | <%= link_to l(:new_story), 38 | contextual_new_story_path, 39 | :accesskey => 'n', 40 | :id => 'contextual_new_story', 41 | :class => 'icon icon-new-task' %> 42 | <%= link_to l(:plan_iteration), 43 | new_project_iteration_path(@project), 44 | :accesskey => 'i', 45 | :id => 'contextual_new_iteration', 46 | :class => 'icon icon-plan-iteration' %> 47 | <%= link_to l(:dashboard), 48 | project_easy_agile_path(@project), 49 | :accesskey => 'd', 50 | :id => 'contextual_ea_dashboard', 51 | :class => 'icon icon-ea-dashboard' %> 52 |
    53 |
    54 | 55 |
    "> 57 |
    58 | 59 |

    <%= yield(:h2) %>

    60 | <% unless yield(:actions).blank? %> 61 | 64 | <% end %> 65 | 66 | <%= yield %> 67 |
    68 |
    69 | <% unless request.user_agent.include?('iPhone') %> 70 | <%= javascript_includes %> 71 | <% end %> 72 | <% end %> 73 | -------------------------------------------------------------------------------- /app/views/layouts/request.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= yield :h1 %>

    2 |
    3 | <%= yield %> 4 |
    5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/views/stories/_list_item.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to h(list_item), [list_item.project, list_item] %> 3 |
  • 4 | -------------------------------------------------------------------------------- /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/stories/_status_form.erb: -------------------------------------------------------------------------------- 1 |
      2 |
    1. 3 | <%= status_form.radio_button(:status, 'pending', 4 | :id => "story_status_pending_#{status_form.object.id}") %> 5 | <%= status_form.label "status_pending_#{status_form.object.id}", l(:status_pending) %> 6 |
    2. 7 |
    3. 8 | <%= status_form.radio_button(:status, 'in_progress', 9 | :id => "story_status_in_progress_#{status_form.object.id}") %> 10 | <%= status_form.label "status_in_progress_#{status_form.object.id}", l(:status_in_progress) %> 11 |
    4. 12 |
    5. 13 | <%= status_form.radio_button(:status, 'testing', 14 | :id => "story_status_testing_#{status_form.object.id}") %> 15 | <%= status_form.label "status_testing_#{status_form.object.id}", l(:status_testing) %> 16 |
    6. 17 |
    7. 18 | <%= status_form.radio_button(:status, 'complete', 19 | :id => "story_status_complete_#{status_form.object.id}") %> 20 | <%= status_form.label "status_complete_#{status_form.object.id}", l(:status_complete) %> 21 |
    8. 22 |
    23 |

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

    26 | -------------------------------------------------------------------------------- /app/views/stories/_story.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <% if body_classes.include?('iteration_planning') %> 3 |
    4 | <%= check_box_tag("stories[#{story.id}][include]", 5 | '1', 6 | story.iteration_id? || (@iteration && @iteration.stories.include?(story))) %> 7 | <%= label_tag "stories_#{story.id}_include", l(:include) %> 8 |
    9 | <% end %> 10 | 11 |
    12 |
    13 | <% if body_classes.include?('iteration_planning') %> 14 |

    <%= h(story) %>

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

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

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

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

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

    <%= l(:acceptance_criteria) %>

    30 | 35 |
    36 | <% end %> 37 | 38 | <% if body_classes.include?('iteration_active') %> 39 | <% form_for [story.project, story] do |form| %> 40 | <%= render :partial => 'stories/status_form', :object => form %> 41 | <% end %> 42 | <% end %> 43 | 44 | <% if controller.action_name == 'backlog' %> 45 | <%= render :partial => 'stories/prioritising_story', :object => story %> 46 | <% end %> 47 |
  • 48 | -------------------------------------------------------------------------------- /app/views/stories/backlog.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@project} #{l(:backlog)}" %> 2 | <% content_for :h2, h("#{@project} #{l(:backlog)}") %> 3 | 4 | <% form_for :project, @project, :url => { :action => 'edit' } do |f| %> 5 | 6 |

    <%= l(:backlog_drag_stories) %>

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

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

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

    <%= l(:next_steps) %>

    17 | 25 |
    26 | -------------------------------------------------------------------------------- /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/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/views/stories/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{l(:story_plural)} #{@project}" %> 2 | <% content_for :h2, h("#{l(:story_plural)} #{@project}") %> 3 | <% unless @project.stories.empty? %> 4 |
      5 | <% @project.stories.each do |story| %> 6 |
    1. <%= link_to h(story), [@project, story] %>
    2. 7 | <% end %> 8 |
    9 | <% end %> 10 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /app/views/stories/show.feature.erb: -------------------------------------------------------------------------------- 1 | <%= l(:feature) %>: <%= @story.name %> 2 | <%= @story.content.gsub(/^/, ' ') %> 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/views/stories/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "#{@story} | #{@project} " + l(:story_plural) %> 2 | <% content_for :h2, l(:story) + " ##{@story.id}" %> 3 | <% content_for :actions do %> 4 |
  • <%= link_to l(:button_edit), [:edit, @project, @story] %>
  • 5 |
  • <%= link_to l(:feature), project_story_path(@project, @story, :format => 'feature') %>
  • 6 | <% end %> 7 | 8 |
    9 |
    10 |
    11 |
    12 |

    <%= h @story %>

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

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

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

    <%= l(:story_team) %>

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

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

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

    <%= l(:acceptance_criteria) %>

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

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

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

    <%= l(:iteration) %>

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

    <%= l(:next_steps) %>

    70 | 78 | <% end %> 79 |
    80 | -------------------------------------------------------------------------------- /app/views/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/images/a_simple_tool_for_effective_agile_teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/a_simple_tool_for_effective_agile_teams.png -------------------------------------------------------------------------------- /assets/images/active_iteration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/active_iteration.png -------------------------------------------------------------------------------- /assets/images/arrow_from_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_from_grey.png -------------------------------------------------------------------------------- /assets/images/arrow_large_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_large_left.png -------------------------------------------------------------------------------- /assets/images/arrow_large_left_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_large_left_highlighted.png -------------------------------------------------------------------------------- /assets/images/arrow_large_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_large_right.png -------------------------------------------------------------------------------- /assets/images/arrow_large_right_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_large_right_highlighted.png -------------------------------------------------------------------------------- /assets/images/arrow_small_right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_small_right.gif -------------------------------------------------------------------------------- /assets/images/arrow_small_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_small_right.png -------------------------------------------------------------------------------- /assets/images/arrow_small_right_green_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_small_right_green_background.png -------------------------------------------------------------------------------- /assets/images/arrow_small_right_grey_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_small_right_grey_background.png -------------------------------------------------------------------------------- /assets/images/arrow_small_right_orange_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/arrow_small_right_orange_background.png -------------------------------------------------------------------------------- /assets/images/burndown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/burndown.png -------------------------------------------------------------------------------- /assets/images/button_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/button_background.png -------------------------------------------------------------------------------- /assets/images/fella_in_progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/fella_in_progress.gif -------------------------------------------------------------------------------- /assets/images/fella_in_progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/fella_in_progress.png -------------------------------------------------------------------------------- /assets/images/fella_testing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/fella_testing.gif -------------------------------------------------------------------------------- /assets/images/fella_testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/fella_testing.png -------------------------------------------------------------------------------- /assets/images/new_story.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/new_story.png -------------------------------------------------------------------------------- /assets/images/new_story_highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/new_story_highlight.png -------------------------------------------------------------------------------- /assets/images/planning_iteration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/planning_iteration.png -------------------------------------------------------------------------------- /assets/images/simply_agile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/simply_agile.png -------------------------------------------------------------------------------- /assets/images/simply_agile_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/simply_agile_large.png -------------------------------------------------------------------------------- /assets/images/simply_agile_twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/simply_agile_twitter.png -------------------------------------------------------------------------------- /assets/images/story_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/story_card.png -------------------------------------------------------------------------------- /assets/images/try_it_free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/try_it_free.png -------------------------------------------------------------------------------- /assets/images/underline_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SphereSoftware/easy_agile/29209453be2bb2da6411d477cb6e0068a03a0651/assets/images/underline_green.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // no JS support for shit browsers 3 | if (!$.support.boxModel) { 4 | var message = "

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

    "; 6 | 7 | if ($('.important_message')[0]) { 8 | $('.important_message div').prepend(message); 9 | } else { 10 | $('#content').after('
    '+message+'
    '); 11 | } 12 | 13 | $('.important_message') 14 | .css({position: 'absolute', textAlign: 'center', opacity:0.9}); 15 | $('.important_message div') 16 | .css({width:'auto', 17 | margin:0}); 18 | return false; 19 | } 20 | $('div.ea_container').addClass('javascript'); 21 | 22 | // highlight first erroneous field / auto focus field 23 | var first_error_field = $('.field_with_errors')[0]; 24 | if (first_error_field) first_error_field.focus(); 25 | else $('.auto_focus').focus(); 26 | 27 | // stories/show 28 | if ($('div#stories_show')) AcceptanceCriteria.init(); 29 | 30 | // iterations/new 31 | if ($('#stories_available')[0]) { 32 | // start swapper 33 | StorySwapper.init(); 34 | } 35 | 36 | // iterations/show when active 37 | if ($('div').hasClass('iteration_active')) { 38 | new DraggableStories(); 39 | // don't enhance stories 40 | } else if (!$('div#home_show')[0]) { 41 | // normal story enhancements 42 | $('#content .story').each( function() { new Story(this) }); 43 | } 44 | 45 | // backlog 46 | if ($('div#stories_backlog')[0]) { 47 | BacklogPrioritisation.init(); 48 | } 49 | 50 | }); 51 | 52 | // source: http://www.hunlock.com/blogs/Mastering_Javascript_Arrays 53 | Array.prototype.compare = function(testArr) { 54 | if (this.length != testArr.length) return false; 55 | for (var i = 0; i < testArr.length; i++) { 56 | if (this[i].compare) { 57 | if (!this[i].compare(testArr[i])) return false; 58 | } 59 | if (this[i] !== testArr[i]) return false; 60 | } 61 | return true; 62 | } 63 | 64 | // add header to AJAX requests to play nice with Rails' content negotiation 65 | jQuery.ajaxSetup({ 66 | 'beforeSend': function(xhr) { 67 | xhr.setRequestHeader("Accept", "text/javascript") 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/javascripts/iteration_active.js: -------------------------------------------------------------------------------- 1 | function objectsFromInput(input) { 2 | this.form = $(input).parents('form'); 3 | this.container = $('#draggables_for_'+this.form.attr('id')); 4 | this.li = $(input).parents('li'); 5 | } 6 | var Burndown = { 7 | refresh: function() { 8 | var location_parts, iteration_id; 9 | location_parts = location.href.split('/'); 10 | iteration_id = location_parts[location_parts.length - 1]; 11 | project_id = location_parts[location_parts.length - 3]; 12 | $('#burndown img').attr('src', 13 | '/projects/' + project_id + 14 | '/iterations/' + iteration_id + 15 | '/burndown?width=350&' + new Date().getTime()); 16 | } 17 | } 18 | function DraggableStories() { 19 | DraggableStories.labelColumns(); 20 | DraggableStories.create(); 21 | 22 | // handle resize event 23 | $(window).resize(function() { 24 | DraggableStories.create(); 25 | DraggableStories.recently_resized = true; 26 | setTimeout('DraggableStories.recently_resized = false', 100); 27 | }); 28 | } 29 | 30 | DraggableStories.statuses = []; 31 | 32 | DraggableStories.create = function() { 33 | var full_width, container, height; 34 | 35 | if (DraggableStories.recently_resized) return false; 36 | 37 | // size div 38 | full_width = $(window).width(); 39 | if (full_width < 831) { 40 | $('#burndown').hide(); 41 | full_width += 370; 42 | } else { 43 | $('#burndown').show(); 44 | } 45 | $('#stories').width(full_width - 430); 46 | 47 | // remove all existing JSy elements 48 | $('#draggables_container').remove(); 49 | 50 | // make a container for all draggables 51 | $('ol.stories').before('
    '); 52 | 53 | container = $('#draggables_container'); 54 | 55 | // make draggable container for each form 56 | $('ol.stories form').each( function() { 57 | container.append('
    '); 58 | }); 59 | 60 | // make droppables for each radio button 61 | DraggableStories.droppables = $.map($('input[name="story[status]"]'), function(input, i) { 62 | return new DroppableStatus(input); 63 | }); 64 | 65 | DraggableStories.draggables = $.map($('input[name="story[status]"]:checked'), function(input, i) { 66 | return new DraggableStory(input); 67 | }); 68 | 69 | // set height of each row to the height of the draggable content 70 | $('.draggables').each( function() { 71 | height = $(this).find('.story .story_content').height() + 9; 72 | 73 | $(this).height(height); 74 | $(this).find('.ui-droppable').height(height); 75 | }); 76 | 77 | // set positions on each draggable 78 | $(DraggableStories.draggables).each( function() { 79 | this.setPosition(); 80 | }); 81 | 82 | DraggableStories.scheduleRefresh(); 83 | } 84 | 85 | DraggableStories.scheduleRefresh = function() { 86 | clearTimeout(DraggableStories.timeout); 87 | if (DraggableStories.refreshedOnce) { 88 | DraggableStories.timeout = setTimeout('DraggableStories.refresh()', 5000); 89 | } else { 90 | DraggableStories.timeout = setTimeout('DraggableStories.refresh()', 0); 91 | DraggableStories.refreshedOnce = true; 92 | } 93 | } 94 | 95 | DraggableStories.refresh = function() { 96 | var url, stories, statuses, droppable, draggable_story; 97 | 98 | if (DraggableStories.refresh_lock) { 99 | DraggableStories.scheduleRefresh(); 100 | return false; 101 | } 102 | 103 | url = window.location.href.split('#')[0] + '/stories.json'; 104 | 105 | $.getJSON(url, 106 | function(stories) { 107 | statuses = $.map(stories, function(obj) { 108 | return obj.status; 109 | }); 110 | if (!DraggableStories.statuses.compare(statuses)) { 111 | DraggableStories.statuses = statuses; 112 | $(stories).each( function(i) { 113 | draggable_story = DraggableStories.draggables[i]; 114 | draggable_story.story.id = this.id; 115 | draggable_story.story.status = this.status; 116 | draggable_story.setStatus(); 117 | draggable_story.setPosition(); 118 | }); 119 | Burndown.refresh(); 120 | } 121 | } 122 | ); 123 | 124 | DraggableStories.scheduleRefresh(); 125 | return false; 126 | } 127 | 128 | // make headings based on first set of labels 129 | DraggableStories.labelColumns = function() { 130 | var html = '
      '; 131 | 132 | $($('form.edit_story')[0]).find('label').each( function() { 133 | var content = $(this).html(); 134 | var label_for = $(this).attr('for'); 135 | var class_name = $('#'+label_for).val(); 136 | 137 | html += '
    1. '+content+'
    2. '; 138 | }); 139 | html += '
    '; 140 | 141 | $('ol.stories').before(html); 142 | } 143 | 144 | function DraggableStory(input) { 145 | var id, id_parts, classes, droppable, droppable_position, objects, content, acceptance_criteria, container; 146 | 147 | this.input = input; 148 | id_parts = this.input.id.split('_'); 149 | 150 | this.story = { 151 | 'id': id_parts[id_parts.length - 1], 152 | 'status': $(input).val() 153 | } 154 | 155 | objects = new objectsFromInput(input); 156 | 157 | content = objects.li.find('.story_content'); 158 | acceptance_criteria = objects.li.find('.acceptance_criteria'); 159 | container = objects.container; 160 | 161 | droppable = this.droppable(); 162 | droppable_position = droppable.position(); 163 | droppable.addClass('ui-state-highlight'); 164 | 165 | classes = 'story'; 166 | if (objects.li.hasClass('with_team')) { 167 | classes += ' with_team'; 168 | } 169 | 170 | container.append('
    '+ 173 | content.html()+ 174 | '
    '); 175 | 176 | this.element = $('#draggable_' + this.input.id); 177 | 178 | if (acceptance_criteria[0]) { 179 | this.element.find('.story_content').append('
    '+ 180 | acceptance_criteria.html()+ 181 | '
    '); 182 | } 183 | 184 | this.element.draggable({ 185 | revert: 'invalid', 186 | axis: 'x', 187 | containment: 'parent', 188 | cursor: 'pointer' 189 | }) 190 | .css('position', 'absolute') 191 | .width(droppable.width()); 192 | 193 | this.setStatus(); 194 | } 195 | DraggableStory.prototype = { 196 | droppable: function() { 197 | return $('#droppable_story_status_' + this.story.status + '_' + this.story.id); 198 | }, 199 | 200 | setPosition: function() { 201 | var droppable_position = this.droppable().position(); 202 | this.element 203 | .css('top', droppable_position.top) 204 | .css('left', droppable_position.left); 205 | }, 206 | 207 | setStatus: function() { 208 | Story.setStatus(this.element, this.story.status); 209 | } 210 | } 211 | 212 | function DroppableStatus(input) { 213 | var instance = this; 214 | this.input = input; 215 | var objects = new objectsFromInput(input); 216 | this.form = objects.form; 217 | this.container = objects.container; 218 | this.li = objects.li; 219 | this.status = $(input).val(); 220 | 221 | this.container.append('
    '); 222 | 223 | this.droppable = $('#droppable_' + input.id); 224 | this.droppable 225 | .droppable({ 226 | drop: function(ev, ui) { 227 | var id_parts = instance.input.id.split('_'); 228 | var story_id = id_parts[id_parts.length - 1]; 229 | 230 | // set the refresh lock to avoid out-of-sync updates 231 | DraggableStories.refresh_lock = true; 232 | 233 | // check the radio button 234 | $('li#story_'+story_id+' ol input').val([instance.status]); 235 | 236 | // send the request 237 | instance.form.ajaxSubmit({ 238 | success: function() { 239 | DroppableStatus.previous_statuses[story_id] = instance.status; 240 | 241 | // clear refresh lock 242 | DraggableStories.refresh_lock = false; 243 | } 244 | }); 245 | 246 | // change class of elements 247 | var draggable = instance.container.find('.ui-draggable'); 248 | Story.setStatus(draggable, instance.status); 249 | 250 | // custom snapping 251 | $(ui.draggable) 252 | .css('left', $(this).position().left) 253 | .css('top', $(this).position().top); 254 | } 255 | }); 256 | } 257 | 258 | DroppableStatus.previous_statuses = {}; 259 | -------------------------------------------------------------------------------- /assets/javascripts/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/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);; -------------------------------------------------------------------------------- /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 = $('