Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

adding episode 210

  • Loading branch information...
commit 59dfd90f3d17fe2bc53803bace5d32c4bf616bd2 1 parent ece998d
Ryan Bates authored
Showing with 9,314 additions and 0 deletions.
  1. +10 −0 episode-210/README
  2. +4 −0 episode-210/todo/.gitignore
  3. +27 −0 episode-210/todo/Gemfile
  4. +1 −0  episode-210/todo/README
  5. +10 −0 episode-210/todo/Rakefile
  6. +15 −0 episode-210/todo/app/controllers/application_controller.rb
  7. +46 −0 episode-210/todo/app/controllers/projects_controller.rb
  8. +36 −0 episode-210/todo/app/controllers/tasks_controller.rb
  9. +3 −0  episode-210/todo/app/helpers/application_helper.rb
  10. +23 −0 episode-210/todo/app/helpers/layout_helper.rb
  11. +2 −0  episode-210/todo/app/helpers/projects_helper.rb
  12. +2 −0  episode-210/todo/app/helpers/tasks_helper.rb
  13. +3 −0  episode-210/todo/app/models/project.rb
  14. +3 −0  episode-210/todo/app/models/task.rb
  15. +9 −0 episode-210/todo/app/models/user.rb
  16. +12 −0 episode-210/todo/app/views/devise/confirmations/new.html.erb
  17. +5 −0 episode-210/todo/app/views/devise/mailer/confirmation_instructions.html.erb
  18. +8 −0 episode-210/todo/app/views/devise/mailer/reset_password_instructions.html.erb
  19. +7 −0 episode-210/todo/app/views/devise/mailer/unlock_instructions.html.erb
  20. +16 −0 episode-210/todo/app/views/devise/passwords/edit.html.erb
  21. +12 −0 episode-210/todo/app/views/devise/passwords/new.html.erb
  22. +25 −0 episode-210/todo/app/views/devise/registrations/edit.html.erb
  23. +20 −0 episode-210/todo/app/views/devise/registrations/new.html.erb
  24. +17 −0 episode-210/todo/app/views/devise/sessions/new.html.erb
  25. +19 −0 episode-210/todo/app/views/devise/shared/_links.erb
  26. +12 −0 episode-210/todo/app/views/devise/unlocks/new.html.erb
  27. +33 −0 episode-210/todo/app/views/layouts/application.html.erb
  28. +8 −0 episode-210/todo/app/views/projects/_form.html.erb
  29. +8 −0 episode-210/todo/app/views/projects/edit.html.erb
  30. +16 −0 episode-210/todo/app/views/projects/index.html.erb
  31. +5 −0 episode-210/todo/app/views/projects/new.html.erb
  32. +17 −0 episode-210/todo/app/views/projects/show.html.erb
  33. +13 −0 episode-210/todo/app/views/tasks/_form.html.erb
  34. +4 −0 episode-210/todo/app/views/tasks/edit.html.erb
  35. +4 −0 episode-210/todo/app/views/tasks/new.html.erb
  36. +4 −0 episode-210/todo/config.ru
  37. +44 −0 episode-210/todo/config/application.rb
  38. +14 −0 episode-210/todo/config/boot.rb
  39. +22 −0 episode-210/todo/config/database.yml
  40. +5 −0 episode-210/todo/config/environment.rb
  41. +21 −0 episode-210/todo/config/environments/development.rb
  42. +42 −0 episode-210/todo/config/environments/production.rb
  43. +29 −0 episode-210/todo/config/environments/test.rb
  44. +7 −0 episode-210/todo/config/initializers/backtrace_silencers.rb
  45. +6 −0 episode-210/todo/config/initializers/cookie_verification_secret.rb
  46. +116 −0 episode-210/todo/config/initializers/devise.rb
  47. +10 −0 episode-210/todo/config/initializers/inflections.rb
  48. +5 −0 episode-210/todo/config/initializers/mime_types.rb
  49. +10 −0 episode-210/todo/config/initializers/session_store.rb
  50. +36 −0 episode-210/todo/config/locales/devise.en.yml
  51. +5 −0 episode-210/todo/config/locales/en.yml
  52. +63 −0 episode-210/todo/config/routes.rb
  53. +13 −0 episode-210/todo/db/migrate/20100412041830_create_projects.rb
  54. +15 −0 episode-210/todo/db/migrate/20100412041924_create_tasks.rb
  55. +23 −0 episode-210/todo/db/migrate/20100412062912_devise_create_users.rb
  56. +9 −0 episode-210/todo/db/migrate/20100419054203_add_username_to_users.rb
  57. +48 −0 episode-210/todo/db/schema.rb
  58. +7 −0 episode-210/todo/db/seeds.rb
  59. +2 −0  episode-210/todo/doc/README_FOR_APP
  60. +1 −0  episode-210/todo/lib/generators
  61. 0  episode-210/todo/lib/tasks/.gitkeep
  62. +26 −0 episode-210/todo/public/404.html
  63. +26 −0 episode-210/todo/public/422.html
  64. +26 −0 episode-210/todo/public/500.html
  65. 0  episode-210/todo/public/favicon.ico
  66. +2 −0  episode-210/todo/public/javascripts/application.js
  67. +965 −0 episode-210/todo/public/javascripts/controls.js
  68. +974 −0 episode-210/todo/public/javascripts/dragdrop.js
  69. +1,123 −0 episode-210/todo/public/javascripts/effects.js
  70. +4,874 −0 episode-210/todo/public/javascripts/prototype.js
  71. +109 −0 episode-210/todo/public/javascripts/rails.js
  72. +5 −0 episode-210/todo/public/robots.txt
  73. 0  episode-210/todo/public/stylesheets/.gitkeep
  74. +88 −0 episode-210/todo/public/stylesheets/application.css
  75. +9 −0 episode-210/todo/script/rails
  76. +7 −0 episode-210/todo/test/fixtures/projects.yml
  77. +11 −0 episode-210/todo/test/fixtures/tasks.yml
  78. +11 −0 episode-210/todo/test/fixtures/users.yml
  79. +9 −0 episode-210/todo/test/performance/browsing_test.rb
  80. +13 −0 episode-210/todo/test/test_helper.rb
  81. +8 −0 episode-210/todo/test/unit/project_test.rb
  82. +8 −0 episode-210/todo/test/unit/task_test.rb
  83. +8 −0 episode-210/todo/test/unit/user_test.rb
  84. 0  episode-210/todo/vendor/plugins/.gitkeep
