Permalink
Browse files

adding episode 159

  • Loading branch information...
1 parent f47b374 commit 287ea29922e0b5d9f5b3c7db0b6a3a433d5adfc7 @ryanb committed Apr 24, 2009
Showing with 9,355 additions and 0 deletions.
  1. +10 −0 episode-159/README
  2. +3 −0 episode-159/blog/.gitignore
  3. +243 −0 episode-159/blog/README
  4. +10 −0 episode-159/blog/Rakefile
  5. +11 −0 episode-159/blog/app/controllers/application_controller.rb
  6. +15 −0 episode-159/blog/app/controllers/articles_controller.rb
  7. +22 −0 episode-159/blog/app/controllers/sessions_controller.rb
  8. +20 −0 episode-159/blog/app/controllers/users_controller.rb
  9. +3 −0 episode-159/blog/app/helpers/application_helper.rb
  10. +2 −0 episode-159/blog/app/helpers/articles_helper.rb
  11. +23 −0 episode-159/blog/app/helpers/layout_helper.rb
  12. +2 −0 episode-159/blog/app/helpers/sessions_helper.rb
  13. +2 −0 episode-159/blog/app/helpers/users_helper.rb
  14. +2 −0 episode-159/blog/app/models/article.rb
  15. +36 −0 episode-159/blog/app/models/user.rb
  16. +6 −0 episode-159/blog/app/views/articles/index.html.erb
  17. +11 −0 episode-159/blog/app/views/articles/new.html.erb
  18. +32 −0 episode-159/blog/app/views/layouts/application.html.erb
  19. +15 −0 episode-159/blog/app/views/sessions/new.html.erb
  20. +24 −0 episode-159/blog/app/views/users/new.html.erb
  21. +9 −0 episode-159/blog/app/views/users/show.html.erb
  22. +110 −0 episode-159/blog/config/boot.rb
  23. +22 −0 episode-159/blog/config/database.yml
  24. +41 −0 episode-159/blog/config/environment.rb
  25. +17 −0 episode-159/blog/config/environments/development.rb
  26. +28 −0 episode-159/blog/config/environments/production.rb
  27. +34 −0 episode-159/blog/config/environments/test.rb
  28. +7 −0 episode-159/blog/config/initializers/backtrace_silencers.rb
  29. +10 −0 episode-159/blog/config/initializers/inflections.rb
  30. +5 −0 episode-159/blog/config/initializers/mime_types.rb
  31. +19 −0 episode-159/blog/config/initializers/new_rails_defaults.rb
  32. +15 −0 episode-159/blog/config/initializers/session_store.rb
  33. +5 −0 episode-159/blog/config/locales/en.yml
  34. +52 −0 episode-159/blog/config/routes.rb
  35. +14 −0 episode-159/blog/db/migrate/20090330052103_create_articles.rb
  36. +15 −0 episode-159/blog/db/migrate/20090422195342_create_users.rb
  37. +9 −0 episode-159/blog/db/migrate/20090422200138_add_admin_to_users.rb
  38. +31 −0 episode-159/blog/db/schema.rb
  39. +2 −0 episode-159/blog/doc/README_FOR_APP
  40. +22 −0 episode-159/blog/features/manage_articles.feature
  41. +21 −0 episode-159/blog/features/manage_users.feature
  42. +13 −0 episode-159/blog/features/step_definitions/article_steps.rb
  43. +19 −0 episode-159/blog/features/step_definitions/user_steps.rb
  44. +115 −0 episode-159/blog/features/step_definitions/webrat_steps.rb
  45. +18 −0 episode-159/blog/features/support/env.rb
  46. +18 −0 episode-159/blog/features/support/paths.rb
  47. +36 −0 episode-159/blog/lib/authentication.rb
  48. +15 −0 episode-159/blog/lib/tasks/cucumber.rake
  49. +30 −0 episode-159/blog/public/404.html
  50. +30 −0 episode-159/blog/public/422.html
  51. +30 −0 episode-159/blog/public/500.html
  52. 0 episode-159/blog/public/favicon.ico
  53. BIN episode-159/blog/public/images/rails.png
  54. +275 −0 episode-159/blog/public/index.html
  55. +2 −0 episode-159/blog/public/javascripts/application.js
  56. +963 −0 episode-159/blog/public/javascripts/controls.js
  57. +973 −0 episode-159/blog/public/javascripts/dragdrop.js
  58. +1,128 −0 episode-159/blog/public/javascripts/effects.js
  59. +4,320 −0 episode-159/blog/public/javascripts/prototype.js
  60. +5 −0 episode-159/blog/public/robots.txt
  61. +88 −0 episode-159/blog/public/stylesheets/application.css
  62. +4 −0 episode-159/blog/script/about
  63. +3 −0 episode-159/blog/script/console
  64. +8 −0 episode-159/blog/script/cucumber
  65. +3 −0 episode-159/blog/script/dbconsole
  66. +3 −0 episode-159/blog/script/destroy
  67. +3 −0 episode-159/blog/script/generate
  68. +3 −0 episode-159/blog/script/performance/benchmarker
  69. +3 −0 episode-159/blog/script/performance/profiler
  70. +3 −0 episode-159/blog/script/plugin
  71. +3 −0 episode-159/blog/script/runner
  72. +3 −0 episode-159/blog/script/server
  73. +17 −0 episode-159/blog/spec/controllers/articles_controller_spec.rb
  74. +25 −0 episode-159/blog/spec/controllers/sessions_controller_spec.rb
  75. +24 −0 episode-159/blog/spec/controllers/users_controller_spec.rb
  76. +6 −0 episode-159/blog/spec/factories.rb
  77. +9 −0 episode-159/blog/spec/fixtures/articles.yml
  78. +12 −0 episode-159/blog/spec/fixtures/users.yml
  79. +11 −0 episode-159/blog/spec/helpers/articles_helper_spec.rb
  80. +14 −0 episode-159/blog/spec/models/article_spec.rb
  81. +81 −0 episode-159/blog/spec/models/user_spec.rb
  82. 0 episode-159/blog/spec/spec_helper.rb
  83. +12 −0 episode-159/blog/spec/views/articles/index.html.erb_spec.rb
  84. +9 −0 episode-159/blog/test/performance/browsing_test.rb
  85. +38 −0 episode-159/blog/test/test_helper.rb
  86. 0 episode-159/blog/vendor/.gitignore
