├── log └── .gitkeep ├── lib ├── tasks │ └── .gitkeep └── assets │ ├── .gitkeep │ └── javascripts │ ├── underscore.js │ └── backbone.js ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html ├── 404.html └── index.html ├── test ├── unit │ ├── .gitkeep │ ├── helpers │ │ └── books_helper_test.rb │ └── book_test.rb ├── fixtures │ ├── .gitkeep │ └── books.yml ├── functional │ ├── .gitkeep │ └── books_controller_test.rb ├── integration │ └── .gitkeep ├── performance │ └── browsing_test.rb └── test_helper.rb ├── app ├── mailers │ └── .gitkeep ├── models │ ├── .gitkeep │ └── book.rb ├── assets │ ├── javascripts │ │ ├── models │ │ │ ├── index.js │ │ │ └── book.js.coffee │ │ ├── routers │ │ │ ├── index.js │ │ │ └── books_router.js.coffee │ │ ├── collections │ │ │ ├── index.js │ │ │ └── books.js.coffee │ │ ├── templates │ │ │ ├── index.js │ │ │ └── books │ │ │ │ ├── show.jst.ejs │ │ │ │ ├── form.jst.ejs │ │ │ │ └── index.jst.ejs │ │ ├── views │ │ │ ├── index.js │ │ │ └── books │ │ │ │ ├── show.js.coffee │ │ │ │ ├── index.js.coffee │ │ │ │ └── form.js.coffee │ │ ├── app.js.coffee │ │ ├── realtime.js.coffee │ │ ├── config.js.coffee │ │ └── application.js │ ├── images │ │ └── rails.png │ └── stylesheets │ │ ├── books.css.scss │ │ ├── application.css │ │ └── scaffolds.css.scss ├── helpers │ ├── books_helper.rb │ └── application_helper.rb ├── views │ ├── books │ │ ├── index.html.erb │ │ ├── show.html.erb │ │ ├── new.html.erb │ │ ├── edit.html.erb │ │ └── _form.html.erb │ └── layouts │ │ └── application.html.erb └── controllers │ ├── application_controller.rb │ └── books_controller.rb ├── vendor ├── plugins │ └── .gitkeep └── assets │ ├── javascripts │ └── .gitkeep │ └── stylesheets │ └── .gitkeep ├── realtime ├── README.md ├── package.json └── realtime-server.js ├── config ├── initializers │ ├── redis.rb │ ├── mime_types.rb │ ├── backtrace_silencers.rb │ ├── session_store.rb │ ├── secret_token.rb │ ├── wrap_parameters.rb │ └── inflections.rb ├── environment.rb ├── boot.rb ├── locales │ └── en.yml ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb ├── database.yml ├── routes.rb └── application.rb ├── config.ru ├── doc └── README_FOR_APP ├── db ├── migrate │ └── 20130201143533_create_books.rb ├── seeds.rb └── schema.rb ├── Rakefile ├── script └── rails ├── .gitignore ├── Gemfile ├── README.md └── Gemfile.lock /log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /realtime/README.md: -------------------------------------------------------------------------------- 1 | Real Time! -------------------------------------------------------------------------------- /test/functional/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/index.js: -------------------------------------------------------------------------------- 1 | //=require_tree . -------------------------------------------------------------------------------- /app/assets/javascripts/routers/index.js: -------------------------------------------------------------------------------- 1 | //=require_tree . -------------------------------------------------------------------------------- /app/helpers/books_helper.rb: -------------------------------------------------------------------------------- 1 | module BooksHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/javascripts/collections/index.js: -------------------------------------------------------------------------------- 1 | //=require_tree . -------------------------------------------------------------------------------- /app/assets/javascripts/templates/index.js: -------------------------------------------------------------------------------- 1 | //= require_tree . -------------------------------------------------------------------------------- /app/assets/javascripts/views/index.js: -------------------------------------------------------------------------------- 1 | //=require_tree . 2 | 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /config/initializers/redis.rb: -------------------------------------------------------------------------------- 1 | $redis = Redis.new(:host => 'localhost', :port=> 6379) -------------------------------------------------------------------------------- /app/views/books/index.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamks/rails-realtime/HEAD/app/assets/images/rails.png -------------------------------------------------------------------------------- /app/views/books/show.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/views/books/new.html.erb: -------------------------------------------------------------------------------- 1 |

New book

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Back', books_path %> 6 | -------------------------------------------------------------------------------- /test/unit/helpers/books_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BooksHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /app/views/books/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing book

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Show', @book %> | 6 | <%= link_to 'Back', books_path %> 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/book.js.coffee: -------------------------------------------------------------------------------- 1 | app.models.Book = Backbone.Model.extend 2 | urlRoot : '/books' 3 | defaults : 4 | title : '' 5 | num_pages : 0 -------------------------------------------------------------------------------- /test/unit/book_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BookTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run RailsRealtime::Application 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | RailsRealtime::Application.initialize! 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/app.js.coffee: -------------------------------------------------------------------------------- 1 | $ () -> 2 | start = () -> 3 | app.realtime.connect(); 4 | booksRouter = new app.routers.Books(); 5 | Backbone.history.start({pushState: true}); 6 | 7 | start(); -------------------------------------------------------------------------------- /app/assets/javascripts/templates/books/show.jst.ejs: -------------------------------------------------------------------------------- 1 |

2 | Title: 3 | <%= title %> 4 |

5 | 6 |

7 | Num pages: 8 | <%= num_pages %> 9 |

10 | 11 | Show All Books -------------------------------------------------------------------------------- /app/assets/stylesheets/books.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Books controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /test/fixtures/books.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html 2 | 3 | one: 4 | title: MyString 5 | num_pages: 1 6 | 7 | two: 8 | title: MyString 9 | num_pages: 1 10 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /realtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "RoR-real-time", 3 | "description" : "providing real-time sychronization for ruby on rails", 4 | "version" : "0.0.1", 5 | "dependencies" : { 6 | "socket.io" : "0.9.12", 7 | "redis": "0.7.3" 8 | } 9 | } -------------------------------------------------------------------------------- /db/migrate/20130201143533_create_books.rb: -------------------------------------------------------------------------------- 1 | class CreateBooks < ActiveRecord::Migration 2 | def change 3 | create_table :books do |t| 4 | t.string :title 5 | t.integer :num_pages 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/realtime.js.coffee: -------------------------------------------------------------------------------- 1 | window.app.realtime = 2 | connect : () -> 3 | window.app.socket = io.connect('http://0.0.0.0:5001'); 4 | 5 | window.app.socket.on 'rt-change', (message) -> 6 | window.app.trigger 'books', message 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | RailsRealtime::Application.load_tasks 8 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /realtime/realtime-server.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io').listen(5001), 2 | redis = require('redis').createClient(); 3 | 4 | redis.subscribe('rt-change'); 5 | 6 | io.on('connection', function(socket){ 7 | redis.on('message', function(channel, message){ 8 | socket.emit('rt-change', JSON.parse(message)); 9 | }); 10 | }); -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RailsRealtime 5 | <%= stylesheet_link_tag "application", :media => "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/books/show.js.coffee: -------------------------------------------------------------------------------- 1 | app.views.books ?= {} 2 | 3 | app.views.books.Show = Backbone.View.extend 4 | id : 'show-view' 5 | 6 | className : 'action-view' 7 | 8 | template : JST['templates/books/show'] 9 | 10 | serialize : -> 11 | @model.toJSON() if @model 12 | 13 | render : -> 14 | @$el.html @template(@serialize()) if @model 15 | @$el -------------------------------------------------------------------------------- /app/assets/javascripts/templates/books/form.jst.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 |
-------------------------------------------------------------------------------- /test/performance/browsing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/performance_test_help' 3 | 4 | class BrowsingTest < ActionDispatch::PerformanceTest 5 | # Refer to the documentation for all available options 6 | # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory] 7 | # :output => 'tmp/performance', :formats => [:flat] } 8 | 9 | def test_homepage 10 | get '/' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/models/book.rb: -------------------------------------------------------------------------------- 1 | class Book < ActiveRecord::Base 2 | attr_accessible :num_pages, :title 3 | after_create {|book| book.message 'create' } 4 | after_update {|book| book.message 'update' } 5 | after_destroy {|book| book.message 'destroy' } 6 | 7 | def message action 8 | msg = { resource: 'books', 9 | action: action, 10 | id: self.id, 11 | obj: self } 12 | 13 | $redis.publish 'rt-change', msg.to_json 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | RailsRealtime::Application.config.session_store :cookie_store, key: '_rails-realtime_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # RailsRealtime::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/collections/books.js.coffee: -------------------------------------------------------------------------------- 1 | app.collections.Books = Backbone.Collection.extend 2 | model : app.models.Book 3 | url : '/books' 4 | 5 | initialize : () -> 6 | app.on 'books', @handle_change, @ 7 | 8 | handle_change : (message) -> 9 | switch message.action 10 | when 'create' 11 | @add message.obj 12 | when 'update' 13 | model = @get message.id 14 | model.set message.obj 15 | when 'destroy' 16 | @remove message.obj 17 | 18 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. 7 | # 8 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 9 | # -- they do not yet inherit this setting 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | RailsRealtime::Application.config.secret_token = '688501f2b23da0a2411c3678cec497ed861be1cd04033c460eadc006a70b30142c5b4ce05dd80f31134e420e53cf8b017cd75e28f0be8d7e3c3ec039f30b1be9' 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | # Ignore bundler config 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/*.log 15 | /tmp 16 | 17 | # ignore node modules 18 | /realtime/node_modules 19 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/config.js.coffee: -------------------------------------------------------------------------------- 1 | window.app = 2 | models : {} 3 | collections : {} 4 | views : {} 5 | routers : {} 6 | navigate : new Backbone.Router().navigate 7 | 8 | _.extend window.app, Backbone.Events 9 | 10 | $.ajaxSetup 11 | headers : 12 | 'X-CSRF-Token' : $('meta[name=csrf-token]').attr('content') 13 | 14 | $(document).on 'click', 'a:not([data-bypass])', (evt) -> 15 | href = $(@).attr 'href' 16 | protocol = @protocol + '//' 17 | 18 | if href.slice(protocol.length) != protocol 19 | evt.preventDefault(); 20 | app.navigate href, true 21 | 22 | 23 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/books/index.jst.ejs: -------------------------------------------------------------------------------- 1 |

Listing Books

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% books.each(function(book){ %> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% }); %> 20 |
TitleNumber of Pages
<%= book.get('title') %><%= book.get('num_pages') %>ShowEditDestroy
21 | New Book -------------------------------------------------------------------------------- /app/views/books/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@book) do |f| %> 2 | <% if @book.errors.any? %> 3 |
4 |

<%= pluralize(@book.errors.count, "error") %> prohibited this book from being saved:

5 | 6 | 11 |
12 | <% end %> 13 | 14 |
15 | <%= f.label :title %>
16 | <%= f.text_field :title %> 17 |
18 |
19 | <%= f.label :num_pages %>
20 | <%= f.number_field :num_pages %> 21 |
22 |
23 | <%= f.submit %> 24 |
25 | <% end %> 26 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/books/index.js.coffee: -------------------------------------------------------------------------------- 1 | app.views.books ?= {} 2 | 3 | app.views.books.Index = Backbone.View.extend 4 | id : 'index-view' 5 | 6 | className : 'action-view' 7 | 8 | template : JST['templates/books/index'] 9 | 10 | events : 11 | 'click a[data-method=delete]' : 'destroy' 12 | 13 | initialize : -> 14 | @collection.on 'reset', @.render, @ 15 | @collection.on 'change add remove', @.render, @ 16 | 17 | destroy : (evt) -> 18 | evt.preventDefault() 19 | $a = $(evt.currentTarget) 20 | id = $a.attr('data-id') 21 | model = @collection.get id 22 | model.destroy() 23 | @collection.remove model 24 | 25 | 26 | serialize : -> 27 | books : @collection 28 | 29 | render : -> 30 | @$el.html @template(@serialize()) 31 | @$el -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '3.2.11' 4 | 5 | # Bundle edge Rails instead: 6 | # gem 'rails', :git => 'git://github.com/rails/rails.git' 7 | 8 | gem 'pg' 9 | gem 'redis' 10 | 11 | # Gems used only for assets and not required 12 | # in production environments by default. 13 | group :assets do 14 | gem 'sass-rails', '~> 3.2.3' 15 | gem 'coffee-rails', '~> 3.2.1' 16 | gem 'ejs' 17 | 18 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 19 | # gem 'therubyracer', :platforms => :ruby 20 | 21 | gem 'uglifier', '>= 1.0.3' 22 | end 23 | 24 | gem 'jquery-rails' 25 | 26 | # To use ActiveModel has_secure_password 27 | # gem 'bcrypt-ruby', '~> 3.0.0' 28 | 29 | # To use Jbuilder templates for JSON 30 | # gem 'jbuilder' 31 | 32 | gem 'thin' 33 | # Use unicorn as the app server 34 | # gem 'unicorn' 35 | 36 | # Deploy with Capistrano 37 | # gem 'capistrano' 38 | 39 | # To use debugger 40 | # gem 'debugger' 41 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require jquery 14 | //= require underscore 15 | //= require backbone 16 | //= require socket.io 17 | // 18 | //= require config 19 | //= require realtime 20 | //= require templates 21 | // 22 | //= require models 23 | //= require collections 24 | //= require views 25 | //= require routers 26 | // 27 | //= require app 28 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20130201143533) do 15 | 16 | create_table "books", :force => true do |t| 17 | t.string "title" 18 | t.integer "num_pages" 19 | t.datetime "created_at", :null => false 20 | t.datetime "updated_at", :null => false 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/books/form.js.coffee: -------------------------------------------------------------------------------- 1 | app.views.books ?= {} 2 | 3 | app.views.books.Form = Backbone.View.extend 4 | id : 'form-view' 5 | 6 | className : 'action-view' 7 | 8 | template : JST['templates/books/form'] 9 | 10 | events : 11 | 'click input[type=submit]' : 'save' 12 | 13 | initialize : -> 14 | @model = new app.models.Book() 15 | 16 | save : (evt) -> 17 | evt.preventDefault() 18 | @isNew = @model.isNew() 19 | @model.save @formValues(), 20 | success : () => 21 | if @isNew 22 | app.collections.books.add @model 23 | 24 | @clear() 25 | app.navigate '/books/', true 26 | error :(error) => 27 | console.log error 28 | 29 | clear : () -> 30 | @model = new app.models.Book() 31 | this.$el.find('input[type=text],input[type=number]').val('') 32 | 33 | serialize : -> 34 | @model.toJSON() 35 | 36 | formValues : -> 37 | title : this.$el.find('input[name=title]').val() 38 | num_pages : this.$el.find('input[name=num_pages]').val() 39 | 40 | render : -> 41 | @$el.html @template(@serialize()) 42 | @$el -------------------------------------------------------------------------------- /app/assets/javascripts/routers/books_router.js.coffee: -------------------------------------------------------------------------------- 1 | app.routers.Books = Backbone.Router.extend 2 | initialize : -> 3 | @books = new app.collections.Books window.books 4 | @indexView = new app.views.books.Index 5 | collection : @books 6 | 7 | @showView = new app.views.books.Show 8 | model : @books.at 0 9 | 10 | @formView = new app.views.books.Form() 11 | 12 | $('body').append @indexView.render() 13 | $('body').append @showView.render() 14 | $('body').append @formView.render() 15 | 16 | routes : 17 | "books/" : "index" 18 | "books/new" : "new" 19 | "books/:id" : "show" 20 | "books/:id/edit" : "edit" 21 | 22 | index : () -> 23 | $('.action-view').hide() 24 | @indexView.$el.show() 25 | 26 | show : (id) -> 27 | $('.action-view').hide() 28 | model = @books.get id 29 | @showView.model = model 30 | @showView.render() 31 | @showView.$el.show() 32 | 33 | edit : (id) -> 34 | $('.action-view').hide() 35 | @formView.model = @books.get id 36 | @formView.render() 37 | @formView.$el.show() 38 | 39 | new : () -> 40 | $('.action-view').hide() 41 | @formView.clear() 42 | @formView.$el.show() -------------------------------------------------------------------------------- /test/functional/books_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BooksControllerTest < ActionController::TestCase 4 | setup do 5 | @book = books(:one) 6 | end 7 | 8 | test "should get index" do 9 | get :index 10 | assert_response :success 11 | assert_not_nil assigns(:books) 12 | end 13 | 14 | test "should get new" do 15 | get :new 16 | assert_response :success 17 | end 18 | 19 | test "should create book" do 20 | assert_difference('Book.count') do 21 | post :create, book: { num_pages: @book.num_pages, title: @book.title } 22 | end 23 | 24 | assert_redirected_to book_path(assigns(:book)) 25 | end 26 | 27 | test "should show book" do 28 | get :show, id: @book 29 | assert_response :success 30 | end 31 | 32 | test "should get edit" do 33 | get :edit, id: @book 34 | assert_response :success 35 | end 36 | 37 | test "should update book" do 38 | put :update, id: @book, book: { num_pages: @book.num_pages, title: @book.title } 39 | assert_redirected_to book_path(assigns(:book)) 40 | end 41 | 42 | test "should destroy book" do 43 | assert_difference('Book.count', -1) do 44 | delete :destroy, id: @book 45 | end 46 | 47 | assert_redirected_to books_path 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/assets/stylesheets/scaffolds.css.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | color: #333; 4 | font-family: verdana, arial, helvetica, sans-serif; 5 | font-size: 13px; 6 | line-height: 18px; 7 | } 8 | 9 | p, ol, ul, td { 10 | font-family: verdana, arial, helvetica, sans-serif; 11 | font-size: 13px; 12 | line-height: 18px; 13 | } 14 | 15 | pre { 16 | background-color: #eee; 17 | padding: 10px; 18 | font-size: 11px; 19 | } 20 | 21 | a { 22 | color: #000; 23 | &:visited { 24 | color: #666; 25 | } 26 | &:hover { 27 | color: #fff; 28 | background-color: #000; 29 | } 30 | } 31 | 32 | div { 33 | &.field, &.actions { 34 | margin-bottom: 10px; 35 | } 36 | } 37 | 38 | #notice { 39 | color: green; 40 | } 41 | 42 | .field_with_errors { 43 | padding: 2px; 44 | background-color: red; 45 | display: table; 46 | } 47 | 48 | #error_explanation { 49 | width: 450px; 50 | border: 2px solid red; 51 | padding: 7px; 52 | padding-bottom: 0; 53 | margin-bottom: 20px; 54 | background-color: #f0f0f0; 55 | h2 { 56 | text-align: left; 57 | font-weight: bold; 58 | padding: 5px 5px 5px 15px; 59 | font-size: 12px; 60 | margin: -7px; 61 | margin-bottom: 0px; 62 | background-color: #c00; 63 | color: #fff; 64 | } 65 | ul li { 66 | font-size: 12px; 67 | list-style: square; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | RailsRealtime::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise exception on mass assignment protection for Active Record models 26 | config.active_record.mass_assignment_sanitizer = :strict 27 | 28 | # Log the query plan for queries taking more than this (works 29 | # with SQLite, MySQL, and PostgreSQL) 30 | config.active_record.auto_explain_threshold_in_seconds = 0.5 31 | 32 | # Do not compress assets 33 | config.assets.compress = false 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = true 37 | end 38 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On Mac OS X with macports: 6 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 7 | # On Windows: 8 | # gem install pg 9 | # Choose the win32 build. 10 | # Install PostgreSQL and put its /bin directory on your path. 11 | # 12 | # Configure Using Gemfile 13 | # gem 'pg' 14 | # 15 | development: 16 | adapter: postgresql 17 | encoding: unicode 18 | database: rails_realtime_development 19 | pool: 5 20 | password: 21 | 22 | # Connect on a TCP socket. Omitted by default since the client uses a 23 | # domain socket that doesn't need configuration. Windows does not have 24 | # domain sockets, so uncomment these lines. 25 | #host: localhost 26 | #port: 5432 27 | 28 | # Schema search path. The server defaults to $user,public 29 | #schema_search_path: myapp,sharedapp,public 30 | 31 | # Minimum log levels, in increasing order: 32 | # debug5, debug4, debug3, debug2, debug1, 33 | # log, notice, warning, error, fatal, and panic 34 | # The server defaults to notice. 35 | #min_messages: warning 36 | 37 | # Warning: The database defined as "test" will be erased and 38 | # re-generated from your development database when you run "rake". 39 | # Do not set this db to the same as development or production. 40 | test: 41 | adapter: postgresql 42 | encoding: unicode 43 | database: rails_realtime_test 44 | pool: 5 45 | password: 46 | 47 | production: 48 | adapter: postgresql 49 | encoding: unicode 50 | database: rails_realtime_production 51 | pool: 5 52 | password: 53 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | RailsRealtime::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Raise exception on mass assignment protection for Active Record models 33 | config.active_record.mass_assignment_sanitizer = :strict 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | end 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adding Real-Time To Your RESTful Rails App 2 | 3 | This repository contains the code for both the Rails app, and the Node app, that accompanies the blog entry ["Adding Real-Time To Your RESTful Rails App"](http://liamkaufman.com/blog/2013/02/27/adding-real-time-to-a-restful-rails-app/). 4 | 5 | ## Steps I Took 6 | 7 | Below are some of the steps I took, that were not outlined in the above blog entry. Make sure you have Redis installed and running! 8 | 9 | ``` 10 | rails new rails_realtime --database=postgresql 11 | cd rails_realtime 12 | 13 | rake db:create 14 | rails generate scaffold Book title:string num_pages:integer 15 | rake db:migrate 16 | ``` 17 | 18 | Add ```gem 'redis'``` and ```gem 'pg'``` to the Gemfile, then run ```bundle install```. 19 | 20 | ### Creating The Node.js App 21 | 22 | ``` 23 | mkdir realtime 24 | cd realtime 25 | echo 'Real-Time' > README.md 26 | ``` 27 | 28 | Then create ```package.json``` (see ```realtime/package.json``` for reference). From the realtime directory run ```npm install```. In your Rails' ```.gitignore``` file add ```/realtime/node_modules``` to ignore the installed node modules. 29 | 30 | ### The Backbone.js App 31 | 32 | The Backbone.js application resides in ```app/assets/javascripts```. ```application.js``` specificies the javascript files that comprise the web application and their load order. If you're building a real production app you may want to look into http://requirejs.org/ to manage your dependencies. ```app.js.coffee``` is the starting point for the Backbone.js application. 33 | 34 | ## To Start The App 35 | 36 | The Rails App: ```rails s``` 37 | 38 | The Node App (from the realtime folder): ```node realtime-server.js``` 39 | 40 | 41 | # Done 42 | 43 | Between the Blog, the code in this repository, and the above steps you should hopefully have the information necessary to add real-time to your Rails app! -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | RailsRealtime::Application.routes.draw do 2 | resources :books 3 | 4 | 5 | # The priority is based upon order of creation: 6 | # first created -> highest priority. 7 | 8 | # Sample of regular route: 9 | # match 'products/:id' => 'catalog#view' 10 | # Keep in mind you can assign values other than :controller and :action 11 | 12 | # Sample of named route: 13 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 14 | # This route can be invoked with purchase_url(:id => product.id) 15 | 16 | # Sample resource route (maps HTTP verbs to controller actions automatically): 17 | # resources :products 18 | 19 | # Sample resource route with options: 20 | # resources :products do 21 | # member do 22 | # get 'short' 23 | # post 'toggle' 24 | # end 25 | # 26 | # collection do 27 | # get 'sold' 28 | # end 29 | # end 30 | 31 | # Sample resource route with sub-resources: 32 | # resources :products do 33 | # resources :comments, :sales 34 | # resource :seller 35 | # end 36 | 37 | # Sample resource route with more complex sub-resources 38 | # resources :products do 39 | # resources :comments 40 | # resources :sales do 41 | # get 'recent', :on => :collection 42 | # end 43 | # end 44 | 45 | # Sample resource route within a namespace: 46 | # namespace :admin do 47 | # # Directs /admin/products/* to Admin::ProductsController 48 | # # (app/controllers/admin/products_controller.rb) 49 | # resources :products 50 | # end 51 | 52 | # You can have the root of your site routed with "root" 53 | # just remember to delete public/index.html. 54 | # root :to => 'welcome#index' 55 | 56 | # See how all your routes lay out with "rake routes" 57 | 58 | # This is a legacy wild controller route that's not recommended for RESTful applications. 59 | # Note: This route will make all actions in every controller accessible via GET requests. 60 | # match ':controller(/:action(/:id))(.:format)' 61 | end 62 | -------------------------------------------------------------------------------- /app/controllers/books_controller.rb: -------------------------------------------------------------------------------- 1 | class BooksController < ApplicationController 2 | 3 | # GET /books 4 | # GET /books.json 5 | def index 6 | @books = Book.all 7 | 8 | respond_to do |format| 9 | format.html # index.html.erb 10 | format.json { render json: @books } 11 | end 12 | end 13 | 14 | # GET /books/1 15 | # GET /books/1.json 16 | def show 17 | @book = Book.find(params[:id]) 18 | 19 | respond_to do |format| 20 | format.html { @books = Book.all }# show.html.erb 21 | format.json { render json: @book } 22 | end 23 | end 24 | 25 | # GET /books/new 26 | # GET /books/new.json 27 | def new 28 | @book = Book.new 29 | 30 | respond_to do |format| 31 | format.html { 32 | @books = Book.all 33 | render :action => 'index' 34 | } 35 | format.json { render json: @book } 36 | end 37 | end 38 | 39 | # GET /books/1/edit 40 | def edit 41 | @book = Book.find(params[:id]) 42 | @books = Book.all 43 | render :action => 'index' 44 | end 45 | 46 | # POST /books 47 | # POST /books.json 48 | def create 49 | @book = Book.new(params[:book]) 50 | 51 | respond_to do |format| 52 | if @book.save 53 | format.html { redirect_to @book, notice: 'Book was successfully created.' } 54 | format.json { render json: @book, status: :created, location: @book } 55 | else 56 | format.html { render action: "new" } 57 | format.json { render json: @book.errors, status: :unprocessable_entity } 58 | end 59 | end 60 | end 61 | 62 | # PUT /books/1 63 | # PUT /books/1.json 64 | def update 65 | @book = Book.find(params[:id]) 66 | 67 | respond_to do |format| 68 | if @book.update_attributes(params[:book]) 69 | format.html { redirect_to @book, notice: 'Book was successfully updated.' } 70 | format.json { render json: @book } 71 | else 72 | format.html { render action: "edit" } 73 | format.json { render json: @book.errors, status: :unprocessable_entity } 74 | end 75 | end 76 | end 77 | 78 | # DELETE /books/1 79 | # DELETE /books/1.json 80 | def destroy 81 | @book = Book.find(params[:id]) 82 | @book.destroy 83 | 84 | respond_to do |format| 85 | format.html { redirect_to books_url } 86 | format.json { render json: @book } 87 | end 88 | end 89 | 90 | 91 | 92 | end 93 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | RailsRealtime::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | if defined?(Bundler) 6 | # If you precompile assets before deploying to production, use this line 7 | Bundler.require(*Rails.groups(:assets => %w(development test))) 8 | # If you want your assets lazily compiled in production, use this line 9 | # Bundler.require(:default, :assets, Rails.env) 10 | end 11 | 12 | module RailsRealtime 13 | class Application < Rails::Application 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | 18 | # Custom directories with classes and modules you want to be autoloadable. 19 | # config.autoload_paths += %W(#{config.root}/extras) 20 | 21 | # Only load the plugins named here, in the order given (default is alphabetical). 22 | # :all can be used as a placeholder for all plugins not explicitly named. 23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 24 | 25 | # Activate observers that should always be running. 26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 27 | 28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 30 | # config.time_zone = 'Central Time (US & Canada)' 31 | 32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 34 | # config.i18n.default_locale = :de 35 | 36 | # Configure the default encoding used in templates for Ruby 1.9. 37 | config.encoding = "utf-8" 38 | 39 | # Configure sensitive parameters which will be filtered from the log file. 40 | config.filter_parameters += [:password] 41 | 42 | # Enable escaping HTML in JSON. 43 | config.active_support.escape_html_entities_in_json = true 44 | 45 | # Use SQL instead of Active Record's schema dumper when creating the database. 46 | # This is necessary if your schema can't be completely dumped by the schema dumper, 47 | # like if you have constraints or database-specific column types 48 | # config.active_record.schema_format = :sql 49 | 50 | # Enforce whitelist mode for mass assignment. 51 | # This will create an empty whitelist of attributes available for mass-assignment for all models 52 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 53 | # parameters by using an attr_accessible or attr_protected declaration. 54 | config.active_record.whitelist_attributes = true 55 | 56 | # Enable the asset pipeline 57 | config.assets.enabled = true 58 | 59 | # Version of your assets, change this if you want to expire all your assets 60 | config.assets.version = '1.0' 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (3.2.11) 5 | actionpack (= 3.2.11) 6 | mail (~> 2.4.4) 7 | actionpack (3.2.11) 8 | activemodel (= 3.2.11) 9 | activesupport (= 3.2.11) 10 | builder (~> 3.0.0) 11 | erubis (~> 2.7.0) 12 | journey (~> 1.0.4) 13 | rack (~> 1.4.0) 14 | rack-cache (~> 1.2) 15 | rack-test (~> 0.6.1) 16 | sprockets (~> 2.2.1) 17 | activemodel (3.2.11) 18 | activesupport (= 3.2.11) 19 | builder (~> 3.0.0) 20 | activerecord (3.2.11) 21 | activemodel (= 3.2.11) 22 | activesupport (= 3.2.11) 23 | arel (~> 3.0.2) 24 | tzinfo (~> 0.3.29) 25 | activeresource (3.2.11) 26 | activemodel (= 3.2.11) 27 | activesupport (= 3.2.11) 28 | activesupport (3.2.11) 29 | i18n (~> 0.6) 30 | multi_json (~> 1.0) 31 | arel (3.0.2) 32 | builder (3.0.4) 33 | coffee-rails (3.2.2) 34 | coffee-script (>= 2.2.0) 35 | railties (~> 3.2.0) 36 | coffee-script (2.2.0) 37 | coffee-script-source 38 | execjs 39 | coffee-script-source (1.4.0) 40 | daemons (1.1.9) 41 | ejs (1.1.1) 42 | erubis (2.7.0) 43 | eventmachine (1.0.0) 44 | execjs (1.4.0) 45 | multi_json (~> 1.0) 46 | hike (1.2.1) 47 | i18n (0.6.1) 48 | journey (1.0.4) 49 | jquery-rails (2.2.0) 50 | railties (>= 3.0, < 5.0) 51 | thor (>= 0.14, < 2.0) 52 | json (1.7.6) 53 | mail (2.4.4) 54 | i18n (>= 0.4.0) 55 | mime-types (~> 1.16) 56 | treetop (~> 1.4.8) 57 | mime-types (1.19) 58 | multi_json (1.5.0) 59 | pg (0.14.1) 60 | polyglot (0.3.3) 61 | rack (1.4.4) 62 | rack-cache (1.2) 63 | rack (>= 0.4) 64 | rack-ssl (1.3.3) 65 | rack 66 | rack-test (0.6.2) 67 | rack (>= 1.0) 68 | rails (3.2.11) 69 | actionmailer (= 3.2.11) 70 | actionpack (= 3.2.11) 71 | activerecord (= 3.2.11) 72 | activeresource (= 3.2.11) 73 | activesupport (= 3.2.11) 74 | bundler (~> 1.0) 75 | railties (= 3.2.11) 76 | railties (3.2.11) 77 | actionpack (= 3.2.11) 78 | activesupport (= 3.2.11) 79 | rack-ssl (~> 1.3.2) 80 | rake (>= 0.8.7) 81 | rdoc (~> 3.4) 82 | thor (>= 0.14.6, < 2.0) 83 | rake (10.0.3) 84 | rdoc (3.12) 85 | json (~> 1.4) 86 | redis (3.0.2) 87 | sass (3.2.5) 88 | sass-rails (3.2.6) 89 | railties (~> 3.2.0) 90 | sass (>= 3.1.10) 91 | tilt (~> 1.3) 92 | sprockets (2.2.2) 93 | hike (~> 1.2) 94 | multi_json (~> 1.0) 95 | rack (~> 1.0) 96 | tilt (~> 1.1, != 1.3.0) 97 | thin (1.5.0) 98 | daemons (>= 1.0.9) 99 | eventmachine (>= 0.12.6) 100 | rack (>= 1.0.0) 101 | thor (0.17.0) 102 | tilt (1.3.3) 103 | treetop (1.4.12) 104 | polyglot 105 | polyglot (>= 0.3.1) 106 | tzinfo (0.3.35) 107 | uglifier (1.3.0) 108 | execjs (>= 0.3.0) 109 | multi_json (~> 1.0, >= 1.0.2) 110 | 111 | PLATFORMS 112 | ruby 113 | 114 | DEPENDENCIES 115 | coffee-rails (~> 3.2.1) 116 | ejs 117 | jquery-rails 118 | pg 119 | rails (= 3.2.11) 120 | redis 121 | sass-rails (~> 3.2.3) 122 | thin 123 | uglifier (>= 1.0.3) 124 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ruby on Rails: Welcome aboard 5 | 174 | 187 | 188 | 189 |
190 | 203 | 204 |
205 | 209 | 210 | 214 | 215 |
216 |