10 episode-210/README
View
@@ -0,0 +1,10 @@
+Railscasts Episode #210: Customizing Devise
+
+http://railscasts.com/episodes/210
+
+Commands
+
+ rails generate devise_views
+ rails generate migration add_username_to_users username:string
+ rake db:migrate
+ rails c
4 episode-210/todo/.gitignore
View
@@ -0,0 +1,4 @@
+.bundle
+db/*.sqlite3
+log/*.log
+tmp/**/*
27 episode-210/todo/Gemfile
View
@@ -0,0 +1,27 @@
+source 'http://rubygems.org'
+
+gem 'rails', '3.0.0.beta2'
+
+# Bundle edge Rails instead:
+# gem 'rails', :git => 'git://github.com/rails/rails.git'
+
+gem 'sqlite3-ruby', :require => 'sqlite3'
+gem 'devise', '1.1.rc0'
+
+# Use unicorn as the web server
+# gem 'unicorn'
+
+# Deploy with Capistrano
+# gem 'capistrano'
+
+# Bundle the extra gems:
+# gem 'bj'
+# gem 'nokogiri', '1.4.1'
+# gem 'sqlite3-ruby', :require => 'sqlite3'
+# gem 'aws-s3', :require => 'aws/s3'
+
+# Bundle gems for certain environments:
+# gem 'rspec', :group => :test
+# group :test do
+# gem 'webrat'
+# end
1  episode-210/todo/README
View
@@ -0,0 +1 @@
+Example rails application used in Railscasts.
10 episode-210/todo/Rakefile
View
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require File.expand_path('../config/application', __FILE__)
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+Rails::Application.load_tasks
15 episode-210/todo/app/controllers/application_controller.rb
View
@@ -0,0 +1,15 @@
+# Filters added to this controller apply to all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+class ApplicationController < ActionController::Base
+ helper :all # include all helpers, all the time
+
+ # See ActionController::RequestForgeryProtection for details
+ # Uncomment the :secret if you're not using the cookie session store
+ protect_from_forgery # :secret => '9ae178eac1fbbd4062a93c8c558a12ef'
+
+ # See ActionController::Base for details
+ # Uncomment this to filter the contents of submitted sensitive data parameters
+ # from your application log (in this case, all fields with names like "password").
+ # filter_parameter_logging :password
+end
46 episode-210/todo/app/controllers/projects_controller.rb
View
@@ -0,0 +1,46 @@
+class ProjectsController < ApplicationController
+ before_filter :authenticate_user!, :except => [:show, :index]
+
+ def index
+ @projects = Project.find(:all)
+ end
+
+ def show
+ @project = Project.find(params[:id])
+ end
+
+ def new
+ @project = Project.new
+ end
+
+ def create
+ @project = Project.new(params[:project])
+ if @project.save
+ flash[:notice] = "Successfully created project."
+ redirect_to @project
+ else
+ render :action => 'new'
+ end
+ end
+
+ def edit
+ @project = Project.find(params[:id])
+ end
+
+ def update
+ @project = Project.find(params[:id])
+ if @project.update_attributes(params[:project])
+ flash[:notice] = "Successfully updated project."
+ redirect_to @project
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def destroy
+ @project = Project.find(params[:id])
+ @project.destroy
+ flash[:notice] = "Successfully destroyed project."
+ redirect_to projects_url
+ end
+end
36 episode-210/todo/app/controllers/tasks_controller.rb
View
@@ -0,0 +1,36 @@
+class TasksController < ApplicationController
+ def new
+ @task = Task.new(:project_id => params[:project_id])
+ end
+
+ def create
+ @task = Task.new(params[:task])
+ if @task.save
+ flash[:notice] = "Successfully created task."
+ redirect_to @task.project
+ else
+ render :action => 'new'
+ end
+ end
+
+ def edit
+ @task = Task.find(params[:id])
+ end
+
+ def update
+ @task = Task.find(params[:id])
+ if @task.update_attributes(params[:task])
+ flash[:notice] = "Successfully updated task."
+ redirect_to @task.project
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def destroy
+ @task = Task.find(params[:id])
+ @task.destroy
+ flash[:notice] = "Successfully destroyed task."
+ redirect_to @task.project
+ end
+end
3  episode-210/todo/app/helpers/application_helper.rb
View
@@ -0,0 +1,3 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+end
23 episode-210/todo/app/helpers/layout_helper.rb
View
@@ -0,0 +1,23 @@
+# These helper methods can be called in your template to set variables to be used in the layout
+# This module should be included in all views globally,
+# to do so you may need to add this line to your ApplicationController
+# helper :layout
+module LayoutHelper
+ def title(page_title, show_title = true)
+ content_for(:title) { page_title.to_s }
+ @show_title = show_title
+ end
+
+ def show_title?
+ @show_title
+ end
+
+ def stylesheet(*args)
+ content_for(:head) { stylesheet_link_tag(*args.map(&:to_s)) }
+ end
+
+ def javascript(*args)
+ args = args.map { |arg| arg == :defaults ? arg : arg.to_s }
+ content_for(:head) { javascript_include_tag(*args) }
+ end
+end
2  episode-210/todo/app/helpers/projects_helper.rb
View
@@ -0,0 +1,2 @@
+module ProjectsHelper
+end
2  episode-210/todo/app/helpers/tasks_helper.rb
View
@@ -0,0 +1,2 @@
+module TasksHelper
+end
3  episode-210/todo/app/models/project.rb
View
@@ -0,0 +1,3 @@
+class Project < ActiveRecord::Base
+ has_many :tasks
+end
3  episode-210/todo/app/models/task.rb
View
@@ -0,0 +1,3 @@
+class Task < ActiveRecord::Base
+ belongs_to :project
+end
9 episode-210/todo/app/models/user.rb
View
@@ -0,0 +1,9 @@
+class User < ActiveRecord::Base
+ # Include default devise modules. Others available are:
+ # :token_authenticatable, :lockable, :timeoutable, :confirmable and :activatable
+ devise :database_authenticatable, :registerable,
+ :recoverable, :rememberable, :trackable, :validatable
+
+ # Setup accessible (or protected) attributes for your model
+ attr_accessible :email, :password, :password_confirmation
+end
12 episode-210/todo/app/views/devise/confirmations/new.html.erb
View
@@ -0,0 +1,12 @@
+<h2>Resend confirmation instructions</h2>
+
+<%= form_for(resource_name, resource, :url => confirmation_path(resource_name)) do |f| %>
+ <%= f.error_messages %>
+
+ <p><%= f.label :email %></p>
+ <p><%= f.text_field :email %></p>
+
+ <p><%= f.submit "Resend confirmation instructions" %></p>
+<% end %>
+
+<%= render :partial => "devise/shared/links" %>
5 episode-210/todo/app/views/devise/mailer/confirmation_instructions.html.erb
View
@@ -0,0 +1,5 @@
+<p>Welcome <%= @resource.email %>!</p>
+
+<p>You can confirm your account through the link below:</p>
+
+<p><%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %></p>
8 episode-210/todo/app/views/devise/mailer/reset_password_instructions.html.erb
View
@@ -0,0 +1,8 @@
+<p>Hello <%= @resource.email %>!</p>
+
+<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
+
+<p><%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %></p>
+
+<p>If you didn't request this, please ignore this email.</p>
+<p>Your password won't change until you access the link above and create a new one.</p>
7 episode-210/todo/app/views/devise/mailer/unlock_instructions.html.erb
View
@@ -0,0 +1,7 @@
+<p>Hello <%= @resource.email %>!</p>
+
+<p>Your account has been locked due to an excessive amount of unsuccessful sign in attempts.</p>
+
+<p>Click the link below to unlock your account:</p>
+
+<p><%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @resource.unlock_token) %></p>
16 episode-210/todo/app/views/devise/passwords/edit.html.erb
View
@@ -0,0 +1,16 @@
+<h2>Change your password</h2>
+
+<%= form_for(resource_name, resource, :url => password_path(resource_name), :html => { :method => :put }) do |f| %>
+ <%= f.error_messages %>
+ <%= f.hidden_field :reset_password_token %>
+
+ <p><%= f.label :password %></p>
+ <p><%= f.password_field :password %></p>
+
+ <p><%= f.label :password_confirmation %></p>
+ <p><%= f.password_field :password_confirmation %></p>
+
+ <p><%= f.submit "Change my password" %></p>
+<% end %>
+
+<%= render :partial => "devise/shared/links" %>
12 episode-210/todo/app/views/devise/passwords/new.html.erb
View
@@ -0,0 +1,12 @@
+<h2>Forgot your password?</h2>
+
+<%= form_for(resource_name, resource, :url => password_path(resource_name)) do |f| %>
+ <%= f.error_messages %>
+
+ <p><%= f.label :email %></p>
+ <p><%= f.text_field :email %></p>
+
+ <p><%= f.submit "Send me reset password instructions" %></p>
+<% end %>
+
+<%= render :partial => "devise/shared/links" %>
25 episode-210/todo/app/views/devise/registrations/edit.html.erb
View
@@ -0,0 +1,25 @@
+<h2>Edit <%= resource_name.to_s.humanize %></h2>
+
+<%= form_for(resource_name, resource, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %>
+ <%= f.error_messages %>
+
+ <p><%= f.label :email %></p>
+ <p><%= f.text_field :email %></p>
+
+ <p><%= f.label :password %> <i>(leave blank if you don't want to change it)</i></p>
+ <p><%= f.password_field :password %></p>
+
+ <p><%= f.label :password_confirmation %></p>
+ <p><%= f.password_field :password_confirmation %></p>
+
+ <p><%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i></p>
+ <p><%= f.password_field :current_password %></p>
+
+ <p><%= f.submit "Update" %></p>
+<% end %>
+
+<h3>Cancel my account</h3>
+
+<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :confirm => "Are you sure?", :method => :delete %>.</p>
+
+<%= link_to "Back", :back %>
20 episode-210/todo/app/views/devise/registrations/new.html.erb
View
@@ -0,0 +1,20 @@
+<h2>Sign up</h2>
+
+<%= form_for(resource_name, resource, :url => registration_path(resource_name)) do |f| %>
+ <%= f.error_messages %>
+ <p><%= f.label :username %></p>
+ <p><%= f.text_field :username %></p>
+
+ <p><%= f.label :email %></p>
+ <p><%= f.text_field :email %></p>
+
+ <p><%= f.label :password %></p>
+ <p><%= f.password_field :password %></p>
+
+ <p><%= f.label :password_confirmation %></p>
+ <p><%= f.password_field :password_confirmation %></p>
+
+ <p><%= f.submit "Sign up" %></p>
+<% end %>
+
+<%= render :partial => "devise/shared/links" %>
17 episode-210/todo/app/views/devise/sessions/new.html.erb
View
@@ -0,0 +1,17 @@
+<% title "Sign in" %>
+
+<%= form_for(resource_name, resource, :url => session_path(resource_name)) do |f| %>
+ <p><%= f.label :username %><br />
+ <%= f.text_field :username %></p>
+
+ <p><%= f.label :password %><br />
+ <%= f.password_field :password %></p>
+
+ <% if devise_mapping.rememberable? -%>
+ <p><%= f.check_box :remember_me %> <%= f.label :remember_me %></p>
+ <% end -%>
+
+ <p><%= f.submit "Sign in" %></p>
+<% end %>
+
+<%= render :partial => "devise/shared/links" %>
19 episode-210/todo/app/views/devise/shared/_links.erb
View
@@ -0,0 +1,19 @@
+<%- if controller_name != 'sessions' %>
+ <%= link_to "Sign in", new_session_path(resource_name) %><br />
+<% end -%>
+
+<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
+ <%= link_to "Sign up", new_registration_path(resource_name) %><br />
+<% end -%>
+
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
+ <%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
+<% end -%>
+
+<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
+ <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
+<% end -%>
+
+<%- if devise_mapping.lockable? && controller_name != 'unlocks' %>
+ <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
+<% end -%>
12 episode-210/todo/app/views/devise/unlocks/new.html.erb
View
@@ -0,0 +1,12 @@
+<h2>Resend unlock instructions</h2>
+
+<%= form_for(resource_name, resource, :url => unlock_path(resource_name)) do |f| %>
+ <%= f.error_messages %>
+
+ <p><%= f.label :email %></p>
+ <p><%= f.text_field :email %></p>
+
+ <p><%= f.submit "Resend unlock instructions" %></p>
+<% end %>
+
+<%= render :partial => "devise/shared/links" %>
33 episode-210/todo/app/views/layouts/application.html.erb
View
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+ <title><%= yield(:title) || "Untitled" %></title>
+ <%= stylesheet_link_tag 'application' %>
+ <%= javascript_include_tag :defaults %>
+ <%= csrf_meta_tag %>
+ <%= yield(:head) %>
+ </head>
+ <body>
+ <div id="container">
+ <div id="user_nav">
+ <% if user_signed_in? %>
+ Signed in as <%= current_user.email %>. Not you?
+ <%= link_to "Sign out", destroy_user_session_path %>
+ <% else %>
+ <%= link_to "Sign up", new_user_registration_path %> or <%= link_to "sign in", new_user_session_path %>
+ <% end %>
+ </div>
+
+ <% flash.each do |name, msg| %>
+ <%= content_tag :div, msg, :id => "flash_#{name}" %>
+ <% end %>
+
+ <% if show_title? %>
+ <h1><%= yield(:title) %></h1>
+ <% end %>
+
+ <%= yield %>
+ </div>
+ </body>
+</html>
8 episode-210/todo/app/views/projects/_form.html.erb
View
@@ -0,0 +1,8 @@
+<%= error_messages_for :project %>
+<%= form_for @project do |f| %>
+ <p>
+ <%= f.label :name %><br />
+ <%= f.text_field :name %>
+ </p>
+ <p><%= f.submit %></p>
+<% end %>
8 episode-210/todo/app/views/projects/edit.html.erb
View
@@ -0,0 +1,8 @@
+<% title "Edit Project" %>
+
+<%= render :partial => 'form' %>
+
+<p>
+ <%= link_to "Show", @project %> |
+ <%= link_to "View All", projects_path %>
+</p>
16 episode-210/todo/app/views/projects/index.html.erb
View
@@ -0,0 +1,16 @@
+<% title "Projects" %>
+
+<table>
+ <tr>
+ <th>Name</th>
+ </tr>
+ <% for project in @projects %>
+ <tr>
+ <td><%= link_to project.name, project %></td>
+ <td><%= link_to "Edit", edit_project_path(project) %></td>
+ <td><%= link_to "Destroy", project, :confirm => 'Are you sure?', :method => :delete %></td>
+ </tr>
+ <% end %>
+</table>
+
+<p><%= link_to "New Project", new_project_path %></p>
5 episode-210/todo/app/views/projects/new.html.erb
View
@@ -0,0 +1,5 @@
+<% title "New Project" %>
+
+<%= render :partial => 'form' %>
+
+<p><%= link_to "Back to List", projects_path %></p>
17 episode-210/todo/app/views/projects/show.html.erb
View
@@ -0,0 +1,17 @@
+<% title @project.name %>
+
+<h2>Tasks</h2>
+<ul>
+ <% for task in @project.tasks %>
+ <li>
+ <%= task.name %>
+ (<%= link_to "Edit", edit_task_path(task) %> |
+ <%= link_to "Destroy", task, :confirm => "Are you sure?", :method => :delete %>)
+ </li>
+ <% end %>
+</ul>
+
+<p>
+ <%= link_to "New Task", new_task_path(:project_id => @project) %> |
+ <%= link_to "View All Projects", projects_path %>
+</p>
13 episode-210/todo/app/views/tasks/_form.html.erb
View
@@ -0,0 +1,13 @@
+<%= error_messages_for :task %>
+<%= form_for @task do |f| %>
+ <%= f.hidden_field :project_id %>
+ <p>
+ <%= f.label :name %><br />
+ <%= f.text_field :name %>
+ </p>
+ <p>
+ <%= f.label :completed_at %><br />
+ <%= f.date_select :completed_at %>
+ </p>
+ <p><%= f.submit %></p>
+<% end %>
4 episode-210/todo/app/views/tasks/edit.html.erb
View
@@ -0,0 +1,4 @@
+<% title "Edit Task" %>
+
+<%= render :partial => 'form' %>
+
4 episode-210/todo/app/views/tasks/new.html.erb
View
@@ -0,0 +1,4 @@
+<% title "New Task" %>
+
+<%= render :partial => 'form' %>
+
4 episode-210/todo/config.ru
View
@@ -0,0 +1,4 @@
+# This file is used by Rack-based servers to start the application.
+
+require ::File.expand_path('../config/environment', __FILE__)
+run Todo::Application
44 episode-210/todo/config/application.rb
View
@@ -0,0 +1,44 @@
+require File.expand_path('../boot', __FILE__)
+
+require 'rails/all'
+
+# If you have a Gemfile, require the gems listed there, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(:default, Rails.env) if defined?(Bundler)
+
+module Todo
+ class Application < Rails::Application
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+
+ # Add additional load paths for your own custom dirs
+ # config.load_paths += %W( #{config.root}/extras )
+
+ # Only load the plugins named here, in the order given (default is alphabetical).
+ # :all can be used as a placeholder for all plugins not explicitly named
+ # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
+
+ # Activate observers that should always be running
+ # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
+
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
+ # config.time_zone = 'Central Time (US & Canada)'
+
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+ # config.i18n.default_locale = :de
+
+ # Configure generators values. Many other options are available, be sure to check the documentation.
+ # config.generators do |g|
+ # g.orm :active_record
+ # g.template_engine :erb
+ # g.test_framework :test_unit, :fixture => true
+ # end
+
+ # Configure sensitive parameters which will be filtered from the log file.
+ config.filter_parameters << :password
+ config.cookie_secret = '980734b832ad1ba050aeba757e50dcddd47e6a137eaa23286234f6a81504a8fdc818fcbf564ceceb01c2154add61930807a17b8876dfed5c6da98217627202a1'
+ end
+end
14 episode-210/todo/config/boot.rb
View
@@ -0,0 +1,14 @@
+# Use locked gems if present.
+begin
+ require File.expand_path('../../.bundle/environment', __FILE__)
+
+rescue LoadError
+ # Otherwise, use RubyGems.
+ require 'rubygems'
+
+ # And set up the gems listed in the Gemfile.
+ if File.exist?(File.expand_path('../../Gemfile', __FILE__))
+ require 'bundler'
+ Bundler.setup
+ end
+end
22 episode-210/todo/config/database.yml
View
@@ -0,0 +1,22 @@
+# SQLite version 3.x
+# gem install sqlite3-ruby (not necessary on OS X Leopard)
+development:
+ adapter: sqlite3
+ database: db/development.sqlite3
+ pool: 5
+ timeout: 5000
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ adapter: sqlite3
+ database: db/test.sqlite3
+ pool: 5
+ timeout: 5000
+
+production:
+ adapter: sqlite3
+ database: db/production.sqlite3
+ pool: 5
+ timeout: 5000
5 episode-210/todo/config/environment.rb
View
@@ -0,0 +1,5 @@
+# Load the rails application
+require File.expand_path('../application', __FILE__)
+
+# Initialize the rails application
+Todo::Application.initialize!
21 episode-210/todo/config/environments/development.rb
View
@@ -0,0 +1,21 @@
+Todo::Application.configure do
+ # Settings specified here will take precedence over those in config/environment.rb
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the webserver when you make code changes.
+ config.cache_classes = false
+
+ # Log error messages when you accidentally call methods on nil.
+ config.whiny_nils = true
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_view.debug_rjs = true
+ config.action_controller.perform_caching = false
+
+ # Don't care if the mailer can't send
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.default_url_options = { :host => 'localhost:3000' }
+end
42 episode-210/todo/config/environments/production.rb
View
@@ -0,0 +1,42 @@
+Todo::Application.configure do
+ # Settings specified here will take precedence over those in config/environment.rb
+
+ # The production environment is meant for finished, "live" apps.
+ # Code is not reloaded between requests
+ config.cache_classes = true
+
+ # Full error reports are disabled and caching is turned on
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Specifies the header that your server uses for sending files
+ config.action_dispatch.x_sendfile_header = "X-Sendfile"
+
+ # For nginx:
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
+
+ # If you have no front-end server that supports something like X-Sendfile,
+ # just comment this out and Rails will serve the files
+
+ # See everything in the log (default is :info)
+ # config.log_level = :debug
+
+ # Use a different logger for distributed setups
+ # config.logger = SyslogLogger.new
+
+ # Use a different cache store in production
+ # config.cache_store = :mem_cache_store
+
+ # Disable Rails's static asset server
+ # In production, Apache or nginx will already do this
+ config.serve_static_assets = false
+
+ # Enable serving of images, stylesheets, and javascripts from an asset server
+ # config.action_controller.asset_host = "http://assets.example.com"
+
+ # Disable delivery errors, bad email addresses will be ignored
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable threaded mode
+ # config.threadsafe!
+end
29 episode-210/todo/config/environments/test.rb
View
@@ -0,0 +1,29 @@
+Todo::Application.configure do
+ # Settings specified here will take precedence over those in config/environment.rb
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Log error messages when you accidentally call methods on nil.
+ config.whiny_nils = true
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Disable request forgery protection in test environment
+ config.action_controller.allow_forgery_protection = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
+ # like if you have constraints or database-specific column types
+ # config.active_record.schema_format = :sql
+end
7 episode-210/todo/config/initializers/backtrace_silencers.rb
View
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
6 episode-210/todo/config/initializers/cookie_verification_secret.rb
View
@@ -0,0 +1,6 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
116 episode-210/todo/config/initializers/devise.rb
View
@@ -0,0 +1,116 @@
+# Use this hook to configure devise mailer, warden hooks and so forth. The first
+# four configuration values can also be set straight in your models.
+Devise.setup do |config|
+ # Configure the e-mail address which will be shown in DeviseMailer.
+ config.mailer_sender = "please-change-me@config-initializers-devise.com"
+
+ # ==> Configuration for any authentication mechanism
+ # Configure which keys are used when authenticating an user. By default is
+ # just :email. You can configure it to use [:username, :subdomain], so for
+ # authenticating an user, both parameters are required. Remember that those
+ # parameters are used only when authenticating and not when retrieving from
+ # session. If you need permissions, you should implement that in a before filter.
+ config.authentication_keys = [ :username ]
+
+ # Tell if authentication through request.params is enabled. True by default.
+ # config.params_authenticatable = true
+
+ # Tell if authentication through HTTP Basic Auth is enabled. True by default.
+ # config.http_authenticatable = true
+
+ # The realm used in Http Basic Authentication
+ # config.http_authentication_realm = "Application"
+
+ # ==> Configuration for :database_authenticatable
+ # Invoke `rake secret` and use the printed value to setup a pepper to generate
+ # the encrypted password. By default no pepper is used.
+ # config.pepper = "rake secret output"
+
+ # Configure how many times you want the password is reencrypted. Default is 10.
+ # config.stretches = 10
+
+ # Define which will be the encryption algorithm. Supported algorithms are :sha1
+ # (default), :sha512 and :bcrypt. Devise also supports encryptors from others
+ # authentication tools as :clearance_sha1, :authlogic_sha512 (then you should set
+ # stretches above to 20 for default behavior) and :restful_authentication_sha1
+ # (then you should set stretches to 10, and copy REST_AUTH_SITE_KEY to pepper)
+ # config.encryptor = :sha1
+
+ # ==> Configuration for :confirmable
+ # The time you want give to your user to confirm his account. During this time
+ # he will be able to access your application without confirming. Default is nil.
+ # config.confirm_within = 2.days
+
+ # ==> Configuration for :rememberable
+ # The time the user will be remembered without asking for credentials again.
+ # config.remember_for = 2.weeks
+
+ # ==> Configuration for :validatable
+ # Range for password length
+ config.password_length = 4..20
+
+ # Regex to use to validate the email address
+ # config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i
+
+ # ==> Configuration for :timeoutable
+ # The time you want to timeout the user session without activity. After this
+ # time the user will be asked for credentials again.
+ # config.timeout_in = 10.minutes
+
+ # ==> Configuration for :lockable
+ # Defines which strategy will be used to lock an account.
+ # :failed_attempts = Locks an account after a number of failed attempts to sign in.
+ # :none = No lock strategy. You should handle locking by yourself.
+ # config.lock_strategy = :failed_attempts
+
+ # Defines which strategy will be used to unlock an account.
+ # :email = Sends an unlock link to the user email
+ # :time = Reanables login after a certain ammount of time (see :unlock_in below)
+ # :both = Enables both strategies
+ # :none = No unlock strategy. You should handle unlocking by yourself.
+ # config.unlock_strategy = :both
+
+ # Number of authentication tries before locking an account if lock_strategy
+ # is failed attempts.
+ # config.maximum_attempts = 20
+
+ # Time interval to unlock the account if :time is enabled as unlock_strategy.
+ # config.unlock_in = 1.hour
+
+ # ==> Configuration for :token_authenticatable
+ # Defines name of the authentication token params key
+ # config.token_authentication_key = :auth_token
+
+ # ==> General configuration
+ # Load and configure the ORM. Supports :active_record (default), :mongoid
+ # (requires mongo_ext installed) and :data_mapper (experimental).
+ require 'devise/orm/active_record'
+
+ # Turn scoped views on. Before rendering "sessions/new", it will first check for
+ # "sessions/users/new". It's turned off by default because it's slower if you
+ # are using only default views.
+ # config.scoped_views = true
+
+ # By default, devise detects the role accessed based on the url. So whenever
+ # accessing "/users/sign_in", it knows you are accessing an User. This makes
+ # routes as "/sign_in" not possible, unless you tell Devise to use the default
+ # scope, setting true below.
+ # config.use_default_scope = true
+
+ # Configure the default scope used by Devise. By default it's the first devise
+ # role declared in your routes.
+ # config.default_scope = :user
+
+ # If you want to use other strategies, that are not (yet) supported by Devise,
+ # you can configure them inside the config.warden block. The example below
+ # allows you to setup OAuth, using http://github.com/roman/warden_oauth
+ #
+ # config.warden do |manager|
+ # manager.oauth(:twitter) do |twitter|
+ # twitter.consumer_secret = <YOUR CONSUMER SECRET>
+ # twitter.consumer_key = <YOUR CONSUMER KEY>
+ # twitter.options :site => 'http://twitter.com'
+ # end
+ # manager.default_strategies(:scope => :user).unshift :twitter_oauth
+ # end
+end
10 episode-210/todo/config/initializers/inflections.rb
View
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format
+# (all these examples are active by default):
+# ActiveSupport::Inflector.inflections do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
5 episode-210/todo/config/initializers/mime_types.rb
View
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
+# Mime::Type.register_alias "text/html", :iphone
10 episode-210/todo/config/initializers/session_store.rb
View
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file.
+
+Rails.application.config.session_store :cookie_store, {
+ :key => '_todo_session',
+}
+
+# Use the database for sessions instead of the cookie-based default,
+# which shouldn't be used to store highly confidential information
+# (create the session table with "rake db:sessions:create")
+# Rails.application.config.session_store :active_record_store
36 episode-210/todo/config/locales/devise.en.yml
View
@@ -0,0 +1,36 @@
+en:
+ errors:
+ messages:
+ not_found: "not found"
+ already_confirmed: "was already confirmed"
+ not_locked: "was not locked"
+
+ devise:
+ failure:
+ unauthenticated: 'You need to sign in or sign up before continuing.'
+ unconfirmed: 'You have to confirm your account before continuing.'
+ locked: 'Your account is locked.'
+ invalid: 'Invalid email address or password.'
+ invalid_token: 'Invalid authentication token.'
+ timeout: 'Your session expired, please sign in again to continue.'
+ inactive: 'Your account was not activated yet.'
+ sessions:
+ signed_in: 'Signed in successfully.'
+ signed_out: 'Signed out successfully.'
+ passwords:
+ send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.'
+ updated: 'Your password was changed successfully. You are now signed in.'
+ confirmations:
+ send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.'
+ confirmed: 'Your account was successfully confirmed. You are now signed in.'
+ registrations:
+ signed_up: 'You have signed up successfully.'
+ updated: 'You updated your account successfully.'
+ destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
+ unlocks:
+ send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
+ unlocked: 'Your account was successfully unlocked. You are now signed in.'
+ mailer:
+ confirmation_instructions: 'Confirmation instructions'
+ reset_password_instructions: 'Reset password instructions'
+ unlock_instructions: 'Unlock Instructions'
5 episode-210/todo/config/locales/en.yml
View
@@ -0,0 +1,5 @@
+# Sample localization file for English. Add more files in this directory for other locales.
+# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+
+en:
+ hello: "Hello world"
63 episode-210/todo/config/routes.rb
View
@@ -0,0 +1,63 @@
+Todo::Application.routes.draw do |map|
+ devise_for :users, :path_names => { :sign_up => "register" }
+
+ resources :projects, :tasks
+ root :to => "projects#index"
+
+ # The priority is based upon order of creation:
+ # first created -> highest priority.
+
+ # Sample of regular route:
+ # match 'products/:id' => 'catalog#view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ # Sample of named route:
+ # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
+ # This route can be invoked with purchase_url(:id => product.id)
+
+ # Sample resource route (maps HTTP verbs to controller actions automatically):
+ # resources :products
+
+ # Sample resource route with options:
+ # resources :products do
+ # member do
+ # get :short
+ # post :toggle
+ # end
+ #
+ # collection do
+ # get :sold
+ # end
+ # end
+
+ # Sample resource route with sub-resources:
+ # resources :products do
+ # resources :comments, :sales
+ # resource :seller
+ # end
+
+ # Sample resource route with more complex sub-resources
+ # resources :products do
+ # resources :comments
+ # resources :sales do
+ # get :recent, :on => :collection
+ # end
+ # end
+
+ # Sample resource route within a namespace:
+ # namespace :admin do
+ # # Directs /admin/products/* to Admin::ProductsController
+ # # (app/controllers/admin/products_controller.rb)
+ # resources :products
+ # end
+
+ # You can have the root of your site routed with "root"
+ # just remember to delete public/index.html.
+ # root :to => "welcome#index"
+
+ # See how all your routes lay out with "rake routes"
+
+ # This is a legacy wild controller route that's not recommended for RESTful applications.
+ # Note: This route will make all actions in every controller accessible via GET requests.
+ # match ':controller(/:action(/:id(.:format)))'
+end
13 episode-210/todo/db/migrate/20100412041830_create_projects.rb
View
@@ -0,0 +1,13 @@
+class CreateProjects < ActiveRecord::Migration
+ def self.up
+ create_table :projects do |t|
+ t.string :name
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :projects
+ end
+end
15 episode-210/todo/db/migrate/20100412041924_create_tasks.rb
View
@@ -0,0 +1,15 @@
+class CreateTasks < ActiveRecord::Migration
+ def self.up
+ create_table :tasks do |t|
+ t.integer :project_id
+ t.string :name
+ t.datetime :completed_at
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :tasks
+ end
+end
23 episode-210/todo/db/migrate/20100412062912_devise_create_users.rb
View
@@ -0,0 +1,23 @@
+class DeviseCreateUsers < ActiveRecord::Migration
+ def self.up
+ create_table(:users) do |t|
+ t.database_authenticatable :null => false
+ # t.confirmable
+ t.recoverable
+ t.rememberable
+ t.trackable
+ # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both
+
+ t.timestamps
+ end
+
+ add_index :users, :email, :unique => true
+ # add_index :users, :confirmation_token, :unique => true
+ add_index :users, :reset_password_token, :unique => true
+ # add_index :users, :unlock_token, :unique => true
+ end
+
+ def self.down
+ drop_table :users
+ end
+end
9 episode-210/todo/db/migrate/20100419054203_add_username_to_users.rb
View
@@ -0,0 +1,9 @@
+class AddUsernameToUsers < ActiveRecord::Migration
+ def self.up
+ add_column :users, :username, :string
+ end
+
+ def self.down
+ remove_column :users, :username
+ end
+end
48 episode-210/todo/db/schema.rb
View
@@ -0,0 +1,48 @@
+# This file is auto-generated from the current state of the database. Instead of editing this file,
+# please use the migrations feature of Active Record to incrementally modify your database, and
+# then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your database schema. If you need
+# to create the application database on another system, you should be using db:schema:load, not running
+# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended to check this file into your version control system.
+
+ActiveRecord::Schema.define(:version => 20100419054203) do
+
+ create_table "projects", :force => true do |t|
+ t.string "name"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "tasks", :force => true do |t|
+ t.integer "project_id"
+ t.string "name"
+ t.datetime "completed_at"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "users", :force => true do |t|
+ t.string "email", :default => "", :null => false
+ t.string "encrypted_password", :limit => 128, :default => "", :null => false
+ t.string "password_salt", :default => "", :null => false
+ t.string "reset_password_token"
+ t.string "remember_token"
+ t.datetime "remember_created_at"
+ t.integer "sign_in_count", :default => 0
+ t.datetime "current_sign_in_at"
+ t.datetime "last_sign_in_at"
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "username"
+ end
+
+ add_index "users", ["email"], :name => "index_users_on_email", :unique => true
+ add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true
+
+end
7 episode-210/todo/db/seeds.rb
View
@@ -0,0 +1,7 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
+#
+# Examples:
+#
+# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
+# Mayor.create(:name => 'Daley', :city => cities.first)
2  episode-210/todo/doc/README_FOR_APP
View
@@ -0,0 +1,2 @@
+Use this README file to introduce your application and point to useful places in the API for learning more.
+Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
1  episode-210/todo/lib/generators
View
0  episode-210/todo/lib/tasks/.gitkeep
View
No changes.
26 episode-210/todo/public/404.html
View
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>The page you were looking for doesn't exist (404)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/404.html -->
+ <div class="dialog">
+ <h1>The page you were looking for doesn't exist.</h1>
+ <p>You may have mistyped the address or the page may have moved.</p>
+ </div>
+</body>
+</html>
26 episode-210/todo/public/422.html
View
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>The change you wanted was rejected (422)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/422.html -->
+ <div class="dialog">
+ <h1>The change you wanted was rejected.</h1>
+ <p>Maybe you tried to change something you didn't have access to.</p>
+ </div>
+</body>
+</html>
26 episode-210/todo/public/500.html
View
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>We're sorry, but something went wrong (500)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/500.html -->
+ <div class="dialog">
+ <h1>We're sorry, but something went wrong.</h1>
+ <p>We've been notified about this issue and we'll take a look at it shortly.</p>
+ </div>
+</body>
+</html>
0  episode-210/todo/public/favicon.ico
View
No changes.
2  episode-210/todo/public/javascripts/application.js
View
@@ -0,0 +1,2 @@
+// Place your application-specific JavaScript functions and classes here
+// This file is automatically included by javascript_include_tag :defaults
965 episode-210/todo/public/javascripts/controls.js
View
@@ -0,0 +1,965 @@
+// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
+
+// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2009 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+if(typeof Effect == 'undefined')
+ throw("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = { };
+Autocompleter.Base = Class.create({
+ baseInitialize: function(element, update, options) {
+ element = $(element);
+ this.element = element;
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+ this.oldElementValue = this.element.value;
+
+ if(this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = options || { };
+
+ this.options.paramName = this.options.paramName || this.element.name;
+ this.options.tokens = this.options.tokens || [];
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.minChars = this.options.minChars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ Position.clone(element, update, {
+ setHeight: false,
+ offsetTop: element.offsetHeight
+ });
+ }
+ Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if(typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+ // Force carriage returns as token delimiters anyway
+ if (!this.options.tokens.include('\n'))
+ this.options.tokens.push('\n');
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (Prototype.Browser.IE) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ },
+
+ hide: function() {
+ this.stopIndicator();
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.options.indicator) Element.show(this.options.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.options.indicator) Element.hide(this.options.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.selectEntry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ Event.stop(event);
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.markPrevious();
+ this.render();
+ Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+ (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
+
+ this.changed = true;
+ this.hasFocus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ activate: function() {
+ this.changed = false;
+ this.hasFocus = true;
+ this.getUpdatedChoices();
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.selectEntry();
+ this.hide();
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.hasFocus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entryCount > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--;
+ else this.index = this.entryCount-1;
+ this.getEntry(this.index).scrollIntoView(true);
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++;
+ else this.index = 0;
+ this.getEntry(this.index).scrollIntoView(false);
+ },
+
+ getEntry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ getCurrentEntry: function() {
+ return this.getEntry(this.index);
+ },
+
+ selectEntry: function() {
+ this.active = false;
+ this.updateElement(this.getCurrentEntry());
+ },
+
+ updateElement: function(selectedElement) {
+ if (this.options.updateElement) {
+ this.options.updateElement(selectedElement);
+ return;
+ }
+ var value = '';
+ if (this.options.select) {
+ var nodes = $(selectedElement).select('.' + this.options.select) || [];
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+ } else
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+
+ var bounds = this.getTokenBounds();
+ if (bounds[0] != -1) {
+ var newValue = this.element.value.substr(0, bounds[0]);
+ var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value + this.element.value.substr(bounds[1]);
+ } else {
+ this.element.value = value;
+ }
+ this.oldElementValue = this.element.value;
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.hasFocus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.down());
+
+ if(this.update.firstChild && this.update.down().childNodes) {
+ this.entryCount =
+ this.update.down().childNodes.length;
+ for (var i = 0; i < this.entryCount; i++) {
+ var entry = this.getEntry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entryCount = 0;
+ }
+
+ this.stopIndicator();
+ this.index = 0;
+
+ if(this.entryCount==1 && this.options.autoSelect) {
+ this.selectEntry();
+ this.hide();
+ } else {
+ this.render();
+ }
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ this.tokenBounds = null;
+ if(this.getToken().length>=this.options.minChars) {
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ this.oldElementValue = this.element.value;
+ },
+
+ getToken: function() {
+ var bounds = this.getTokenBounds();
+ return this.element.value.substring(bounds[0], bounds[1]).strip();
+ },
+
+ getTokenBounds: function() {
+ if (null != this.tokenBounds) return this.tokenBounds;
+ var value = this.element.value;
+ if (value.strip().empty()) return [-1, 0];
+ var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
+ var offset = (diff == this.oldElementValue.length ? 1 : 0);
+ var prevTokenPos = -1, nextTokenPos = value.length;
+ var tp;
+ for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
+ tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
+ if (tp > prevTokenPos) prevTokenPos = tp;
+ tp = value.indexOf(this.options.tokens[index], diff + offset);
+ if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
+ }
+ return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
+ }
+});
+
+Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
+ var boundary = Math.min(newS.length, oldS.length);
+ for (var index = 0; index < boundary; ++index)
+ if (newS[index] != oldS[index])
+ return index;
+ return boundary;
+};
+
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, url, options) {
+ this.baseInitialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this);
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ this.startIndicator();
+
+ var entry = encodeURIComponent(this.options.paramName) + '=' +
+ encodeURIComponent(this.getToken());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+// a partial match (unlike minChars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, array, options) {
+ this.baseInitialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partialSearch: true,
+ partialChars: 2,
+ ignoreCase: true,
+ fullSearch: false,
+ selector: function(instance) {
+ var ret = []; // Beginning matches
+ var partial = []; // Inside matches
+ var entry = instance.getToken();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+
+ var elem = instance.options.array[i];
+ var foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (foundPos != -1) {
+ if (foundPos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+ foundPos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || { });
+ }
+});
+
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+ setTimeout(function() {
+ Field.activate(field);
+ }, 1);
+};
+
+Ajax.InPlaceEditor = Class.create({
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = element = $(element);
+ this.prepareOptions();
+ this._controls = { };
+ arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
+ Object.extend(this.options, options || { });
+ if (!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + '-inplaceeditor';
+ if ($(this.options.formId))
+ this.options.formId = '';
+ }
+ if (this.options.externalControl)
+ this.options.externalControl = $(this.options.externalControl);
+ if (!this.options.externalControl)
+ this.options.externalControlOnly = false;
+ this._originalBackground = this.element.getStyle('background-color') || 'transparent';
+ this.element.title = this.options.clickToEditText;
+ this._boundCancelHandler = this.handleFormCancellation.bind(this);
+ this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+ this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+ this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+ this._boundWrapperHandler = this.wrapUp.bind(this);
+ this.registerListeners();
+ },
+ checkForEscapeOrReturn: function(e) {
+ if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+ if (Event.KEY_ESC == e.keyCode)
+ this.handleFormCancellation(e);
+ else if (Event.KEY_RETURN == e.keyCode)
+ this.handleFormSubmission(e);
+ },
+ createControl: function(mode, handler, extraClasses) {
+ var control = this.options[mode + 'Control'];
+ var text = this.options[mode + 'Text'];
+ if ('button' == control) {
+ var btn = document.createElement('input');
+ btn.type = 'submit';
+ btn.value = text;
+ btn.className = 'editor_' + mode + '_button';
+ if ('cancel' == mode)
+ btn.onclick = this._boundCancelHandler;
+ this._form.appendChild(btn);
+ this._controls[mode] = btn;
+ } else if ('link' == control) {
+ var link = document.createElement('a');
+ link.href = '#';
+ link.appendChild(document.createTextNode(text));
+ link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
+ link.className = 'editor_' + mode + '_link';
+ if (extraClasses)
+ link.className += ' ' + extraClasses;
+ this._form.appendChild(link);
+ this._controls[mode] = link;
+ }
+ },
+ createEditField: function() {
+ var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+ var fld;
+ if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
+ fld = document.createElement('input');
+ fld.type = 'text';
+ var size = this.options.size || this.options.cols || 0;
+ if (0 < size) fld.size = size;
+ } else {
+ fld = document.createElement('textarea');
+ fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
+ fld.cols = this.options.cols || 40;
+ }
+ fld.name = this.options.paramName;
+ fld.value = text; // No HTML breaks conversion anymore
+ fld.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ fld.onblur = this._boundSubmitHandler;
+ this._controls.editor = fld;
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+ createForm: function() {
+ var ipe = this;
+ function addText(mode, condition) {
+ var text = ipe.options['text' + mode + 'Controls'];
+ if (!text || condition === false) return;
+ ipe._form.appendChild(document.createTextNode(text));
+ };
+ this._form = $(document.createElement('form'));
+ this._form.id = this.options.formId;
+ this._form.addClassName(this.options.formClassName);
+ this._form.onsubmit = this._boundSubmitHandler;
+ this.createEditField();
+ if ('textarea' == this._controls.editor.tagName.toLowerCase())
+ this._form.appendChild(document.createElement('br'));
+ if (this.options.onFormCustomization)
+ this.options.onFormCustomization(this, this._form);
+ addText('Before', this.options.okControl || this.options.cancelControl);
+ this.createControl('ok', this._boundSubmitHandler);
+ addText('Between', this.options.okControl && this.options.cancelControl);
+ this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+ addText('After', this.options.okControl || this.options.cancelControl);
+ },
+ destroy: function() {
+ if (this._oldInnerHTML)
+ this.element.innerHTML = this._oldInnerHTML;
+ this.leaveEditMode();
+ this.unregisterListeners();
+ },
+ enterEditMode: function(e) {
+ if (this._saving || this._editing) return;
+ this._editing = true;
+ this.triggerCallback('onEnterEditMode');
+ if (this.options.externalControl)
+ this.options.externalControl.hide();
+ this.element.hide();
+ this.createForm();
+ this.element.parentNode.insertBefore(this._form, this.element);
+ if (!this.options.loadTextURL)
+ this.postProcessEditField();
+ if (e) Event.stop(e);
+ },
+ enterHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.addClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onEnterHover');
+ },
+ getText: function() {
+ return this.element.innerHTML.unescapeHTML();
+ },
+ handleAJAXFailure: function(transport) {
+ this.triggerCallback('onFailure', transport);
+ if (this._oldInnerHTML) {
+ this.element.innerHTML = this._oldInnerHTML;
+ this._oldInnerHTML = null;
+ }
+ },
+ handleFormCancellation: function(e) {
+ this.wrapUp();
+ if (e) Event.stop(e);
+ },
+ handleFormSubmission: function(e) {
+ var form = this._form;
+ var value = $F(this._controls.editor);
+ this.prepareSubmission();
+ var params = this.options.callback(form, value) || '';
+ if (Object.isString(params))
+ params = params.toQueryParams();
+ params.editorId = this.element.id;
+ if (this.options.htmlResponse) {
+ var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Updater({ success: this.element }, this.url, options);
+ } else {
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.url, options);
+ }
+ if (e) Event.stop(e);
+ },
+ leaveEditMode: function() {
+ this.element.removeClassName(this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ if (this.options.externalControl)
+ this.options.externalControl.show();
+ this._saving = false;
+ this._editing = false;
+ this._oldInnerHTML = null;
+ this.triggerCallback('onLeaveEditMode');
+ },
+ leaveHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.removeClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onLeaveHover');
+ },
+ loadExternalText: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this._controls.editor.disabled = true;
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._form.removeClassName(this.options.loadingClassName);
+ var text = transport.responseText;
+ if (this.options.stripLoadedTextTags)
+ text = text.stripTags();
+ this._controls.editor.value = text;
+ this._controls.editor.disabled = false;
+ this.postProcessEditField();
+ }.bind(this),
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+ postProcessEditField: function() {
+ var fpc = this.options.fieldPostCreation;
+ if (fpc)
+ $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
+ },
+ prepareOptions: function() {
+ this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
+ Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
+ [this._extraDefaultOptions].flatten().compact().each(function(defs) {
+ Object.extend(this.options, defs);
+ }.bind(this));
+ },
+ prepareSubmission: function() {
+ this._saving = true;
+ this.removeForm();
+ this.leaveHover();
+ this.showSaving();
+ },
+ registerListeners: function() {
+ this._listeners = { };
+ var listener;
+ $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
+ listener = this[pair.value].bind(this);
+ this._listeners[pair.key] = listener;
+ if (!this.options.externalControlOnly)
+ this.element.observe(pair.key, listener);
+ if (this.options.externalControl)
+ this.options.externalControl.observe(pair.key, listener);
+ }.bind(this));
+ },
+ removeForm: function() {
+ if (!this._form) return;
+ this._form.remove();
+ this._form = null;
+ this._controls = { };
+ },
+ showSaving: function() {
+ this._oldInnerHTML = this.element.innerHTML;
+ this.element.innerHTML = this.options.savingText;
+ this.element.addClassName(this.options.savingClassName);
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ },
+ triggerCallback: function(cbName, arg) {
+ if ('function' == typeof this.options[cbName]) {
+ this.options[cbName](this, arg);
+ }
+ },
+ unregisterListeners: function() {
+ $H(this._listeners).each(function(pair) {
+ if (!this.options.externalControlOnly)
+ this.element.stopObserving(pair.key, pair.value);
+ if (this.options.externalControl)
+ this.options.externalControl.stopObserving(pair.key, pair.value);
+ }.bind(this));
+ },
+ wrapUp: function(transport) {
+ this.leaveEditMode();
+ // Can't use triggerCallback due to backward compatibility: requires
+ // binding + direct element
+ this._boundComplete(transport, this.element);
+ }
+});
+
+Object.extend(Ajax.InPlaceEditor.prototype, {
+ dispose: Ajax.InPlaceEditor.prototype.destroy
+});
+
+Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
+ initialize: function($super, element, url, options) {
+ this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
+ $super(element, url, options);
+ },
+
+ createEditField: function() {
+ var list = document.createElement('select');
+ list.name = this.options.paramName;
+ list.size = 1;
+ this._controls.editor = list;
+ this._collection = this.options.collection || [];
+ if (this.options.loadCollectionURL)
+ this.loadCollection();
+ else
+ this.checkForExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+
+ loadCollection: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this.showLoadingText(this.options.loadingCollectionText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ var js = transport.responseText.strip();
+ if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
+ throw('Server returned an invalid collection representation.');
+ this._collection = eval(js);
+ this.checkForExternalText();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadCollectionURL, options);
+ },
+
+ showLoadingText: function(text) {
+ this._controls.editor.disabled = true;
+ var tempOption = this._controls.editor.firstChild;
+ if (!tempOption) {
+ tempOption = document.createElement('option');
+ tempOption.value = '';
+ this._controls.editor.appendChild(tempOption);
+ tempOption.selected = true;
+ }
+ tempOption.update((text || '').stripScripts().stripTags());
+ },
+
+ checkForExternalText: function() {
+ this._text = this.getText();
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ else
+ this.buildOptionList();
+ },
+
+ loadExternalText: function() {
+ this.showLoadingText(this.options.loadingText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._text = transport.responseText.strip();
+ this.buildOptionList();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+
+ buildOptionList: function() {
+ this._form.removeClassName(this.options.loadingClassName);
+ this._collection = this._collection.map(function(entry) {
+ return 2 === entry.length ? entry : [entry, entry].flatten();
+ });
+ var marker = ('value' in this.options) ? this.options.value : this._text;
+ var textFound = this._collection.any(function(entry) {
+ return entry[0] == marker;
+ }.bind(this));
+ this._controls.editor.update('');
+ var option;
+ this._collection.each(function(entry, index) {
+ option = document.createElement('option');
+ option.value = entry[0];
+ option.selected = textFound ? entry[0] == marker : 0 == index;
+ option.appendChild(document.createTextNode(entry[1]));
+ this._controls.editor.appendChild(option);
+ }.bind(this));
+ this._controls.editor.disabled = false;
+ Field.scrollFreeActivate(this._controls.editor);
+ }
+});
+
+//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
+//**** This only exists for a while, in order to let ****
+//**** users adapt to the new API. Read up on the new ****
+//**** API and convert your code to it ASAP! ****
+
+Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
+ if (!options) return;
+ function fallback(name, expr) {
+ if (name in options || expr === undefined) return;
+ options[name] = expr;
+ };
+ fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
+ options.cancelLink == options.cancelButton == false ? false : undefined)));
+ fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
+ options.okLink == options.okButton == false ? false : undefined)));
+ fallback('highlightColor', options.highlightcolor);
+ fallback('highlightEndColor', options.highlightendcolor);
+};
+
+Object.extend(Ajax.InPlaceEditor, {
+ DefaultOptions: {
+ ajaxOptions: { },
+ autoRows: 3, // Use when multi-line w/ rows == 1
+ cancelControl: 'link', // 'link'|'button'|false
+ cancelText: 'cancel',
+ clickToEditText: 'Click to edit',
+ externalControl: null, // id|elt
+ externalControlOnly: false,
+ fieldPostCreation: 'activate', // 'activate'|'focus'|false
+ formClassName: 'inplaceeditor-form',
+ formId: null, // id|elt
+ highlightColor: '#ffff99',
+ highlightEndColor: '#ffffff',
+ hoverClassName: '',
+ htmlResponse: true,
+ loadingClassName: 'inplaceeditor-loading',
+ loadingText: 'Loading...',
+ okControl: 'button', // 'link'|'button'|false
+ okText: 'ok',
+ paramName: 'value',
+ rows: 1, // If 1 and multi-line, uses autoRows
+ savingClassName: 'inplaceeditor-saving',
+ savingText: 'Saving...',
+ size: 0,
+ stripLoadedTextTags: false,
+ submitOnBlur: false,
+ textAfterControls: '',
+ textBeforeControls: '',
+ textBetweenControls: ''
+ },
+ DefaultCallbacks: {
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ onComplete: function(transport, element) {
+ // For backward compatibility, this one is bound to the IPE, and passes
+ // the element directly. It was too often customized, so we don't break it.
+ new Effect.Highlight(element, {
+ startcolor: this.options.highlightColor, keepBackgroundImage: true });
+ },
+ onEnterEditMode: null,
+ onEnterHover: function(ipe) {
+ ipe.element.style.backgroundColor = ipe.options.highlightColor;
+ if (ipe._effect)
+ ipe._effect.cancel();
+ },
+ onFailure: function(transport, ipe) {
+ alert('Error communication with the server: ' + transport.responseText.stripTags());
+ },
+ onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
+ onLeaveEditMode: null,
+ onLeaveHover: function(ipe) {
+ ipe._effect = new Effect.Highlight(ipe.element, {
+ startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
+ restorecolor: ipe._originalBackground, keepBackgroundImage: true
+ });
+ }
+ },
+ Listeners: {
+ click: 'enterEditMode',
+ keydown: 'checkForEscapeOrReturn',
+ mouseover: 'enterHover',
+ mouseout: 'leaveHover'
+ }
+});
+
+Ajax.InPlaceCollectionEditor.DefaultOptions = {
+ loadingCollectionText: 'Loading options...'
+};
+
+// Delayed observer, like Form.Element.Observer,
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create({
+ initialize: function(element, delay, callback) {
+ this.delay = delay || 0.5;
+ this.element = $(element);
+ this.callback = callback;
+ this.timer = null;
+ this.lastValue = $F(this.element);
+ Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+ },
+ delayedListener: function(event) {
+ if(this.lastValue == $F(this.element)) return;