View
10 episode-159/README
@@ -0,0 +1,10 @@
+Railscasts Episode #159: More on Cucumber
+
+http://railscasts.com/episodes/159
+
+Commands
+
+ sudo rake gems:install RAILS_ENV=test
+ cucumber features -n
+ cucumber features -n -t focus
+ cucumber features -n -t \~focus
View
3 episode-159/blog/.gitignore
@@ -0,0 +1,3 @@
+tmp/*
+log/*
+*.sqlite3
View
243 episode-159/blog/README
@@ -0,0 +1,243 @@
+== Welcome to Rails
+
+Rails is a web-application framework that includes everything needed to create
+database-backed web applications according to the Model-View-Control pattern.
+
+This pattern splits the view (also called the presentation) into "dumb" templates
+that are primarily responsible for inserting pre-built data in between HTML tags.
+The model contains the "smart" domain objects (such as Account, Product, Person,
+Post) that holds all the business logic and knows how to persist themselves to
+a database. The controller handles the incoming requests (such as Save New Account,
+Update Product, Show Post) by manipulating the model and directing data to the view.
+
+In Rails, the model is handled by what's called an object-relational mapping
+layer entitled Active Record. This layer allows you to present the data from
+database rows as objects and embellish these data objects with business logic
+methods. You can read more about Active Record in
+link:files/vendor/rails/activerecord/README.html.
+
+The controller and view are handled by the Action Pack, which handles both
+layers by its two parts: Action View and Action Controller. These two layers
+are bundled in a single package due to their heavy interdependence. This is
+unlike the relationship between the Active Record and Action Pack that is much
+more separate. Each of these packages can be used independently outside of
+Rails. You can read more about Action Pack in
+link:files/vendor/rails/actionpack/README.html.
+
+
+== Getting Started
+
+1. At the command prompt, start a new Rails application using the <tt>rails</tt> command
+ and your application name. Ex: rails myapp
+2. Change directory into myapp and start the web server: <tt>script/server</tt> (run with --help for options)
+3. Go to http://localhost:3000/ and get "Welcome aboard: You're riding the Rails!"
+4. Follow the guidelines to start developing your application
+
+
+== Web Servers
+
+By default, Rails will try to use Mongrel if it's are installed when started with script/server, otherwise Rails will use WEBrick, the webserver that ships with Ruby. But you can also use Rails
+with a variety of other web servers.
+
+Mongrel is a Ruby-based webserver with a C component (which requires compilation) that is
+suitable for development and deployment of Rails applications. If you have Ruby Gems installed,
+getting up and running with mongrel is as easy as: <tt>gem install mongrel</tt>.
+More info at: http://mongrel.rubyforge.org
+
+Say other Ruby web servers like Thin and Ebb or regular web servers like Apache or LiteSpeed or
+Lighttpd or IIS. The Ruby web servers are run through Rack and the latter can either be setup to use
+FCGI or proxy to a pack of Mongrels/Thin/Ebb servers.
+
+== Apache .htaccess example for FCGI/CGI
+
+# General Apache options
+AddHandler fastcgi-script .fcgi
+AddHandler cgi-script .cgi
+Options +FollowSymLinks +ExecCGI
+
+# If you don't want Rails to look in certain directories,
+# use the following rewrite rules so that Apache won't rewrite certain requests
+#
+# Example:
+# RewriteCond %{REQUEST_URI} ^/notrails.*
+# RewriteRule .* - [L]
+
+# Redirect all requests not available on the filesystem to Rails
+# By default the cgi dispatcher is used which is very slow
+#
+# For better performance replace the dispatcher with the fastcgi one
+#
+# Example:
+# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
+RewriteEngine On
+
+# If your Rails application is accessed via an Alias directive,
+# then you MUST also set the RewriteBase in this htaccess file.
+#
+# Example:
+# Alias /myrailsapp /path/to/myrailsapp/public
+# RewriteBase /myrailsapp
+
+RewriteRule ^$ index.html [QSA]
+RewriteRule ^([^.]+)$ $1.html [QSA]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
+
+# In case Rails experiences terminal errors
+# Instead of displaying this message you can supply a file here which will be rendered instead
+#
+# Example:
+# ErrorDocument 500 /500.html
+
+ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
+
+
+== Debugging Rails
+
+Sometimes your application goes wrong. Fortunately there are a lot of tools that
+will help you debug it and get it back on the rails.
+
+First area to check is the application log files. Have "tail -f" commands running
+on the server.log and development.log. Rails will automatically display debugging
+and runtime information to these files. Debugging info will also be shown in the
+browser on requests from 127.0.0.1.
+
+You can also log your own messages directly into the log file from your code using
+the Ruby logger class from inside your controllers. Example:
+
+ class WeblogController < ActionController::Base
+ def destroy
+ @weblog = Weblog.find(params[:id])
+ @weblog.destroy
+ logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!")
+ end
+ end
+
+The result will be a message in your log file along the lines of:
+
+ Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1
+
+More information on how to use the logger is at http://www.ruby-doc.org/core/
+
+Also, Ruby documentation can be found at http://www.ruby-lang.org/ including:
+
+* The Learning Ruby (Pickaxe) Book: http://www.ruby-doc.org/docs/ProgrammingRuby/
+* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide)
+
+These two online (and free) books will bring you up to speed on the Ruby language
+and also on programming in general.
+
+
+== Debugger
+
+Debugger support is available through the debugger command when you start your Mongrel or
+Webrick server with --debugger. This means that you can break out of execution at any point
+in the code, investigate and change the model, AND then resume execution!
+You need to install ruby-debug to run the server in debugging mode. With gems, use 'gem install ruby-debug'
+Example:
+
+ class WeblogController < ActionController::Base
+ def index
+ @posts = Post.find(:all)
+ debugger
+ end
+ end
+
+So the controller will accept the action, run the first line, then present you
+with a IRB prompt in the server window. Here you can do things like:
+
+ >> @posts.inspect
+ => "[#<Post:0x14a6be8 @attributes={\"title\"=>nil, \"body\"=>nil, \"id\"=>\"1\"}>,
+ #<Post:0x14a6620 @attributes={\"title\"=>\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]"
+ >> @posts.first.title = "hello from a debugger"
+ => "hello from a debugger"
+
+...and even better is that you can examine how your runtime objects actually work:
+
+ >> f = @posts.first
+ => #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>
+ >> f.
+ Display all 152 possibilities? (y or n)
+
+Finally, when you're ready to resume execution, you enter "cont"
+
+
+== Console
+
+You can interact with the domain model by starting the console through <tt>script/console</tt>.
+Here you'll have all parts of the application configured, just like it is when the
+application is running. You can inspect domain models, change values, and save to the
+database. Starting the script without arguments will launch it in the development environment.
+Passing an argument will specify a different environment, like <tt>script/console production</tt>.
+
+To reload your controllers and models after launching the console run <tt>reload!</tt>
+
+== dbconsole
+
+You can go to the command line of your database directly through <tt>script/dbconsole</tt>.
+You would be connected to the database with the credentials defined in database.yml.
+Starting the script without arguments will connect you to the development database. Passing an
+argument will connect you to a different database, like <tt>script/dbconsole production</tt>.
+Currently works for mysql, postgresql and sqlite.
+
+== Description of Contents
+
+app
+ Holds all the code that's specific to this particular application.
+
+app/controllers
+ Holds controllers that should be named like weblogs_controller.rb for
+ automated URL mapping. All controllers should descend from ApplicationController
+ which itself descends from ActionController::Base.
+
+app/models
+ Holds models that should be named like post.rb.
+ Most models will descend from ActiveRecord::Base.
+
+app/views
+ Holds the template files for the view that should be named like
+ weblogs/index.html.erb for the WeblogsController#index action. All views use eRuby
+ syntax.
+
+app/views/layouts
+ Holds the template files for layouts to be used with views. This models the common
+ header/footer method of wrapping views. In your views, define a layout using the
+ <tt>layout :default</tt> and create a file named default.html.erb. Inside default.html.erb,
+ call <% yield %> to render the view using this layout.
+
+app/helpers
+ Holds view helpers that should be named like weblogs_helper.rb. These are generated
+ for you automatically when using script/generate for controllers. Helpers can be used to
+ wrap functionality for your views into methods.
+
+config
+ Configuration files for the Rails environment, the routing map, the database, and other dependencies.
+
+db
+ Contains the database schema in schema.rb. db/migrate contains all
+ the sequence of Migrations for your schema.
+
+doc
+ This directory is where your application documentation will be stored when generated
+ using <tt>rake doc:app</tt>
+
+lib
+ Application specific libraries. Basically, any kind of custom code that doesn't
+ belong under controllers, models, or helpers. This directory is in the load path.
+
+public
+ The directory available for the web server. Contains subdirectories for images, stylesheets,
+ and javascripts. Also contains the dispatchers and the default HTML files. This should be
+ set as the DOCUMENT_ROOT of your web server.
+
+script
+ Helper scripts for automation and generation.
+
+test
+ Unit and functional tests along with fixtures. When using the script/generate scripts, template
+ test files will be generated for you and placed in this directory.
+
+vendor
+ External libraries that the application depends on. Also includes the plugins subdirectory.
+ If the app has frozen rails, those gems also go here, under vendor/rails/.
+ This directory is in the load path.
View
10 episode-159/blog/Rakefile
@@ -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.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
View
11 episode-159/blog/app/controllers/application_controller.rb
@@ -0,0 +1,11 @@
+# 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
+ include Authentication
+ helper :all # include all helpers, all the time
+ protect_from_forgery # See ActionController::RequestForgeryProtection for details
+
+ # Scrub sensitive parameters from your log
+ # filter_parameter_logging :password
+end
View
15 episode-159/blog/app/controllers/articles_controller.rb
@@ -0,0 +1,15 @@
+class ArticlesController < ApplicationController
+ def index
+ @articles = Article.all
+ end
+
+ def new
+ @article = Article.new
+ end
+
+ def create
+ @article = Article.create!(params[:article])
+ flash[:notice] = "New article created."
+ redirect_to articles_path
+ end
+end
View
22 episode-159/blog/app/controllers/sessions_controller.rb
@@ -0,0 +1,22 @@
+class SessionsController < ApplicationController
+ def new
+ end
+
+ def create
+ user = User.authenticate(params[:login], params[:password])
+ if user
+ session[:user_id] = user.id
+ flash[:notice] = "Logged in successfully."
+ redirect_to user
+ else
+ flash.now[:error] = "Invalid login or password."
+ render :action => 'new'
+ end
+ end
+
+ def destroy
+ session[:user_id] = nil
+ flash[:notice] = "You have been logged out."
+ redirect_to root_url
+ end
+end
View
20 episode-159/blog/app/controllers/users_controller.rb
@@ -0,0 +1,20 @@
+class UsersController < ApplicationController
+ def show
+ @user = User.find(params[:id])
+ end
+
+ def new
+ @user = User.new
+ end
+
+ def create
+ @user = User.new(params[:user])
+ if @user.save
+ session[:user_id] = @user.id
+ flash[:notice] = "Thank you for signing up! You are now logged in."
+ redirect_to @user
+ else
+ render :action => 'new'
+ end
+ end
+end
View
3 episode-159/blog/app/helpers/application_helper.rb
@@ -0,0 +1,3 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+end
View
2 episode-159/blog/app/helpers/articles_helper.rb
@@ -0,0 +1,2 @@
+module ArticlesHelper
+end
View
23 episode-159/blog/app/helpers/layout_helper.rb
@@ -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
View
2 episode-159/blog/app/helpers/sessions_helper.rb
@@ -0,0 +1,2 @@
+module SessionsHelper
+end
View
2 episode-159/blog/app/helpers/users_helper.rb
@@ -0,0 +1,2 @@
+module UsersHelper
+end
View
2 episode-159/blog/app/models/article.rb
@@ -0,0 +1,2 @@
+class Article < ActiveRecord::Base
+end
View
36 episode-159/blog/app/models/user.rb
@@ -0,0 +1,36 @@
+class User < ActiveRecord::Base
+ # new columns need to be added here to be writable through mass assignment
+ attr_accessible :username, :email, :password, :password_confirmation
+
+ attr_accessor :password
+ before_create :prepare_password
+
+ validates_presence_of :username
+ validates_uniqueness_of :username, :email, :allow_blank => true
+ validates_format_of :username, :with => /^[-\w\._@]+$/i, :allow_blank => true, :message => "should only contain letters, numbers, or .-_@"
+ validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
+ validates_presence_of :password, :on => :create
+ validates_confirmation_of :password
+ validates_length_of :password, :minimum => 4, :allow_blank => true
+
+ # login can be either username or email address
+ def self.authenticate(login, pass)
+ user = find_by_username(login) || find_by_email(login)
+ return user if user && user.matching_password?(pass)
+ end
+
+ def matching_password?(pass)
+ self.password_hash == encrypt_password(pass)
+ end
+
+ private
+
+ def prepare_password
+ self.password_salt = Digest::SHA1.hexdigest([Time.now, rand].join)
+ self.password_hash = encrypt_password(password)
+ end
+
+ def encrypt_password(pass)
+ Digest::SHA1.hexdigest([pass, password_salt].join)
+ end
+end
View
6 episode-159/blog/app/views/articles/index.html.erb
@@ -0,0 +1,6 @@
+<%= flash[:notice] %>
+<% for article in @articles %>
+ <p><%=h article.title %></p>
+ <p><%=h article.content %></p>
+<% end %>
+<p><%= link_to "New Article", new_article_path %></p>
View
11 episode-159/blog/app/views/articles/new.html.erb
@@ -0,0 +1,11 @@
+<% form_for @article do |f| %>
+ <p>
+ <%= f.label :title %><br />
+ <%= f.text_field :title %>
+ </p>
+ <p>
+ <%= f.label :content %><br />
+ <%= f.text_field :content %>
+ </p>
+ <p class="button"><%= f.submit "Create" %></p>
+<% end %>
View
32 episode-159/blog/app/views/layouts/application.html.erb
@@ -0,0 +1,32 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+ <title><%= h(yield(:title) || "Untitled") %></title>
+ <%= stylesheet_link_tag 'application' %>
+ <%= yield(:head) %>
+ </head>
+ <body>
+ <div id="container">
+ <p id="loginbar">
+ <% if logged_in? %>
+ Currently logged in as <%=h current_user.username %>! Not you?
+ <%= link_to "Log out", logout_path %>
+ <% else %>
+ <%= link_to "Sign up", signup_path %> or
+ <%= link_to "log in", login_path %>.
+ <% end %>
+ </p>
+
+ <%- flash.each do |name, msg| -%>
+ <%= content_tag :div, msg, :id => "flash_#{name}" %>
+ <%- end -%>
+
+ <%- if show_title? -%>
+ <h1><%=h yield(:title) %></h1>
+ <%- end -%>
+
+ <%= yield %>
+ </div>
+ </body>
+</html>
View
15 episode-159/blog/app/views/sessions/new.html.erb
@@ -0,0 +1,15 @@
+<% title "Log in" %>
+
+<p>Don't have an account? <%= link_to "Sign up!", signup_path %></p>
+
+<% form_tag sessions_path do %>
+ <p>
+ <%= label_tag :login, "Username or Email Address" %><br />
+ <%= text_field_tag :login, params[:login] %>
+ </p>
+ <p>
+ <%= label_tag :password %><br />
+ <%= password_field_tag :password %>
+ </p>
+ <p><%= submit_tag "Log in" %></p>
+<% end %>
View
24 episode-159/blog/app/views/users/new.html.erb
@@ -0,0 +1,24 @@
+<% title "Sign up" %>
+
+<p>Already have an account? <%= link_to "Log in", login_path %>.</p>
+
+<% form_for @user do |f| %>
+ <%= f.error_messages %>
+ <p>
+ <%= f.label :username %><br />
+ <%= f.text_field :username %>
+ </p>
+ <p>
+ <%= f.label :email, "Email Address" %><br />
+ <%= f.text_field :email %>
+ </p>
+ <p>
+ <%= f.label :password %><br />
+ <%= f.password_field :password %>
+ </p>
+ <p>
+ <%= f.label :password_confirmation, "Confirm Password" %><br />
+ <%= f.password_field :password_confirmation %>
+ </p>
+ <p><%= f.submit "Sign up" %></p>
+<% end %>
View
9 episode-159/blog/app/views/users/show.html.erb
@@ -0,0 +1,9 @@
+<% title "Profile for " + @user.username %>
+
+<p>This user currently has no bio.</p>
+
+<p>Email: <%=h @user.email %></p>
+
+<% if current_user && current_user.admin? || current_user == @user %>
+ <p><%= link_to "Edit Profile", edit_user_path(@user) %></p>
+<% end %>
View
110 episode-159/blog/config/boot.rb
@@ -0,0 +1,110 @@
+# Don't change this file!
+# Configure your app in config/environment.rb and config/environments/*.rb
+
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
+
+module Rails
+ class << self
+ def boot!
+ unless booted?
+ preinitialize
+ pick_boot.run
+ end
+ end
+
+ def booted?
+ defined? Rails::Initializer
+ end
+
+ def pick_boot
+ (vendor_rails? ? VendorBoot : GemBoot).new
+ end
+
+ def vendor_rails?
+ File.exist?("#{RAILS_ROOT}/vendor/rails")
+ end
+
+ def preinitialize
+ load(preinitializer_path) if File.exist?(preinitializer_path)
+ end
+
+ def preinitializer_path
+ "#{RAILS_ROOT}/config/preinitializer.rb"
+ end
+ end
+
+ class Boot
+ def run
+ load_initializer
+ Rails::Initializer.run(:set_load_path)
+ end
+ end
+
+ class VendorBoot < Boot
+ def load_initializer
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+ Rails::Initializer.run(:install_gem_spec_stubs)
+ Rails::GemDependency.add_frozen_gem_path
+ end
+ end
+
+ class GemBoot < Boot
+ def load_initializer
+ self.class.load_rubygems
+ load_rails_gem
+ require 'initializer'
+ end
+
+ def load_rails_gem
+ if version = self.class.gem_version
+ gem 'rails', version
+ else
+ gem 'rails'
+ end
+ rescue Gem::LoadError => load_error
+ $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+ exit 1
+ end
+
+ class << self
+ def rubygems_version
+ Gem::RubyGemsVersion rescue nil
+ end
+
+ def gem_version
+ if defined? RAILS_GEM_VERSION
+ RAILS_GEM_VERSION
+ elsif ENV.include?('RAILS_GEM_VERSION')
+ ENV['RAILS_GEM_VERSION']
+ else
+ parse_gem_version(read_environment_rb)
+ end
+ end
+
+ def load_rubygems
+ require 'rubygems'
+ min_version = '1.3.1'
+ unless rubygems_version >= min_version
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
+ exit 1
+ end
+
+ rescue LoadError
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
+ exit 1
+ end
+
+ def parse_gem_version(text)
+ $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+ end
+
+ private
+ def read_environment_rb
+ File.read("#{RAILS_ROOT}/config/environment.rb")
+ end
+ end
+ end
+end
+
+# All that for this:
+Rails.boot!
View
22 episode-159/blog/config/database.yml
@@ -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
View
41 episode-159/blog/config/environment.rb
@@ -0,0 +1,41 @@
+# Be sure to restart your server when you modify this file
+
+# Specifies gem version of Rails to use when vendor/rails is not present
+RAILS_GEM_VERSION = '2.3.2' unless defined? RAILS_GEM_VERSION
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+ # 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( #{RAILS_ROOT}/extras )
+
+ # Specify gems that this application depends on and have them installed with rake gems:install
+ # config.gem "bj"
+ # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
+ # config.gem "sqlite3-ruby", :lib => "sqlite3"
+ # config.gem "aws-s3", :lib => "aws/s3"
+
+ # 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 ]
+
+ # Skip frameworks you're not going to use. To use Rails without a database,
+ # you must remove the Active Record framework.
+ # config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
+
+ # 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.
+ config.time_zone = 'UTC'
+
+ # 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}')]
+ # config.i18n.default_locale = :de
+end
View
17 episode-159/blog/config/environments/development.rb
@@ -0,0 +1,17 @@
+# 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.action_controller.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
View
28 episode-159/blog/config/environments/production.rb
@@ -0,0 +1,28 @@
+# 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.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching = true
+config.action_view.cache_template_loading = true
+
+# 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
+
+# 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!
View
34 episode-159/blog/config/environments/test.rb
@@ -0,0 +1,34 @@
+# 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.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+config.action_view.cache_template_loading = true
+
+# 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
+
+config.gem "rspec", :lib => false, :version => ">=1.2.2"
+config.gem "rspec-rails", :lib => false, :version => ">=1.2.2"
+config.gem "webrat", :lib => false, :version => ">=0.4.3"
+config.gem "cucumber", :lib => false, :version => ">=0.3.0"
+config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :source => "http://gems.github.com"
View
7 episode-159/blog/config/initializers/backtrace_silencers.rb
@@ -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 do debug a problem that might steem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
View
10 episode-159/blog/config/initializers/inflections.rb
@@ -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
View
5 episode-159/blog/config/initializers/mime_types.rb
@@ -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
View
19 episode-159/blog/config/initializers/new_rails_defaults.rb
@@ -0,0 +1,19 @@
+# Be sure to restart your server when you modify this file.
+
+# These settings change the behavior of Rails 2 apps and will be defaults
+# for Rails 3. You can remove this initializer when Rails 3 is released.
+
+if defined?(ActiveRecord)
+ # Include Active Record class name as root for JSON serialized output.
+ ActiveRecord::Base.include_root_in_json = true
+
+ # Store the full class name (including module namespace) in STI type column.
+ ActiveRecord::Base.store_full_sti_class = true
+end
+
+# Use ISO 8601 format for JSON serialized times and dates.
+ActiveSupport.use_standard_json_time_format = true
+
+# Don't escape HTML entities in JSON, leave that for the #json_escape helper.
+# if you're including raw json in an HTML page.
+ActiveSupport.escape_html_entities_in_json = false
View
15 episode-159/blog/config/initializers/session_store.rb
@@ -0,0 +1,15 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key for verifying cookie session data integrity.
+# If you change this key, all old sessions 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.
+ActionController::Base.session = {
+ :key => '_blog_session',
+ :secret => '58ffde92e5b3c99848d13e6dd0ba42f26d0ad82592355d9e9fd2ed5f263677f9e5d9e77be97304c78fc930fbd6e63bc341f657843413ff6851af1807761aefb1'
+}
+
+# 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")
+# ActionController::Base.session_store = :active_record_store
View
5 episode-159/blog/config/locales/en.yml
@@ -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"
View
52 episode-159/blog/config/routes.rb
@@ -0,0 +1,52 @@
+ActionController::Routing::Routes.draw do |map|
+ map.signup 'signup', :controller => 'users', :action => 'new'
+ map.logout 'logout', :controller => 'sessions', :action => 'destroy'
+ map.login 'login', :controller => 'sessions', :action => 'new'
+ map.resources :sessions
+
+ map.resources :users
+
+ map.resources :articles
+
+ # The priority is based upon order of creation: first created -> highest priority.
+
+ # Sample of regular route:
+ # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ # Sample of named route:
+ # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase'
+ # This route can be invoked with purchase_url(:id => product.id)
+
+ # Sample resource route (maps HTTP verbs to controller actions automatically):
+ # map.resources :products
+
+ # Sample resource route with options:
+ # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }
+
+ # Sample resource route with sub-resources:
+ # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller
+
+ # Sample resource route with more complex sub-resources
+ # map.resources :products do |products|
+ # products.resources :comments
+ # products.resources :sales, :collection => { :recent => :get }
+ # end
+
+ # Sample resource route within a namespace:
+ # map.namespace :admin do |admin|
+ # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb)
+ # admin.resources :products
+ # end
+
+ # You can have the root of your site routed with map.root -- just remember to delete public/index.html.
+ # map.root :controller => "welcome"
+
+ # See how all your routes lay out with "rake routes"
+
+ # Install the default routes as the lowest priority.
+ # Note: These default routes make all actions in every controller accessible via GET requests. You should
+ # consider removing the them or commenting them out if you're using named routes and resources.
+ map.connect ':controller/:action/:id'
+ map.connect ':controller/:action/:id.:format'
+end
View
14 episode-159/blog/db/migrate/20090330052103_create_articles.rb
@@ -0,0 +1,14 @@
+class CreateArticles < ActiveRecord::Migration
+ def self.up
+ create_table :articles do |t|
+ t.string :title
+ t.text :content
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :articles
+ end
+end
View
15 episode-159/blog/db/migrate/20090422195342_create_users.rb
@@ -0,0 +1,15 @@
+class CreateUsers < ActiveRecord::Migration
+ def self.up
+ create_table :users do |t|
+ t.column :username, :string
+ t.column :email, :string
+ t.column :password_hash, :string
+ t.column :password_salt, :string
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :users
+ end
+end
View
9 episode-159/blog/db/migrate/20090422200138_add_admin_to_users.rb
@@ -0,0 +1,9 @@
+class AddAdminToUsers < ActiveRecord::Migration
+ def self.up
+ add_column :users, :admin, :boolean, :default => false, :null => false
+ end
+
+ def self.down
+ remove_column :users, :admin
+ end
+end
View
31 episode-159/blog/db/schema.rb
@@ -0,0 +1,31 @@
+# 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 => 20090422200138) do
+
+ create_table "articles", :force => true do |t|
+ t.string "title"
+ t.text "content"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "users", :force => true do |t|
+ t.string "username"
+ t.string "email"
+ t.string "password_hash"
+ t.string "password_salt"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "admin", :default => false, :null => false
+ end
+
+end
View
2 episode-159/blog/doc/README_FOR_APP
@@ -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.
View
22 episode-159/blog/features/manage_articles.feature
@@ -0,0 +1,22 @@
+Feature: Manage Articles
+ In order to make a blog
+ As an author
+ I want to create and manage articles
+
+ Scenario: Articles List
+ Given I have articles titled Pizza, Breadsticks
+ When I go to the list of articles
+ Then I should see "Pizza"
+ And I should see "Breadsticks"
+
+ Scenario: Create Valid Article
+ Given I have no articles
+ And I am on the list of articles
+ When I follow "New Article"
+ And I fill in "Title" with "Spuds"
+ And I fill in "Content" with "Delicious potato wedges!"
+ And I press "Create"
+ Then I should see "New article created."
+ And I should see "Spuds"
+ And I should see "Delicious potato wedges!"
+ And I should have 1 article
View
21 episode-159/blog/features/manage_users.feature
@@ -0,0 +1,21 @@
+@focus
+Feature: Manage Users
+ In order to manage user details
+ As a security enthusiast
+ I want to edit user profiles only when authorized
+
+ Scenario Outline: Show or hide edit profile link
+ Given the following user records
+ | username | password | admin |
+ | bob | secret | false |
+ | admin | secret | true |
+ Given I am logged in as "<login>" with password "secret"
+ When I visit profile for "<profile>"
+ Then I should <action>
+
+ Examples:
+ | login | profile | action |
+ | admin | bob | see "Edit Profile" |
+ | bob | bob | see "Edit Profile" |
+ | | bob | not see "Edit Profile" |
+ | bob | admin | not see "Edit Profile" |
View
13 episode-159/blog/features/step_definitions/article_steps.rb
@@ -0,0 +1,13 @@
+Given /^I have articles titled (.+)$/ do |titles|
+ titles.split(', ').each do |title|
+ Article.create!(:title => title)
+ end
+end
+
+Given /^I have no articles$/ do
+ Article.delete_all
+end
+
+Then /^I should have ([0-9]+) articles?$/ do |count|
+ Article.count.should == count.to_i
+end
View
19 episode-159/blog/features/step_definitions/user_steps.rb
@@ -0,0 +1,19 @@
+Given /^the following (.+) records?$/ do |factory, table|
+ table.hashes.each do |hash|
+ Factory(factory, hash)
+ end
+end
+
+Given /^I am logged in as "([^\"]*)" with password "([^\"]*)"$/ do |username, password|
+ unless username.blank?
+ visit login_url
+ fill_in "Username", :with => username
+ fill_in "Password", :with => password
+ click_button "Log in"
+ end
+end
+
+When /^I visit profile for "([^\"]*)"$/ do |username|
+ user = User.find_by_username!(username)
+ visit user_url(user)
+end
View
115 episode-159/blog/features/step_definitions/webrat_steps.rb
@@ -0,0 +1,115 @@
+require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
+
+# Commonly used webrat steps
+# http://github.com/brynary/webrat
+
+Given /^I am on (.+)$/ do |page_name|
+ visit path_to(page_name)
+end
+
+When /^I go to (.+)$/ do |page_name|
+ visit path_to(page_name)
+end
+
+When /^I press "([^\"]*)"$/ do |button|
+ click_button(button)
+end
+
+When /^I follow "([^\"]*)"$/ do |link|
+ click_link(link)
+end
+
+When /^I fill in "([^\"]*)" with "([^\"]*)"$/ do |field, value|
+ fill_in(field, :with => value)
+end
+
+When /^I select "([^\"]*)" from "([^\"]*)"$/ do |value, field|
+ select(value, :from => field)
+end
+
+# Use this step in conjunction with Rail's datetime_select helper. For example:
+# When I select "December 25, 2008 10:00" as the date and time
+When /^I select "([^\"]*)" as the date and time$/ do |time|
+ select_datetime(time)
+end
+
+# Use this step when using multiple datetime_select helpers on a page or
+# you want to specify which datetime to select. Given the following view:
+# <%= f.label :preferred %><br />
+# <%= f.datetime_select :preferred %>
+# <%= f.label :alternative %><br />
+# <%= f.datetime_select :alternative %>
+# The following steps would fill out the form:
+# When I select "November 23, 2004 11:20" as the "Preferred" data and time
+# And I select "November 25, 2004 10:30" as the "Alternative" data and time
+When /^I select "([^\"]*)" as the "([^\"]*)" date and time$/ do |datetime, datetime_label|
+ select_datetime(datetime, :from => datetime_label)
+end
+
+# Use this step in conjunction with Rail's time_select helper. For example:
+# When I select "2:20PM" as the time
+# Note: Rail's default time helper provides 24-hour time-- not 12 hour time. Webrat
+# will convert the 2:20PM to 14:20 and then select it.
+When /^I select "([^\"]*)" as the time$/ do |time|
+ select_time(time)
+end
+
+# Use this step when using multiple time_select helpers on a page or you want to
+# specify the name of the time on the form. For example:
+# When I select "7:30AM" as the "Gym" time
+When /^I select "([^\"]*)" as the "([^\"]*)" time$/ do |time, time_label|
+ select_time(time, :from => time_label)
+end
+
+# Use this step in conjunction with Rail's date_select helper. For example:
+# When I select "February 20, 1981" as the date
+When /^I select "([^\"]*)" as the date$/ do |date|
+ select_date(date)
+end
+
+# Use this step when using multiple date_select helpers on one page or
+# you want to specify the name of the date on the form. For example:
+# When I select "April 26, 1982" as the "Date of Birth" date
+When /^I select "([^\"]*)" as the "([^\"]*)" date$/ do |date, date_label|
+ select_date(date, :from => date_label)
+end
+
+When /^I check "([^\"]*)"$/ do |field|
+ check(field)
+end
+
+When /^I uncheck "([^\"]*)"$/ do |field|
+ uncheck(field)
+end
+
+When /^I choose "([^\"]*)"$/ do |field|
+ choose(field)
+end
+
+When /^I attach the file at "([^\"]*)" to "([^\"]*)"$/ do |path, field|
+ attach_file(field, path)
+end
+
+Then /^I should see "([^\"]*)"$/ do |text|
+ response.should contain(text)
+end
+
+Then /^I should not see "([^\"]*)"$/ do |text|
+ response.should_not contain(text)
+end
+
+Then /^the "([^\"]*)" field should contain "([^\"]*)"$/ do |field, value|
+ field_labeled(field).value.should =~ /#{value}/
+ end
+
+Then /^the "([^\"]*)" field should not contain "([^\"]*)"$/ do |field, value|
+ field_labeled(field).value.should_not =~ /#{value}/
+ end
+
+Then /^the "([^\"]*)" checkbox should be checked$/ do |label|
+ field_labeled(label).should be_checked
+end
+
+Then /^I should be on (.+)$/ do |page_name|
+ URI.parse(current_url).path.should == path_to(page_name)
+end
View
18 episode-159/blog/features/support/env.rb
@@ -0,0 +1,18 @@
+# Sets up the Rails environment for Cucumber
+ENV["RAILS_ENV"] ||= "test"
+require File.expand_path(File.dirname(__FILE__) + '/../../config/environment')
+require 'cucumber/rails/world'
+require 'cucumber/formatter/unicode' # Comment out this line if you don't want Cucumber Unicode support
+Cucumber::Rails.use_transactional_fixtures
+Cucumber::Rails.bypass_rescue # Comment out this line if you want Rails own error handling
+ # (e.g. rescue_action_in_public / rescue_responses / rescue_from)
+
+require 'webrat'
+
+Webrat.configure do |config|
+ config.mode = :rails
+end
+
+require 'cucumber/rails/rspec'
+require 'webrat/core/matchers'
+require "#{Rails.root}/spec/factories"
View
18 episode-159/blog/features/support/paths.rb
@@ -0,0 +1,18 @@
+module NavigationHelpers
+ def path_to(page_name)
+ case page_name
+
+ when /the homepage/
+ root_path
+ when /the list of articles/
+ articles_path
+
+ # Add more page name => path mappings here
+
+ else
+ raise "Can't find mapping from \"#{page_name}\" to a path."
+ end
+ end
+end
+
+World(NavigationHelpers)
View
36 episode-159/blog/lib/authentication.rb
@@ -0,0 +1,36 @@
+# This module is included in your application controller which makes
+# several methods available to all controllers and views. Here's a
+# common example you might add to your application layout file.
+#
+# <% if logged_in? %>
+# Welcome <%= current_user.username %>! Not you?
+# <%= link_to "Log out", logout_path %>
+# <% else %>
+# <%= link_to "Sign up", signup_path %> or
+# <%= link_to "log in", login_path %>.
+# <% end %>
+#
+# You can also restrict unregistered users from accessing a controller using
+# a before filter. For example.
+#
+# before_filter :login_required, :except => [:index, :show]
+module Authentication
+ def self.included(controller)
+ controller.send :helper_method, :current_user, :logged_in?
+ end
+
+ def current_user
+ @current_user ||= User.find(session[:user_id]) if session[:user_id]
+ end
+
+ def logged_in?
+ current_user
+ end
+
+ def login_required
+ unless logged_in?
+ flash[:error] = "You must first log in or sign up before accessing this page."
+ redirect_to login_url
+ end
+ end
+end
View
15 episode-159/blog/lib/tasks/cucumber.rake
@@ -0,0 +1,15 @@
+$LOAD_PATH.unshift(RAILS_ROOT + '/vendor/plugins/cucumber/lib') if File.directory?(RAILS_ROOT + '/vendor/plugins/cucumber/lib')
+
+begin
+ require 'cucumber/rake/task'
+
+ Cucumber::Rake::Task.new(:features) do |t|
+ t.cucumber_opts = "--format pretty"
+ end
+ task :features => 'db:test:prepare'
+rescue LoadError
+ desc 'Cucumber rake task not available'
+ task :features do
+ abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin'
+ end
+end
View
30 episode-159/blog/public/404.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <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>
View
30 episode-159/blog/public/422.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <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>
View
30 episode-159/blog/public/500.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <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>
View
0 episode-159/blog/public/favicon.ico
No changes.
View
BIN episode-159/blog/public/images/rails.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
275 episode-159/blog/public/index.html
@@ -0,0 +1,275 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+ <head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <title>Ruby on Rails: Welcome aboard</title>
+ <style type="text/css" media="screen">
+ body {
+ margin: 0;
+ margin-bottom: 25px;
+ padding: 0;
+ background-color: #f0f0f0;
+ font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
+ font-size: 13px;
+ color: #333;
+ }
+
+ h1 {
+ font-size: 28px;
+ color: #000;
+ }
+
+ a {color: #03c}
+ a:hover {
+ background-color: #03c;
+ color: white;
+ text-decoration: none;
+ }
+
+
+ #page {
+ background-color: #f0f0f0;
+ width: 750px;
+ margin: 0;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ #content {
+ float: left;
+ background-color: white;
+ border: 3px solid #aaa;
+ border-top: none;
+ padding: 25px;
+ width: 500px;
+ }
+
+ #sidebar {
+ float: right;
+ width: 175px;
+ }
+
+ #footer {
+ clear: both;
+ }
+
+
+ #header, #about, #getting-started {
+ padding-left: 75px;
+ padding-right: 30px;
+ }
+
+
+ #header {
+ background-image: url("images/rails.png");
+ background-repeat: no-repeat;
+ background-position: top left;
+ height: 64px;
+ }
+ #header h1, #header h2 {margin: 0}
+ #header h2 {
+ color: #888;
+ font-weight: normal;
+ font-size: 16px;
+ }
+
+
+ #about h3 {
+ margin: 0;
+ margin-bottom: 10px;
+ font-size: 14px;
+ }
+
+ #about-content {
+ background-color: #ffd;
+ border: 1px solid #fc0;
+ margin-left: -11px;
+ }
+ #about-content table {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 11px;
+ border-collapse: collapse;
+ }
+ #about-content td {
+ padding: 10px;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ }
+ #about-content td.name {color: #555}
+ #about-content td.value {color: #000}
+
+ #about-content.failure {
+ background-color: #fcc;
+ border: 1px solid #f00;
+ }
+ #about-content.failure p {
+ margin: 0;
+ padding: 10px;
+ }
+
+
+ #getting-started {
+ border-top: 1px solid #ccc;
+ margin-top: 25px;
+ padding-top: 15px;
+ }
+ #getting-started h1 {
+ margin: 0;
+ font-size: 20px;
+ }
+ #getting-started h2 {
+ margin: 0;
+ font-size: 14px;
+ font-weight: normal;
+ color: #333;
+ margin-bottom: 25px;
+ }
+ #getting-started ol {
+ margin-left: 0;
+ padding-left: 0;
+ }
+ #getting-started li {
+ font-size: 18px;
+ color: #888;
+ margin-bottom: 25px;
+ }
+ #getting-started li h2 {
+ margin: 0;
+ font-weight: normal;
+ font-size: 18px;
+ color: #333;
+ }
+ #getting-started li p {
+ color: #555;
+ font-size: 13px;
+ }
+
+
+ #search {
+ margin: 0;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ font-size: 11px;
+ }
+ #search input {
+ font-size: 11px;
+ margin: 2px;
+ }
+ #search-text {width: 170px}
+
+
+ #sidebar ul {
+ margin-left: 0;
+ padding-left: 0;
+ }
+ #sidebar ul h3 {
+ margin-top: 25px;
+ font-size: 16px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #ccc;
+ }
+ #sidebar li {
+ list-style-type: none;
+ }
+ #sidebar ul.links li {
+ margin-bottom: 5px;
+ }
+
+ </style>
+ <script type="text/javascript" src="javascripts/prototype.js"></script>
+ <script type="text/javascript" src="javascripts/effects.js"></script>
+ <script type="text/javascript">
+ function about() {
+ if (Element.empty('about-content')) {
+ new Ajax.Updater('about-content', 'rails/info/properties', {
+ method: 'get',
+ onFailure: function() {Element.classNames('about-content').add('failure')},
+ onComplete: function() {new Effect.BlindDown('about-content', {duration: 0.25})}
+ });
+ } else {
+ new Effect[Element.visible('about-content') ?
+ 'BlindUp' : 'BlindDown']('about-content', {duration: 0.25});
+ }
+ }
+
+ window.onload = function() {
+ $('search-text').value = '';
+ $('search').onsubmit = function() {
+ $('search-text').value = 'site:rubyonrails.org ' + $F('search-text');
+ }
+ }
+ </script>
+ </head>
+ <body>
+ <div id="page">
+ <div id="sidebar">
+ <ul id="sidebar-items">
+ <li>
+ <form id="search" action="http://www.google.com/search" method="get">
+ <input type="hidden" name="hl" value="en" />
+ <input type="text" id="search-text" name="q" value="site:rubyonrails.org " />
+ <input type="submit" value="Search" /> the Rails site
+ </form>
+ </li>
+
+ <li>
+ <h3>Join the community</h3>
+ <ul class="links">
+ <li><a href="http://www.rubyonrails.org/">Ruby on Rails</a></li>
+ <li><a href="http://weblog.rubyonrails.org/">Official weblog</a></li>
+ <li><a href="http://wiki.rubyonrails.org/">Wiki</a></li>
+ </ul>
+ </li>
+
+ <li>
+ <h3>Browse the documentation</h3>
+ <ul class="links">
+ <li><a href="http://api.rubyonrails.org/">Rails API</a></li>
+ <li><a href="http://stdlib.rubyonrails.org/">Ruby standard library</a></li>
+ <li><a href="http://corelib.rubyonrails.org/">Ruby core</a></li>
+ <li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+
+ <div id="content">
+ <div id="header">
+ <h1>Welcome aboard</h1>
+ <h2>You&rsquo;re riding Ruby on Rails!</h2>
+ </div>
+
+ <div id="about">
+ <h3><a href="rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
+ <div id="about-content" style="display: none"></div>
+ </div>
+
+ <div id="getting-started">
+ <h1>Getting started</h1>
+ <h2>Here&rsquo;s how to get rolling:</h2>
+
+ <ol>
+ <li>
+ <h2>Use <tt>script/generate</tt> to create your models and controllers</h2>
+ <p>To see all available options, run it without parameters.</p>
+ </li>
+
+ <li>
+ <h2>Set up a default route and remove or rename this file</h2>
+ <p>Routes are set up in config/routes.rb.</p>
+ </li>
+
+ <li>
+ <h2>Create your database</h2>
+ <p>Run <tt>rake db:migrate</tt> to create your database. If you're not using SQLite (the default), edit <tt>config/database.yml</tt> with your username and password.</p>
+ </li>
+ </ol>
+ </div>
+ </div>
+
+ <div id="footer">&nbsp;</div>
+ </div>
+ </body>
+</html>
View
2 episode-159/blog/public/javascripts/application.js
@@ -0,0 +1,2 @@
+// Place your application-specific JavaScript functions and classes here
+// This file is automatically included by javascript_include_tag :defaults
View
963 episode-159/blog/public/javascripts/controls.js
@@ -0,0 +1,963 @@
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2008 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() {