Getting started

217 |

Here’s how to get rolling:

218 | 219 |
    220 |
  1. 221 |

    Use rails generate to create your models and controllers

    222 |

    To see all available options, run it without parameters.

    223 |
  2. 224 | 225 |
  3. 226 |

    Set up a default route and remove public/index.html

    227 |

    Routes are set up in config/routes.rb.

    228 |
  4. 229 | 230 |
  5. 231 |

    Create your database

    232 |

    Run rake db:create to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

    233 |
  6. 234 |
235 |
236 |
237 | 238 | 239 |
240 | 241 | 242 | -------------------------------------------------------------------------------- /lib/assets/javascripts/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.4.4 2 | // http://underscorejs.org 3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `global` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Establish the object that gets returned to break out of a loop iteration. 18 | var breaker = {}; 19 | 20 | // Save bytes in the minified (but not gzipped) version: 21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 22 | 23 | // Create quick reference variables for speed access to core prototypes. 24 | var push = ArrayProto.push, 25 | slice = ArrayProto.slice, 26 | concat = ArrayProto.concat, 27 | toString = ObjProto.toString, 28 | hasOwnProperty = ObjProto.hasOwnProperty; 29 | 30 | // All **ECMAScript 5** native function implementations that we hope to use 31 | // are declared here. 32 | var 33 | nativeForEach = ArrayProto.forEach, 34 | nativeMap = ArrayProto.map, 35 | nativeReduce = ArrayProto.reduce, 36 | nativeReduceRight = ArrayProto.reduceRight, 37 | nativeFilter = ArrayProto.filter, 38 | nativeEvery = ArrayProto.every, 39 | nativeSome = ArrayProto.some, 40 | nativeIndexOf = ArrayProto.indexOf, 41 | nativeLastIndexOf = ArrayProto.lastIndexOf, 42 | nativeIsArray = Array.isArray, 43 | nativeKeys = Object.keys, 44 | nativeBind = FuncProto.bind; 45 | 46 | // Create a safe reference to the Underscore object for use below. 47 | var _ = function(obj) { 48 | if (obj instanceof _) return obj; 49 | if (!(this instanceof _)) return new _(obj); 50 | this._wrapped = obj; 51 | }; 52 | 53 | // Export the Underscore object for **Node.js**, with 54 | // backwards-compatibility for the old `require()` API. If we're in 55 | // the browser, add `_` as a global object via a string identifier, 56 | // for Closure Compiler "advanced" mode. 57 | if (typeof exports !== 'undefined') { 58 | if (typeof module !== 'undefined' && module.exports) { 59 | exports = module.exports = _; 60 | } 61 | exports._ = _; 62 | } else { 63 | root._ = _; 64 | } 65 | 66 | // Current version. 67 | _.VERSION = '1.4.4'; 68 | 69 | // Collection Functions 70 | // -------------------- 71 | 72 | // The cornerstone, an `each` implementation, aka `forEach`. 73 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 74 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 75 | var each = _.each = _.forEach = function(obj, iterator, context) { 76 | if (obj == null) return; 77 | if (nativeForEach && obj.forEach === nativeForEach) { 78 | obj.forEach(iterator, context); 79 | } else if (obj.length === +obj.length) { 80 | for (var i = 0, l = obj.length; i < l; i++) { 81 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 82 | } 83 | } else { 84 | for (var key in obj) { 85 | if (_.has(obj, key)) { 86 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 87 | } 88 | } 89 | } 90 | }; 91 | 92 | // Return the results of applying the iterator to each element. 93 | // Delegates to **ECMAScript 5**'s native `map` if available. 94 | _.map = _.collect = function(obj, iterator, context) { 95 | var results = []; 96 | if (obj == null) return results; 97 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 98 | each(obj, function(value, index, list) { 99 | results[results.length] = iterator.call(context, value, index, list); 100 | }); 101 | return results; 102 | }; 103 | 104 | var reduceError = 'Reduce of empty array with no initial value'; 105 | 106 | // **Reduce** builds up a single result from a list of values, aka `inject`, 107 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 108 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 109 | var initial = arguments.length > 2; 110 | if (obj == null) obj = []; 111 | if (nativeReduce && obj.reduce === nativeReduce) { 112 | if (context) iterator = _.bind(iterator, context); 113 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 114 | } 115 | each(obj, function(value, index, list) { 116 | if (!initial) { 117 | memo = value; 118 | initial = true; 119 | } else { 120 | memo = iterator.call(context, memo, value, index, list); 121 | } 122 | }); 123 | if (!initial) throw new TypeError(reduceError); 124 | return memo; 125 | }; 126 | 127 | // The right-associative version of reduce, also known as `foldr`. 128 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 129 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 130 | var initial = arguments.length > 2; 131 | if (obj == null) obj = []; 132 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 133 | if (context) iterator = _.bind(iterator, context); 134 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 135 | } 136 | var length = obj.length; 137 | if (length !== +length) { 138 | var keys = _.keys(obj); 139 | length = keys.length; 140 | } 141 | each(obj, function(value, index, list) { 142 | index = keys ? keys[--length] : --length; 143 | if (!initial) { 144 | memo = obj[index]; 145 | initial = true; 146 | } else { 147 | memo = iterator.call(context, memo, obj[index], index, list); 148 | } 149 | }); 150 | if (!initial) throw new TypeError(reduceError); 151 | return memo; 152 | }; 153 | 154 | // Return the first value which passes a truth test. Aliased as `detect`. 155 | _.find = _.detect = function(obj, iterator, context) { 156 | var result; 157 | any(obj, function(value, index, list) { 158 | if (iterator.call(context, value, index, list)) { 159 | result = value; 160 | return true; 161 | } 162 | }); 163 | return result; 164 | }; 165 | 166 | // Return all the elements that pass a truth test. 167 | // Delegates to **ECMAScript 5**'s native `filter` if available. 168 | // Aliased as `select`. 169 | _.filter = _.select = function(obj, iterator, context) { 170 | var results = []; 171 | if (obj == null) return results; 172 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 173 | each(obj, function(value, index, list) { 174 | if (iterator.call(context, value, index, list)) results[results.length] = value; 175 | }); 176 | return results; 177 | }; 178 | 179 | // Return all the elements for which a truth test fails. 180 | _.reject = function(obj, iterator, context) { 181 | return _.filter(obj, function(value, index, list) { 182 | return !iterator.call(context, value, index, list); 183 | }, context); 184 | }; 185 | 186 | // Determine whether all of the elements match a truth test. 187 | // Delegates to **ECMAScript 5**'s native `every` if available. 188 | // Aliased as `all`. 189 | _.every = _.all = function(obj, iterator, context) { 190 | iterator || (iterator = _.identity); 191 | var result = true; 192 | if (obj == null) return result; 193 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 194 | each(obj, function(value, index, list) { 195 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 196 | }); 197 | return !!result; 198 | }; 199 | 200 | // Determine if at least one element in the object matches a truth test. 201 | // Delegates to **ECMAScript 5**'s native `some` if available. 202 | // Aliased as `any`. 203 | var any = _.some = _.any = function(obj, iterator, context) { 204 | iterator || (iterator = _.identity); 205 | var result = false; 206 | if (obj == null) return result; 207 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 208 | each(obj, function(value, index, list) { 209 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 210 | }); 211 | return !!result; 212 | }; 213 | 214 | // Determine if the array or object contains a given value (using `===`). 215 | // Aliased as `include`. 216 | _.contains = _.include = function(obj, target) { 217 | if (obj == null) return false; 218 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 219 | return any(obj, function(value) { 220 | return value === target; 221 | }); 222 | }; 223 | 224 | // Invoke a method (with arguments) on every item in a collection. 225 | _.invoke = function(obj, method) { 226 | var args = slice.call(arguments, 2); 227 | var isFunc = _.isFunction(method); 228 | return _.map(obj, function(value) { 229 | return (isFunc ? method : value[method]).apply(value, args); 230 | }); 231 | }; 232 | 233 | // Convenience version of a common use case of `map`: fetching a property. 234 | _.pluck = function(obj, key) { 235 | return _.map(obj, function(value){ return value[key]; }); 236 | }; 237 | 238 | // Convenience version of a common use case of `filter`: selecting only objects 239 | // containing specific `key:value` pairs. 240 | _.where = function(obj, attrs, first) { 241 | if (_.isEmpty(attrs)) return first ? null : []; 242 | return _[first ? 'find' : 'filter'](obj, function(value) { 243 | for (var key in attrs) { 244 | if (attrs[key] !== value[key]) return false; 245 | } 246 | return true; 247 | }); 248 | }; 249 | 250 | // Convenience version of a common use case of `find`: getting the first object 251 | // containing specific `key:value` pairs. 252 | _.findWhere = function(obj, attrs) { 253 | return _.where(obj, attrs, true); 254 | }; 255 | 256 | // Return the maximum element or (element-based computation). 257 | // Can't optimize arrays of integers longer than 65,535 elements. 258 | // See: https://bugs.webkit.org/show_bug.cgi?id=80797 259 | _.max = function(obj, iterator, context) { 260 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 261 | return Math.max.apply(Math, obj); 262 | } 263 | if (!iterator && _.isEmpty(obj)) return -Infinity; 264 | var result = {computed : -Infinity, value: -Infinity}; 265 | each(obj, function(value, index, list) { 266 | var computed = iterator ? iterator.call(context, value, index, list) : value; 267 | computed >= result.computed && (result = {value : value, computed : computed}); 268 | }); 269 | return result.value; 270 | }; 271 | 272 | // Return the minimum element (or element-based computation). 273 | _.min = function(obj, iterator, context) { 274 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 275 | return Math.min.apply(Math, obj); 276 | } 277 | if (!iterator && _.isEmpty(obj)) return Infinity; 278 | var result = {computed : Infinity, value: Infinity}; 279 | each(obj, function(value, index, list) { 280 | var computed = iterator ? iterator.call(context, value, index, list) : value; 281 | computed < result.computed && (result = {value : value, computed : computed}); 282 | }); 283 | return result.value; 284 | }; 285 | 286 | // Shuffle an array. 287 | _.shuffle = function(obj) { 288 | var rand; 289 | var index = 0; 290 | var shuffled = []; 291 | each(obj, function(value) { 292 | rand = _.random(index++); 293 | shuffled[index - 1] = shuffled[rand]; 294 | shuffled[rand] = value; 295 | }); 296 | return shuffled; 297 | }; 298 | 299 | // An internal function to generate lookup iterators. 300 | var lookupIterator = function(value) { 301 | return _.isFunction(value) ? value : function(obj){ return obj[value]; }; 302 | }; 303 | 304 | // Sort the object's values by a criterion produced by an iterator. 305 | _.sortBy = function(obj, value, context) { 306 | var iterator = lookupIterator(value); 307 | return _.pluck(_.map(obj, function(value, index, list) { 308 | return { 309 | value : value, 310 | index : index, 311 | criteria : iterator.call(context, value, index, list) 312 | }; 313 | }).sort(function(left, right) { 314 | var a = left.criteria; 315 | var b = right.criteria; 316 | if (a !== b) { 317 | if (a > b || a === void 0) return 1; 318 | if (a < b || b === void 0) return -1; 319 | } 320 | return left.index < right.index ? -1 : 1; 321 | }), 'value'); 322 | }; 323 | 324 | // An internal function used for aggregate "group by" operations. 325 | var group = function(obj, value, context, behavior) { 326 | var result = {}; 327 | var iterator = lookupIterator(value || _.identity); 328 | each(obj, function(value, index) { 329 | var key = iterator.call(context, value, index, obj); 330 | behavior(result, key, value); 331 | }); 332 | return result; 333 | }; 334 | 335 | // Groups the object's values by a criterion. Pass either a string attribute 336 | // to group by, or a function that returns the criterion. 337 | _.groupBy = function(obj, value, context) { 338 | return group(obj, value, context, function(result, key, value) { 339 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value); 340 | }); 341 | }; 342 | 343 | // Counts instances of an object that group by a certain criterion. Pass 344 | // either a string attribute to count by, or a function that returns the 345 | // criterion. 346 | _.countBy = function(obj, value, context) { 347 | return group(obj, value, context, function(result, key) { 348 | if (!_.has(result, key)) result[key] = 0; 349 | result[key]++; 350 | }); 351 | }; 352 | 353 | // Use a comparator function to figure out the smallest index at which 354 | // an object should be inserted so as to maintain order. Uses binary search. 355 | _.sortedIndex = function(array, obj, iterator, context) { 356 | iterator = iterator == null ? _.identity : lookupIterator(iterator); 357 | var value = iterator.call(context, obj); 358 | var low = 0, high = array.length; 359 | while (low < high) { 360 | var mid = (low + high) >>> 1; 361 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 362 | } 363 | return low; 364 | }; 365 | 366 | // Safely convert anything iterable into a real, live array. 367 | _.toArray = function(obj) { 368 | if (!obj) return []; 369 | if (_.isArray(obj)) return slice.call(obj); 370 | if (obj.length === +obj.length) return _.map(obj, _.identity); 371 | return _.values(obj); 372 | }; 373 | 374 | // Return the number of elements in an object. 375 | _.size = function(obj) { 376 | if (obj == null) return 0; 377 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 378 | }; 379 | 380 | // Array Functions 381 | // --------------- 382 | 383 | // Get the first element of an array. Passing **n** will return the first N 384 | // values in the array. Aliased as `head` and `take`. The **guard** check 385 | // allows it to work with `_.map`. 386 | _.first = _.head = _.take = function(array, n, guard) { 387 | if (array == null) return void 0; 388 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 389 | }; 390 | 391 | // Returns everything but the last entry of the array. Especially useful on 392 | // the arguments object. Passing **n** will return all the values in 393 | // the array, excluding the last N. The **guard** check allows it to work with 394 | // `_.map`. 395 | _.initial = function(array, n, guard) { 396 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 397 | }; 398 | 399 | // Get the last element of an array. Passing **n** will return the last N 400 | // values in the array. The **guard** check allows it to work with `_.map`. 401 | _.last = function(array, n, guard) { 402 | if (array == null) return void 0; 403 | if ((n != null) && !guard) { 404 | return slice.call(array, Math.max(array.length - n, 0)); 405 | } else { 406 | return array[array.length - 1]; 407 | } 408 | }; 409 | 410 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 411 | // Especially useful on the arguments object. Passing an **n** will return 412 | // the rest N values in the array. The **guard** 413 | // check allows it to work with `_.map`. 414 | _.rest = _.tail = _.drop = function(array, n, guard) { 415 | return slice.call(array, (n == null) || guard ? 1 : n); 416 | }; 417 | 418 | // Trim out all falsy values from an array. 419 | _.compact = function(array) { 420 | return _.filter(array, _.identity); 421 | }; 422 | 423 | // Internal implementation of a recursive `flatten` function. 424 | var flatten = function(input, shallow, output) { 425 | each(input, function(value) { 426 | if (_.isArray(value)) { 427 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 428 | } else { 429 | output.push(value); 430 | } 431 | }); 432 | return output; 433 | }; 434 | 435 | // Return a completely flattened version of an array. 436 | _.flatten = function(array, shallow) { 437 | return flatten(array, shallow, []); 438 | }; 439 | 440 | // Return a version of the array that does not contain the specified value(s). 441 | _.without = function(array) { 442 | return _.difference(array, slice.call(arguments, 1)); 443 | }; 444 | 445 | // Produce a duplicate-free version of the array. If the array has already 446 | // been sorted, you have the option of using a faster algorithm. 447 | // Aliased as `unique`. 448 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 449 | if (_.isFunction(isSorted)) { 450 | context = iterator; 451 | iterator = isSorted; 452 | isSorted = false; 453 | } 454 | var initial = iterator ? _.map(array, iterator, context) : array; 455 | var results = []; 456 | var seen = []; 457 | each(initial, function(value, index) { 458 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 459 | seen.push(value); 460 | results.push(array[index]); 461 | } 462 | }); 463 | return results; 464 | }; 465 | 466 | // Produce an array that contains the union: each distinct element from all of 467 | // the passed-in arrays. 468 | _.union = function() { 469 | return _.uniq(concat.apply(ArrayProto, arguments)); 470 | }; 471 | 472 | // Produce an array that contains every item shared between all the 473 | // passed-in arrays. 474 | _.intersection = function(array) { 475 | var rest = slice.call(arguments, 1); 476 | return _.filter(_.uniq(array), function(item) { 477 | return _.every(rest, function(other) { 478 | return _.indexOf(other, item) >= 0; 479 | }); 480 | }); 481 | }; 482 | 483 | // Take the difference between one array and a number of other arrays. 484 | // Only the elements present in just the first array will remain. 485 | _.difference = function(array) { 486 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 487 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 488 | }; 489 | 490 | // Zip together multiple lists into a single array -- elements that share 491 | // an index go together. 492 | _.zip = function() { 493 | var args = slice.call(arguments); 494 | var length = _.max(_.pluck(args, 'length')); 495 | var results = new Array(length); 496 | for (var i = 0; i < length; i++) { 497 | results[i] = _.pluck(args, "" + i); 498 | } 499 | return results; 500 | }; 501 | 502 | // Converts lists into objects. Pass either a single array of `[key, value]` 503 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 504 | // the corresponding values. 505 | _.object = function(list, values) { 506 | if (list == null) return {}; 507 | var result = {}; 508 | for (var i = 0, l = list.length; i < l; i++) { 509 | if (values) { 510 | result[list[i]] = values[i]; 511 | } else { 512 | result[list[i][0]] = list[i][1]; 513 | } 514 | } 515 | return result; 516 | }; 517 | 518 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 519 | // we need this function. Return the position of the first occurrence of an 520 | // item in an array, or -1 if the item is not included in the array. 521 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 522 | // If the array is large and already in sort order, pass `true` 523 | // for **isSorted** to use binary search. 524 | _.indexOf = function(array, item, isSorted) { 525 | if (array == null) return -1; 526 | var i = 0, l = array.length; 527 | if (isSorted) { 528 | if (typeof isSorted == 'number') { 529 | i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); 530 | } else { 531 | i = _.sortedIndex(array, item); 532 | return array[i] === item ? i : -1; 533 | } 534 | } 535 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 536 | for (; i < l; i++) if (array[i] === item) return i; 537 | return -1; 538 | }; 539 | 540 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 541 | _.lastIndexOf = function(array, item, from) { 542 | if (array == null) return -1; 543 | var hasIndex = from != null; 544 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 545 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 546 | } 547 | var i = (hasIndex ? from : array.length); 548 | while (i--) if (array[i] === item) return i; 549 | return -1; 550 | }; 551 | 552 | // Generate an integer Array containing an arithmetic progression. A port of 553 | // the native Python `range()` function. See 554 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 555 | _.range = function(start, stop, step) { 556 | if (arguments.length <= 1) { 557 | stop = start || 0; 558 | start = 0; 559 | } 560 | step = arguments[2] || 1; 561 | 562 | var len = Math.max(Math.ceil((stop - start) / step), 0); 563 | var idx = 0; 564 | var range = new Array(len); 565 | 566 | while(idx < len) { 567 | range[idx++] = start; 568 | start += step; 569 | } 570 | 571 | return range; 572 | }; 573 | 574 | // Function (ahem) Functions 575 | // ------------------ 576 | 577 | // Create a function bound to a given object (assigning `this`, and arguments, 578 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 579 | // available. 580 | _.bind = function(func, context) { 581 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 582 | var args = slice.call(arguments, 2); 583 | return function() { 584 | return func.apply(context, args.concat(slice.call(arguments))); 585 | }; 586 | }; 587 | 588 | // Partially apply a function by creating a version that has had some of its 589 | // arguments pre-filled, without changing its dynamic `this` context. 590 | _.partial = function(func) { 591 | var args = slice.call(arguments, 1); 592 | return function() { 593 | return func.apply(this, args.concat(slice.call(arguments))); 594 | }; 595 | }; 596 | 597 | // Bind all of an object's methods to that object. Useful for ensuring that 598 | // all callbacks defined on an object belong to it. 599 | _.bindAll = function(obj) { 600 | var funcs = slice.call(arguments, 1); 601 | if (funcs.length === 0) funcs = _.functions(obj); 602 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 603 | return obj; 604 | }; 605 | 606 | // Memoize an expensive function by storing its results. 607 | _.memoize = function(func, hasher) { 608 | var memo = {}; 609 | hasher || (hasher = _.identity); 610 | return function() { 611 | var key = hasher.apply(this, arguments); 612 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 613 | }; 614 | }; 615 | 616 | // Delays a function for the given number of milliseconds, and then calls 617 | // it with the arguments supplied. 618 | _.delay = function(func, wait) { 619 | var args = slice.call(arguments, 2); 620 | return setTimeout(function(){ return func.apply(null, args); }, wait); 621 | }; 622 | 623 | // Defers a function, scheduling it to run after the current call stack has 624 | // cleared. 625 | _.defer = function(func) { 626 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 627 | }; 628 | 629 | // Returns a function, that, when invoked, will only be triggered at most once 630 | // during a given window of time. 631 | _.throttle = function(func, wait) { 632 | var context, args, timeout, result; 633 | var previous = 0; 634 | var later = function() { 635 | previous = new Date; 636 | timeout = null; 637 | result = func.apply(context, args); 638 | }; 639 | return function() { 640 | var now = new Date; 641 | var remaining = wait - (now - previous); 642 | context = this; 643 | args = arguments; 644 | if (remaining <= 0) { 645 | clearTimeout(timeout); 646 | timeout = null; 647 | previous = now; 648 | result = func.apply(context, args); 649 | } else if (!timeout) { 650 | timeout = setTimeout(later, remaining); 651 | } 652 | return result; 653 | }; 654 | }; 655 | 656 | // Returns a function, that, as long as it continues to be invoked, will not 657 | // be triggered. The function will be called after it stops being called for 658 | // N milliseconds. If `immediate` is passed, trigger the function on the 659 | // leading edge, instead of the trailing. 660 | _.debounce = function(func, wait, immediate) { 661 | var timeout, result; 662 | return function() { 663 | var context = this, args = arguments; 664 | var later = function() { 665 | timeout = null; 666 | if (!immediate) result = func.apply(context, args); 667 | }; 668 | var callNow = immediate && !timeout; 669 | clearTimeout(timeout); 670 | timeout = setTimeout(later, wait); 671 | if (callNow) result = func.apply(context, args); 672 | return result; 673 | }; 674 | }; 675 | 676 | // Returns a function that will be executed at most one time, no matter how 677 | // often you call it. Useful for lazy initialization. 678 | _.once = function(func) { 679 | var ran = false, memo; 680 | return function() { 681 | if (ran) return memo; 682 | ran = true; 683 | memo = func.apply(this, arguments); 684 | func = null; 685 | return memo; 686 | }; 687 | }; 688 | 689 | // Returns the first function passed as an argument to the second, 690 | // allowing you to adjust arguments, run code before and after, and 691 | // conditionally execute the original function. 692 | _.wrap = function(func, wrapper) { 693 | return function() { 694 | var args = [func]; 695 | push.apply(args, arguments); 696 | return wrapper.apply(this, args); 697 | }; 698 | }; 699 | 700 | // Returns a function that is the composition of a list of functions, each 701 | // consuming the return value of the function that follows. 702 | _.compose = function() { 703 | var funcs = arguments; 704 | return function() { 705 | var args = arguments; 706 | for (var i = funcs.length - 1; i >= 0; i--) { 707 | args = [funcs[i].apply(this, args)]; 708 | } 709 | return args[0]; 710 | }; 711 | }; 712 | 713 | // Returns a function that will only be executed after being called N times. 714 | _.after = function(times, func) { 715 | if (times <= 0) return func(); 716 | return function() { 717 | if (--times < 1) { 718 | return func.apply(this, arguments); 719 | } 720 | }; 721 | }; 722 | 723 | // Object Functions 724 | // ---------------- 725 | 726 | // Retrieve the names of an object's properties. 727 | // Delegates to **ECMAScript 5**'s native `Object.keys` 728 | _.keys = nativeKeys || function(obj) { 729 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 730 | var keys = []; 731 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 732 | return keys; 733 | }; 734 | 735 | // Retrieve the values of an object's properties. 736 | _.values = function(obj) { 737 | var values = []; 738 | for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); 739 | return values; 740 | }; 741 | 742 | // Convert an object into a list of `[key, value]` pairs. 743 | _.pairs = function(obj) { 744 | var pairs = []; 745 | for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); 746 | return pairs; 747 | }; 748 | 749 | // Invert the keys and values of an object. The values must be serializable. 750 | _.invert = function(obj) { 751 | var result = {}; 752 | for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; 753 | return result; 754 | }; 755 | 756 | // Return a sorted list of the function names available on the object. 757 | // Aliased as `methods` 758 | _.functions = _.methods = function(obj) { 759 | var names = []; 760 | for (var key in obj) { 761 | if (_.isFunction(obj[key])) names.push(key); 762 | } 763 | return names.sort(); 764 | }; 765 | 766 | // Extend a given object with all the properties in passed-in object(s). 767 | _.extend = function(obj) { 768 | each(slice.call(arguments, 1), function(source) { 769 | if (source) { 770 | for (var prop in source) { 771 | obj[prop] = source[prop]; 772 | } 773 | } 774 | }); 775 | return obj; 776 | }; 777 | 778 | // Return a copy of the object only containing the whitelisted properties. 779 | _.pick = function(obj) { 780 | var copy = {}; 781 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 782 | each(keys, function(key) { 783 | if (key in obj) copy[key] = obj[key]; 784 | }); 785 | return copy; 786 | }; 787 | 788 | // Return a copy of the object without the blacklisted properties. 789 | _.omit = function(obj) { 790 | var copy = {}; 791 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 792 | for (var key in obj) { 793 | if (!_.contains(keys, key)) copy[key] = obj[key]; 794 | } 795 | return copy; 796 | }; 797 | 798 | // Fill in a given object with default properties. 799 | _.defaults = function(obj) { 800 | each(slice.call(arguments, 1), function(source) { 801 | if (source) { 802 | for (var prop in source) { 803 | if (obj[prop] == null) obj[prop] = source[prop]; 804 | } 805 | } 806 | }); 807 | return obj; 808 | }; 809 | 810 | // Create a (shallow-cloned) duplicate of an object. 811 | _.clone = function(obj) { 812 | if (!_.isObject(obj)) return obj; 813 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 814 | }; 815 | 816 | // Invokes interceptor with the obj, and then returns obj. 817 | // The primary purpose of this method is to "tap into" a method chain, in 818 | // order to perform operations on intermediate results within the chain. 819 | _.tap = function(obj, interceptor) { 820 | interceptor(obj); 821 | return obj; 822 | }; 823 | 824 | // Internal recursive comparison function for `isEqual`. 825 | var eq = function(a, b, aStack, bStack) { 826 | // Identical objects are equal. `0 === -0`, but they aren't identical. 827 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 828 | if (a === b) return a !== 0 || 1 / a == 1 / b; 829 | // A strict comparison is necessary because `null == undefined`. 830 | if (a == null || b == null) return a === b; 831 | // Unwrap any wrapped objects. 832 | if (a instanceof _) a = a._wrapped; 833 | if (b instanceof _) b = b._wrapped; 834 | // Compare `[[Class]]` names. 835 | var className = toString.call(a); 836 | if (className != toString.call(b)) return false; 837 | switch (className) { 838 | // Strings, numbers, dates, and booleans are compared by value. 839 | case '[object String]': 840 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 841 | // equivalent to `new String("5")`. 842 | return a == String(b); 843 | case '[object Number]': 844 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 845 | // other numeric values. 846 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 847 | case '[object Date]': 848 | case '[object Boolean]': 849 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 850 | // millisecond representations. Note that invalid dates with millisecond representations 851 | // of `NaN` are not equivalent. 852 | return +a == +b; 853 | // RegExps are compared by their source patterns and flags. 854 | case '[object RegExp]': 855 | return a.source == b.source && 856 | a.global == b.global && 857 | a.multiline == b.multiline && 858 | a.ignoreCase == b.ignoreCase; 859 | } 860 | if (typeof a != 'object' || typeof b != 'object') return false; 861 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 862 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 863 | var length = aStack.length; 864 | while (length--) { 865 | // Linear search. Performance is inversely proportional to the number of 866 | // unique nested structures. 867 | if (aStack[length] == a) return bStack[length] == b; 868 | } 869 | // Add the first object to the stack of traversed objects. 870 | aStack.push(a); 871 | bStack.push(b); 872 | var size = 0, result = true; 873 | // Recursively compare objects and arrays. 874 | if (className == '[object Array]') { 875 | // Compare array lengths to determine if a deep comparison is necessary. 876 | size = a.length; 877 | result = size == b.length; 878 | if (result) { 879 | // Deep compare the contents, ignoring non-numeric properties. 880 | while (size--) { 881 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 882 | } 883 | } 884 | } else { 885 | // Objects with different constructors are not equivalent, but `Object`s 886 | // from different frames are. 887 | var aCtor = a.constructor, bCtor = b.constructor; 888 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 889 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) { 890 | return false; 891 | } 892 | // Deep compare objects. 893 | for (var key in a) { 894 | if (_.has(a, key)) { 895 | // Count the expected number of properties. 896 | size++; 897 | // Deep compare each member. 898 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 899 | } 900 | } 901 | // Ensure that both objects contain the same number of properties. 902 | if (result) { 903 | for (key in b) { 904 | if (_.has(b, key) && !(size--)) break; 905 | } 906 | result = !size; 907 | } 908 | } 909 | // Remove the first object from the stack of traversed objects. 910 | aStack.pop(); 911 | bStack.pop(); 912 | return result; 913 | }; 914 | 915 | // Perform a deep comparison to check if two objects are equal. 916 | _.isEqual = function(a, b) { 917 | return eq(a, b, [], []); 918 | }; 919 | 920 | // Is a given array, string, or object empty? 921 | // An "empty" object has no enumerable own-properties. 922 | _.isEmpty = function(obj) { 923 | if (obj == null) return true; 924 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 925 | for (var key in obj) if (_.has(obj, key)) return false; 926 | return true; 927 | }; 928 | 929 | // Is a given value a DOM element? 930 | _.isElement = function(obj) { 931 | return !!(obj && obj.nodeType === 1); 932 | }; 933 | 934 | // Is a given value an array? 935 | // Delegates to ECMA5's native Array.isArray 936 | _.isArray = nativeIsArray || function(obj) { 937 | return toString.call(obj) == '[object Array]'; 938 | }; 939 | 940 | // Is a given variable an object? 941 | _.isObject = function(obj) { 942 | return obj === Object(obj); 943 | }; 944 | 945 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 946 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 947 | _['is' + name] = function(obj) { 948 | return toString.call(obj) == '[object ' + name + ']'; 949 | }; 950 | }); 951 | 952 | // Define a fallback version of the method in browsers (ahem, IE), where 953 | // there isn't any inspectable "Arguments" type. 954 | if (!_.isArguments(arguments)) { 955 | _.isArguments = function(obj) { 956 | return !!(obj && _.has(obj, 'callee')); 957 | }; 958 | } 959 | 960 | // Optimize `isFunction` if appropriate. 961 | if (typeof (/./) !== 'function') { 962 | _.isFunction = function(obj) { 963 | return typeof obj === 'function'; 964 | }; 965 | } 966 | 967 | // Is a given object a finite number? 968 | _.isFinite = function(obj) { 969 | return isFinite(obj) && !isNaN(parseFloat(obj)); 970 | }; 971 | 972 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 973 | _.isNaN = function(obj) { 974 | return _.isNumber(obj) && obj != +obj; 975 | }; 976 | 977 | // Is a given value a boolean? 978 | _.isBoolean = function(obj) { 979 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 980 | }; 981 | 982 | // Is a given value equal to null? 983 | _.isNull = function(obj) { 984 | return obj === null; 985 | }; 986 | 987 | // Is a given variable undefined? 988 | _.isUndefined = function(obj) { 989 | return obj === void 0; 990 | }; 991 | 992 | // Shortcut function for checking if an object has a given property directly 993 | // on itself (in other words, not on a prototype). 994 | _.has = function(obj, key) { 995 | return hasOwnProperty.call(obj, key); 996 | }; 997 | 998 | // Utility Functions 999 | // ----------------- 1000 | 1001 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1002 | // previous owner. Returns a reference to the Underscore object. 1003 | _.noConflict = function() { 1004 | root._ = previousUnderscore; 1005 | return this; 1006 | }; 1007 | 1008 | // Keep the identity function around for default iterators. 1009 | _.identity = function(value) { 1010 | return value; 1011 | }; 1012 | 1013 | // Run a function **n** times. 1014 | _.times = function(n, iterator, context) { 1015 | var accum = Array(n); 1016 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); 1017 | return accum; 1018 | }; 1019 | 1020 | // Return a random integer between min and max (inclusive). 1021 | _.random = function(min, max) { 1022 | if (max == null) { 1023 | max = min; 1024 | min = 0; 1025 | } 1026 | return min + Math.floor(Math.random() * (max - min + 1)); 1027 | }; 1028 | 1029 | // List of HTML entities for escaping. 1030 | var entityMap = { 1031 | escape: { 1032 | '&': '&', 1033 | '<': '<', 1034 | '>': '>', 1035 | '"': '"', 1036 | "'": ''', 1037 | '/': '/' 1038 | } 1039 | }; 1040 | entityMap.unescape = _.invert(entityMap.escape); 1041 | 1042 | // Regexes containing the keys and values listed immediately above. 1043 | var entityRegexes = { 1044 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1045 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1046 | }; 1047 | 1048 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1049 | _.each(['escape', 'unescape'], function(method) { 1050 | _[method] = function(string) { 1051 | if (string == null) return ''; 1052 | return ('' + string).replace(entityRegexes[method], function(match) { 1053 | return entityMap[method][match]; 1054 | }); 1055 | }; 1056 | }); 1057 | 1058 | // If the value of the named property is a function then invoke it; 1059 | // otherwise, return it. 1060 | _.result = function(object, property) { 1061 | if (object == null) return null; 1062 | var value = object[property]; 1063 | return _.isFunction(value) ? value.call(object) : value; 1064 | }; 1065 | 1066 | // Add your own custom functions to the Underscore object. 1067 | _.mixin = function(obj) { 1068 | each(_.functions(obj), function(name){ 1069 | var func = _[name] = obj[name]; 1070 | _.prototype[name] = function() { 1071 | var args = [this._wrapped]; 1072 | push.apply(args, arguments); 1073 | return result.call(this, func.apply(_, args)); 1074 | }; 1075 | }); 1076 | }; 1077 | 1078 | // Generate a unique integer id (unique within the entire client session). 1079 | // Useful for temporary DOM ids. 1080 | var idCounter = 0; 1081 | _.uniqueId = function(prefix) { 1082 | var id = ++idCounter + ''; 1083 | return prefix ? prefix + id : id; 1084 | }; 1085 | 1086 | // By default, Underscore uses ERB-style template delimiters, change the 1087 | // following template settings to use alternative delimiters. 1088 | _.templateSettings = { 1089 | evaluate : /<%([\s\S]+?)%>/g, 1090 | interpolate : /<%=([\s\S]+?)%>/g, 1091 | escape : /<%-([\s\S]+?)%>/g 1092 | }; 1093 | 1094 | // When customizing `templateSettings`, if you don't want to define an 1095 | // interpolation, evaluation or escaping regex, we need one that is 1096 | // guaranteed not to match. 1097 | var noMatch = /(.)^/; 1098 | 1099 | // Certain characters need to be escaped so that they can be put into a 1100 | // string literal. 1101 | var escapes = { 1102 | "'": "'", 1103 | '\\': '\\', 1104 | '\r': 'r', 1105 | '\n': 'n', 1106 | '\t': 't', 1107 | '\u2028': 'u2028', 1108 | '\u2029': 'u2029' 1109 | }; 1110 | 1111 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1112 | 1113 | // JavaScript micro-templating, similar to John Resig's implementation. 1114 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1115 | // and correctly escapes quotes within interpolated code. 1116 | _.template = function(text, data, settings) { 1117 | var render; 1118 | settings = _.defaults({}, settings, _.templateSettings); 1119 | 1120 | // Combine delimiters into one regular expression via alternation. 1121 | var matcher = new RegExp([ 1122 | (settings.escape || noMatch).source, 1123 | (settings.interpolate || noMatch).source, 1124 | (settings.evaluate || noMatch).source 1125 | ].join('|') + '|$', 'g'); 1126 | 1127 | // Compile the template source, escaping string literals appropriately. 1128 | var index = 0; 1129 | var source = "__p+='"; 1130 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1131 | source += text.slice(index, offset) 1132 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1133 | 1134 | if (escape) { 1135 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1136 | } 1137 | if (interpolate) { 1138 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1139 | } 1140 | if (evaluate) { 1141 | source += "';\n" + evaluate + "\n__p+='"; 1142 | } 1143 | index = offset + match.length; 1144 | return match; 1145 | }); 1146 | source += "';\n"; 1147 | 1148 | // If a variable is not specified, place data values in local scope. 1149 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1150 | 1151 | source = "var __t,__p='',__j=Array.prototype.join," + 1152 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1153 | source + "return __p;\n"; 1154 | 1155 | try { 1156 | render = new Function(settings.variable || 'obj', '_', source); 1157 | } catch (e) { 1158 | e.source = source; 1159 | throw e; 1160 | } 1161 | 1162 | if (data) return render(data, _); 1163 | var template = function(data) { 1164 | return render.call(this, data, _); 1165 | }; 1166 | 1167 | // Provide the compiled function source as a convenience for precompilation. 1168 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1169 | 1170 | return template; 1171 | }; 1172 | 1173 | // Add a "chain" function, which will delegate to the wrapper. 1174 | _.chain = function(obj) { 1175 | return _(obj).chain(); 1176 | }; 1177 | 1178 | // OOP 1179 | // --------------- 1180 | // If Underscore is called as a function, it returns a wrapped object that 1181 | // can be used OO-style. This wrapper holds altered versions of all the 1182 | // underscore functions. Wrapped objects may be chained. 1183 | 1184 | // Helper function to continue chaining intermediate results. 1185 | var result = function(obj) { 1186 | return this._chain ? _(obj).chain() : obj; 1187 | }; 1188 | 1189 | // Add all of the Underscore functions to the wrapper object. 1190 | _.mixin(_); 1191 | 1192 | // Add all mutator Array functions to the wrapper. 1193 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1194 | var method = ArrayProto[name]; 1195 | _.prototype[name] = function() { 1196 | var obj = this._wrapped; 1197 | method.apply(obj, arguments); 1198 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1199 | return result.call(this, obj); 1200 | }; 1201 | }); 1202 | 1203 | // Add all accessor Array functions to the wrapper. 1204 | each(['concat', 'join', 'slice'], function(name) { 1205 | var method = ArrayProto[name]; 1206 | _.prototype[name] = function() { 1207 | return result.call(this, method.apply(this._wrapped, arguments)); 1208 | }; 1209 | }); 1210 | 1211 | _.extend(_.prototype, { 1212 | 1213 | // Start chaining a wrapped Underscore object. 1214 | chain: function() { 1215 | this._chain = true; 1216 | return this; 1217 | }, 1218 | 1219 | // Extracts the result from a wrapped and chained object. 1220 | value: function() { 1221 | return this._wrapped; 1222 | } 1223 | 1224 | }); 1225 | 1226 | }).call(this); 1227 | -------------------------------------------------------------------------------- /lib/assets/javascripts/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.9.10 2 | 3 | // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | (function(){ 9 | 10 | // Initial Setup 11 | // ------------- 12 | 13 | // Save a reference to the global object (`window` in the browser, `exports` 14 | // on the server). 15 | var root = this; 16 | 17 | // Save the previous value of the `Backbone` variable, so that it can be 18 | // restored later on, if `noConflict` is used. 19 | var previousBackbone = root.Backbone; 20 | 21 | // Create a local reference to array methods. 22 | var array = []; 23 | var push = array.push; 24 | var slice = array.slice; 25 | var splice = array.splice; 26 | 27 | // The top-level namespace. All public Backbone classes and modules will 28 | // be attached to this. Exported for both CommonJS and the browser. 29 | var Backbone; 30 | if (typeof exports !== 'undefined') { 31 | Backbone = exports; 32 | } else { 33 | Backbone = root.Backbone = {}; 34 | } 35 | 36 | // Current version of the library. Keep in sync with `package.json`. 37 | Backbone.VERSION = '0.9.10'; 38 | 39 | // Require Underscore, if we're on the server, and it's not already present. 40 | var _ = root._; 41 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); 42 | 43 | // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. 44 | Backbone.$ = root.jQuery || root.Zepto || root.ender; 45 | 46 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 47 | // to its previous owner. Returns a reference to this Backbone object. 48 | Backbone.noConflict = function() { 49 | root.Backbone = previousBackbone; 50 | return this; 51 | }; 52 | 53 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 54 | // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and 55 | // set a `X-Http-Method-Override` header. 56 | Backbone.emulateHTTP = false; 57 | 58 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 59 | // `application/json` requests ... will encode the body as 60 | // `application/x-www-form-urlencoded` instead and will send the model in a 61 | // form param named `model`. 62 | Backbone.emulateJSON = false; 63 | 64 | // Backbone.Events 65 | // --------------- 66 | 67 | // Regular expression used to split event strings. 68 | var eventSplitter = /\s+/; 69 | 70 | // Implement fancy features of the Events API such as multiple event 71 | // names `"change blur"` and jQuery-style event maps `{change: action}` 72 | // in terms of the existing API. 73 | var eventsApi = function(obj, action, name, rest) { 74 | if (!name) return true; 75 | if (typeof name === 'object') { 76 | for (var key in name) { 77 | obj[action].apply(obj, [key, name[key]].concat(rest)); 78 | } 79 | } else if (eventSplitter.test(name)) { 80 | var names = name.split(eventSplitter); 81 | for (var i = 0, l = names.length; i < l; i++) { 82 | obj[action].apply(obj, [names[i]].concat(rest)); 83 | } 84 | } else { 85 | return true; 86 | } 87 | }; 88 | 89 | // Optimized internal dispatch function for triggering events. Tries to 90 | // keep the usual cases speedy (most Backbone events have 3 arguments). 91 | var triggerEvents = function(events, args) { 92 | var ev, i = -1, l = events.length; 93 | switch (args.length) { 94 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); 95 | return; 96 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]); 97 | return; 98 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]); 99 | return; 100 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]); 101 | return; 102 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); 103 | } 104 | }; 105 | 106 | // A module that can be mixed in to *any object* in order to provide it with 107 | // custom events. You may bind with `on` or remove with `off` callback 108 | // functions to an event; `trigger`-ing an event fires all callbacks in 109 | // succession. 110 | // 111 | // var object = {}; 112 | // _.extend(object, Backbone.Events); 113 | // object.on('expand', function(){ alert('expanded'); }); 114 | // object.trigger('expand'); 115 | // 116 | var Events = Backbone.Events = { 117 | 118 | // Bind one or more space separated events, or an events map, 119 | // to a `callback` function. Passing `"all"` will bind the callback to 120 | // all events fired. 121 | on: function(name, callback, context) { 122 | if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this; 123 | this._events || (this._events = {}); 124 | var list = this._events[name] || (this._events[name] = []); 125 | list.push({callback: callback, context: context, ctx: context || this}); 126 | return this; 127 | }, 128 | 129 | // Bind events to only be triggered a single time. After the first time 130 | // the callback is invoked, it will be removed. 131 | once: function(name, callback, context) { 132 | if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this; 133 | var self = this; 134 | var once = _.once(function() { 135 | self.off(name, once); 136 | callback.apply(this, arguments); 137 | }); 138 | once._callback = callback; 139 | this.on(name, once, context); 140 | return this; 141 | }, 142 | 143 | // Remove one or many callbacks. If `context` is null, removes all 144 | // callbacks with that function. If `callback` is null, removes all 145 | // callbacks for the event. If `name` is null, removes all bound 146 | // callbacks for all events. 147 | off: function(name, callback, context) { 148 | var list, ev, events, names, i, l, j, k; 149 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 150 | if (!name && !callback && !context) { 151 | this._events = {}; 152 | return this; 153 | } 154 | 155 | names = name ? [name] : _.keys(this._events); 156 | for (i = 0, l = names.length; i < l; i++) { 157 | name = names[i]; 158 | if (list = this._events[name]) { 159 | events = []; 160 | if (callback || context) { 161 | for (j = 0, k = list.length; j < k; j++) { 162 | ev = list[j]; 163 | if ((callback && callback !== ev.callback && 164 | callback !== ev.callback._callback) || 165 | (context && context !== ev.context)) { 166 | events.push(ev); 167 | } 168 | } 169 | } 170 | this._events[name] = events; 171 | } 172 | } 173 | 174 | return this; 175 | }, 176 | 177 | // Trigger one or many events, firing all bound callbacks. Callbacks are 178 | // passed the same arguments as `trigger` is, apart from the event name 179 | // (unless you're listening on `"all"`, which will cause your callback to 180 | // receive the true name of the event as the first argument). 181 | trigger: function(name) { 182 | if (!this._events) return this; 183 | var args = slice.call(arguments, 1); 184 | if (!eventsApi(this, 'trigger', name, args)) return this; 185 | var events = this._events[name]; 186 | var allEvents = this._events.all; 187 | if (events) triggerEvents(events, args); 188 | if (allEvents) triggerEvents(allEvents, arguments); 189 | return this; 190 | }, 191 | 192 | // An inversion-of-control version of `on`. Tell *this* object to listen to 193 | // an event in another object ... keeping track of what it's listening to. 194 | listenTo: function(obj, name, callback) { 195 | var listeners = this._listeners || (this._listeners = {}); 196 | var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); 197 | listeners[id] = obj; 198 | obj.on(name, typeof name === 'object' ? this : callback, this); 199 | return this; 200 | }, 201 | 202 | // Tell this object to stop listening to either specific events ... or 203 | // to every object it's currently listening to. 204 | stopListening: function(obj, name, callback) { 205 | var listeners = this._listeners; 206 | if (!listeners) return; 207 | if (obj) { 208 | obj.off(name, typeof name === 'object' ? this : callback, this); 209 | if (!name && !callback) delete listeners[obj._listenerId]; 210 | } else { 211 | if (typeof name === 'object') callback = this; 212 | for (var id in listeners) { 213 | listeners[id].off(name, callback, this); 214 | } 215 | this._listeners = {}; 216 | } 217 | return this; 218 | } 219 | }; 220 | 221 | // Aliases for backwards compatibility. 222 | Events.bind = Events.on; 223 | Events.unbind = Events.off; 224 | 225 | // Allow the `Backbone` object to serve as a global event bus, for folks who 226 | // want global "pubsub" in a convenient place. 227 | _.extend(Backbone, Events); 228 | 229 | // Backbone.Model 230 | // -------------- 231 | 232 | // Create a new model, with defined attributes. A client id (`cid`) 233 | // is automatically generated and assigned for you. 234 | var Model = Backbone.Model = function(attributes, options) { 235 | var defaults; 236 | var attrs = attributes || {}; 237 | this.cid = _.uniqueId('c'); 238 | this.attributes = {}; 239 | if (options && options.collection) this.collection = options.collection; 240 | if (options && options.parse) attrs = this.parse(attrs, options) || {}; 241 | if (defaults = _.result(this, 'defaults')) { 242 | attrs = _.defaults({}, attrs, defaults); 243 | } 244 | this.set(attrs, options); 245 | this.changed = {}; 246 | this.initialize.apply(this, arguments); 247 | }; 248 | 249 | // Attach all inheritable methods to the Model prototype. 250 | _.extend(Model.prototype, Events, { 251 | 252 | // A hash of attributes whose current and previous value differ. 253 | changed: null, 254 | 255 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 256 | // CouchDB users may want to set this to `"_id"`. 257 | idAttribute: 'id', 258 | 259 | // Initialize is an empty function by default. Override it with your own 260 | // initialization logic. 261 | initialize: function(){}, 262 | 263 | // Return a copy of the model's `attributes` object. 264 | toJSON: function(options) { 265 | return _.clone(this.attributes); 266 | }, 267 | 268 | // Proxy `Backbone.sync` by default. 269 | sync: function() { 270 | return Backbone.sync.apply(this, arguments); 271 | }, 272 | 273 | // Get the value of an attribute. 274 | get: function(attr) { 275 | return this.attributes[attr]; 276 | }, 277 | 278 | // Get the HTML-escaped value of an attribute. 279 | escape: function(attr) { 280 | return _.escape(this.get(attr)); 281 | }, 282 | 283 | // Returns `true` if the attribute contains a value that is not null 284 | // or undefined. 285 | has: function(attr) { 286 | return this.get(attr) != null; 287 | }, 288 | 289 | // ---------------------------------------------------------------------- 290 | 291 | // Set a hash of model attributes on the object, firing `"change"` unless 292 | // you choose to silence it. 293 | set: function(key, val, options) { 294 | var attr, attrs, unset, changes, silent, changing, prev, current; 295 | if (key == null) return this; 296 | 297 | // Handle both `"key", value` and `{key: value}` -style arguments. 298 | if (typeof key === 'object') { 299 | attrs = key; 300 | options = val; 301 | } else { 302 | (attrs = {})[key] = val; 303 | } 304 | 305 | options || (options = {}); 306 | 307 | // Run validation. 308 | if (!this._validate(attrs, options)) return false; 309 | 310 | // Extract attributes and options. 311 | unset = options.unset; 312 | silent = options.silent; 313 | changes = []; 314 | changing = this._changing; 315 | this._changing = true; 316 | 317 | if (!changing) { 318 | this._previousAttributes = _.clone(this.attributes); 319 | this.changed = {}; 320 | } 321 | current = this.attributes, prev = this._previousAttributes; 322 | 323 | // Check for changes of `id`. 324 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 325 | 326 | // For each `set` attribute, update or delete the current value. 327 | for (attr in attrs) { 328 | val = attrs[attr]; 329 | if (!_.isEqual(current[attr], val)) changes.push(attr); 330 | if (!_.isEqual(prev[attr], val)) { 331 | this.changed[attr] = val; 332 | } else { 333 | delete this.changed[attr]; 334 | } 335 | unset ? delete current[attr] : current[attr] = val; 336 | } 337 | 338 | // Trigger all relevant attribute changes. 339 | if (!silent) { 340 | if (changes.length) this._pending = true; 341 | for (var i = 0, l = changes.length; i < l; i++) { 342 | this.trigger('change:' + changes[i], this, current[changes[i]], options); 343 | } 344 | } 345 | 346 | if (changing) return this; 347 | if (!silent) { 348 | while (this._pending) { 349 | this._pending = false; 350 | this.trigger('change', this, options); 351 | } 352 | } 353 | this._pending = false; 354 | this._changing = false; 355 | return this; 356 | }, 357 | 358 | // Remove an attribute from the model, firing `"change"` unless you choose 359 | // to silence it. `unset` is a noop if the attribute doesn't exist. 360 | unset: function(attr, options) { 361 | return this.set(attr, void 0, _.extend({}, options, {unset: true})); 362 | }, 363 | 364 | // Clear all attributes on the model, firing `"change"` unless you choose 365 | // to silence it. 366 | clear: function(options) { 367 | var attrs = {}; 368 | for (var key in this.attributes) attrs[key] = void 0; 369 | return this.set(attrs, _.extend({}, options, {unset: true})); 370 | }, 371 | 372 | // Determine if the model has changed since the last `"change"` event. 373 | // If you specify an attribute name, determine if that attribute has changed. 374 | hasChanged: function(attr) { 375 | if (attr == null) return !_.isEmpty(this.changed); 376 | return _.has(this.changed, attr); 377 | }, 378 | 379 | // Return an object containing all the attributes that have changed, or 380 | // false if there are no changed attributes. Useful for determining what 381 | // parts of a view need to be updated and/or what attributes need to be 382 | // persisted to the server. Unset attributes will be set to undefined. 383 | // You can also pass an attributes object to diff against the model, 384 | // determining if there *would be* a change. 385 | changedAttributes: function(diff) { 386 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 387 | var val, changed = false; 388 | var old = this._changing ? this._previousAttributes : this.attributes; 389 | for (var attr in diff) { 390 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 391 | (changed || (changed = {}))[attr] = val; 392 | } 393 | return changed; 394 | }, 395 | 396 | // Get the previous value of an attribute, recorded at the time the last 397 | // `"change"` event was fired. 398 | previous: function(attr) { 399 | if (attr == null || !this._previousAttributes) return null; 400 | return this._previousAttributes[attr]; 401 | }, 402 | 403 | // Get all of the attributes of the model at the time of the previous 404 | // `"change"` event. 405 | previousAttributes: function() { 406 | return _.clone(this._previousAttributes); 407 | }, 408 | 409 | // --------------------------------------------------------------------- 410 | 411 | // Fetch the model from the server. If the server's representation of the 412 | // model differs from its current attributes, they will be overriden, 413 | // triggering a `"change"` event. 414 | fetch: function(options) { 415 | options = options ? _.clone(options) : {}; 416 | if (options.parse === void 0) options.parse = true; 417 | var success = options.success; 418 | options.success = function(model, resp, options) { 419 | if (!model.set(model.parse(resp, options), options)) return false; 420 | if (success) success(model, resp, options); 421 | }; 422 | return this.sync('read', this, options); 423 | }, 424 | 425 | // Set a hash of model attributes, and sync the model to the server. 426 | // If the server returns an attributes hash that differs, the model's 427 | // state will be `set` again. 428 | save: function(key, val, options) { 429 | var attrs, success, method, xhr, attributes = this.attributes; 430 | 431 | // Handle both `"key", value` and `{key: value}` -style arguments. 432 | if (key == null || typeof key === 'object') { 433 | attrs = key; 434 | options = val; 435 | } else { 436 | (attrs = {})[key] = val; 437 | } 438 | 439 | // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. 440 | if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; 441 | 442 | options = _.extend({validate: true}, options); 443 | 444 | // Do not persist invalid models. 445 | if (!this._validate(attrs, options)) return false; 446 | 447 | // Set temporary attributes if `{wait: true}`. 448 | if (attrs && options.wait) { 449 | this.attributes = _.extend({}, attributes, attrs); 450 | } 451 | 452 | // After a successful server-side save, the client is (optionally) 453 | // updated with the server-side state. 454 | if (options.parse === void 0) options.parse = true; 455 | success = options.success; 456 | options.success = function(model, resp, options) { 457 | // Ensure attributes are restored during synchronous saves. 458 | model.attributes = attributes; 459 | var serverAttrs = model.parse(resp, options); 460 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 461 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 462 | return false; 463 | } 464 | if (success) success(model, resp, options); 465 | }; 466 | 467 | // Finish configuring and sending the Ajax request. 468 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 469 | if (method === 'patch') options.attrs = attrs; 470 | xhr = this.sync(method, this, options); 471 | 472 | // Restore attributes. 473 | if (attrs && options.wait) this.attributes = attributes; 474 | 475 | return xhr; 476 | }, 477 | 478 | // Destroy this model on the server if it was already persisted. 479 | // Optimistically removes the model from its collection, if it has one. 480 | // If `wait: true` is passed, waits for the server to respond before removal. 481 | destroy: function(options) { 482 | options = options ? _.clone(options) : {}; 483 | var model = this; 484 | var success = options.success; 485 | 486 | var destroy = function() { 487 | model.trigger('destroy', model, model.collection, options); 488 | }; 489 | 490 | options.success = function(model, resp, options) { 491 | if (options.wait || model.isNew()) destroy(); 492 | if (success) success(model, resp, options); 493 | }; 494 | 495 | if (this.isNew()) { 496 | options.success(this, null, options); 497 | return false; 498 | } 499 | 500 | var xhr = this.sync('delete', this, options); 501 | if (!options.wait) destroy(); 502 | return xhr; 503 | }, 504 | 505 | // Default URL for the model's representation on the server -- if you're 506 | // using Backbone's restful methods, override this to change the endpoint 507 | // that will be called. 508 | url: function() { 509 | var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); 510 | if (this.isNew()) return base; 511 | return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); 512 | }, 513 | 514 | // **parse** converts a response into the hash of attributes to be `set` on 515 | // the model. The default implementation is just to pass the response along. 516 | parse: function(resp, options) { 517 | return resp; 518 | }, 519 | 520 | // Create a new model with identical attributes to this one. 521 | clone: function() { 522 | return new this.constructor(this.attributes); 523 | }, 524 | 525 | // A model is new if it has never been saved to the server, and lacks an id. 526 | isNew: function() { 527 | return this.id == null; 528 | }, 529 | 530 | // Check if the model is currently in a valid state. 531 | isValid: function(options) { 532 | return !this.validate || !this.validate(this.attributes, options); 533 | }, 534 | 535 | // Run validation against the next complete set of model attributes, 536 | // returning `true` if all is well. Otherwise, fire a general 537 | // `"error"` event and call the error callback, if specified. 538 | _validate: function(attrs, options) { 539 | if (!options.validate || !this.validate) return true; 540 | attrs = _.extend({}, this.attributes, attrs); 541 | var error = this.validationError = this.validate(attrs, options) || null; 542 | if (!error) return true; 543 | this.trigger('invalid', this, error, options || {}); 544 | return false; 545 | } 546 | 547 | }); 548 | 549 | // Backbone.Collection 550 | // ------------------- 551 | 552 | // Provides a standard collection class for our sets of models, ordered 553 | // or unordered. If a `comparator` is specified, the Collection will maintain 554 | // its models in sort order, as they're added and removed. 555 | var Collection = Backbone.Collection = function(models, options) { 556 | options || (options = {}); 557 | if (options.model) this.model = options.model; 558 | if (options.comparator !== void 0) this.comparator = options.comparator; 559 | this.models = []; 560 | this._reset(); 561 | this.initialize.apply(this, arguments); 562 | if (models) this.reset(models, _.extend({silent: true}, options)); 563 | }; 564 | 565 | // Define the Collection's inheritable methods. 566 | _.extend(Collection.prototype, Events, { 567 | 568 | // The default model for a collection is just a **Backbone.Model**. 569 | // This should be overridden in most cases. 570 | model: Model, 571 | 572 | // Initialize is an empty function by default. Override it with your own 573 | // initialization logic. 574 | initialize: function(){}, 575 | 576 | // The JSON representation of a Collection is an array of the 577 | // models' attributes. 578 | toJSON: function(options) { 579 | return this.map(function(model){ return model.toJSON(options); }); 580 | }, 581 | 582 | // Proxy `Backbone.sync` by default. 583 | sync: function() { 584 | return Backbone.sync.apply(this, arguments); 585 | }, 586 | 587 | // Add a model, or list of models to the set. 588 | add: function(models, options) { 589 | models = _.isArray(models) ? models.slice() : [models]; 590 | options || (options = {}); 591 | var i, l, model, attrs, existing, doSort, add, at, sort, sortAttr; 592 | add = []; 593 | at = options.at; 594 | sort = this.comparator && (at == null) && options.sort != false; 595 | sortAttr = _.isString(this.comparator) ? this.comparator : null; 596 | 597 | // Turn bare objects into model references, and prevent invalid models 598 | // from being added. 599 | for (i = 0, l = models.length; i < l; i++) { 600 | if (!(model = this._prepareModel(attrs = models[i], options))) { 601 | this.trigger('invalid', this, attrs, options); 602 | continue; 603 | } 604 | 605 | // If a duplicate is found, prevent it from being added and 606 | // optionally merge it into the existing model. 607 | if (existing = this.get(model)) { 608 | if (options.merge) { 609 | existing.set(attrs === model ? model.attributes : attrs, options); 610 | if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true; 611 | } 612 | continue; 613 | } 614 | 615 | // This is a new model, push it to the `add` list. 616 | add.push(model); 617 | 618 | // Listen to added models' events, and index models for lookup by 619 | // `id` and by `cid`. 620 | model.on('all', this._onModelEvent, this); 621 | this._byId[model.cid] = model; 622 | if (model.id != null) this._byId[model.id] = model; 623 | } 624 | 625 | // See if sorting is needed, update `length` and splice in new models. 626 | if (add.length) { 627 | if (sort) doSort = true; 628 | this.length += add.length; 629 | if (at != null) { 630 | splice.apply(this.models, [at, 0].concat(add)); 631 | } else { 632 | push.apply(this.models, add); 633 | } 634 | } 635 | 636 | // Silently sort the collection if appropriate. 637 | if (doSort) this.sort({silent: true}); 638 | 639 | if (options.silent) return this; 640 | 641 | // Trigger `add` events. 642 | for (i = 0, l = add.length; i < l; i++) { 643 | (model = add[i]).trigger('add', model, this, options); 644 | } 645 | 646 | // Trigger `sort` if the collection was sorted. 647 | if (doSort) this.trigger('sort', this, options); 648 | 649 | return this; 650 | }, 651 | 652 | // Remove a model, or a list of models from the set. 653 | remove: function(models, options) { 654 | models = _.isArray(models) ? models.slice() : [models]; 655 | options || (options = {}); 656 | var i, l, index, model; 657 | for (i = 0, l = models.length; i < l; i++) { 658 | model = this.get(models[i]); 659 | if (!model) continue; 660 | delete this._byId[model.id]; 661 | delete this._byId[model.cid]; 662 | index = this.indexOf(model); 663 | this.models.splice(index, 1); 664 | this.length--; 665 | if (!options.silent) { 666 | options.index = index; 667 | model.trigger('remove', model, this, options); 668 | } 669 | this._removeReference(model); 670 | } 671 | return this; 672 | }, 673 | 674 | // Add a model to the end of the collection. 675 | push: function(model, options) { 676 | model = this._prepareModel(model, options); 677 | this.add(model, _.extend({at: this.length}, options)); 678 | return model; 679 | }, 680 | 681 | // Remove a model from the end of the collection. 682 | pop: function(options) { 683 | var model = this.at(this.length - 1); 684 | this.remove(model, options); 685 | return model; 686 | }, 687 | 688 | // Add a model to the beginning of the collection. 689 | unshift: function(model, options) { 690 | model = this._prepareModel(model, options); 691 | this.add(model, _.extend({at: 0}, options)); 692 | return model; 693 | }, 694 | 695 | // Remove a model from the beginning of the collection. 696 | shift: function(options) { 697 | var model = this.at(0); 698 | this.remove(model, options); 699 | return model; 700 | }, 701 | 702 | // Slice out a sub-array of models from the collection. 703 | slice: function(begin, end) { 704 | return this.models.slice(begin, end); 705 | }, 706 | 707 | // Get a model from the set by id. 708 | get: function(obj) { 709 | if (obj == null) return void 0; 710 | this._idAttr || (this._idAttr = this.model.prototype.idAttribute); 711 | return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj]; 712 | }, 713 | 714 | // Get the model at the given index. 715 | at: function(index) { 716 | return this.models[index]; 717 | }, 718 | 719 | // Return models with matching attributes. Useful for simple cases of `filter`. 720 | where: function(attrs) { 721 | if (_.isEmpty(attrs)) return []; 722 | return this.filter(function(model) { 723 | for (var key in attrs) { 724 | if (attrs[key] !== model.get(key)) return false; 725 | } 726 | return true; 727 | }); 728 | }, 729 | 730 | // Force the collection to re-sort itself. You don't need to call this under 731 | // normal circumstances, as the set will maintain sort order as each item 732 | // is added. 733 | sort: function(options) { 734 | if (!this.comparator) { 735 | throw new Error('Cannot sort a set without a comparator'); 736 | } 737 | options || (options = {}); 738 | 739 | // Run sort based on type of `comparator`. 740 | if (_.isString(this.comparator) || this.comparator.length === 1) { 741 | this.models = this.sortBy(this.comparator, this); 742 | } else { 743 | this.models.sort(_.bind(this.comparator, this)); 744 | } 745 | 746 | if (!options.silent) this.trigger('sort', this, options); 747 | return this; 748 | }, 749 | 750 | // Pluck an attribute from each model in the collection. 751 | pluck: function(attr) { 752 | return _.invoke(this.models, 'get', attr); 753 | }, 754 | 755 | // Smartly update a collection with a change set of models, adding, 756 | // removing, and merging as necessary. 757 | update: function(models, options) { 758 | options = _.extend({add: true, merge: true, remove: true}, options); 759 | if (options.parse) models = this.parse(models, options); 760 | var model, i, l, existing; 761 | var add = [], remove = [], modelMap = {}; 762 | 763 | // Allow a single model (or no argument) to be passed. 764 | if (!_.isArray(models)) models = models ? [models] : []; 765 | 766 | // Proxy to `add` for this case, no need to iterate... 767 | if (options.add && !options.remove) return this.add(models, options); 768 | 769 | // Determine which models to add and merge, and which to remove. 770 | for (i = 0, l = models.length; i < l; i++) { 771 | model = models[i]; 772 | existing = this.get(model); 773 | if (options.remove && existing) modelMap[existing.cid] = true; 774 | if ((options.add && !existing) || (options.merge && existing)) { 775 | add.push(model); 776 | } 777 | } 778 | if (options.remove) { 779 | for (i = 0, l = this.models.length; i < l; i++) { 780 | model = this.models[i]; 781 | if (!modelMap[model.cid]) remove.push(model); 782 | } 783 | } 784 | 785 | // Remove models (if applicable) before we add and merge the rest. 786 | if (remove.length) this.remove(remove, options); 787 | if (add.length) this.add(add, options); 788 | return this; 789 | }, 790 | 791 | // When you have more items than you want to add or remove individually, 792 | // you can reset the entire set with a new list of models, without firing 793 | // any `add` or `remove` events. Fires `reset` when finished. 794 | reset: function(models, options) { 795 | options || (options = {}); 796 | if (options.parse) models = this.parse(models, options); 797 | for (var i = 0, l = this.models.length; i < l; i++) { 798 | this._removeReference(this.models[i]); 799 | } 800 | options.previousModels = this.models.slice(); 801 | this._reset(); 802 | if (models) this.add(models, _.extend({silent: true}, options)); 803 | if (!options.silent) this.trigger('reset', this, options); 804 | return this; 805 | }, 806 | 807 | // Fetch the default set of models for this collection, resetting the 808 | // collection when they arrive. If `update: true` is passed, the response 809 | // data will be passed through the `update` method instead of `reset`. 810 | fetch: function(options) { 811 | options = options ? _.clone(options) : {}; 812 | if (options.parse === void 0) options.parse = true; 813 | var success = options.success; 814 | options.success = function(collection, resp, options) { 815 | var method = options.update ? 'update' : 'reset'; 816 | collection[method](resp, options); 817 | if (success) success(collection, resp, options); 818 | }; 819 | return this.sync('read', this, options); 820 | }, 821 | 822 | // Create a new instance of a model in this collection. Add the model to the 823 | // collection immediately, unless `wait: true` is passed, in which case we 824 | // wait for the server to agree. 825 | create: function(model, options) { 826 | options = options ? _.clone(options) : {}; 827 | if (!(model = this._prepareModel(model, options))) return false; 828 | if (!options.wait) this.add(model, options); 829 | var collection = this; 830 | var success = options.success; 831 | options.success = function(model, resp, options) { 832 | if (options.wait) collection.add(model, options); 833 | if (success) success(model, resp, options); 834 | }; 835 | model.save(null, options); 836 | return model; 837 | }, 838 | 839 | // **parse** converts a response into a list of models to be added to the 840 | // collection. The default implementation is just to pass it through. 841 | parse: function(resp, options) { 842 | return resp; 843 | }, 844 | 845 | // Create a new collection with an identical list of models as this one. 846 | clone: function() { 847 | return new this.constructor(this.models); 848 | }, 849 | 850 | // Reset all internal state. Called when the collection is reset. 851 | _reset: function() { 852 | this.length = 0; 853 | this.models.length = 0; 854 | this._byId = {}; 855 | }, 856 | 857 | // Prepare a model or hash of attributes to be added to this collection. 858 | _prepareModel: function(attrs, options) { 859 | if (attrs instanceof Model) { 860 | if (!attrs.collection) attrs.collection = this; 861 | return attrs; 862 | } 863 | options || (options = {}); 864 | options.collection = this; 865 | var model = new this.model(attrs, options); 866 | if (!model._validate(attrs, options)) return false; 867 | return model; 868 | }, 869 | 870 | // Internal method to remove a model's ties to a collection. 871 | _removeReference: function(model) { 872 | if (this === model.collection) delete model.collection; 873 | model.off('all', this._onModelEvent, this); 874 | }, 875 | 876 | // Internal method called every time a model in the set fires an event. 877 | // Sets need to update their indexes when models change ids. All other 878 | // events simply proxy through. "add" and "remove" events that originate 879 | // in other collections are ignored. 880 | _onModelEvent: function(event, model, collection, options) { 881 | if ((event === 'add' || event === 'remove') && collection !== this) return; 882 | if (event === 'destroy') this.remove(model, options); 883 | if (model && event === 'change:' + model.idAttribute) { 884 | delete this._byId[model.previous(model.idAttribute)]; 885 | if (model.id != null) this._byId[model.id] = model; 886 | } 887 | this.trigger.apply(this, arguments); 888 | }, 889 | 890 | sortedIndex: function (model, value, context) { 891 | value || (value = this.comparator); 892 | var iterator = _.isFunction(value) ? value : function(model) { 893 | return model.get(value); 894 | }; 895 | return _.sortedIndex(this.models, model, iterator, context); 896 | } 897 | 898 | }); 899 | 900 | // Underscore methods that we want to implement on the Collection. 901 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 902 | 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 903 | 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 904 | 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 905 | 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 906 | 'isEmpty', 'chain']; 907 | 908 | // Mix in each Underscore method as a proxy to `Collection#models`. 909 | _.each(methods, function(method) { 910 | Collection.prototype[method] = function() { 911 | var args = slice.call(arguments); 912 | args.unshift(this.models); 913 | return _[method].apply(_, args); 914 | }; 915 | }); 916 | 917 | // Underscore methods that take a property name as an argument. 918 | var attributeMethods = ['groupBy', 'countBy', 'sortBy']; 919 | 920 | // Use attributes instead of properties. 921 | _.each(attributeMethods, function(method) { 922 | Collection.prototype[method] = function(value, context) { 923 | var iterator = _.isFunction(value) ? value : function(model) { 924 | return model.get(value); 925 | }; 926 | return _[method](this.models, iterator, context); 927 | }; 928 | }); 929 | 930 | // Backbone.Router 931 | // --------------- 932 | 933 | // Routers map faux-URLs to actions, and fire events when routes are 934 | // matched. Creating a new one sets its `routes` hash, if not set statically. 935 | var Router = Backbone.Router = function(options) { 936 | options || (options = {}); 937 | if (options.routes) this.routes = options.routes; 938 | this._bindRoutes(); 939 | this.initialize.apply(this, arguments); 940 | }; 941 | 942 | // Cached regular expressions for matching named param parts and splatted 943 | // parts of route strings. 944 | var optionalParam = /\((.*?)\)/g; 945 | var namedParam = /(\(\?)?:\w+/g; 946 | var splatParam = /\*\w+/g; 947 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 948 | 949 | // Set up all inheritable **Backbone.Router** properties and methods. 950 | _.extend(Router.prototype, Events, { 951 | 952 | // Initialize is an empty function by default. Override it with your own 953 | // initialization logic. 954 | initialize: function(){}, 955 | 956 | // Manually bind a single named route to a callback. For example: 957 | // 958 | // this.route('search/:query/p:num', 'search', function(query, num) { 959 | // ... 960 | // }); 961 | // 962 | route: function(route, name, callback) { 963 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 964 | if (!callback) callback = this[name]; 965 | Backbone.history.route(route, _.bind(function(fragment) { 966 | var args = this._extractParameters(route, fragment); 967 | callback && callback.apply(this, args); 968 | this.trigger.apply(this, ['route:' + name].concat(args)); 969 | this.trigger('route', name, args); 970 | Backbone.history.trigger('route', this, name, args); 971 | }, this)); 972 | return this; 973 | }, 974 | 975 | // Simple proxy to `Backbone.history` to save a fragment into the history. 976 | navigate: function(fragment, options) { 977 | Backbone.history.navigate(fragment, options); 978 | return this; 979 | }, 980 | 981 | // Bind all defined routes to `Backbone.history`. We have to reverse the 982 | // order of the routes here to support behavior where the most general 983 | // routes can be defined at the bottom of the route map. 984 | _bindRoutes: function() { 985 | if (!this.routes) return; 986 | var route, routes = _.keys(this.routes); 987 | while ((route = routes.pop()) != null) { 988 | this.route(route, this.routes[route]); 989 | } 990 | }, 991 | 992 | // Convert a route string into a regular expression, suitable for matching 993 | // against the current location hash. 994 | _routeToRegExp: function(route) { 995 | route = route.replace(escapeRegExp, '\\$&') 996 | .replace(optionalParam, '(?:$1)?') 997 | .replace(namedParam, function(match, optional){ 998 | return optional ? match : '([^\/]+)'; 999 | }) 1000 | .replace(splatParam, '(.*?)'); 1001 | return new RegExp('^' + route + '$'); 1002 | }, 1003 | 1004 | // Given a route, and a URL fragment that it matches, return the array of 1005 | // extracted parameters. 1006 | _extractParameters: function(route, fragment) { 1007 | return route.exec(fragment).slice(1); 1008 | } 1009 | 1010 | }); 1011 | 1012 | // Backbone.History 1013 | // ---------------- 1014 | 1015 | // Handles cross-browser history management, based on URL fragments. If the 1016 | // browser does not support `onhashchange`, falls back to polling. 1017 | var History = Backbone.History = function() { 1018 | this.handlers = []; 1019 | _.bindAll(this, 'checkUrl'); 1020 | 1021 | // Ensure that `History` can be used outside of the browser. 1022 | if (typeof window !== 'undefined') { 1023 | this.location = window.location; 1024 | this.history = window.history; 1025 | } 1026 | }; 1027 | 1028 | // Cached regex for stripping a leading hash/slash and trailing space. 1029 | var routeStripper = /^[#\/]|\s+$/g; 1030 | 1031 | // Cached regex for stripping leading and trailing slashes. 1032 | var rootStripper = /^\/+|\/+$/g; 1033 | 1034 | // Cached regex for detecting MSIE. 1035 | var isExplorer = /msie [\w.]+/; 1036 | 1037 | // Cached regex for removing a trailing slash. 1038 | var trailingSlash = /\/$/; 1039 | 1040 | // Has the history handling already been started? 1041 | History.started = false; 1042 | 1043 | // Set up all inheritable **Backbone.History** properties and methods. 1044 | _.extend(History.prototype, Events, { 1045 | 1046 | // The default interval to poll for hash changes, if necessary, is 1047 | // twenty times a second. 1048 | interval: 50, 1049 | 1050 | // Gets the true hash value. Cannot use location.hash directly due to bug 1051 | // in Firefox where location.hash will always be decoded. 1052 | getHash: function(window) { 1053 | var match = (window || this).location.href.match(/#(.*)$/); 1054 | return match ? match[1] : ''; 1055 | }, 1056 | 1057 | // Get the cross-browser normalized URL fragment, either from the URL, 1058 | // the hash, or the override. 1059 | getFragment: function(fragment, forcePushState) { 1060 | if (fragment == null) { 1061 | if (this._hasPushState || !this._wantsHashChange || forcePushState) { 1062 | fragment = this.location.pathname; 1063 | var root = this.root.replace(trailingSlash, ''); 1064 | if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); 1065 | } else { 1066 | fragment = this.getHash(); 1067 | } 1068 | } 1069 | return fragment.replace(routeStripper, ''); 1070 | }, 1071 | 1072 | // Start the hash change handling, returning `true` if the current URL matches 1073 | // an existing route, and `false` otherwise. 1074 | start: function(options) { 1075 | if (History.started) throw new Error("Backbone.history has already been started"); 1076 | History.started = true; 1077 | 1078 | // Figure out the initial configuration. Do we need an iframe? 1079 | // Is pushState desired ... is it available? 1080 | this.options = _.extend({}, {root: '/'}, this.options, options); 1081 | this.root = this.options.root; 1082 | this._wantsHashChange = this.options.hashChange !== false; 1083 | this._wantsPushState = !!this.options.pushState; 1084 | this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); 1085 | var fragment = this.getFragment(); 1086 | var docMode = document.documentMode; 1087 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1088 | 1089 | // Normalize root to always include a leading and trailing slash. 1090 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1091 | 1092 | if (oldIE && this._wantsHashChange) { 1093 | this.iframe = Backbone.$('